Class: HQ::GraphQL::PaginatedAssociationLoader

Inherits:
GraphQL::Batch::Loader
  • Object
show all
Defined in:
lib/hq/graphql/paginated_association_loader.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil) ⇒ PaginatedAssociationLoader

Returns a new instance of PaginatedAssociationLoader.



19
20
21
22
23
24
25
26
27
28
29
30
31
# 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

Class Method Details

.for(*args, scope: nil, **kwargs) ⇒ Object



8
9
10
11
12
13
14
15
16
17
# 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

Instance Method Details

#cache_key(record) ⇒ Object



38
39
40
# File 'lib/hq/graphql/paginated_association_loader.rb', line 38

def cache_key(record)
  record.send(primary_key)
end

#load(record) ⇒ Object

Raises:

  • (TypeError)


33
34
35
36
# 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) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 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