graphql-stitching 0.3.3 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b07a54288795f5b321029340a61e97dd9209d395712dc9ac5921ebd5c52523a1
4
- data.tar.gz: 83e4dea46c7b4ff7a3716c2936773893df58e732a50c36ee1a5af8e4e5cab4bd
3
+ metadata.gz: d3cb133db35cdb705f296a2fe67909bfe2f07baefc873cf502ba48dbd53e8c6a
4
+ data.tar.gz: c81040a57f364a1a8a045bf079428cfae3c8a5886d2f7d0aaae45844c055b92e
5
5
  SHA512:
6
- metadata.gz: 831a54675315529b6bc1d489296a15325f88bcccbbb8f480c786641410de853989425591f4e60a6249ef123a3aa9e204780388925f2a027d758ac5594ca6c306
7
- data.tar.gz: f66ab6dfc996aa9982f6c5d09e2bf98b5a5de8b85220ea839f854e53edd3f55144404b5ddff91f2681f7d8fee21071bb54f20265951424840c527b7203dd9ace
6
+ metadata.gz: dc2a8d99942c2a5e1558ded0fbb467dd96d4bdb30c5a2ff9aa2348161ae28073cf36b2b56e51b0d92b757f68a87366ab1750e716bf0939d1645bf1ddee2049f5
7
+ data.tar.gz: 599760134f3f59f6ec5285e0d0976e1e9c07ba4103a74341bb35d1dacb89d0026383da2f4c2ca47d33ce3971510d9dddcc61d90a5248342406a4787f650d6126
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## GraphQL Stitching for Ruby
2
2
 
3
- GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly delegates portions of incoming requests to their respective service locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
3
+ GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
4
4
 
5
5
  ![Stitched graph](./docs/images/stitching.png)
6
6
 
@@ -14,7 +14,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
14
14
  - Computed fields (ie: federation-style `@requires`).
15
15
  - Subscriptions, defer/stream.
16
16
 
17
- This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. While Ruby is not the fastest language for a high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local schema onto a remote schema (making itself a superset of the remote) without requiring an additional gateway service.
17
+ This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. While Ruby is not the fastest language for a purely high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language.
18
18
 
19
19
  ## Getting started
20
20
 
@@ -32,16 +32,16 @@ module GraphQL
32
32
 
33
33
  # only one boundary allowed per type/location/key
34
34
  boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
35
- if memo.dig(boundary["location"], boundary["selection"])
36
- raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["selection"]}` "\
35
+ if memo.dig(boundary["location"], boundary["key"])
36
+ raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["key"]}` "\
37
37
  "found in #{boundary["location"]}. Limit one boundary query per type and key in each location. "\
38
38
  "Abstract boundaries provide all possible types."
39
39
  end
40
40
  memo[boundary["location"]] ||= {}
41
- memo[boundary["location"]][boundary["selection"]] = boundary
41
+ memo[boundary["location"]][boundary["key"]] = boundary
42
42
  end
43
43
 
44
- boundary_keys = boundaries.map { _1["selection"] }.uniq
44
+ boundary_keys = boundaries.map { _1["key"] }.uniq
45
45
  key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
46
46
  subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
47
47
  end
@@ -56,8 +56,9 @@ module GraphQL
56
56
  raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
57
57
  raise ComposerError, "The subscription operation is not supported." if schema.subscription
58
58
 
59
+ introspection_types = schema.introspection_system.types.keys
59
60
  schema.types.each do |type_name, type_candidate|
60
- next if Supergraph::INTROSPECTION_TYPES.include?(type_name)
61
+ next if introspection_types.include?(type_name)
61
62
 
62
63
  if type_name == @query_name && type_candidate != schema.query
63
64
  raise ComposerError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
@@ -491,7 +492,7 @@ module GraphQL
491
492
  @boundary_map[boundary_type_name] ||= []
