Module: ConcernsOnRails::Controllers::Includable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/controllers/includable.rb

Overview

Whitelisted association sideloading + sparse fieldsets for JSON APIs. Same allow-list philosophy as Controllers::Sortable: a client can only ask for associations/fields you’ve explicitly permitted, so ‘?include=` can never trigger an arbitrary `.includes` (N+1 / data-exposure risk).

class ArticlesController < ApplicationController
  include ConcernsOnRails::Controllers::Includable

  includable :author, :comments,
             fields: { articles: %i[id title], authors: %i[id name] }

  def index
    render json: with_includes(Article.all),
           include: requested_includes,
           fields: requested_fields
  end
end

URL params:

?include=author,comments         -> eager-loads only whitelisted associations
?fields[articles]=id,title       -> sanitized down to the allowed columns

‘requested_includes` / `requested_fields` return sanitized values you can hand to your serializer; they never mutate the rendered output themselves.

Instance Method Summary collapse

Instance Method Details

#requested_fieldsObject

Sanitized sparse fieldsets: { table => [cols] }, each intersected with the allowed columns for that table. Unknown tables/columns are dropped.



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/concerns_on_rails/controllers/includable.rb', line 63

def requested_fields
  raw = params[:fields]
  return {} unless raw.respond_to?(:each_pair)

  allowed = self.class.includable_fields
  raw.each_with_object({}) do |(table, cols), memo|
    key = table.to_sym
    next unless allowed.key?(key)

    permitted = split_field_list(cols) & allowed[key]
    memo[key] = permitted unless permitted.empty?
  end
end

#requested_includesObject

Sanitized association list: requested ∩ allow-list.



56
57
58
59
# File 'lib/concerns_on_rails/controllers/includable.rb', line 56

def requested_includes
  requested = params[:include].to_s.split(",").map { |token| token.strip.to_sym }
  requested & self.class.includable_associations
end

#with_includes(relation) ⇒ Object

Eager-load only the whitelisted associations requested via ?include=. Returns the relation unchanged when nothing valid was requested.



50
51
52
53
# File 'lib/concerns_on_rails/controllers/includable.rb', line 50

def with_includes(relation)
  associations = requested_includes
  associations.empty? ? relation : relation.includes(*associations)
end