graphql-stitching 0.3.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,12 +30,96 @@ 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
125
  selections_by_location = {}
@@ -45,31 +130,37 @@ module GraphQL
45
130
  end
46
131
 
47
132
  selections_by_location.each do |location, selections|
48
- 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
+ )
49
139
  end
50
140
 
51
141
  when "mutation"
142
+ # A.2) Partition mutation fields by consecutive location for serial execution.
52
143
  parent_type = @supergraph.schema.mutation
53
144
 
54
- location_groups = []
145
+ partitions = []
55
146
  each_selection_in_type(parent_type, @request.operation.selections) do |node|
56
147
  next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
57
148
 
58
- if location_groups.none? || location_groups.last[:location] != next_location
59
- location_groups << { location: next_location, selections: [] }
149
+ if partitions.none? || partitions.last[:location] != next_location
150
+ partitions << { location: next_location, selections: [] }
60
151
  end
61
152
 
62
- location_groups.last[:selections] << node
153
+ partitions.last[:selections] << node
63
154
  end
64
155
 
65
- location_groups.reduce(0) do |after_key, group|
66
- add_operation(
67
- location: group[:location],
68
- selections: group[:selections],
69
- operation_type: "mutation",
156
+ partitions.reduce(ROOT_ORDER) do |parent_order, partition|
157
+ add_entrypoint(
158
+ location: partition[:location],
159
+ parent_order: parent_order,
70
160
  parent_type: parent_type,
71
- after_key: after_key
72
- ).key
161
+ selections: partition[:selections],
162
+ operation_type: "mutation",
163
+ ).order
73
164
  end
74
165
 
75
166
  else
@@ -84,7 +175,7 @@ module GraphQL
84
175
  yield(node)
85
176
 
86
177
  when GraphQL::Language::Nodes::InlineFragment
87
- next unless parent_type.graphql_name == node.type.name
178
+ next unless node.type.nil? || parent_type.graphql_name == node.type.name
88
179
  each_selection_in_type(parent_type, node.selections, &block)
89
180
 
90
181
  when GraphQL::Language::Nodes::FragmentSpread
@@ -98,67 +189,23 @@ module GraphQL
98
189
  end
99
190
  end
100
191
 
101
- # adds an operation (data access) to the plan which maps a data selection to an insertion point.
102
- # note that planned operations are NOT always 1:1 with executed requests, as the executor can
103
- # frequently batch different insertion points with the same location into a single request.
104
- def add_operation(
105
- location:,
106
- parent_type:,
107
- selections:,
108
- insertion_path: [],
109
- operation_type: "query",
110
- after_key: 0,
111
- boundary: 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,
200
+ locale_selections = []
112
201
  )
113
- parent_key = @sequence_key += 1
114
- locale_variables = {}
115
- locale_selections = if selections.any?
116
- extract_locale_selections(location, parent_type, selections, insertion_path, parent_key, locale_variables)
117
- else
118
- selections
119
- end
120
-
121
- # groupings coalesce similar operation parameters into a single operation
122
- # multiple operations per service may still occur with different insertion points,
123
- # but those will get query-batched together during execution.
124
- grouping = String.new("#{after_key}/#{location}/#{parent_type.graphql_name}")
125
- insertion_path.each { grouping << "/#{_1}" }
126
-
127
- if op = @operations_by_grouping[grouping]
128
- op.selections.concat(locale_selections)
129
- op.variables.merge!(locale_variables)
130
- op
131
- else
132
- # concrete types that are not root Query/Mutation report themselves as a type condition
133
- # executor must check the __typename of loaded objects to see if they match subsequent operations
134
- # this prevents the executor from taking action on unused fragment selections
135
- type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
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)
136
204
 