492
493
  @boundary_map[boundary_type_name] << {
493
494
  "location" => location,
494
- "selection" => key_selections[0].name,
495
+ "key" => key_selections[0].name,
495
496
  "field" => field_candidate.name,
496
497
  "arg" => argument_name,
497
498
  "list" => boundary_structure.first[:list],
@@ -538,8 +539,9 @@ module GraphQL
538
539
  writes = []
539
540
 
540
541
  schemas.each do |schema|
542
+ introspection_types = schema.introspection_system.types.keys
541
543
  schema.types.values.each do |type|
542
- next if Supergraph::INTROSPECTION_TYPES.include?(type.graphql_name)
544
+ next if introspection_types.include?(type.graphql_name)
543
545
 
544
546
  if type.kind.object? || type.kind.interface?
545
547
  type.fields.values.each do |field|
@@ -26,7 +26,7 @@ module GraphQL
26
26
  @executor.errors.concat(result["errors"])
27
27
  end
28
28
 
29
- ops.map { op["key"] }
29
+ ops.map { op["order"] }
30
30
  end
31
31
 
32
32
  # Builds root source documents
@@ -36,12 +36,12 @@ module GraphQL
36
36
  doc << op["operation_type"]
37
37
 
38
38
  if operation_name
39
- doc << " " << operation_name << "_" << op["key"].to_s
39
+ doc << " #{operation_name}_#{op["order"]}"
40
40
  end
41
41
 
42
42
  if op["variables"].any?
43
43
  variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
44
- doc << "(" << variable_defs << ")"
44
+ doc << "(#{variable_defs})"
45
45
  end
46
46
 
47
47
  doc << op["selections"]
@@ -57,13 +57,13 @@ module GraphQL
57
57
 
58
58
  def fetch(ops)
59
59
  origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
60
- origin_set = op["insertion_path"].reduce([@executor.data]) do |set, path_segment|
60
+ origin_set = op["path"].reduce([@executor.data]) do |set, path_segment|
61
61
  set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
62
62
  end
63
63
 
64
- if op["type_condition"]
64
+ if op["if_type"]
65
65
  # operations planned around unused fragment conditions should not trigger requests
66
- origin_set.select! { _1["_STITCH_typename"] == op["type_condition"] }
66
+ origin_set.select! { _1["_STITCH_typename"] == op["if_type"] }
67
67
  end
68
68
 
69
69
  memo[op] = origin_set if origin_set.any?
@@ -81,7 +81,7 @@ module GraphQL
81
81
  @executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
82
82
  end
83
83
 
84
- ops.map { origin_sets_by_operation[_1] ? _1["key"] : nil }
84
+ ops.map { origin_sets_by_operation[_1] ? _1["order"] : nil }
85
85
  end
86
86
 
87
87
  # Builds batched boundary queries
@@ -96,7 +96,7 @@ module GraphQL
96
96
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
97
97
  variable_defs.merge!(op["variables"])
98
98
  boundary = op["boundary"]
99
- key_selection = "_STITCH_#{boundary["selection"]}"
99
+ key_selection = "_STITCH_#{boundary["key"]}"
100
100
 
101
101
  if boundary["list"]
102
102
  input = JSON.generate(origin_set.map { _1[key_selection] })
@@ -113,18 +113,18 @@ module GraphQL
113
113
  doc << "query" # << boundary fulfillment always uses query
114
114
 
115
115
  if operation_name
116
- doc << " " << operation_name
116
+ doc << " #{operation_name}"
117
117
  origin_sets_by_operation.each_key do |op|
118
- doc << "_" << op["key"].to_s
118
+ doc << "_#{op["order"]}"
119
119
  end
120
120
  end
121
121
 
122
122
  if variable_defs.any?
123
123
  variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
124
- doc << "(" << variable_str << ")"
124
+ doc << "(#{variable_str})"
125
125
  end
126
126
 
127
- doc << "{ " << query_fields.join(" ") << " }"
127
+ doc << "{ #{query_fields.join(" ")} }"
128
128
 
129
129
  return doc, variable_defs.keys
130
130
  end
@@ -183,7 +183,7 @@ module GraphQL
183
183
 
184
184
  if pathed_errors_by_op_index_and_object_id.any?
185
185
  pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
186
- repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "insertion_path"))
186
+ repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
187
187
  errors_result.concat(pathed_errors_by_object_id.values)
188
188
  end
189
189
  end
@@ -251,7 +251,7 @@ module GraphQL
251
251
 
252
252
  if @data && @data.length > 0
253
253
  result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
254
- schema: @supergraph.schema,
254
+ supergraph: @supergraph,
255
255
  request: @request,
256
256
  ).perform!(@data)
257
257
  end
@@ -265,7 +265,7 @@ module GraphQL
265
265
 
266
266
  private
267
267
 
268
- def exec!(after_keys = [0])
268
+ def exec!(next_ordinals = [0])
269
269
  if @exec_cycles > @queue.length
270
270
  # sanity check... if we've exceeded queue size, then something went wrong.
271
271
  raise StitchingError, "Too many execution requests attempted."
@@ -273,7 +273,7 @@ module GraphQL
273
273
 
274
274
  @dataloader.append_job do
275
275
  tasks = @queue
276
- .select { after_keys.include?(_1["after_key"]) }
276
+ .select { next_ordinals.include?(_1["after"]) }
277
277
  .group_by { [_1["location"], _1["boundary"].nil?] }
278
278
  .map do |(location, root_source), ops|
279
279
  if root_source
@@ -291,9 +291,8 @@ module GraphQL
291
291
  end
292
292
 
293
293
  def exec_task(task)
294
- next_keys = task.load
295
- next_keys.compact!
296
- exec!(next_keys) if next_keys.any?
294
+ next_ordinals = task.load.tap(&:compact!)
295
+ exec!(next_ordinals) if next_ordinals.any?
297
296
  end
298
297
  end
299
298
  end
@@ -5,22 +5,23 @@ module GraphQL
5
5
  class Planner
6
6
  SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
7
7
  TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
8
+ ROOT_ORDER = 0
8
9
 
9
10
  def initialize(supergraph:, request:)
10
11
  @supergraph = supergraph
11
12
  @request = request
12
- @sequence_key = 0
13
- @operations_by_grouping = {}
13
+ @planning_order = ROOT_ORDER
14
+ @operations_by_entrypoint = {}
14
15
  end
15
16
 
16
17
  def perform
17
- build_root_operations
18
+ build_root_entrypoints
18
19
  expand_abstract_boundaries
19
20
  self
20
21
  end
21
22
 
22
23
  def operations
23
- @operations_by_grouping.values.sort_by!(&:key)
24
+ @operations_by_entrypoint.values.sort_by!(&:order)
24
25
  end
25
26
 
26
27
  def to_h
@@ -29,45 +30,137 @@ module GraphQL
29
30
 
30
31
  private
31
32
 
