class JSONAPI::ActiveRelationResource

Public Class Methods

apply_join(records:, relationship:, resource_type:, join_type:, options:) click to toggle source

end `records` methods

# File lib/jsonapi/active_relation_resource.rb, line 310
def apply_join(records:, relationship:, resource_type:, join_type:, options:)
  if relationship.polymorphic? && relationship.belongs_to?
    case join_type
    when :inner
      records = records.joins(resource_type.to_s.singularize.to_sym)
    when :left
      records = records.joins_left(resource_type.to_s.singularize.to_sym)
    end
  else
    relation_name = relationship.relation_name(options)
    case join_type
    when :inner
      records = records.joins(relation_name)
    when :left
      records = records.joins_left(relation_name)
    end
  end
  records
end
count(filters, options = {}) click to toggle source

Counts Resources found using the `filters`

@param filters [Hash] the filters hash @option options [Hash] :context The context of the request, set in the controller

@return [Integer] the count

# File lib/jsonapi/active_relation_resource.rb, line 38
def count(filters, options = {})
  join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
                                                 filters: filters)

  records = apply_request_settings_to_records(records: records(options),
                         filters: filters,
                         join_manager: join_manager,
                         options: options)

  count_records(records)
end
find(filters, options = {}) click to toggle source

Finds Resources using the `filters`. Pagination and sort options are used when provided

@param filters [Hash] the filters hash @option options [Hash] :context The context of the request, set in the controller @option options [Hash] :sort_criteria The `sort criteria` @option options [Hash] :include_directives The `include_directives`

@return [Array<Resource>] the Resource instances matching the filters, sorting and pagination rules.

# File lib/jsonapi/active_relation_resource.rb, line 14
def find(filters, options = {})
  sort_criteria = options.fetch(:sort_criteria) { [] }

  join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
                                                 filters: filters,
                                                 sort_criteria: sort_criteria)

  paginator = options[:paginator]

  records = apply_request_settings_to_records(records: records(options),
                         sort_criteria: sort_criteria,filters: filters,
                         join_manager: join_manager,
                         paginator: paginator,
                         options: options)

  resources_for(records, options[:context])
end
find_by_key(key, options = {}) click to toggle source

Returns the single Resource identified by `key`

@param key the primary key of the resource to find @option options [Hash] :context The context of the request, set in the controller

# File lib/jsonapi/active_relation_resource.rb, line 54
def find_by_key(key, options = {})
  record = find_record_by_key(key, options)
  resource_for(record, options[:context])
end
find_by_keys(keys, options = {}) click to toggle source

Returns an array of Resources identified by the `keys` array

@param keys [Array<key>] Array of primary keys to find resources for @option options [Hash] :context The context of the request, set in the controller

# File lib/jsonapi/active_relation_resource.rb, line 63
def find_by_keys(keys, options = {})
  records = find_records_by_keys(keys, options)
  resources_for(records, options[:context])
end
find_fragments(filters, options = {}) click to toggle source

Finds Resource fragments using the `filters`. Pagination and sort options are used when provided. Retrieving the ResourceIdentities and attributes does not instantiate a model instance. Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)

@param filters [Hash] the filters hash @option options [Hash] :context The context of the request, set in the controller @option options [Hash] :sort_criteria The `sort criteria` @option options [Hash] :include_directives The `include_directives` @option options [Hash] :attributes Additional fields to be retrieved. @option options [Boolean] :cache Return the resources' cache field

@return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}}}]

