graphql-stitching 0.3.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)