Class: GraphQL::Execution::Lookahead

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql/execution/lookahead.rb

Overview

Lookahead creates a uniform interface to inspect the forthcoming selections.

It assumes that the AST it's working with is valid. (So, it's safe to use during execution, but if you're using it directly, be sure to validate first.)

A field may get access to its lookahead by adding extras: [:lookahead] to its configuration.

Examples:

looking ahead in a field

field :articles, [Types::Article], null: false,
  extras: [:lookahead]

# For example, imagine a faster database call
# may be issued when only some fields are requested.
#
# Imagine that _full_ fetch must be made to satisfy `fullContent`,
# we can look ahead to see if we need that field. If we do,
# we make the expensive database call instead of the cheap one.
def articles(lookahead:)
  if lookahead.selects?(:full_content)
    fetch_full_articles(object)
  else
    fetch_preview_articles(object)
  end
end

Direct Known Subclasses

NullLookahead

Defined Under Namespace

Classes: NullLookahead

Constant Summary collapse

NULL_LOOKAHEAD =

A singleton, so that misses don't come with overhead.

NullLookahead.new

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(query:, ast_nodes:, field: nil, root_type: nil, owner_type: nil) ⇒ Lookahead

Returns a new instance of Lookahead.

Parameters:



34
35
36
37
38
39
40
41
# File 'lib/graphql/execution/lookahead.rb', line 34

def initialize(query:, ast_nodes:, field: nil, root_type: nil, owner_type: nil)
  @ast_nodes = ast_nodes.freeze
  @field = field
  @root_type = root_type
  @query = query
  @selected_type = @field ? @field.type.unwrap : root_type
  @owner_type = owner_type
end

Instance Attribute Details

#ast_nodesArray<GraphQL::Language::Nodes::Field> (readonly)



44
45
46
# File 'lib/graphql/execution/lookahead.rb', line 44

def ast_nodes
  @ast_nodes
end

#fieldGraphQL::Schema::Field (readonly)



47
48
49
# File 'lib/graphql/execution/lookahead.rb', line 47

def field
  @field
end

#owner_typeGraphQL::Schema::Object, ... (readonly)



50
51
52
# File 'lib/graphql/execution/lookahead.rb', line 50

def owner_type
  @owner_type
end

Instance Method Details

#alias_selection(alias_name, selected_type: @selected_type, arguments: nil) ⇒ GraphQL::Execution::Lookahead

Like #selection, but for aliases. It returns a null object (check with #selected?)



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/graphql/execution/lookahead.rb', line 147

def alias_selection(alias_name, selected_type: @selected_type, arguments: nil)
  alias_cache_key = [alias_name, arguments]
  return alias_selections[key] if alias_selections.key?(alias_name)

  alias_node = lookup_alias_node(ast_nodes, alias_name)
  return NULL_LOOKAHEAD unless alias_node

  next_field_defn = @query.types.field(selected_type, alias_node.name)

  alias_arguments = @query.arguments_for(alias_node, next_field_defn)
  if alias_arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments)
    alias_arguments = alias_arguments.keyword_arguments
  end

  return NULL_LOOKAHEAD if arguments && arguments != alias_arguments

  alias_selections[alias_cache_key] = lookahead_for_selection(next_field_defn, selected_type, alias_arguments, alias_name)
end

#argumentsHash<Symbol, Object>

Returns:

  • (Hash<Symbol, Object>)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/graphql/execution/lookahead.rb', line 53

def arguments
  if defined?(@arguments)
    @arguments
  else
    @arguments = if @field
      @query.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args|
        case args
        when Execution::Interpreter::Arguments
          args.keyword_arguments
        when GraphQL::ExecutionError
          EmptyObjects::EMPTY_HASH
        else
          args
        end
      end
    else
      nil
    end
  end
end

#inspectObject



216
217
218
# File 'lib/graphql/execution/lookahead.rb', line 216

def inspect
  "#<GraphQL::Execution::Lookahead #{@field ? "@field=#{@field.path.inspect}": "@root_type=#{@root_type}"} @ast_nodes.size=#{@ast_nodes.size}>"
end

#nameSymbol

The method name of the field. It returns the method_sym of the Lookahead's field.

Examples:

getting the name of a selection

def articles(lookahead:)
  article.selection(:full_content).name # => :full_content
  # ...
end

Returns:

  • (Symbol)


212
213
214
# File 'lib/graphql/execution/lookahead.rb', line 212

def name
  @field && @field.original_name
end

#selected?Boolean

Returns True if this lookahead represents a field that was requested.

