lib/graphql/execution/lookahead.rb in graphql-1.9.0.pre1 vs lib/graphql/execution/lookahead.rb in graphql-1.9.0.pre2

- old
+ new

@@ -34,16 +34,22 @@ # @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 def initialize(query:, ast_nodes:, field: nil, root_type: nil) - @ast_nodes = ast_nodes + @ast_nodes = ast_nodes.freeze @field = field @root_type = root_type @query = query end + # @return [Array<GraphQL::Language::Nodes::Field>] + attr_reader :ast_nodes + + # @return [GraphQL::Schema::Field] + attr_reader :field + # 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. # @@ -92,10 +98,49 @@ 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>] + def selections(arguments: nil) + subselections_by_name = {} + @ast_nodes.each do |node| + find_selections(subselections_by_name, node.selections, arguments) + end + + # Items may be filtered out if `arguments` doesn't match + subselections_by_name.values.select(&:selected?) + 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] + def name + @field && @field.original_name + end + # This is returned for {Lookahead#selection} when a non-existent field is passed class NullLookahead < Lookahead # No inputs required here. def initialize end @@ -109,10 +154,14 @@ end def selection(*) NULL_LOOKAHEAD end + + def selections(*) + [] + end end # A singleton, so that misses don't come with overhead. NULL_LOOKAHEAD = NullLookahead.new @@ -133,10 +182,26 @@ else keyword end end + def find_selections(subselections_by_name, ast_selections, arguments) + ast_selections.each do |ast_selection| + case ast_selection + when GraphQL::Language::Nodes::Field + subselections_by_name[ast_selection.name] ||= selection(ast_selection.name, arguments: arguments) + when GraphQL::Language::Nodes::InlineFragment + find_selections(subselections_by_name, 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})") + find_selections(subselections_by_name, frag_defn.selections, arguments) + else + raise "Invariant: Unexpected selection type: #{ast_selection.class}" + end + 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` def find_selected_nodes(node, field_name, field_defn, arguments:, matches:) case node when GraphQL::Language::Nodes::Field @@ -155,14 +220,14 @@ 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) } + 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] - frag_defn.selections.find { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) } + 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 @@ -257,17 +322,17 @@ # TODO dedup with interpreter module FieldHelpers module_function - def get_field(schema, owner_type, field_name ) + def get_field(schema, owner_type, field_name) field_defn = owner_type.get_field(field_name) field_defn ||= if owner_type == schema.query.metadata[:type_class] && (entry_point_field = schema.introspection_system.entry_point(name: field_name)) entry_point_field.metadata[:type_class] elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name)) dynamic_field.metadata[:type_class] else - raise "Invariant: no field for #{owner_type}.#{field_name}" + nil end field_defn end end