Module: GraphWeaver::Selection

Includes:
Kernel
Included in:
Codegen, Testing::Anonymizer, Testing::FakeClient
Defined in:
lib/graph_weaver/selection.rb

Overview

Shared query-selection walking — the rules Codegen, FakeClient, and the cassette Anonymizer all follow, in one place so they can't drift: how fragments flatten into selections, and when a type condition applies. Hosts set @schema and call load_operation before walking.

Instance Method Summary collapse

Instance Method Details

#applies?(condition, type) ⇒ Boolean

A fragment's type condition applies when it names this type exactly, or an interface/union this type belongs to (... on Named { ... }).

Returns:

  • (Boolean)


74
75
76
77
78
79
80
81
82
83
# File 'lib/graph_weaver/selection.rb', line 74

def applies?(condition, type)
  return true if condition.nil? || condition == type.graphql_name

  condition_type = @schema.get_type(condition)
  return false unless condition_type

  kind = condition_type.kind.name
  (kind == "INTERFACE" || kind == "UNION") &&
    @schema.possible_types(condition_type).include?(type)
end

#each_field(type, selections, visiting = Set.new, &block) ⇒ Object

Flatten a selection set as seen by type, yielding (result_key, field_node) per field: plain fields yield directly; inline fragments and named spreads recurse when their type condition applies.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/graph_weaver/selection.rb', line 37

def each_field(type, selections, visiting = Set.new, &block)
  selections.each do |selection|
    case selection
    when GraphQL::Language::Nodes::Field
      yield(selection.alias || selection.name, selection)
    when GraphQL::Language::Nodes::InlineFragment
      each_field(type, selection.selections, visiting, &block) if applies?(selection.type&.name, type)
    when GraphQL::Language::Nodes::FragmentSpread
      fragment = @fragments.fetch(selection.name) do
        raise ArgumentError, "unknown fragment: #{selection.name}"
      end
      if visiting.include?(selection.name)
        raise GraphWeaver::Error, "fragment cycle through #{selection.name}"
      end

      if applies?(fragment.type.name, type)
        each_field(type, fragment.selections, visiting | [selection.name], &block)
      end
    else
      raise GraphWeaver::Error, "unsupported selection: #{selection.class}"
    end
  end
end

#gather(type, selections) ⇒ Object

each_field grouped by result key: repeated selections of one field (a { x } a { y }, or the same field reached through two fragments) collect together, so callers MERGE their sub-selections rather than last-writer-wins. Codegen relies on this; FakeClient/Anonymizer must too, or they'd fabricate/keep a shape the generated struct can't cast.



66
67
68
69
70
# File 'lib/graph_weaver/selection.rb', line 66

def gather(type, selections)
  out = {}
  each_field(type, selections) { |key, node| (out[key] ||= []) << node }
  out
end

#load_operation(query) ⇒ Object

Parse a query, stash its fragment definitions for the walk, and return the operation.



16
17
18
19
20
21
22
23
# File 'lib/graph_weaver/selection.rb', line 16

def load_operation(query)
  doc = GraphQL.parse(query)
  @fragments = doc.definitions
    .grep(GraphQL::Language::Nodes::FragmentDefinition)
    .to_h { |fragment| [fragment.name, fragment] }

  doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).first
end

#operation_root_type(operation) ⇒ Object

The schema type an operation's selections start from.



26
27
28
29
30
31
32
# File 'lib/graph_weaver/selection.rb', line 26

def operation_root_type(operation)
  case operation&.operation_type
  when "query", nil then @schema.query
  when "mutation" then @schema.mutation
  else raise GraphWeaver::Error, "unsupported operation: #{operation.operation_type}"
  end
end