# 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. # class Rewrite include GraphQL::Language NO_DIRECTIVES = [].freeze # @return InternalRepresentation::Document attr_reader :document def initialize @document = InternalRepresentation::Document.new end # @return [Hash] Roots of this query def operations warn "#{self.class}#operations is deprecated; use `document.operation_definitions` instead" document.operation_definitions end def validate(context) visitor = context.visitor query = context.query # Hash Set> # A record of fragment spreads and the irep nodes that used them spread_parents = Hash.new { |h, k| h[k] = Set.new } # Hash Scope> spread_scopes = {} # Array> # The current point of the irep_tree during visitation nodes_stack = [] # Array scopes_stack = [] skip_nodes = Set.new visit_op = VisitDefinition.new(context, @document.operation_definitions, nodes_stack, scopes_stack) visitor[Nodes::OperationDefinition].enter << visit_op.method(:enter) visitor[Nodes::OperationDefinition].leave << visit_op.method(:leave) visit_frag = VisitDefinition.new(context, @document.fragment_definitions, nodes_stack, scopes_stack) visitor[Nodes::FragmentDefinition].enter << visit_frag.method(:enter) visitor[Nodes::FragmentDefinition].leave << visit_frag.method(:leave) visitor[Nodes::InlineFragment].enter << ->(ast_node, ast_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?(ast_node, query) skip_nodes.add(ast_node) end if skip_nodes.none? scopes_stack.push(scopes_stack.last.enter(context.type_definition)) end } visitor[Nodes::InlineFragment].leave << ->(ast_node, ast_parent) { if skip_nodes.none? scopes_stack.pop end if skip_nodes.include?(ast_node) skip_nodes.delete(ast_node) end } visitor[Nodes::Field].enter << ->(ast_node, ast_parent) { if skip?(ast_node, query) skip_nodes.add(ast_node) end if skip_nodes.none? node_name = ast_node.alias || ast_node.name parent_nodes = 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 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 nodes_stack.push(next_nodes) scopes_stack.push(new_scope) end } visitor[Nodes::Field].leave << ->(ast_node, ast_parent) { if skip_nodes.none? nodes_stack.pop scopes_stack.pop end if skip_nodes.include?(ast_node) skip_nodes.delete(ast_node) end } visitor[Nodes::FragmentSpread].enter << ->(ast_node, ast_parent) { if skip_nodes.none? && !skip?(ast_node, query) # Register the irep nodes that depend on this AST node: spread_parents[ast_node].merge(nodes_stack.last) spread_scopes[ast_node] = scopes_stack.last end } # 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 = @document.fragment_definitions[frag_name] if fragment_node spread_ast_nodes.each do |spread_ast_node| parent_nodes = spread_parents[spread_ast_node] parent_scope = 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 def skip?(ast_node, query) dir = ast_node.directives dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, query) end class VisitDefinition def initialize(context, definitions, nodes_stack, scopes_stack) @context = context @query = context.query @definitions = definitions @nodes_stack = nodes_stack @scopes_stack = scopes_stack end def enter(ast_node, ast_parent) # Either QueryType or the fragment type condition owner_type = @context.type_definition && @context.type_definition.unwrap 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: @context.type_definition, ) @definitions[defn_name] = node @scopes_stack.push(Scope.new(@query, owner_type)) @nodes_stack.push([node]) end def leave(ast_node, ast_parent) @nodes_stack.pop @scopes_stack.pop end end end end end