Class: GraphqlDeclarative::Preloader

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql_declarative/preloader.rb

Overview

Builds the preload list from the query's actual selection set, so adding a field to a query never reintroduces an N+1 and no .includes list has to be kept in sync by hand.

Walk lookahead for selections whose field maps to an association on the model, recursing to build a nested preload hash:

{author: [:profile], enrollments: []}

Ignore selections that are plain columns, and connection wrapper fields (edges/node) must be unwrapped before matching.

Constant Summary collapse

CONNECTION_FIELDS =

Connection plumbing. These are structural, not associations: the entity selections of courses { edges { node { author { ... } } } } live two levels below courses. Unwrap them BEFORE matching names against associations, otherwise every connection field looks like a leaf column and nothing is ever preloaded.

An association is checked for first, so a model that genuinely has an association called nodes still wins over the unwrap rule.

%i[edges node nodes].freeze
DEFAULT_MAX_DEPTH =

A deeply nested query would otherwise let a client dictate the size of the preload tree: a { b { c { d { ... } } } } is cheap to write and expensive to serve. Three levels of associations covers the real cases; anything below that is simply not preloaded (it still resolves, just lazily).

3

Class Method Summary collapse

Class Method Details

.from_lookahead(lookahead, model, max_depth: DEFAULT_MAX_DEPTH, static: nil) ⇒ Hash

Returns suitable for .preload, e.g. [:profile].

Parameters:

  • lookahead (GraphQL::Execution::Lookahead)

    the field's lookahead

  • model (Class)

    the ActiveRecord model the scope selects

  • max_depth (Integer) (defaults to: DEFAULT_MAX_DEPTH)

    association nesting levels to descend

  • static (Symbol, Array, Hash, nil) (defaults to: nil)

    declared preloads to merge in

Returns:

  • (Hash)

    suitable for .preload, e.g. [:profile]



38
39
40
41
42
43
44
45
46
# File 'lib/graphql_declarative/preloader.rb', line 38

def from_lookahead(lookahead, model, max_depth: DEFAULT_MAX_DEPTH, static: nil)
  derived = if lookahead.respond_to?(:selections) && model.respond_to?(:reflect_on_association)
    walk(lookahead, model, Integer(max_depth))
  else
    {}
  end

  denormalize(deep_merge(normalize(static), derived))
end

.merge(static, derived) ⇒ Hash

Merge declared (preload author: :profile) preloads with derived ones.

Both sides are normalized to a canonical tree and unioned, so a key present in both keeps everything the static declaration asked for and gains whatever the query additionally selected underneath it. Static is authoritative in the sense that nothing it declares can be dropped or flattened by the derived tree -- which is the only "conflict" that can arise between two preload trees. Preloading a little extra is harmless; preloading too little is an N+1.

Returns:

  • (Hash)

    suitable for .preload



59
60
61
# File 'lib/graphql_declarative/preloader.rb', line 59

def merge(static, derived)
  denormalize(deep_merge(normalize(static), normalize(derived)))
end