Module: Rhino::HidableColumns

Extended by:
ActiveSupport::Concern
Included in:
RhinoModel
Defined in:
lib/rhino/concerns/hidable_columns.rb

Overview

Column-level visibility control concern. Mirrors the Laravel HidableColumns trait.

Base hidden columns: password, remember_token, created_at, updated_at, deleted_at, discarded_at, email_verified_at

Usage:

class User < ApplicationRecord
include Rhino::HidableColumns

rhino_additional_hidden :secret_field, :internal_notes
end

Adding computed attributes to JSON responses:

class Comment < Rhino::RhinoModel
def author_name
  user&.name || 'Anonymous'
end

def rhino_computed_attributes
  {
    'author_name' => author_name
  }
end
end

Policy-based hiding:

class UserPolicy < Rhino::ResourcePolicy
def hidden_attributes_for_show(user)
  has_role?(user, 'admin') ? [] : ['email', 'phone']
end

def permitted_attributes_for_show(user)
  has_role?(user, 'admin') ? ['*'] : ['id', 'name', 'avatar']
end
end

Constant Summary collapse

BASE_HIDDEN_COLUMNS =
%w[
  password
  password_digest
  remember_token
  created_at
  updated_at
  deleted_at
  discarded_at
  email_verified_at
].freeze

Instance Method Summary collapse

Instance Method Details

#as_rhino_json(computed_attributes: []) ⇒ Hash

Serialize to JSON excluding hidden columns and respecting policy whitelist.

The current user is resolved automatically from RequestStore. Policy filtering (blacklist + whitelist) is applied AFTER computed attributes are merged, so computed attributes are always subject to policy control.

Do NOT override this method. Override rhino_computed_attributes instead to add computed/virtual attributes to the JSON response.

Returns:

  • (Hash)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/rhino/concerns/hidable_columns.rb', line 112

def as_rhino_json(computed_attributes: [])
  user = rhino_current_user
  hidden = hidden_columns_for(user)
  result = as_json(except: hidden)

  # Merge computed attributes from model BEFORE applying policy filtering
  computed = rhino_computed_attributes
  result.merge!(computed) if computed.is_a?(Hash) && computed.any?

  # Merge the OPT-IN record-level computed attributes the client selected via
  # ?computed_attributes=. Nothing here is evaluated unless it was asked for
  # by name, so declaring an expensive attribute costs nothing on requests
  # that don't want it. Merged before policy filtering, so the blacklist and
  # whitelist below still govern them.
  result.merge!(rhino_resolve_record_computed_attributes(computed_attributes, user))

  # Apply blacklist to the final hash (covers DB columns from as_json
  # overrides AND computed attributes from rhino_computed_attributes)
  hidden_set = Set.new(hidden)
  result.reject! { |key, _| hidden_set.include?(key) }

  # Apply whitelist to the final hash (covers computed attributes too)
  permitted = policy_permitted_attributes(user)
  if permitted && permitted != ['*']
    permitted_set = Set.new(permitted.map(&:to_s))
    permitted_set.add('id') # id is always allowed
    # The route key column is always allowed too — responses must stay
    # routable even when a policy whitelist omits it.
    route_key = self.class.try(:rhino_resolved_route_key)
    permitted_set.add(route_key.to_s) if route_key
    result.select! { |key, _| permitted_set.include?(key) }
  end

  result
end

#hidden_columns_for(user = nil) ⇒ Array<String>

Get the list of columns to hide for the current user. Merges base + static + policy-defined hidden columns. Resolves the user from RequestStore automatically.

Returns:

  • (Array<String>)

    Column names to hide



94
95
96
97
98
99
100
# File 'lib/rhino/concerns/hidable_columns.rb', line 94

def hidden_columns_for(user = nil)
  user ||= rhino_current_user
  columns = BASE_HIDDEN_COLUMNS.dup
  columns.concat(additional_hidden_columns)
  columns.concat(policy_hidden_columns(user))
  columns.uniq
end

#rhino_computed_attributesHash

Override this method in your model to add computed/virtual attributes to the JSON response. These attributes are subject to policy-level blacklist (+hidden_attributes_for_show+) and whitelist (+permitted_attributes_for_show+) just like database columns.

Examples:

def rhino_computed_attributes
  {
    'full_name' => "#{first_name} #{last_name}",
    'is_overdue' => due_date&.past?,
    'days_until_expiry' => expiry_date ? (expiry_date - Date.current).to_i : nil
  }
end

Returns:

  • (Hash)

    key-value pairs to merge into the JSON response



163
164
165
# File 'lib/rhino/concerns/hidable_columns.rb', line 163

def rhino_computed_attributes
  {}
end

#rhino_record_computed_attributesHash{String => #call}

Override this method to declare OPT-IN record-level computed attributes.

Unlike rhino_computed_attributes, nothing here is evaluated unless the client names it in ?computed_attributes=a,b on index/show/trashed — so expensive per-row work is only paid for when it is actually wanted.

Return a hash of attribute name => callable. The callable may accept zero, one (record) or two (record, user) arguments.

Examples:

def rhino_record_computed_attributes
  {
    'open_tickets_count' => ->(record, _user) { record.tickets.where(closed_at: nil).count },
    'full_name' => ->(record, _user) { "#{record.first_name} #{record.last_name}" }
  }
end

Returns:

  • (Hash{String => #call})


185
186
187
# File 'lib/rhino/concerns/hidable_columns.rb', line 185

def rhino_record_computed_attributes
  {}
end