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.
- checksums.yaml +4 -4
- data/README.md +79 -14
- data/docs/README.md +1 -1
- data/docs/client.md +103 -0
- data/docs/composer.md +2 -2
- data/docs/supergraph.md +1 -1
- data/example/gateway.rb +4 -4
- data/lib/graphql/stitching/{gateway.rb → client.rb} +7 -7
- data/lib/graphql/stitching/composer/validate_boundaries.rb +4 -4
- data/lib/graphql/stitching/composer.rb +44 -22
- data/lib/graphql/stitching/executor/boundary_source.rb +199 -0
- data/lib/graphql/stitching/executor/root_source.rb +48 -0
- data/lib/graphql/stitching/executor.rb +7 -231
- data/lib/graphql/stitching/{remote_client.rb → http_executable.rb} +1 -1
- data/lib/graphql/stitching/planner.rb +264 -189
- data/lib/graphql/stitching/planner_operation.rb +18 -16
- data/lib/graphql/stitching/shaper.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +21 -28
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +3 -2
- metadata +7 -5
- data/docs/gateway.md +0 -103
@@ -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
|
-
@
|
13
|
-
@
|
13
|
+
@planning_order = ROOT_ORDER
|
14
|
+
@operations_by_entrypoint = {}
|
14
15
|
end
|
15
16
|
|
16
17
|
def perform
|
17
|
-
|
18
|
+
build_root_entrypoints
|
18
19
|
expand_abstract_boundaries
|
19
20
|
self
|
20
21
|
end
|
21
22
|
|
22
23
|
def operations
|
23
|
-
@
|
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
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
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
|
-
|
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
|
-
|
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
|
59
|
-
|
149
|
+
if partitions.none? || partitions.last[:location] != next_location
|
150
|
+
partitions << { location: next_location, selections: [] }
|
60
151
|
end
|
61
152
|
|
62
|
-
|
153
|
+
partitions.last[:selections] << node
|
63
154
|
end
|
64
155
|
|
65
|
-
|
66
|
-
|
67
|
-
location:
|
68
|
-
|
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
|
-
|
72
|
-
|
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
|
-
#
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
114
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
185
|
-
selection_set = extract_locale_selections(current_location, field_type, node.selections,
|
186
|
-
|
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
|
-
|
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
|
-
|
195
|
-
selection_set =
|
196
|
-
|
197
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
#
|
234
|
-
#
|
235
|
-
def
|
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
|
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
|
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
|
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
|
-
|
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
|
-
#
|
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
|
-
@
|
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 :
|
9
|
-
attr_accessor :
|
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
|
-
"
|
49
|
-
"
|
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
|
-
"
|
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)
|