class HQ::GraphQL::PaginatedAssociationLoader

Public Class Methods

for(*args, scope: nil, **kwargs) click to toggle source
Calls superclass method
# File lib/hq/graphql/paginated_association_loader.rb, line 8
def self.for(*args, scope: nil, **kwargs)
  if scope
    raise TypeError, "scope must be an ActiveRecord::Relation" unless scope.is_a?(::ActiveRecord::Relation)
    executor = ::GraphQL::Batch::Executor.current
    loader_key = loader_key_for(*args, **kwargs, scope: scope.to_sql)
    executor.loader(loader_key) { new(*args, **kwargs, scope: scope) }
  else
    super
  end
end
new(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil) click to toggle source
Calls superclass method
# File lib/hq/graphql/paginated_association_loader.rb, line 19
def initialize(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil)
  super()
  @model                = model
  @association_name     = association_name
  @internal_association = internal_association
  @limit                = [0, limit].max if limit
  @offset               = [0, offset].max if offset
  @scope                = scope
  @sort_by              = sort_by || :created_at
  @sort_order           = normalize_sort_order(sort_order)

  validate!
end

Public Instance Methods

cache_key(record) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 38
def cache_key(record)
  record.send(primary_key)
end
load(record) click to toggle source
Calls superclass method
# File lib/hq/graphql/paginated_association_loader.rb, line 33
def load(record)
  raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
  super
end
perform(records) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 42
def perform(records)
  values = records.map { |r| source_value(r) }
  scope  =
    if @limit || @offset
      # If a limit or offset is added, then we need to transform the query
      # into a lateral join so that we can limit on groups of data.
      #
      # > SELECT * FROM addresses WHERE addresses.user_id IN ($1, $2, ..., $N) ORDER BY addresses.created_at DESC;
      # ...becomes
      # > SELECT DISTINCT a_top.*
      # > FROM addresses
      # > INNER JOIN LATERAL (
      # >   SELECT inner.*
      # >   FROM addresses inner
      # >   WHERE inner.user_id = addresses.user_id
      # >   ORDER BY inner.created_at DESC
      # >   LIMIT 1
      # > ) a_top ON TRUE
      # > WHERE addresses.user_id IN ($1, $2, ..., $N)
      # > ORDER BY a_top.created_at DESC
      inner_table        = association_class.arel_table
      lateral_join_table = through_reflection? ? through_association.klass.arel_table : inner_table
      from_table         = lateral_join_table.alias("outer")

      inside_scope = default_scope.
        select(inner_table[::Arel.star]).
        where(lateral_join_table[target_join_key].eq(from_table[target_join_key])).
        reorder(arel_order(inner_table)).
        limit(@limit).
        offset(@offset)

      if through_reflection?
        # expose the through_reflection key
        inside_scope = inside_scope.select(lateral_join_table[target_join_key])
      end

      lateral_table = ::Arel::Table.new("top")
      association_class.
        select(lateral_table[::Arel.star]).distinct.
        from(from_table).
        where(from_table[target_join_key].in(values)).
        joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{lateral_table.name} ON TRUE").
        reorder(arel_order(lateral_table))
    else
      scope = default_scope.reorder(arel_order(association_class.arel_table))

      if through_reflection?
        scope.where(through_association.name => { target_join_key => values }).
          # expose the through_reflection key
          select(association_class.arel_table[::Arel.star], through_association.klass.arel_table[target_join_key])
      else
        scope.where(target_join_key => values)
      end
    end

  results = scope.to_a
  records.each do |record|
    fulfill(record, target_value(record, results)) unless fulfilled?(record)
  end
end

Private Instance Methods

arel_order(table) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 176
def arel_order(table)
  table[@sort_by].send(@sort_order)
end
association() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 160
def association
  if @internal_association
    Types[@model].reflect_on_association(@association_name)
  else
    @model.reflect_on_association(@association_name)
  end
end
association_class() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 168
def association_class
  association.klass
end
belongs_to?() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 144
def belongs_to?
  association.macro == :belongs_to
end
default_scope() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 128
def default_scope
  scope = association_class
  scope = association.scopes.reduce(scope, &:merge)
  scope = association_class.default_scopes.reduce(scope, &:merge)
  scope = scope.merge(@scope) if @scope

  if through_reflection?
    source = association_class.arel_table
    target = through_association.klass.arel_table
    join   = source.join(target).on(target[association.foreign_key].eq(source[source_join_key]))
    scope  = scope.joins(join.join_sources)
  end

  scope
end
has_many?() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 148
def has_many?
  association.macro == :has_many
end
normalize_sort_order(input) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 180
def normalize_sort_order(input)
  if input.to_s.casecmp("asc").zero?
    :asc
  else
    :desc
  end
end
primary_key() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 172
def primary_key
  @model.primary_key
end
source_join_key() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 105
def source_join_key
  belongs_to? ? association.foreign_key : association.association_primary_key
end
source_value(record) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 109
def source_value(record)
  record.send(source_join_key)
end
target_join_key() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 113
def target_join_key
  if through_reflection?
    through_association.foreign_key
  elsif belongs_to?
    association.association_primary_key
  else
    association.foreign_key
  end
end
target_value(record, results) click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 123
def target_value(record, results)
  enumerator = has_many? ? :select : :detect
  results.send(enumerator) { |r| r.send(target_join_key) == source_value(record) }
end
through_association() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 152
def through_association
  association.through_reflection
end
through_reflection?() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 156
def through_reflection?
  association.through_reflection?
end
validate!() click to toggle source
# File lib/hq/graphql/paginated_association_loader.rb, line 188
def validate!
  raise ArgumentError, "No association #{@association_name} on #{@model}" unless association
end