lib/graphql/internal_representation/rewrite.rb
# frozen_string_literal: true module GraphQL module InternalRepresentation # While visiting an AST, build a normalized, flattened tree of {InternalRepresentation::Node}s. # # No unions or interfaces are present in this tree, only object types. # # Selections from the AST are attached to the object types they apply to. # # Inline fragments and fragment spreads are preserved in {InternalRepresentation::Node#ast_spreads}, # where they can be used to check for the presence of directives. This might not be sufficient # for future directives, since the selections' grouping is lost. # # The rewritten query tree serves as the basis for the `FieldsWillMerge` validation. # module Rewrite include GraphQL::Language NO_DIRECTIVES = [].freeze # @return InternalRepresentation::Document attr_reader :rewrite_document def initialize(*) super @query = context.query @rewrite_document = InternalRepresentation::Document.new # Hash<Nodes::FragmentSpread => Set<InternalRepresentation::Node>> # A record of fragment spreads and the irep nodes that used them @rewrite_spread_parents = Hash.new { |h, k| h[k] = Set.new } # Hash<Nodes::FragmentSpread => Scope> @rewrite_spread_scopes = {} # Array<Set<InternalRepresentation::Node>> # The current point of the irep_tree during visitation @rewrite_nodes_stack = [] # Array<Scope> @rewrite_scopes_stack = [] @rewrite_skip_nodes = Set.new # Resolve fragment spreads. # Fragment definitions got their own irep trees during visitation. # Those nodes are spliced in verbatim (not copied), but this is OK # because fragments are resolved from the "bottom up", each fragment # can be shared between its usages. context.on_dependency_resolve do |defn_ast_node, spread_ast_nodes, frag_ast_node| frag_name = frag_ast_node.name fragment_node = @rewrite_document.fragment_definitions[frag_name] if fragment_node spread_ast_nodes.each do |spread_ast_node| parent_nodes = @rewrite_spread_parents[spread_ast_node] parent_scope = @rewrite_spread_scopes[spread_ast_node] parent_nodes.each do |parent_node| parent_node.deep_merge_node(fragment_node, scope: parent_scope, merge_self: false) end end end end end # @return [Hash<String, Node>] Roots of this query def operations warn "#{self.class}#operations is deprecated; use `document.operation_definitions` instead" @document.operation_definitions end def on_operation_definition(ast_node, parent) push_root_node(ast_node, @rewrite_document.operation_definitions) { super } end def on_fragment_definition(ast_node, parent) push_root_node(ast_node, @rewrite_document.fragment_definitions) { super } end def push_root_node(ast_node, definitions) # Either QueryType or the fragment type condition owner_type = context.type_definition defn_name = ast_node.name node = Node.new( parent: nil, name: defn_name, owner_type: owner_type, query: @query, ast_nodes: [ast_node], return_type: owner_type, ) definitions[defn_name] = node @rewrite_scopes_stack.push(Scope.new(@query, owner_type)) @rewrite_nodes_stack.push([node]) yield @rewrite_nodes_stack.pop @rewrite_scopes_stack.pop end def on_inline_fragment(node, parent) # Inline fragments provide two things to the rewritten tree: # - They _may_ narrow the scope by their type condition # - They _may_ apply their directives to their children if skip?(node) @rewrite_skip_nodes.add(node) end if @rewrite_skip_nodes.empty? @rewrite_scopes_stack.push(@rewrite_scopes_stack.last.enter(context.type_definition)) end super if @rewrite_skip_nodes.empty? @rewrite_scopes_stack.pop end if @rewrite_skip_nodes.include?(node) @rewrite_skip_nodes.delete(node) end end def on_field(ast_node, ast_parent) if skip?(ast_node) @rewrite_skip_nodes.add(ast_node) end if @rewrite_skip_nodes.empty? node_name = ast_node.alias || ast_node.name parent_nodes = @rewrite_nodes_stack.last next_nodes = [] field_defn = context.field_definition if field_defn.nil? # It's a non-existent field new_scope = nil else field_return_type = field_defn.type @rewrite_scopes_stack.last.each do |scope_type| parent_nodes.each do |parent_node| node = parent_node.scoped_children[scope_type][node_name] ||= Node.new( parent: parent_node, name: node_name, owner_type: scope_type, query: @query, return_type: field_return_type, ) node.ast_nodes << ast_node node.definitions << field_defn next_nodes << node end end new_scope = Scope.new(@query, field_return_type.unwrap) end @rewrite_nodes_stack.push(next_nodes) @rewrite_scopes_stack.push(new_scope) end super if @rewrite_skip_nodes.empty? @rewrite_nodes_stack.pop @rewrite_scopes_stack.pop end if @rewrite_skip_nodes.include?(ast_node) @rewrite_skip_nodes.delete(ast_node) end end def on_fragment_spread(ast_node, ast_parent) if @rewrite_skip_nodes.empty? && !skip?(ast_node) # Register the irep nodes that depend on this AST node: @rewrite_spread_parents[ast_node].merge(@rewrite_nodes_stack.last) @rewrite_spread_scopes[ast_node] = @rewrite_scopes_stack.last end super end def skip?(ast_node) dir = ast_node.directives dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, @query) end end end end