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

new(query:, ast_nodes:, field: nil, root_type: nil) click to toggle source

@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

name() click to toggle source

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
selected?() click to toggle source

@return [Boolean] True if this lookahead represents a field that was requested

# File lib/graphql/execution/lookahead.rb, line 62
def selected?
  true
end
selection(field_name, arguments: nil) click to toggle source

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
selections(arguments: nil) click to toggle source

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
selects?(field_name, arguments: nil) click to toggle source

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

find_selected_nodes(node, field_name, field_defn, arguments:, matches:) click to toggle source

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
normalize_keyword(keyword) click to toggle source
# 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
normalize_name(name) click to toggle source

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