32
- # groups root fields by operational strategy:
33
- # - query immedaitely groups all root fields by location for async resolution
34
- # - mutation groups sequential root fields by location for serial resolution
35
- def build_root_operations
33
+ # **
34
+ # Algorithm:
35
+ #
36
+ # A) Group all root selections by their preferred entrypoint locations.
37
+ # A.1) Group query fields by location for parallel execution.
38
+ # A.2) Partition mutation fields by consecutive location for serial execution.
39
+ #
40
+ # B) Extract contiguous selections for each entrypoint location.
41
+ #
42
+ # B.1) Selections on interface types that do not belong to the interface at the
43
+ # entrypoint location are expanded into concrete type fragments prior to extraction.
44
+ #
45
+ # B.2) Filter the selection tree down to just fields of the entrypoint location.
46
+ # Adjoining selections not available here get split off into new entrypoints (C).
47
+ #
48
+ # B.3) Collect all variable definitions used within the filtered selection.
49
+ # These specify which request variables to pass along with the selection.
50
+ #
51
+ # B.4) Add a `__typename` selection to concrete types and abstracts that implement
52
+ # fragments. This provides resolved type information used during execution.
53
+ #
54
+ # C) Delegate adjoining selections to new entrypoint locations.
55
+ # C.1) Distribute unique fields among their required locations.
56
+ # C.2) Distribute non-unique fields among locations that were added during C.1.
57
+ # C.3) Distribute remaining fields among locations weighted by greatest availability.
58
+ #
59
+ # D) Create paths routing to new entrypoint locations via boundary queries.
60
+ # D.1) Types joining through multiple keys route using a-star search.
61
+ # D.2) Types joining through a single key route via quick location match.
62
+ # (D.2 is an optional optimization of D.1)
63
+ #
64
+ # E) Translate boundary pathways into new entrypoints.
65
+ # E.1) Add the key of each boundary query into the prior location's selection set.
66
+ # E.2) Add a planner operation for each new entrypoint location, then extract it (B).
67
+ #
68
+ # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
69
+ # **
70
+
71
+ # adds an entrypoint for fetching and inserting data into the aggregate result.
72
+ def add_entrypoint(
73
+ location:,
74
+ parent_order:,
75
+ parent_type:,
76
+ selections:,
77
+ variables: {},
78
+ path: [],
79
+ operation_type: "query",
80
+ boundary: nil
81
+ )
82
+ # coalesce repeat parameters into a single entrypoint
83
+ boundary_key = boundary ? boundary["key"] : "_"
84
+ entrypoint = String.new("#{parent_order}/#{location}/#{parent_type.graphql_name}/#{boundary_key}")
85
+ path.each { entrypoint << "/#{_1}" }
86
+
87
+ op = @operations_by_entrypoint[entrypoint]
88
+ next_order = op ? parent_order : @planning_order += 1
89
+
90
+ if selections.any?
91
+ selections = extract_locale_selections(location, parent_type, next_order, selections, path, variables)
92
+ end
93
+
94
+ if op.nil?
95
+ # concrete types that are not root Query/Mutation report themselves as a type condition
96
+ # executor must check the __typename of loaded objects to see if they match subsequent operations
97
+ # this prevents the executor from taking action on unused fragment selections
98
+ conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.root_type_for_operation(operation_type)
99
+
100
+ @operations_by_entrypoint[entrypoint] = PlannerOperation.new(
101
+ order: next_order,
102
+ after: parent_order,
103
+ location: location,
104
+ parent_type: parent_type,
105
+ operation_type: operation_type,
106
+ selections: selections,
107
+ variables: variables,
108
+ path: path,
109
+ if_type: conditional ? parent_type.graphql_name : nil,
110
+ boundary: boundary,
111
+ )
112
+ else
113
+ op.selections.concat(selections)
114
+ op
115
+ end
116
+ end
117
+
118
+ # A) Group all root selections by their preferred entrypoint locations.
119
+ def build_root_entrypoints
36
120
  case @request.operation.operation_type
37
121
  when "query"
122
+ # A.1) Group query fields by location for parallel execution.
38
123
  parent_type = @supergraph.schema.query
39
124
 
40
- selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
125
+ selections_by_location = {}
126
+ each_selection_in_type(parent_type, @request.operation.selections) do |node|
41
127
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
42
- memo[locations.first] ||= []
43
- memo[locations.first] << node
128
+ selections_by_location[locations.first] ||= []
129
+ selections_by_location[locations.first] << node
44
130
  end
45
131
 
46
132
  selections_by_location.each do |location, selections|
47
- add_operation(location: location, parent_type: parent_type, selections: selections)
133
+ add_entrypoint(
134
+ location: location,
135
+ parent_order: ROOT_ORDER,
136
+ parent_type: parent_type,
137
+ selections: selections,
138
+ )
48
139
  end
49
140
 
50
141
  when "mutation"
142
+ # A.2) Partition mutation fields by consecutive location for serial execution.
51
143
  parent_type = @supergraph.schema.mutation
52
144
 
53
- location_groups = @request.operation.selections.each_with_object([]) do |node, memo|
145
+ partitions = []
146
+ each_selection_in_type(parent_type, @request.operation.selections) do |node|
54
147
  next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
55
148
 
56
- if memo.none? || memo.last[:location] != next_location
57
- memo << { location: next_location, selections: [] }
149
+ if partitions.none? || partitions.last[:location] != next_location
150
+ partitions << { location: next_location, selections: [] }
58
151
  end
59
152
 
60
- memo.last[:selections] << node
153
+ partitions.last[:selections] << node
61
154
  end
62
155
 
63
- location_groups.reduce(0) do |after_key, group|
64
- add_operation(
65
- location: group[:location],
66
- selections: group[:selections],
67
- operation_type: "mutation",
156
+ partitions.reduce(ROOT_ORDER) do |parent_order, partition|
157
+ add_entrypoint(
158
+ location: partition[:location],
159
+ parent_order: parent_order,
68
160
  parent_type: parent_type,
69
- after_key: after_key
70
- ).key
161
+ selections: partition[:selections],
162
+ operation_type: "mutation",
163
+ ).order
71
164
  end
