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: [], computed_arguments: {}) ⇒ 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.

Parameters:

  • computed_attributes (Array<String>) (defaults to: []) —

    opt-in record-level computed attributes to evaluate, selected via ?computed_attributes=.

  • computed_arguments (Hash{String => Array}) (defaults to: {}) —

    positional arguments per attribute name. An attribute with required parameters and no entry here is skipped rather than called with too few arguments, so an existing direct caller that passes only names keeps working.

Returns:

  • (Hash)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rhino/concerns/hidable_columns.rb', line 130

def as_rhino_json(computed_attributes: [], computed_arguments: {})
  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, computed_arguments)
  )

  # 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



106
107
108
109
110
111
112
# File 'lib/rhino/concerns/hidable_columns.rb', line 106

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_attributes ⇒ Hash

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



183
184
185
# File 'lib/rhino/concerns/hidable_columns.rb', line 183

def rhino_computed_attributes
  {}
end

#rhino_record_computed_attributes ⇒ Hash{String => Object}

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.

An attribute may also declare PARAMETERS the client supplies as ?computed_attributes[param]=value. Use the extended form — a hash carrying params (and optionally optional and with) — and the bound arguments are appended after user, in declared order. A parameterised entry is always called as call(record, user, *args). Any other declared value (a callable, a scalar, a plain array) keeps its current meaning.

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}" },
    'tickets_since' => {
      params: [:since],
      with: ->(record, _user, since) { record.tickets.where("created_at >= ?", since).count }
    }
  }
end

Returns:

  • (Hash{String => Object})


217
218
219
# File 'lib/rhino/concerns/hidable_columns.rb', line 217

def rhino_record_computed_attributes
  {}
end