the ResourceInstances matching the filters, sorting, and pagination rules along with any request
additional_field values
# File lib/jsonapi/active_relation_resource.rb, line 92
def find_fragments(filters, options = {})
  include_directives = options[:include_directives] ? options[:include_directives].include_directives : {}
  resource_klass = self
  linkage_relationships = to_one_relationships_for_linkage(include_directives[:include_related])

  sort_criteria = options.fetch(:sort_criteria) { [] }

  join_manager = ActiveRelation::JoinManager.new(resource_klass: resource_klass,
                                                 source_relationship: nil,
                                                 relationships: linkage_relationships,
                                                 sort_criteria: sort_criteria,
                                                 filters: filters)

  paginator = options[:paginator]

  records = apply_request_settings_to_records(records: records(options),
                         filters: filters,
                         sort_criteria: sort_criteria,
                         paginator: paginator,
                         join_manager: join_manager,
                         options: options)

  # This alias is going to be resolve down to the model's table name and will not actually be an alias
  resource_table_alias = resource_klass._table_name

  pluck_fields = [sql_field_with_alias(resource_table_alias, resource_klass._primary_key)]

  cache_field = attribute_to_model_field(:_cache_field) if options[:cache]
  if cache_field
    pluck_fields << sql_field_with_alias(resource_table_alias, cache_field[:name])
  end

  linkage_fields = []

  linkage_relationships.each do |name|
    linkage_relationship = resource_klass._relationship(name)

    if linkage_relationship.polymorphic? && linkage_relationship.belongs_to?
      linkage_relationship.resource_types.each do |resource_type|
        klass = resource_klass_for(resource_type)
        linkage_fields << {relationship_name: name, resource_klass: klass}

        linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias]
        primary_key = klass._primary_key
        pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
      end
    else
      klass = linkage_relationship.resource_klass
      linkage_fields << {relationship_name: name, resource_klass: klass}

      linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias]
      primary_key = klass._primary_key
      pluck_fields << sql_field_with_alias(linkage_table_alias, primary_key)
    end
  end

  model_fields = {}
  attributes = options[:attributes]
  attributes.try(:each) do |attribute|
    model_field = resource_klass.attribute_to_model_field(attribute)
    model_fields[attribute] = model_field
    pluck_fields << sql_field_with_alias(resource_table_alias, model_field[:name])
  end

  sort_fields = options.dig(:_relation_helper_options, :sort_fields)
  sort_fields.try(:each) do |field|
    pluck_fields << Arel.sql(field)
  end

  fragments = {}
  rows = records.pluck(*pluck_fields)
  rows.each do |row|
    rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])

    fragments[rid] ||= JSONAPI::ResourceFragment.new(rid)
    attributes_offset = 1

    if cache_field
      fragments[rid].cache = cast_to_attribute_type(row[1], cache_field[:type])
      attributes_offset+= 1
    end

    linkage_fields.each do |linkage_field_details|
      fragments[rid].initialize_related(linkage_field_details[:relationship_name])
      related_id = row[attributes_offset]
      if related_id
        related_rid = JSONAPI::ResourceIdentity.new(linkage_field_details[:resource_klass], related_id)
        fragments[rid].add_related_identity(linkage_field_details[:relationship_name], related_rid)
      end
      attributes_offset+= 1
    end

    model_fields.each_with_index do |k, idx|
      fragments[rid].attributes[k[0]]= cast_to_attribute_type(row[idx + attributes_offset], k[1][:type])
    end
  end

  if JSONAPI.configuration.warn_on_performance_issues && (rows.length > fragments.length)
    warn "Performance issue detected: `#{self.name.to_s}.records` returned non-normalized results in `#{self.name.to_s}.find_fragments`."
  end

  fragments