72
165
 
73
166
  else
@@ -75,70 +168,44 @@ module GraphQL
75
168
  end
76
169
  end
77
170
 
78
- # adds an operation (data access) to the plan which maps a data selection to an insertion point.
79
- # note that planned operations are NOT always 1:1 with executed requests, as the executor can
80
- # frequently batch different insertion points with the same location into a single request.
81
- def add_operation(
82
- location:,
83
- parent_type:,
84
- selections:,
85
- insertion_path: [],
86
- operation_type: "query",
87
- after_key: 0,
88
- boundary: nil
89
- )
90
- parent_key = @sequence_key += 1
91
- locale_variables = {}
92
- locale_selections = if selections.any?
93
- extract_locale_selections(location, parent_type, selections, insertion_path, parent_key, locale_variables)
94
- else
95
- selections
96
- end
171
+ def each_selection_in_type(parent_type, input_selections, &block)
172
+ input_selections.each do |node|
173
+ case node
174
+ when GraphQL::Language::Nodes::Field
175
+ yield(node)
97
176
 
98
- # groupings coalesce similar operation parameters into a single operation
99
- # multiple operations per service may still occur with different insertion points,
100
- # but those will get query-batched together during execution.
101
- grouping = String.new
102
- grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
103
- grouping = insertion_path.reduce(grouping) do |memo, segment|
104
- memo << "/" << segment
105
- end
177
+ when GraphQL::Language::Nodes::InlineFragment
178
+ next unless node.type.nil? || parent_type.graphql_name == node.type.name
179
+ each_selection_in_type(parent_type, node.selections, &block)
106
180
 
107
- if op = @operations_by_grouping[grouping]
108
- op.selections.concat(locale_selections)
109
- op.variables.merge!(locale_variables)
110
- op
111
- else
112
- # concrete types that are not root Query/Mutation report themselves as a type condition
113
- # executor must check the __typename of loaded objects to see if they match subsequent operations
114
- # this prevents the executor from taking action on unused fragment selections
115
- type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
181
+ when GraphQL::Language::Nodes::FragmentSpread
182
+ fragment = @request.fragment_definitions[node.name]
183
+ next unless parent_type.graphql_name == fragment.type.name
184
+ each_selection_in_type(parent_type, fragment.selections, &block)
116
185
 
117
- @operations_by_grouping[grouping] = PlannerOperation.new(
118
- key: parent_key,
119
- after_key: after_key,
120
- location: location,
121
- parent_type: parent_type,
122
- operation_type: operation_type,
123
- insertion_path: insertion_path,
124
- type_condition: type_conditional ? parent_type.graphql_name : nil,
125
- selections: locale_selections,
126
- variables: locale_variables,
127
- boundary: boundary,
128
- )
186
+ else
187
+ raise "Unexpected node of type #{node.class.name} in selection set."
188
+ end
129
189
  end
130
190
  end
131
191
 
132
- # extracts a selection tree that can all be fulfilled through the current planning location.
133
- # adjoining remote selections will fork new insertion points and extract selections at those locations.
134
- def extract_locale_selections(current_location, parent_type, input_selections, insertion_path, after_key, locale_variables)
135
- remote_selections = nil
192
+ # B) Contiguous selections are extracted for each entrypoint location.
193
+ def extract_locale_selections(
194
+ current_location,
195
+ parent_type,
196
+ parent_order,
197
+ input_selections,
198
+ path,
199
+ locale_variables,
136
200
  locale_selections = []
137
- implements_fragments = false
201
+ )
202
+ # B.1) Expand selections on interface types that do not belong to this location.
203
+ input_selections = expand_interface_selections(current_location, parent_type, input_selections)
138
204
 
139
- if parent_type.kind.interface?
140
- input_selections = expand_interface_selections(current_location, parent_type, input_selections)
141
- end
205
+ # B.2) Filter the selection tree down to just fields of the entrypoint location.
206
+ # Adjoining selections not available here get split off into new entrypoints (C).
207
+ remote_selections = nil
208
+ requires_typename = parent_type.kind.abstract?
142
209
 
143
210
  input_selections.each do |node|
144
211
  case node
@@ -155,68 +222,172 @@ module GraphQL
155
222
  next
156
223
  end
157
224
 
158
- field_type = Util.named_type_for_field_node(@supergraph.schema, parent_type, node)
225
+ # B.3) Collect all variable definitions used within the filtered selection.
159
226
  extract_node_variables(node, locale_variables)
227
+ field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
160
228
 
161
229
  if Util.is_leaf_type?(field_type)
162
230
  locale_selections << node
163
231
  else
164
- insertion_path.push(node.alias || node.name)
165
- selection_set = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key, locale_variables)
166
- insertion_path.pop
232
+ path.push(node.alias || node.name)
233
+ selection_set = extract_locale_selections(current_location, field_type, parent_order, node.selections, path, locale_variables)
234
+ path.pop
167
235
 
168
236
  locale_selections << node.merge(selections: selection_set)
169
237
  end
170
238
 
171
239
  when GraphQL::Language::Nodes::InlineFragment
172
- next unless @supergraph.locations_by_type[node.type.name].include?(current_location)
240
+ fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
241
+ next unless @supergraph.locations_by_type[fragment_type.graphql_name].include?(current_location)
173
242
 