137
- @operations_by_grouping[grouping] = PlannerOperation.new(
138
- key: parent_key,
139
- after_key: after_key,
140
- location: location,
141
- parent_type: parent_type,
142
- operation_type: operation_type,
143
- insertion_path: insertion_path,
144
- type_condition: type_conditional ? parent_type.graphql_name : nil,
145
- selections: locale_selections,
146
- variables: locale_variables,
147
- boundary: boundary,
148
- )
149
- end
150
- end
151
-
152
- # extracts a selection tree that can all be fulfilled through the current planning location.
153
- # adjoining remote selections will fork new insertion points and extract selections at those locations.
154
- def extract_locale_selections(current_location, parent_type, input_selections, insertion_path, after_key, locale_variables)
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).
155
207
  remote_selections = nil
156
- locale_selections = []
157
- implements_fragments = false
158
-
159
- if parent_type.kind.interface?
160
- input_selections = expand_interface_selections(current_location, parent_type, input_selections)
161
- end
208
+ requires_typename = parent_type.kind.abstract?
162
209
 
163
210
  input_selections.each do |node|
164
211
  case node
@@ -175,68 +222,172 @@ module GraphQL
175
222
  next
176
223
  end
177
224
 
178
- field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
225
+ # B.3) Collect all variable definitions used within the filtered selection.
179
226
  extract_node_variables(node, locale_variables)
227
+ field_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type.unwrap
180
228
 
181
229
  if Util.is_leaf_type?(field_type)
182
230
  locale_selections << node
183
231
  else
184
- insertion_path.push(node.alias || node.name)
185
- selection_set = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key, locale_variables)
186
- 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
187
235
 
188
236
  locale_selections << node.merge(selections: selection_set)
189
237
  end
190
238
 
191
239
  when GraphQL::Language::Nodes::InlineFragment
192
- 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)
193
242
 
194
- fragment_type = @supergraph.memoized_schema_types[node.type.name]
195
- selection_set = extract_locale_selections(current_location, fragment_type, node.selections, insertion_path, after_key, locale_variables)
196
- locale_selections << node.merge(selections: selection_set)
197
- 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
198
251
 
199
252
  when GraphQL::Language::Nodes::FragmentSpread
200
253
  fragment = @request.fragment_definitions[node.name]
201
254
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
202
255
 
203
256
  fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
204
- selection_set = extract_locale_selections(current_location, fragment_type, fragment.selections, insertion_path, after_key, locale_variables)
205
- locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
206
- implements_fragments = true
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
207
265
 
208
266
  else
209
267
  raise "Unexpected node of type #{node.class.name} in selection set."
210
268
  end
211
269
  end
212
270
 
213
- if remote_selections
214
- delegate_remote_selections(
215
- current_location,
216
- parent_type,
217
- locale_selections,
218
- remote_selections,
219
- insertion_path,
220
- after_key
221
- )
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
222
275
  end
223
276
 
224
- # always include a __typename on abstracts and scopes that implement fragments
225
- # this provides type information to inspect while shaping the final result
226
- if parent_type.kind.abstract? || implements_fragments
227
- 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
228
317
  end
229
318
 
230
319
  locale_selections
231
320
  end
232
321
 
233
- # distributes remote selections across locations,
234
- # while spawning new operations for each new fulfillment.
235
- 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)
236
387
  possible_locations_by_field = @supergraph.locations_by_type_and_field[parent_type.graphql_name]
237
388
  selections_by_location = {}
238
389
 
239
- # 1. distribute unique fields among required locations
390
+ # C.1) Distribute unique fields among their required locations.
240
391
  remote_selections.reject! do |node|
241
392
  possible_locations = possible_locations_by_field[node.name]
242
393
  if possible_locations.length == 1
@@ -246,7 +397,7 @@ module GraphQL
246
397
  end
247
398
  end
248
399
 
249
- # 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.
250
401
  if selections_by_location.any? && remote_selections.any?
251
402
  remote_selections.reject! do |node|
252
403
  used_location = possible_locations_by_field[node.name].find { selections_by_location[_1] }
@@ -257,7 +408,7 @@ module GraphQL
257
408
  end
258
409
  end
259
410
 