end
find_included_fragments(source_rids, relationship_name, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 217
def find_included_fragments(source_rids, relationship_name, options)
  relationship = _relationship(relationship_name)

  if relationship.polymorphic? # && relationship.foreign_key_on == :self
    find_related_polymorphic_fragments(source_rids, relationship, options, true)
  else
    find_related_monomorphic_fragments(source_rids, relationship, options, true)
  end
end
find_to_populate_by_keys(keys, options = {}) click to toggle source

Returns an array of Resources identified by the `keys` array. The resources are not filtered as this will have been done in a prior step

@param keys [Array<key>] Array of primary keys to find resources for @option options [Hash] :context The context of the request, set in the controller

# File lib/jsonapi/active_relation_resource.rb, line 73
def find_to_populate_by_keys(keys, options = {})
  records = records_for_populate(options).where(_primary_key => keys)
  resources_for(records, options[:context])
end
join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 347
def join_relationship(records:, relationship:, resource_type: nil, join_type: :inner, options: {})
  relationship_records = relationship_records(relationship: relationship,
                                              join_type: join_type,
                                              resource_type: resource_type,
                                              options: options)
  records.merge(relationship_records)
end
records(options = {}) click to toggle source

The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce permissions checks on the request.

@option options [Hash] :context The context of the request, set in the controller

@return [ActiveRecord::Relation]

# File lib/jsonapi/active_relation_resource.rb, line 282
def records(options = {})
  records_base(options)
end
records_base(_options = {}) click to toggle source

Base for the `records` methods that follow and is not directly used for accessing model data by this class. Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.

@option options [Hash] :context The context of the request, set in the controller

@return [ActiveRecord::Relation]

# File lib/jsonapi/active_relation_resource.rb, line 272
def records_base(_options = {})
  _model_class.all
end
records_for_populate(options = {}) click to toggle source

The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions checks. However if the model needs to include other models adding `includes` is appropriate

@option options [Hash] :context The context of the request, set in the controller

@return [ActiveRecord::Relation]

# File lib/jsonapi/active_relation_resource.rb, line 293
def records_for_populate(options = {})
  records_base(options)
end
relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 330
def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
  records = relationship.parent_resource.records_for_source_to_related(options)
  strategy = relationship.options[:apply_join]

  if strategy
    records = call_method_or_proc(strategy, records, relationship, resource_type, join_type, options)
  else
    records = apply_join(records: records,
                         relationship: relationship,
                         resource_type: resource_type,
                         join_type: join_type,
                         options: options)
  end

  records
end

Protected Class Methods

alias_table_field(table, field, quoted = false) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 813
def alias_table_field(table, field, quoted = false)
  if table.blank? || field.to_s.include?('.')
    # :nocov:
    if quoted
      quote(field)
    else
      field.to_s
    end
    # :nocov:
  else
    if quoted
      # :nocov:
      quote("#{table.to_s}_#{field.to_s}")
      # :nocov:
    else
      "#{table.to_s}_#{field.to_s}"
    end
  end
end
apply_filter(records, filter, value, options = {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 864
def apply_filter(records, filter, value, options = {})
  strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]

  if strategy
    records = call_method_or_proc(strategy, records, value, options)
  else
    join_manager = options.dig(:_relation_helper_options, :join_manager)
    field = join_manager ? get_aliased_field(filter, join_manager) : filter
    records = records.where(Arel.sql(field) => value)
  end

  records
end
apply_filters(records, filters, options = {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 838
def apply_filters(records, filters, options = {})
  if filters
    filters.each do |filter, value|
      records = apply_filter(records, filter, value, options)
    end
  end

  records
end
apply_joins(records, join_manager, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 719
def apply_joins(records, join_manager, options)
  join_manager.join(records, options)
end
apply_pagination(records, paginator, order_options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 723
def apply_pagination(records, paginator, order_options)
  records = paginator.apply(records, order_options) if paginator
  records
end
apply_request_settings_to_records(records:, join_manager: ActiveRelation::JoinManager.new(resource_klass: self), resource_klass: self, filters: {}, primary_keys: nil, sort_criteria: nil, sort_primary: nil, paginator: nil, options: {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 683
def apply_request_settings_to_records(records:,
                                      join_manager: ActiveRelation::JoinManager.new(resource_klass: self),
                                      resource_klass: self,
                                      filters: {},
                                      primary_keys: nil,
                                      sort_criteria: nil,
                                      sort_primary: nil,
                                      paginator: nil,
                                      options: {})

  options[:_relation_helper_options] = { join_manager: join_manager, sort_fields: [] }

  records = resource_klass.apply_joins(records, join_manager, options)

  if primary_keys
    records = records.where(_primary_key => primary_keys)
  end

  unless filters.empty?
    records = resource_klass.filter_records(records, filters, options)
  end

  if sort_primary
    records = records.order(_primary_key => :asc)
  else
    order_options = resource_klass.construct_order_options(sort_criteria)
    records = resource_klass.sort_records(records, order_options, options)
  end

  if paginator
    records = resource_klass.apply_pagination(records, paginator, order_options)
  end

  records
end
apply_single_sort(records, field, direction, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 738
def apply_single_sort(records, field, direction, options)
  context = options[:context]

  strategy = _allowed_sort.fetch(field.to_sym, {})[:apply]

  options[:_relation_helper_options] ||= {}
  options[:_relation_helper_options][:sort_fields] ||= []

  if strategy
    records = call_method_or_proc(strategy, records, direction, context)
  else
    join_manager = options.dig(:_relation_helper_options, :join_manager)
    sort_field = join_manager ? get_aliased_field(field, join_manager) : field
    options[:_relation_helper_options][:sort_fields].push("#{sort_field}")
    records = records.order(Arel.sql("#{sort_field} #{direction}"))
  end
  records
end
apply_sort(records, order_options, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 728
def apply_sort(records, order_options, options)
  if order_options.any?
    order_options.each_pair do |field, direction|
      records = apply_single_sort(records, field, direction, options)
    end
  end

  records
end
concat_table_field(table, field, quoted = false) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 789
def concat_table_field(table, field, quoted = false)
  if table.blank? || field.to_s.include?('.')
    # :nocov:
    if quoted
      quote(field)
    else
      field.to_s
    end
    # :nocov:
  else
    if quoted
      "#{quote(table)}.#{quote(field)}"
    else
      # :nocov:
      "#{table.to_s}.#{field.to_s}"
      # :nocov:
    end
  end
end
construct_order_options(sort_params) click to toggle source
Calls superclass method
# File lib/jsonapi/active_relation_resource.rb, line 777
def construct_order_options(sort_params)
  if _polymorphic
    warn "Sorting is not supported on polymorphic relationships"
  else
    super(sort_params)
  end
end
count_records(records) click to toggle source

Assumes ActiveRecord's counting. Override if you need a different counting method

# File lib/jsonapi/active_relation_resource.rb, line 758
def count_records(records)
  if (Rails::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR >= 6
    records.count(:all)
  else
    records.count
  end
end
filter_records(records, filters, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 766
def filter_records(records, filters, options)
  if _polymorphic
    _polymorphic_resource_klasses.each do |klass|
      records = klass.apply_filters(records, filters, options)
    end
  else
    records = apply_filters(records, filters, options)
  end
  records
end
find_record_by_key(key, options = {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 368
def find_record_by_key(key, options = {})
  record = apply_request_settings_to_records(records: records(options), primary_keys: key, options: options).first
  fail JSONAPI::Exceptions::RecordNotFound.new(key) if record.nil?
  record
end
find_records_by_keys(keys, options = {}) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 374
def find_records_by_keys(keys, options = {})
  apply_request_settings_to_records(records: records(options), primary_keys: keys, options: options)
end
get_aliased_field(path_with_field, join_manager) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 848
def get_aliased_field(path_with_field, join_manager)
  path = JSONAPI::Path.new(resource_klass: self, path_string: path_with_field)

  relationship_segment = path.segments[-2]
  field_segment = path.segments[-1]

  if relationship_segment
    join_details = join_manager.join_details[path.last_relationship]
    table_alias = join_details[:alias]
  else
    table_alias = self._table_name
  end

  concat_table_field(table_alias, field_segment.delegated_field_name)
end
quote(field) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 833
def quote(field)
  "\"#{field.to_s}\""
end
sort_records(records, order_options, options) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 785
def sort_records(records, order_options, options)
  apply_sort(records, order_options, options)
end
sql_field_with_alias(table, field, quoted = true) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 809
def sql_field_with_alias(table, field, quoted = true)
  Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}")
end
to_one_relationships_for_linkage(include_related) click to toggle source
# File lib/jsonapi/active_relation_resource.rb, line 357
def to_one_relationships_for_linkage(include_related)
  include_related ||= {}
  relationships = []
  _relationships.each do |name, relationship|
    if relationship.is_a?(JSONAPI::Relationship::ToOne) && !include_related.has_key?(name) && relationship.include_optional_linkage_data?
      relationships << name
    end
  end
  relationships
end