174
- fragment_type = @supergraph.schema.types[node.type.name]
175
- selection_set = extract_locale_selections(current_location, fragment_type, node.selections, insertion_path, after_key, locale_variables)
176
- locale_selections << node.merge(selections: selection_set)
177
- implements_fragments = true
243
+ is_same_scope = fragment_type == parent_type
244
+ selection_set = is_same_scope ? locale_selections : []
245
+ extract_locale_selections(current_location, fragment_type, parent_order, node.selections, path, locale_variables, selection_set)
246
+
247
+ unless is_same_scope
248
+ locale_selections << node.merge(selections: selection_set)
249
+ requires_typename = true
250
+ end
178
251
 
179
252
  when GraphQL::Language::Nodes::FragmentSpread
180
253
  fragment = @request.fragment_definitions[node.name]
181
254
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
182
255
 
183
- fragment_type = @supergraph.schema.types[fragment.type.name]
184
- selection_set = extract_locale_selections(current_location, fragment_type, fragment.selections, insertion_path, after_key, locale_variables)
185
- locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
186
- implements_fragments = true
256
+ fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
257
+ is_same_scope = fragment_type == parent_type
258
+ selection_set = is_same_scope ? locale_selections : []
259
+ extract_locale_selections(current_location, fragment_type, parent_order, fragment.selections, path, locale_variables, selection_set)
260
+
261
+ unless is_same_scope
262
+ locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
263
+ requires_typename = true
264
+ end
187
265
 
188
266
  else
189
267
  raise "Unexpected node of type #{node.class.name} in selection set."
190
268
  end
191
269
  end
192
270
 
193
- if remote_selections
194
- delegate_remote_selections(
195
- current_location,
196
- parent_type,
197
- locale_selections,
198
- remote_selections,
199
- insertion_path,
200
- after_key
201
- )
271
+ # B.4) Add a `__typename` selection to concrete types and abstracts that implement
272
+ # fragments so that resolved type information is available during execution.
273
+ if requires_typename
274
+ locale_selections << TYPENAME_NODE
202
275
  end
203
276
 
204
- # always include a __typename on abstracts and scopes that implement fragments
205
- # this provides type information to inspect while shaping the final result
206
- if parent_type.kind.abstract? || implements_fragments
207
- locale_selections << TYPENAME_NODE
277
+ if remote_selections
278
+ # C) Delegate adjoining selections to new entrypoint locations.
279
+ remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
280
+
281
+ # D) Create paths routing to new entrypoint locations via boundary queries.
282
+ routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.keys)
283
+
284
+ # E) Translate boundary pathways into new entrypoints.
285
+ routes.each_value do |route|
286
+ route.reduce(locale_selections) do |parent_selections, boundary|
287
+ # E.1) Add the key of each boundary query into the prior location's selection set.
288
+ foreign_key = "_STITCH_#{boundary["key"]}"
289
+ has_key = false
290
+ has_typename = false
291
+
292
+ parent_selections.each do |selection|
293
+ next unless selection.is_a?(GraphQL::Language::Nodes::Field)
294
+ case selection.alias
295
+ when foreign_key
296
+ has_key = true
297
+ when TYPENAME_NODE.alias
298
+ has_typename = true
299
+ end
300
+ end
301
+
302
+ parent_selections << GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["key"]) unless has_key
303
+ parent_selections << TYPENAME_NODE unless has_typename
304
+
305
+ # E.2) Add a planner operation for each new entrypoint location.
306
+ location = boundary["location"]
307
+ add_entrypoint(
308
+ location: location,
309
+ parent_order: parent_order,
310
+ parent_type: parent_type,
311
+ selections: remote_selections_by_location[location] || [],
312
+ path: path.dup,
313
+ boundary: boundary,
314
+ ).selections
315
+ end
316
+ end
208
317
  end
209
318
 
210
319
  locale_selections
211
320
  end
212
321
 
213
- # distributes remote selections across locations,
214
- # while spawning new operations for each new fulfillment.
215
- def delegate_remote_selections(current_location, parent_type, locale_selections, remote_selections, insertion_path, after_key)
322
+ # B.1) Selections on interface types that do not belong to the interface at the
323
+ # entrypoint location are expanded into concrete type fragments prior to extraction.
324
+ def expand_interface_selections(current_location, parent_type, input_selections)
325
+ return input_selections unless parent_type.kind.interface?
326
+
327
+ local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
328
+
329
+ expanded_selections = nil
330
+ input_selections = input_selections.filter_map do |node|
331
+ case node
332
+ when GraphQL::Language::Nodes::Field
333
+ if node.name != "__typename" && !local_interface_fields.include?(node.name)
334
+ expanded_selections ||= []
335
+ expanded_selections << node
336
+ next nil
337
+ end
338
+
339
+ when GraphQL::Language::Nodes::InlineFragment
340
+ fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
341
+ selection_set = expand_interface_selections(current_location, fragment_type, node.selections)
342
+ node = node.merge(selections: selection_set)
343
+
344
+ when GraphQL::Language::Nodes::FragmentSpread
345
+ fragment = @request.fragment_definitions[node.name]
346
+ fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
347
+ selection_set = expand_interface_selections(current_location, fragment_type, fragment.selections)
348
+ node = GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
349
+
350
+ end
351
+ node
352
+ end
353
+
354
+ if expanded_selections
355
+ @supergraph.memoized_schema_possible_types(parent_type.graphql_name).each do |possible_type|
356
+ next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
357
+
358
+ type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
359
+ input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
360
+ end
361
+ end
362
+
363
+ input_selections
364
+ end
365
+
366
+ # B.3) Collect all variable definitions used within the filtered selection.
367
+ # These specify which request variables to pass along with the selection.
368
+ def extract_node_variables(node_with_args, variable_definitions)
369
+ node_with_args.arguments.each do |argument|
370
+ case argument.value
371
+ when GraphQL::Language::Nodes::InputObject
372
+ extract_node_variables(argument.value, variable_definitions)
373
+ when GraphQL::Language::Nodes::VariableIdentifier
374
+ variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
375
+ end
376
+ end
377
+
378
+ if node_with_args.respond_to?(:directives)
379
+ node_with_args.directives.each do |directive|
380
+ extract_node_variables(directive, variable_definitions)
381
+ end
382
+ end
383
+ end
384
+
385
+ # C) Delegate adjoining selections to new entrypoint locations.
386
+ def delegate_remote_selections(parent_type, remote_selections)
216
387
  possible_locations_by_field = @supergraph.locations_by_type_and_field[parent_type.graphql_name]
