class GraphQL::Execution::Lookahead
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.
@example 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
Constants
- NULL_LOOKAHEAD
A singleton, so that misses don’t come with overhead.
Attributes
@return [Array<GraphQL::Language::Nodes::Field>]
@return [GraphQL::Schema::Field]
@return [GraphQL::Schema::Object, GraphQL::Schema::Union
, GraphQL::Schema::Interface
]
Public Class Methods
@param query [GraphQL::Query] @param ast_nodes
[Array<GraphQL::Language::Nodes::Field>, Array<GraphQL::Language::Nodes::OperationDefinition>] @param field [GraphQL::Schema::Field] if ‘ast_nodes` are fields, this is the field definition matching those nodes @param root_type [Class] if `ast_nodes` are operation definition, this is the root type for that operation
# 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
Public Instance Methods
@return [Hash<Symbol, Object>]
# File lib/graphql/execution/lookahead.rb, line 53 def arguments if defined?(@arguments) @arguments else @arguments = if @field @query.schema.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args| args.is_a?(Execution::Interpreter::Arguments) ? args.keyword_arguments : args end else nil end end end
# File lib/graphql/execution/lookahead.rb, line 163 def inspect "#<GraphQL::Execution::Lookahead #{@field ? "@field=#{@field.path.inspect}": "@root_type=#{@root_type}"} @ast_nodes.size=#{@ast_nodes.size}>" end
The method name of the field. It returns the method_sym of the Lookahead’s field.
@example getting the name of a selection
def articles(lookahead:) article.selection(:full_content).name # => :full_content # ... end
@return [Symbol]
# File lib/graphql/execution/lookahead.rb, line 159 def name @field && @field.original_name end
@return [Boolean] True if this lookahead represents a field that was requested
# File lib/graphql/execution/lookahead.rb, line 84 def selected? true end
Like {#selects?}, but can be used for chaining. It returns a null object (check with {#selected?}) @return [GraphQL::Execution::Lookahead]
# File lib/graphql/execution/lookahead.rb, line 91 def selection(field_name, selected_type: @selected_type, arguments: nil) next_field_name = normalize_name(field_name) next_field_defn = @query.get_field(selected_type, next_field_name) if next_field_defn next_nodes = [] @ast_nodes.each do |ast_node| ast_node.selections.each do |selection| find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes) end end if next_nodes.any? Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type) else NULL_LOOKAHEAD end else NULL_LOOKAHEAD end end
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.
@example getting the name of a selection
def articles(lookahead:) next_lookaheads = lookahead.selections # => [#<GraphQL::Execution::Lookahead ...>, ...] next_lookaheads.map(&:name) #=> [:full_content, :title] end
@param arguments [Hash] Arguments which must match in the selection @return [Array<GraphQL::Execution::Lookahead>]
# File lib/graphql/execution/lookahead.rb, line 128 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.get_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
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.) @param field_name [String, Symbol] @param arguments [Hash] Arguments which must match in the selection @return [Boolean]
# File lib/graphql/execution/lookahead.rb, line 79 def selects?(field_name, arguments: nil) selection(field_name, arguments: arguments).selected? end
Private Instance Methods
# File lib/graphql/execution/lookahead.rb, line 290 def arguments_match?(arguments, field_defn, field_node) query_kwargs = @query.arguments_for(field_node, field_defn) arguments.all? do |arg_name, arg_value| arg_name = normalize_keyword(arg_name) # Make sure the constraint is present with a matching value query_kwargs.key?(arg_name) && query_kwargs[arg_name] == arg_value end end
If a selection on ‘node` matches `field_name` (which is backed by `field_defn`) and matches the `arguments:` constraints, then add that node to `matches`
# File lib/graphql/execution/lookahead.rb, line 268 def find_selected_nodes(node, field_name, field_defn, arguments:, matches:) return if skipped_by_directive?(node) case node when GraphQL::Language::Nodes::Field if node.name == field_name if arguments.nil? || arguments.empty? # No constraint applied matches << node elsif arguments_match?(arguments, field_defn, node) matches << node end end when GraphQL::Language::Nodes::InlineFragment node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) } when GraphQL::Language::Nodes::FragmentSpread frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) } else raise "Unexpected selection comparison on #{node.class.name} (#{node})" end end
# File lib/graphql/execution/lookahead.rb, line 228 def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments) ast_selections.each do |ast_selection| next if skipped_by_directive?(ast_selection) case ast_selection when GraphQL::Language::Nodes::Field response_key = ast_selection.alias || ast_selection.name if selections_on_type.key?(response_key) selections_on_type[response_key] << ast_selection elsif arguments.nil? || arguments.empty? selections_on_type[response_key] = [ast_selection] else field_defn = @query.get_field(selected_type, ast_selection.name) if arguments_match?(arguments, field_defn, ast_selection) selections_on_type[response_key] = [ast_selection] end end when GraphQL::Language::Nodes::InlineFragment on_type = selected_type subselections_on_type = selections_on_type if (t = ast_selection.type) # Assuming this is valid, that `t` will be found. on_type = @query.get_type(t.name) subselections_on_type = subselections_by_type[on_type] ||= {} end find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments) when GraphQL::Language::Nodes::FragmentSpread frag_defn = @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})") # Again, assuming a valid AST on_type = @query.get_type(frag_defn.type.name) subselections_on_type = subselections_by_type[on_type] ||= {} find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments) else raise "Invariant: Unexpected selection type: #{ast_selection.class}" end end end
# File lib/graphql/execution/lookahead.rb, line 208 def normalize_keyword(keyword) if keyword.is_a?(String) Schema::Member::BuildType.underscore(keyword).to_sym else keyword end end
If it’s a symbol, stringify and camelize it
# File lib/graphql/execution/lookahead.rb, line 200 def normalize_name(name) if name.is_a?(Symbol) Schema::Member::BuildType.camelize(name.to_s) else name end end
# File lib/graphql/execution/lookahead.rb, line 216 def skipped_by_directive?(ast_selection) ast_selection.directives.each do |directive| dir_defn = @query.schema.directives.fetch(directive.name) directive_class = dir_defn if directive_class dir_args = @query.arguments_for(directive, dir_defn) return true unless directive_class.static_include?(dir_args, @query.context) end end false end