Returns:

  • (Boolean)

    True if this lookahead represents a field that was requested



107
108
109
# File 'lib/graphql/execution/lookahead.rb', line 107

def selected?
  true
end

#selection(field_name, selected_type: @selected_type, arguments: nil) ⇒ GraphQL::Execution::Lookahead

Like #selects?, but can be used for chaining. It returns a null object (check with #selected?)

Parameters:

  • field_name (String, Symbol)

Returns:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/graphql/execution/lookahead.rb', line 115

def selection(field_name, selected_type: @selected_type, arguments: nil)
  next_field_defn = case field_name
  when String
    @query.types.field(selected_type, field_name)
  when Symbol
    # Try to avoid the `.to_s` below, if possible
    all_fields = if selected_type.kind.fields?
      @query.types.fields(selected_type)
    else
      # Handle unions by checking possible
      @query.types
        .possible_types(selected_type)
        .map { |t| @query.types.fields(t) }
        .tap(&:flatten!)
    end


    if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name })
      match_by_orig_name
    else
      # Symbol#name is only present on 3.0+
      sym_s = field_name.respond_to?(:name) ? field_name.name : field_name.to_s
      guessed_name = Schema::Member::BuildType.camelize(sym_s)
      @query.types.field(selected_type, guessed_name)
    end
  end
  lookahead_for_selection(next_field_defn, selected_type, arguments)
end

#selections(arguments: nil) ⇒ Array<GraphQL::Execution::Lookahead>

Like #selection, but for all nodes. It returns a list of Lookaheads for all Selections

If arguments: is provided, each provided key/value will be matched against the arguments in each selection. This method will filter the selections if any of the given arguments: do not match the given selection.

Examples:

getting the name of a selection

def articles(lookahead:)
  next_lookaheads = lookahead.selections # => [#<GraphQL::Execution::Lookahead ...>, ...]
  next_lookaheads.map(&:name) #=> [:full_content, :title]
end

Parameters:

  • arguments (Hash) (defaults to: nil)

    Arguments which must match in the selection

Returns:



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/graphql/execution/lookahead.rb', line 181

def selections(arguments: nil)
  subselections_by_type = {}
  subselections_on_type = subselections_by_type[@selected_type] = {}

  @ast_nodes.each do |node|
    find_selections(subselections_by_type, subselections_on_type, @selected_type, node.selections, arguments)
  end

  subselections = []

  subselections_by_type.each do |type, ast_nodes_by_response_key|
    ast_nodes_by_response_key.each do |response_key, ast_nodes|
      field_defn = @query.types.field(type, ast_nodes.first.name)
      lookahead = Lookahead.new(query: @query, ast_nodes: ast_nodes, field: field_defn, owner_type: type)
      subselections.push(lookahead)
    end
  end

  subselections
end

#selects?(field_name, selected_type: @selected_type, arguments: nil) ⇒ Boolean

True if this node has a selection on field_name. If field_name is a String, it is treated as a GraphQL-style (camelized) field name and used verbatim. If field_name is a Symbol, it is treated as a Ruby-style (underscored) name and camelized before comparing.

If arguments: is provided, each provided key/value will be matched against the arguments in the next selection. This method will return false if any of the given arguments: are not present and matching in the next selection. (But, the next selection may contain more than the given arguments.)

Parameters:

  • field_name (String, Symbol)
  • arguments (Hash) (defaults to: nil)

    Arguments which must match in the selection

Returns:

  • (Boolean)


86
87
88
# File 'lib/graphql/execution/lookahead.rb', line 86

def selects?(field_name, selected_type: @selected_type, arguments: nil)
  selection(field_name, selected_type: selected_type, arguments: arguments).selected?
end

#selects_alias?(alias_name, arguments: nil) ⇒ Boolean

True if this node has a selection with alias matching alias_name. If alias_name is a String, it is treated as a GraphQL-style (camelized) field name and used verbatim. If alias_name is a Symbol, it is treated as a Ruby-style (underscored) name and camelized before comparing.

If arguments: is provided, each provided key/value will be matched against the arguments in the next selection. This method will return false if any of the given arguments: are not present and matching in the next selection. (But, the next selection may contain more than the given arguments.)

Parameters:

  • alias_name (String, Symbol)
  • arguments (Hash) (defaults to: nil)

    Arguments which must match in the selection

Returns:

  • (Boolean)


102
103
104
# File 'lib/graphql/execution/lookahead.rb', line 102

def selects_alias?(alias_name, arguments: nil)
  alias_selection(alias_name, arguments: arguments).selected?
end