217
388
  selections_by_location = {}
218
389
 
219
- # 1. distribute unique fields among required locations
390
+ # C.1) Distribute unique fields among their required locations.
220
391
  remote_selections.reject! do |node|
221
392
  possible_locations = possible_locations_by_field[node.name]
222
393
  if possible_locations.length == 1
@@ -226,7 +397,7 @@ module GraphQL
226
397
  end
227
398
  end
228
399
 
229
- # 2. distribute non-unique fields among locations that are already used
400
+ # C.2) Distribute non-unique fields among locations that were added during C.1.
230
401
  if selections_by_location.any? && remote_selections.any?
231
402
  remote_selections.reject! do |node|
232
403
  used_location = possible_locations_by_field[node.name].find { selections_by_location[_1] }
@@ -237,7 +408,7 @@ module GraphQL
237
408
  end
238
409
  end
239
410
 
240
- # 3. distribute remaining fields among locations weighted by greatest availability
411
+ # C.3) Distribute remaining fields among locations weighted by greatest availability.
241
412
  if remote_selections.any?
242
413
  field_count_by_location = if remote_selections.length > 1
243
414
  remote_selections.each_with_object({}) do |node, memo|
@@ -270,91 +441,15 @@ module GraphQL
270
441
  end
271
442
  end
272
443
 
273
- # route from current location to target locations via boundary queries,
274
- # then translate those routes into planner operations
275
- routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
276
- routes.values.each_with_object({}) do |route, ops_by_location|
277
- route.reduce(nil) do |parent_op, boundary|
278
- location = boundary["location"]
279
-
280
- unless op = ops_by_location[location]
281
- op = ops_by_location[location] = add_operation(
282
- location: location,
283
- # routing locations added as intermediaries have no initial selections,
284
- # but will be given foreign keys by subsequent operations
285
- selections: selections_by_location[location] || [],
286
- parent_type: parent_type,
287
- insertion_path: insertion_path.dup,
288
- boundary: boundary,
289
- after_key: after_key,
290
- )
291
- end
292
-
293
- foreign_key = "_STITCH_#{boundary["selection"]}"
294
- parent_selections = parent_op ? parent_op.selections : locale_selections
295
-
296
- if parent_selections.none? { _1.is_a?(GraphQL::Language::Nodes::Field) && _1.alias == foreign_key }
297
- foreign_key_node = GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["selection"])
298
- parent_selections << foreign_key_node << TYPENAME_NODE
299
- end
300
-
301
- op
302
- end
303
- end
304
- end
305
-
306
- # extracts variable definitions used by a node
307
- # (each operation tracks the specific variables used in its tree)
308
- def extract_node_variables(node_with_args, variable_definitions)
309
- node_with_args.arguments.each do |argument|
310
- case argument.value
311
- when GraphQL::Language::Nodes::InputObject
312
- extract_node_variables(argument.value, variable_definitions)
313
- when GraphQL::Language::Nodes::VariableIdentifier
314
- variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
315
- end
316
- end
317
-
318
- if node_with_args.respond_to?(:directives)
319
- node_with_args.directives.each do |directive|
320
- extract_node_variables(directive, variable_definitions)
321
- end
322
- end
323
- end
324
-
325
- # fields of a merged interface may not belong to the interface at the local level,
326
- # so any non-local interface fields get expanded into typed fragments before planning
327
- def expand_interface_selections(current_location, parent_type, input_selections)
328
- local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
329
-
330
- expanded_selections = nil
331
- input_selections = input_selections.reject do |node|
332
- if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
333
- expanded_selections ||= []
334
- expanded_selections << node
335
- true
336
- end
337
- end
338
-
339
- if expanded_selections
340
- @supergraph.schema.possible_types(parent_type).each do |possible_type|
341
- next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
342
-
343
- type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
344
- input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
345
- end
346
- end
347
-
348
- input_selections
444
+ selections_by_location
349
445
  end
350
446
 
351
- # expand concrete type selections into typed fragments when sending to abstract boundaries
352
- # this shifts all loose selection fields into a wrapping concrete type fragment
447
+ # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
353
448
  def expand_abstract_boundaries
354
- @operations_by_grouping.each do |_grouping, op|
449
+ @operations_by_entrypoint.each_value do |op|
355
450
  next unless op.boundary
356
451
 
357
- boundary_type = @supergraph.schema.types[op.boundary["type_name"]]
452
+ boundary_type = @supergraph.memoized_schema_types[op.boundary["type_name"]]
358
453
  next unless boundary_type.kind.abstract?
359
454
  next if boundary_type == op.parent_type
360
455
 
@@ -5,30 +5,30 @@ module GraphQL
5
5
  class PlannerOperation
6
6
  LANGUAGE_PRINTER = GraphQL::Language::Printer.new
7
7
 