260
- # 3. distribute remaining fields among locations weighted by greatest availability
411
+ # C.3) Distribute remaining fields among locations weighted by greatest availability.
261
412
  if remote_selections.any?
262
413
  field_count_by_location = if remote_selections.length > 1
263
414
  remote_selections.each_with_object({}) do |node, memo|
@@ -290,88 +441,12 @@ module GraphQL
290
441
  end
291
442
  end
292
443
 
293
- # route from current location to target locations via boundary queries,
294
- # then translate those routes into planner operations
295
- routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
296
- routes.values.each_with_object({}) do |route, ops_by_location|
297
- route.reduce(nil) do |parent_op, boundary|
298
- location = boundary["location"]
299
-
300
- unless op = ops_by_location[location]
301
- op = ops_by_location[location] = add_operation(
302
- location: location,
303
- # routing locations added as intermediaries have no initial selections,
304
- # but will be given foreign keys by subsequent operations
305
- selections: selections_by_location[location] || [],
306
- parent_type: parent_type,
307
- insertion_path: insertion_path.dup,
308
- boundary: boundary,
309
- after_key: after_key,
310
- )
311
- end
312
-
313
- foreign_key = "_STITCH_#{boundary["selection"]}"
314
- parent_selections = parent_op ? parent_op.selections : locale_selections
315
-
316
- if parent_selections.none? { _1.is_a?(GraphQL::Language::Nodes::Field) && _1.alias == foreign_key }
317
- foreign_key_node = GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["selection"])
318
- parent_selections << foreign_key_node << TYPENAME_NODE
319
- end
320
-
321
- op
322
- end
323
- end
324
- end
325
-
326
- # extracts variable definitions used by a node
327
- # (each operation tracks the specific variables used in its tree)
328
- def extract_node_variables(node_with_args, variable_definitions)
329
- node_with_args.arguments.each do |argument|
330
- case argument.value
331
- when GraphQL::Language::Nodes::InputObject
332
- extract_node_variables(argument.value, variable_definitions)
333
- when GraphQL::Language::Nodes::VariableIdentifier
334
- variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
335
- end
336
- end
337
-
338
- if node_with_args.respond_to?(:directives)
339
- node_with_args.directives.each do |directive|
340
- extract_node_variables(directive, variable_definitions)
341
- end
342
- end
343
- end
344
-
345
- # fields of a merged interface may not belong to the interface at the local level,
346
- # so any non-local interface fields get expanded into typed fragments before planning
347
- def expand_interface_selections(current_location, parent_type, input_selections)
348
- local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
349
-
350
- expanded_selections = nil
351
- input_selections = input_selections.reject do |node|
352
- if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
353
- expanded_selections ||= []
354
- expanded_selections << node
355
- true
356
- end
357
- end
358
-
359
- if expanded_selections
360
- @supergraph.memoized_schema_possible_types(parent_type.graphql_name).each do |possible_type|
361
- next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
362
-
363
- type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
364
- input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
365
- end
366
- end
367
-
368
- input_selections
444
+ selections_by_location
369
445
  end
370
446
 
371
- # expand concrete type selections into typed fragments when sending to abstract boundaries
372
- # this shifts all loose selection fields into a wrapping concrete type fragment
447
+ # F) Wrap concrete selections targeting abstract boundaries in typed fragments.
373
448
  def expand_abstract_boundaries
374
- @operations_by_grouping.each do |_grouping, op|
449
+ @operations_by_entrypoint.each_value do |op|
375
450
  next unless op.boundary
376
451
 
377
452
  boundary_type = @supergraph.memoized_schema_types[op.boundary["type_name"]]
@@ -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
@@ -45,7 +45,7 @@ module GraphQL
45
45
  return nil if raw_object[field_name].nil? && node_type.non_null?
46
46
 
47
47
  when GraphQL::Language::Nodes::InlineFragment
48
- fragment_type = @supergraph.memoized_schema_types[node.type.name]
48
+ fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
49
49
  next unless typename_in_type?(typename, fragment_type)
50
50
 
51
51
  result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)