lib/graphql/execution/lookahead.rb in graphql-2.1.3 vs lib/graphql/execution/lookahead.rb in graphql-2.1.4

- old
+ new

@@ -78,10 +78,26 @@ # @return [Boolean] def selects?(field_name, selected_type: @selected_type, arguments: nil) selection(field_name, selected_type: selected_type, arguments: arguments).selected? end + # 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.) + # @param alias_name [String, Symbol] + # @param arguments [Hash] Arguments which must match in the selection + # @return [Boolean] + def selects_alias?(alias_name, arguments: nil) + alias_selection(alias_name, arguments: arguments).selected? + end + # @return [Boolean] True if this lookahead represents a field that was requested def selected? true end @@ -103,36 +119,43 @@ .possible_types(selected_type) .map { |t| @query.warden.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.get_field(selected_type, guessed_name) end end + lookahead_for_selection(next_field_defn, selected_type, arguments) + end - if next_field_defn - next_nodes = [] - @ast_nodes.each do |ast_node| - ast_node.selections.each do |selection| - find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes) - end - end + # Like {#selection}, but for aliases. + # It returns a null object (check with {#selected?}) + # @return [GraphQL::Execution::Lookahead] + 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) - 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 + alias_node = lookup_alias_node(ast_nodes, alias_name) + return NULL_LOOKAHEAD unless alias_node + + next_field_defn = @query.get_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 # Like {#selection}, but for all nodes. # It returns a list of Lookaheads for all Selections # @@ -256,11 +279,11 @@ 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})") + frag_defn = lookup_fragment(ast_selection) # 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 @@ -269,27 +292,27 @@ 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_defn, arguments:, matches:) + def find_selected_nodes(node, field_name, field_defn, arguments:, matches:, alias_name: NOT_CONFIGURED) return if skipped_by_directive?(node) case node when GraphQL::Language::Nodes::Field - if node.name == field_defn.graphql_name + if node.name == field_name && (NOT_CONFIGURED.equal?(alias_name) || node.alias == alias_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_defn, arguments: arguments, matches: matches) } + node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) } 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_defn, arguments: arguments, matches: matches) } + frag_defn = lookup_fragment(node) + frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) } else raise "Unexpected selection comparison on #{node.class.name} (#{node})" end end @@ -303,9 +326,53 @@ end # Make sure the constraint is present with a matching value query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value end + end + + def lookahead_for_selection(field_defn, selected_type, arguments, alias_name = NOT_CONFIGURED) + return NULL_LOOKAHEAD unless field_defn + + next_nodes = [] + field_name = field_defn.name + @ast_nodes.each do |ast_node| + ast_node.selections.each do |selection| + find_selected_nodes(selection, field_name, field_defn, arguments: arguments, matches: next_nodes, alias_name: alias_name) + end + end + + return NULL_LOOKAHEAD if next_nodes.empty? + + Lookahead.new(query: @query, ast_nodes: next_nodes, field: field_defn, owner_type: selected_type) + end + + def alias_selections + return @alias_selections if defined?(@alias_selections) + @alias_selections ||= {} + end + + def lookup_alias_node(nodes, name) + return if nodes.empty? + + nodes.flat_map(&:children) + .flat_map { |child| unwrap_fragments(child) } + .find { |child| child.is_a?(GraphQL::Language::Nodes::Field) && child.alias == name } + end + + def unwrap_fragments(node) + case node + when GraphQL::Language::Nodes::InlineFragment + node.children + when GraphQL::Language::Nodes::FragmentSpread + lookup_fragment(node).children + else + [node] + end + end + + def lookup_fragment(ast_selection) + @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})") end end end end