8
- attr_reader :key, :location, :parent_type, :type_condition, :operation_type, :insertion_path
9
- attr_accessor :after_key, :selections, :variables, :boundary
8
+ attr_reader :order, :location, :parent_type, :if_type, :operation_type, :path
9
+ attr_accessor :after, :selections, :variables, :boundary
10
10
 
11
11
  def initialize(
12
- key:,
13
12
  location:,
14
13
  parent_type:,
14
+ order:,
15
+ after: nil,
15
16
  operation_type: "query",
16
- insertion_path: [],
17
- type_condition: nil,
18
- after_key: nil,
19
17
  selections: [],
20
18
  variables: [],
19
+ path: [],
20
+ if_type: nil,
21
21
  boundary: nil
22
22
  )
23
- @key = key
24
- @after_key = after_key
25
23
  @location = location
26
24
  @parent_type = parent_type
25
+ @order = order
26
+ @after = after
27
27
  @operation_type = operation_type
28
- @insertion_path = insertion_path
29
- @type_condition = type_condition
30
28
  @selections = selections
31
29
  @variables = variables
30
+ @path = path
31
+ @if_type = if_type
32
32
  @boundary = boundary
33
33
  end
34
34
 
@@ -44,17 +44,19 @@ module GraphQL
44
44
  end
45
45
 
46
46
  def to_h
47
- {
48
- "key" => @key,
49
- "after_key" => @after_key,
47
+ data = {
48
+ "order" => @order,
49
+ "after" => @after,
50
50
  "location" => @location,
51
51
  "operation_type" => @operation_type,
52
- "insertion_path" => @insertion_path,
53
- "type_condition" => @type_condition,
54
52
  "selections" => selection_set,
55
53
  "variables" => variable_set,
56
- "boundary" => @boundary,
54
+ "path" => @path,
57
55
  }
56
+
57
+ data["if_type"] = @if_type if @if_type
58
+ data["boundary"] = @boundary if @boundary
59
+ data
58
60
  end
59
61
  end
60
62
  end
@@ -4,14 +4,14 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  class Shaper
7
- def initialize(schema:, request:)
8
- @schema = schema
7
+ def initialize(supergraph:, request:)
8
+ @supergraph = supergraph
9
9
  @request = request
10
10
  end
11
11
 
12
12
  def perform!(raw)
13
- root_type = @schema.public_send(@request.operation.operation_type)
14
- resolve_object_scope(raw, root_type, @request.operation.selections)
13
+ @root_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
14
+ resolve_object_scope(raw, @root_type, @request.operation.selections, @root_type.graphql_name)
15
15
  end
16
16
 
17
17
  private
@@ -25,11 +25,14 @@ module GraphQL
25
25
  selections.each do |node|
26
26
  case node
27
27
  when GraphQL::Language::Nodes::Field
28
- next if node.name.start_with?("__")
29
-
30
28
  field_name = node.alias || node.name
31
- node_type = parent_type.fields[node.name].type
32
- named_type = Util.named_type_for_field_node(@schema, parent_type, node)
29
+
30
+ next if introspection_field?(parent_type, node) do |is_root_typename|
31
+ raw_object[field_name] = @root_type.graphql_name if is_root_typename
32
+ end
33
+
34
+ node_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type
35
+ named_type = node_type.unwrap
33
36
 
34
37
  raw_object[field_name] = if node_type.list?
35
38
  resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
@@ -42,7 +45,7 @@ module GraphQL
42
45
  return nil if raw_object[field_name].nil? && node_type.non_null?
43
46
 
44
47
  when GraphQL::Language::Nodes::InlineFragment
45
- fragment_type = @schema.types[node.type.name]
48
+ fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
46
49
  next unless typename_in_type?(typename, fragment_type)
47
50
 
48
51
  result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
@@ -50,7 +53,7 @@ module GraphQL
50
53
 
51
54
  when GraphQL::Language::Nodes::FragmentSpread
52
55
  fragment = @request.fragment_definitions[node.name]
53
- fragment_type = @schema.types[fragment.type.name]
56
+ fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
54
57
  next unless typename_in_type?(typename, fragment_type)
55
58
 
56
59
  result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
@@ -93,9 +96,27 @@ module GraphQL
93
96
  resolved_list
94
97
  end
95
98
 
99
+ def introspection_field?(parent_type, node)
100
+ return false unless node.name.start_with?("__")
101
+ is_root = parent_type == @root_type
102
+
103
+ case node.name
104
+ when "__typename"
105
+ yield(is_root)
106
+ true
107
+ when "__schema", "__type"
108
+ is_root && @request.operation.operation_type == "query"
109
+ else
110
+ false
111
+ end
112
+ end
113
+
96
114
  def typename_in_type?(typename, type)
97
115
  return true if type.graphql_name == typename
98
- type.kind.abstract? && @schema.possible_types(type).any? { _1.graphql_name == typename }
116
+
117
+ type.kind.abstract? && @supergraph.memoized_schema_possible_types(type.graphql_name).any? do |t|
118
+ t.graphql_name == typename
119
+ end
99
120
  end
100
121
  end
101
122
  end
@@ -4,16 +4,6 @@ module GraphQL
4
4
  module Stitching
5
5
  class Supergraph
6
6
  LOCATION = "__super"
7
- INTROSPECTION_TYPES = [
8
- "__Schema",
9
- "__Type",
10
- "__Field",
11
- "__Directive",
12
- "__EnumValue",
13
- "__InputValue",
14
- "__TypeKind",
15
- "__DirectiveLocation",
16
- ].freeze
17
7
 
18
8
  def self.validate_executable!(location, executable)
19
9
  return true if executable.is_a?(Class) && executable <= GraphQL::Schema
@@ -46,13 +36,14 @@ module GraphQL
46
36
  @boundaries = boundaries
47
37
  @possible_keys_by_type = {}
48
38
  @possible_keys_by_type_and_location = {}
39
+ @memoized_schema_possible_types = {}
40
+ @memoized_schema_fields = {}
49
41
 
50
42
  # add introspection types into the fields mapping
51
- @locations_by_type_and_field = INTROSPECTION_TYPES.each_with_object(fields) do |type_name, memo|
52
- introspection_type = schema.get_type(type_name)
53
- next unless introspection_type.kind.fields?
43
+ @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
44
+ next unless type.kind.fields?
54
45
 
55
- memo[type_name] = introspection_type.fields.keys.each_with_object({}) do |field_name, m|
46
+ memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
56
47
  m[field_name] = [LOCATION]
57
48
  end
58
49
  end.freeze
@@ -66,7 +57,7 @@ module GraphQL
66
57
  end
67
58
 
68
59
  def fields
69
- @locations_by_type_and_field.reject { |k, _v| INTROSPECTION_TYPES.include?(k) }
60
+ @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
70
61
  end
71
62
 
72
63
  def locations
@@ -81,6 +72,35 @@ module GraphQL
81
72
  }
