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.
__NOTE__: Lookahead
for typed fragments (eg `node { … on Thing { … } }`) hasn't been implemented yet. It's possible, I just didn't need it yet. Feel free to open a PR or an issue if you want to add it.
@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.
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 38 def initialize(query:, ast_nodes:, field: nil, root_type: nil) @ast_nodes = ast_nodes @field = field @root_type = root_type @query = query end
Public Instance Methods
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 134 def name return unless @field.respond_to?(:original_name) @field.original_name end
@return [Boolean] True if this lookahead represents a field that was requested
# File lib/graphql/execution/lookahead.rb, line 62 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 69 def selection(field_name, arguments: nil) next_field_name = normalize_name(field_name) next_field_owner = if @field @field.type.unwrap else @root_type end next_field_defn = FieldHelpers.get_field(@query.schema, next_field_owner, 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) 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 112 def selections(arguments: nil) subselections_by_name = {} @ast_nodes.each do |node| node.selections.each do |subselection| subselections_by_name[subselection.name] ||= selection(subselection.name, arguments: arguments) end end # Items may be filtered out if `arguments` doesn't match subselections_by_name.values.select(&:selected?) 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 57 def selects?(field_name, arguments: nil) selection(field_name, arguments: arguments).selected? end
Private Instance Methods
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 187 def find_selected_nodes(node, field_name, field_defn, arguments:, matches:) case node when GraphQL::Language::Nodes::Field if node.name == field_name if arguments.nil? || arguments.none? # No constraint applied matches << node else query_kwargs = ArgumentHelpers.arguments(@query, nil, field_defn, node) passes_args = 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 if passes_args matches << node end end end when GraphQL::Language::Nodes::InlineFragment node.selections.find { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) } when GraphQL::Language::Nodes::FragmentSpread frag_defn = @query.fragments[node.name] frag_defn.selections.find { |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 177 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 169 def normalize_name(name) if name.is_a?(Symbol) Schema::Member::BuildType.camelize(name.to_s) else name end end