82
73
  end
83
74
 
75
+ def memoized_introspection_types
76
+ @memoized_introspection_types ||= schema.introspection_system.types
77
+ end
78
+
79
+ def memoized_schema_types
80
+ @memoized_schema_types ||= @schema.types
81
+ end
82
+
83
+ def memoized_schema_possible_types(type_name)
84
+ @memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
85
+ end
86
+
87
+ def memoized_schema_fields(type_name)
88
+ @memoized_schema_fields[type_name] ||= begin
89
+ fields = memoized_schema_types[type_name].fields
90
+ @schema.introspection_system.dynamic_fields.each do |field|
91
+ fields[field.name] ||= field # adds __typename
92
+ end
93
+
94
+ if type_name == @schema.query.graphql_name
95
+ @schema.introspection_system.entry_points.each do |field|
96
+ fields[field.name] ||= field # adds __schema, __type
97
+ end
98
+ end
99
+
100
+ fields
101
+ end
102
+ end
103
+
84
104
  def execute_at_location(location, source, variables, context)
85
105
  executable = executables[location]
86
106
 
@@ -124,9 +144,7 @@ module GraphQL
124
144
  # ("Type") => ["id", ...]
125
145
  def possible_keys_for_type(type_name)
126
146
  @possible_keys_by_type[type_name] ||= begin
127
- keys = @boundaries[type_name].map { _1["selection"] }
128
- keys.uniq!
129
- keys
147
+ @boundaries[type_name].map { _1["key"] }.tap(&:uniq!)
130
148
  end
131
149
  end
132
150
 
@@ -166,18 +184,18 @@ module GraphQL
166
184
  costs = {}
167
185
 
168
186
  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
169
- [{ location: start_location, selection: possible_key, cost: 0 }]
187
+ [{ location: start_location, key: possible_key, cost: 0 }]
170
188
  end
171
189
 
172
190
  while paths.any?
173
191
  path = paths.pop
174
192
  current_location = path.last[:location]
175
- current_selection = path.last[:selection]
193
+ current_key = path.last[:key]
176
194
  current_cost = path.last[:cost]
177
195
 
178
196
  @boundaries[type_name].each do |boundary|
179
197
  forward_location = boundary["location"]
180
- next if current_selection != boundary["selection"]
198
+ next if current_key != boundary["key"]
181
199
  next if path.any? { _1[:location] == forward_location }
182
200
 
183
201
  best_cost = costs[forward_location] || Float::INFINITY
@@ -186,7 +204,7 @@ module GraphQL
186
204
  path.pop
187
205
  path << {
188
206
  location: current_location,
189
- selection: current_selection,
207
+ key: current_key,
190
208
  cost: current_cost,
191
209
  boundary: boundary,
192
210
  }
@@ -204,14 +222,13 @@ module GraphQL
204
222
  costs[forward_location] = forward_cost if forward_cost < best_cost
205
223
 
206
224
  possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
207
- paths << [*path, { location: forward_location, selection: possible_key, cost: forward_cost }]
225
+ paths << [*path, { location: forward_location, key: possible_key, cost: forward_cost }]
208
226
  end
209
227
  end
210
228
 
211
229
  paths.sort! do |a, b|
212
230
  cost_diff = a.last[:cost] - b.last[:cost]
213
- next cost_diff unless cost_diff.zero?
214
- a.length - b.length
231
+ cost_diff.zero? ? a.length - b.length : cost_diff
215
232
  end.reverse!
216
233
  end
217
234
 
@@ -37,15 +37,6 @@ module GraphQL
37
37
  structure
38
38
  end
39
39
 
40
- # gets a named type for a field node, including hidden root introspections
41
- def self.named_type_for_field_node(schema, parent_type, node)
42
- if node.name == "__schema" && parent_type == schema.query
43
- schema.types["__Schema"]
44
- else
45
- parent_type.fields[node.name].type.unwrap
46
- end
47
- end
48
-
49
40
  # expands interfaces and unions to an array of their memberships
50
41
  # like `schema.possible_types`, but includes child interfaces
51
42
  def self.expand_abstract_type(schema, parent_type)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.3.3"
5
+ VERSION = "0.3.6"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-stitching
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-24 00:00:00.000000000 Z
11
+ date: 2023-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql