graphql-stitching 1.0.0 → 1.0.2
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 +21 -1
- data/lib/graphql/stitching/boundary.rb +29 -0
- data/lib/graphql/stitching/client.rb +5 -5
- data/lib/graphql/stitching/composer/validate_boundaries.rb +6 -6
- data/lib/graphql/stitching/composer/validate_interfaces.rb +2 -2
- data/lib/graphql/stitching/composer.rb +14 -14
- data/lib/graphql/stitching/executor/boundary_source.rb +24 -16
- data/lib/graphql/stitching/executor/root_source.rb +18 -10
- data/lib/graphql/stitching/executor.rb +12 -14
- data/lib/graphql/stitching/plan.rb +67 -0
- data/lib/graphql/stitching/planner.rb +91 -125
- data/lib/graphql/stitching/planner_step.rb +71 -0
- data/lib/graphql/stitching/request.rb +9 -51
- data/lib/graphql/stitching/selection_hint.rb +31 -0
- data/lib/graphql/stitching/shaper.rb +4 -2
- data/lib/graphql/stitching/skip_include.rb +81 -0
- data/lib/graphql/stitching/supergraph.rb +29 -23
- data/lib/graphql/stitching/util.rb +49 -38
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +5 -2
- metadata +7 -3
- data/lib/graphql/stitching/planner_operation.rb +0 -63
@@ -4,28 +4,26 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Planner
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
|
-
|
8
|
-
|
7
|
+
TYPENAME = "__typename"
|
8
|
+
QUERY_OP = "query"
|
9
|
+
MUTATION_OP = "mutation"
|
10
|
+
ROOT_INDEX = 0
|
9
11
|
|
10
12
|
def initialize(supergraph:, request:)
|
11
13
|
@supergraph = supergraph
|
12
14
|
@request = request
|
13
|
-
@
|
14
|
-
@
|
15
|
+
@planning_index = ROOT_INDEX
|
16
|
+
@steps_by_entrypoint = {}
|
15
17
|
end
|
16
18
|
|
17
19
|
def perform
|
18
20
|
build_root_entrypoints
|
19
21
|
expand_abstract_boundaries
|
20
|
-
|
22
|
+
Plan.new(ops: steps.map(&:to_plan_op))
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
24
|
-
@
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
{ "ops" => operations.map!(&:to_h) }
|
25
|
+
def steps
|
26
|
+
@steps_by_entrypoint.values.sort_by!(&:index)
|
29
27
|
end
|
30
28
|
|
31
29
|
private
|
@@ -38,18 +36,14 @@ module GraphQL
|
|
38
36
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
39
37
|
#
|
40
38
|
# B) Extract contiguous selections for each entrypoint location.
|
41
|
-
#
|
42
39
|
# B.1) Selections on interface types that do not belong to the interface at the
|
43
|
-
#
|
44
|
-
#
|
40
|
+
# entrypoint location are expanded into concrete type fragments prior to extraction.
|
45
41
|
# B.2) Filter the selection tree down to just fields of the entrypoint location.
|
46
|
-
#
|
47
|
-
#
|
42
|
+
# Adjoining selections not available here get split off into new entrypoints (C).
|
48
43
|
# B.3) Collect all variable definitions used within the filtered selection.
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# fragments. This provides resolved type information used during execution.
|
44
|
+
# These specify which request variables to pass along with each step.
|
45
|
+
# B.4) Add a `__typename` hint to abstracts and types that implement fragments.
|
46
|
+
# This provides resolved type information used during execution.
|
53
47
|
#
|
54
48
|
# C) Delegate adjoining selections to new entrypoint locations.
|
55
49
|
# C.1) Distribute unique fields among their required locations.
|
@@ -57,110 +51,105 @@ module GraphQL
|
|
57
51
|
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
58
52
|
#
|
59
53
|
# D) Create paths routing to new entrypoint locations via boundary queries.
|
60
|
-
# D.1) Types joining through multiple keys route using
|
54
|
+
# D.1) Types joining through multiple keys route using A* search.
|
61
55
|
# D.2) Types joining through a single key route via quick location match.
|
62
56
|
# (D.2 is an optional optimization of D.1)
|
63
57
|
#
|
64
58
|
# E) Translate boundary pathways into new entrypoints.
|
65
59
|
# E.1) Add the key of each boundary query into the prior location's selection set.
|
66
|
-
# E.2) Add a planner
|
60
|
+
# E.2) Add a planner step for each new entrypoint location, then extract it (B).
|
67
61
|
#
|
68
62
|
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
69
63
|
# **
|
70
64
|
|
71
|
-
# adds
|
72
|
-
def
|
65
|
+
# adds a planning step for fetching and inserting data into the aggregate result.
|
66
|
+
def add_step(
|
73
67
|
location:,
|
74
|
-
|
68
|
+
parent_index:,
|
75
69
|
parent_type:,
|
76
70
|
selections:,
|
77
71
|
variables: {},
|
78
72
|
path: [],
|
79
|
-
operation_type:
|
73
|
+
operation_type: QUERY_OP,
|
80
74
|
boundary: nil
|
81
75
|
)
|
82
76
|
# coalesce repeat parameters into a single entrypoint
|
83
|
-
|
84
|
-
entrypoint = String.new("#{parent_order}/#{location}/#{parent_type.graphql_name}/#{boundary_key}")
|
77
|
+
entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{boundary&.key}")
|
85
78
|
path.each { entrypoint << "/#{_1}" }
|
86
79
|
|
87
|
-
|
88
|
-
|
80
|
+
step = @steps_by_entrypoint[entrypoint]
|
81
|
+
next_index = step ? parent_index : @planning_index += 1
|
89
82
|
|
90
83
|
if selections.any?
|
91
|
-
selections = extract_locale_selections(location, parent_type,
|
84
|
+
selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
|
92
85
|
end
|
93
86
|
|
94
|
-
if
|
95
|
-
|
96
|
-
|
97
|
-
|
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,
|
87
|
+
if step.nil?
|
88
|
+
@steps_by_entrypoint[entrypoint] = PlannerStep.new(
|
89
|
+
index: next_index,
|
90
|
+
after: parent_index,
|
103
91
|
location: location,
|
104
92
|
parent_type: parent_type,
|
105
93
|
operation_type: operation_type,
|
106
94
|
selections: selections,
|
107
95
|
variables: variables,
|
108
96
|
path: path,
|
109
|
-
if_type: conditional ? parent_type.graphql_name : nil,
|
110
97
|
boundary: boundary,
|
111
98
|
)
|
112
99
|
else
|
113
|
-
|
114
|
-
|
100
|
+
step.selections.concat(selections)
|
101
|
+
step
|
115
102
|
end
|
116
103
|
end
|
117
104
|
|
105
|
+
ScopePartition = Struct.new(:location, :selections, keyword_init: true)
|
106
|
+
|
118
107
|
# A) Group all root selections by their preferred entrypoint locations.
|
119
108
|
def build_root_entrypoints
|
120
109
|
case @request.operation.operation_type
|
121
|
-
when
|
110
|
+
when QUERY_OP
|
122
111
|
# A.1) Group query fields by location for parallel execution.
|
123
112
|
parent_type = @supergraph.schema.query
|
124
113
|
|
125
114
|
selections_by_location = {}
|
126
|
-
|
115
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
127
116
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
128
117
|
selections_by_location[locations.first] ||= []
|
129
118
|
selections_by_location[locations.first] << node
|
130
119
|
end
|
131
120
|
|
132
121
|
selections_by_location.each do |location, selections|
|
133
|
-
|
122
|
+
add_step(
|
134
123
|
location: location,
|
135
|
-
|
124
|
+
parent_index: ROOT_INDEX,
|
136
125
|
parent_type: parent_type,
|
137
126
|
selections: selections,
|
138
127
|
)
|
139
128
|
end
|
140
129
|
|
141
|
-
when
|
130
|
+
when MUTATION_OP
|
142
131
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
143
132
|
parent_type = @supergraph.schema.mutation
|
144
133
|
|
145
134
|
partitions = []
|
146
|
-
|
135
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
147
136
|
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
|
148
137
|
|
149
|
-
if partitions.none? || partitions.last
|
150
|
-
partitions <<
|
138
|
+
if partitions.none? || partitions.last.location != next_location
|
139
|
+
partitions << ScopePartition.new(location: next_location, selections: [])
|
151
140
|
end
|
152
141
|
|
153
|
-
partitions.last
|
142
|
+
partitions.last.selections << node
|
154
143
|
end
|
155
144
|
|
156
|
-
partitions.reduce(
|
157
|
-
|
158
|
-
location: partition
|
159
|
-
|
145
|
+
partitions.reduce(ROOT_INDEX) do |parent_index, partition|
|
146
|
+
add_step(
|
147
|
+
location: partition.location,
|
148
|
+
parent_index: parent_index,
|
160
149
|
parent_type: parent_type,
|
161
|
-
selections: partition
|
162
|
-
operation_type:
|
163
|
-
).
|
150
|
+
selections: partition.selections,
|
151
|
+
operation_type: MUTATION_OP,
|
152
|
+
).index
|
164
153
|
end
|
165
154
|
|
166
155
|
else
|
@@ -168,7 +157,7 @@ module GraphQL
|
|
168
157
|
end
|
169
158
|
end
|
170
159
|
|
171
|
-
def
|
160
|
+
def each_field_in_scope(parent_type, input_selections, &block)
|
172
161
|
input_selections.each do |node|
|
173
162
|
case node
|
174
163
|
when GraphQL::Language::Nodes::Field
|
@@ -176,12 +165,12 @@ module GraphQL
|
|
176
165
|
|
177
166
|
when GraphQL::Language::Nodes::InlineFragment
|
178
167
|
next unless node.type.nil? || parent_type.graphql_name == node.type.name
|
179
|
-
|
168
|
+
each_field_in_scope(parent_type, node.selections, &block)
|
180
169
|
|
181
170
|
when GraphQL::Language::Nodes::FragmentSpread
|
182
171
|
fragment = @request.fragment_definitions[node.name]
|
183
172
|
next unless parent_type.graphql_name == fragment.type.name
|
184
|
-
|
173
|
+
each_field_in_scope(parent_type, fragment.selections, &block)
|
185
174
|
|
186
175
|
else
|
187
176
|
raise "Unexpected node of type #{node.class.name} in selection set."
|
@@ -193,7 +182,7 @@ module GraphQL
|
|
193
182
|
def extract_locale_selections(
|
194
183
|
current_location,
|
195
184
|
parent_type,
|
196
|
-
|
185
|
+
parent_index,
|
197
186
|
input_selections,
|
198
187
|
path,
|
199
188
|
locale_variables,
|
@@ -210,7 +199,7 @@ module GraphQL
|
|
210
199
|
input_selections.each do |node|
|
211
200
|
case node
|
212
201
|
when GraphQL::Language::Nodes::Field
|
213
|
-
if node.name ==
|
202
|
+
if node.name == TYPENAME
|
214
203
|
locale_selections << node
|
215
204
|
next
|
216
205
|
end
|
@@ -230,7 +219,7 @@ module GraphQL
|
|
230
219
|
locale_selections << node
|
231
220
|
else
|
232
221
|
path.push(node.alias || node.name)
|
233
|
-
selection_set = extract_locale_selections(current_location, field_type,
|
222
|
+
selection_set = extract_locale_selections(current_location, field_type, parent_index, node.selections, path, locale_variables)
|
234
223
|
path.pop
|
235
224
|
|
236
225
|
locale_selections << node.merge(selections: selection_set)
|
@@ -242,7 +231,7 @@ module GraphQL
|
|
242
231
|
|
243
232
|
is_same_scope = fragment_type == parent_type
|
244
233
|
selection_set = is_same_scope ? locale_selections : []
|
245
|
-
extract_locale_selections(current_location, fragment_type,
|
234
|
+
extract_locale_selections(current_location, fragment_type, parent_index, node.selections, path, locale_variables, selection_set)
|
246
235
|
|
247
236
|
unless is_same_scope
|
248
237
|
locale_selections << node.merge(selections: selection_set)
|
@@ -256,7 +245,7 @@ module GraphQL
|
|
256
245
|
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
257
246
|
is_same_scope = fragment_type == parent_type
|
258
247
|
selection_set = is_same_scope ? locale_selections : []
|
259
|
-
extract_locale_selections(current_location, fragment_type,
|
248
|
+
extract_locale_selections(current_location, fragment_type, parent_index, fragment.selections, path, locale_variables, selection_set)
|
260
249
|
|
261
250
|
unless is_same_scope
|
262
251
|
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
@@ -268,10 +257,10 @@ module GraphQL
|
|
268
257
|
end
|
269
258
|
end
|
270
259
|
|
271
|
-
# B.4) Add a `__typename`
|
260
|
+
# B.4) Add a `__typename` hint to abstracts and types that implement
|
272
261
|
# fragments so that resolved type information is available during execution.
|
273
262
|
if requires_typename
|
274
|
-
locale_selections <<
|
263
|
+
locale_selections << SelectionHint.typename_node
|
275
264
|
end
|
276
265
|
|
277
266
|
if remote_selections
|
@@ -285,30 +274,25 @@ module GraphQL
|
|
285
274
|
routes.each_value do |route|
|
286
275
|
route.reduce(locale_selections) do |parent_selections, boundary|
|
287
276
|
# E.1) Add the key of each boundary query into the prior location's selection set.
|
288
|
-
foreign_key =
|
277
|
+
foreign_key = SelectionHint.key(boundary.key)
|
289
278
|
has_key = false
|
290
279
|
has_typename = false
|
291
280
|
|
292
|
-
parent_selections.each do |
|
293
|
-
next unless
|
294
|
-
|
295
|
-
|
296
|
-
has_key = true
|
297
|
-
when TYPENAME_NODE.alias
|
298
|
-
has_typename = true
|
299
|
-
end
|
281
|
+
parent_selections.each do |node|
|
282
|
+
next unless node.is_a?(GraphQL::Language::Nodes::Field)
|
283
|
+
has_key ||= node.alias == foreign_key
|
284
|
+
has_typename ||= node.alias == SelectionHint.typename_node.alias
|
300
285
|
end
|
301
286
|
|
302
|
-
parent_selections <<
|
303
|
-
parent_selections <<
|
287
|
+
parent_selections << SelectionHint.key_node(boundary.key) unless has_key
|
288
|
+
parent_selections << SelectionHint.typename_node unless has_typename
|
304
289
|
|
305
|
-
# E.2) Add a planner
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
parent_order: parent_order,
|
290
|
+
# E.2) Add a planner step for each new entrypoint location.
|
291
|
+
add_step(
|
292
|
+
location: boundary.location,
|
293
|
+
parent_index: parent_index,
|
310
294
|
parent_type: parent_type,
|
311
|
-
selections: remote_selections_by_location[location] || [],
|
295
|
+
selections: remote_selections_by_location[boundary.location] || [],
|
312
296
|
path: path.dup,
|
313
297
|
boundary: boundary,
|
314
298
|
).selections
|
@@ -328,27 +312,13 @@ module GraphQL
|
|
328
312
|
|
329
313
|
expanded_selections = nil
|
330
314
|
input_selections = input_selections.filter_map do |node|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
|
315
|
+
if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != TYPENAME && !local_interface_fields.include?(node.name)
|
316
|
+
expanded_selections ||= []
|
317
|
+
expanded_selections << node
|
318
|
+
nil
|
319
|
+
else
|
320
|
+
node
|
350
321
|
end
|
351
|
-
node
|
352
322
|
end
|
353
323
|
|
354
324
|
if expanded_selections
|
@@ -364,7 +334,7 @@ module GraphQL
|
|
364
334
|
end
|
365
335
|
|
366
336
|
# B.3) Collect all variable definitions used within the filtered selection.
|
367
|
-
# These specify which request variables to pass along with
|
337
|
+
# These specify which request variables to pass along with each step.
|
368
338
|
def extract_node_variables(node_with_args, variable_definitions)
|
369
339
|
node_with_args.arguments.each do |argument|
|
370
340
|
case argument.value
|
@@ -410,15 +380,11 @@ module GraphQL
|
|
410
380
|
|
411
381
|
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
412
382
|
if remote_selections.any?
|
413
|
-
field_count_by_location =
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
memo[location] += 1
|
418
|
-
end
|
383
|
+
field_count_by_location = remote_selections.each_with_object({}) do |node, memo|
|
384
|
+
possible_locations_by_field[node.name].each do |location|
|
385
|
+
memo[location] ||= 0
|
386
|
+
memo[location] += 1
|
419
387
|
end
|
420
|
-
else
|
421
|
-
GraphQL::Stitching::EMPTY_OBJECT
|
422
388
|
end
|
423
389
|
|
424
390
|
remote_selections.each do |node|
|
@@ -426,11 +392,11 @@ module GraphQL
|
|
426
392
|
preferred_location = possible_locations.first
|
427
393
|
|
428
394
|
possible_locations.reduce(0) do |max_availability, possible_location|
|
429
|
-
|
395
|
+
availability = field_count_by_location.fetch(possible_location, 0)
|
430
396
|
|
431
|
-
if
|
397
|
+
if availability > max_availability
|
432
398
|
preferred_location = possible_location
|
433
|
-
|
399
|
+
availability
|
434
400
|
else
|
435
401
|
max_availability
|
436
402
|
end
|
@@ -446,15 +412,15 @@ module GraphQL
|
|
446
412
|
|
447
413
|
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
448
414
|
def expand_abstract_boundaries
|
449
|
-
@
|
450
|
-
next unless
|
415
|
+
@steps_by_entrypoint.each_value do |step|
|
416
|
+
next unless step.boundary
|
451
417
|
|
452
|
-
boundary_type = @supergraph.memoized_schema_types[
|
418
|
+
boundary_type = @supergraph.memoized_schema_types[step.boundary.type_name]
|
453
419
|
next unless boundary_type.kind.abstract?
|
454
|
-
next if boundary_type ==
|
420
|
+
next if boundary_type == step.parent_type
|
455
421
|
|
456
422
|
expanded_selections = nil
|
457
|
-
|
423
|
+
step.selections.reject! do |node|
|
458
424
|
if node.is_a?(GraphQL::Language::Nodes::Field)
|
459
425
|
expanded_selections ||= []
|
460
426
|
expanded_selections << node
|
@@ -463,8 +429,8 @@ module GraphQL
|
|
463
429
|
end
|
464
430
|
|
465
431
|
if expanded_selections
|
466
|
-
type_name = GraphQL::Language::Nodes::TypeName.new(name:
|
467
|
-
|
432
|
+
type_name = GraphQL::Language::Nodes::TypeName.new(name: step.parent_type.graphql_name)
|
433
|
+
step.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
468
434
|
end
|
469
435
|
end
|
470
436
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
# A planned step in the sequence of stitching entrypoints together.
|
6
|
+
# This is a mutable object that may change throughout the planning process.
|
7
|
+
# It ultimately builds an immutable Plan::Op at the end of planning.
|
8
|
+
class PlannerStep
|
9
|
+
GRAPHQL_PRINTER = GraphQL::Language::Printer.new
|
10
|
+
|
11
|
+
attr_reader :index, :location, :parent_type, :operation_type, :path
|
12
|
+
attr_accessor :after, :selections, :variables, :boundary
|
13
|
+
|
14
|
+
def initialize(
|
15
|
+
location:,
|
16
|
+
parent_type:,
|
17
|
+
index:,
|
18
|
+
after: nil,
|
19
|
+
operation_type: "query",
|
20
|
+
selections: [],
|
21
|
+
variables: {},
|
22
|
+
path: [],
|
23
|
+
boundary: nil
|
24
|
+
)
|
25
|
+
@location = location
|
26
|
+
@parent_type = parent_type
|
27
|
+
@index = index
|
28
|
+
@after = after
|
29
|
+
@operation_type = operation_type
|
30
|
+
@selections = selections
|
31
|
+
@variables = variables
|
32
|
+
@path = path
|
33
|
+
@boundary = boundary
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_plan_op
|
37
|
+
GraphQL::Stitching::Plan::Op.new(
|
38
|
+
step: @index,
|
39
|
+
after: @after,
|
40
|
+
location: @location,
|
41
|
+
operation_type: @operation_type,
|
42
|
+
selections: rendered_selections,
|
43
|
+
variables: rendered_variables,
|
44
|
+
path: @path,
|
45
|
+
if_type: type_condition,
|
46
|
+
boundary: @boundary,
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Concrete types going to a boundary report themselves as a type condition.
|
53
|
+
# This is used by the executor to evalute which planned fragment selections
|
54
|
+
# actually apply to the resolved object types.
|
55
|
+
def type_condition
|
56
|
+
@parent_type.graphql_name if @boundary && !parent_type.kind.abstract?
|
57
|
+
end
|
58
|
+
|
59
|
+
def rendered_selections
|
60
|
+
op = GraphQL::Language::Nodes::OperationDefinition.new(operation_type: "", selections: @selections)
|
61
|
+
GRAPHQL_PRINTER.print(op).gsub!(/\s+/, " ").strip!
|
62
|
+
end
|
63
|
+
|
64
|
+
def rendered_variables
|
65
|
+
@variables.each_with_object({}) do |(variable_name, value_type), memo|
|
66
|
+
memo[variable_name] = GRAPHQL_PRINTER.print(value_type)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -5,54 +5,6 @@ module GraphQL
|
|
5
5
|
class Request
|
6
6
|
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
7
|
|
8
|
-
class ApplyRuntimeDirectives < GraphQL::Language::Visitor
|
9
|
-
def initialize(document, variables)
|
10
|
-
@changed = false
|
11
|
-
@variables = variables
|
12
|
-
super(document)
|
13
|
-
end
|
14
|
-
|
15
|
-
def changed?
|
16
|
-
@changed
|
17
|
-
end
|
18
|
-
|
19
|
-
def on_field(node, parent)
|
20
|
-
delete_node = false
|
21
|
-
filtered_directives = if node.directives.any?
|
22
|
-
node.directives.select do |directive|
|
23
|
-
if directive.name == "skip"
|
24
|
-
delete_node = assess_argument_value(directive.arguments.first)
|
25
|
-
false
|
26
|
-
elsif directive.name == "include"
|
27
|
-
delete_node = !assess_argument_value(directive.arguments.first)
|
28
|
-
false
|
29
|
-
else
|
30
|
-
true
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
if delete_node
|
36
|
-
@changed = true
|
37
|
-
super(DELETE_NODE, parent)
|
38
|
-
elsif filtered_directives && filtered_directives.length != node.directives.length
|
39
|
-
@changed = true
|
40
|
-
super(node.merge(directives: filtered_directives), parent)
|
41
|
-
else
|
42
|
-
super
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def assess_argument_value(arg)
|
49
|
-
if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
50
|
-
return @variables[arg.value.name]
|
51
|
-
end
|
52
|
-
arg.value
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
8
|
attr_reader :document, :variables, :operation_name, :context
|
57
9
|
|
58
10
|
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
@@ -96,6 +48,13 @@ module GraphQL
|
|
96
48
|
end
|
97
49
|
end
|
98
50
|
|
51
|
+
def operation_directives
|
52
|
+
@operation_directives ||= if operation.directives.any?
|
53
|
+
printer = GraphQL::Language::Printer.new
|
54
|
+
operation.directives.map { printer.print(_1) }.join(" ")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
99
58
|
def variable_definitions
|
100
59
|
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
101
60
|
memo[v.name] = v.type
|
@@ -114,10 +73,9 @@ module GraphQL
|
|
114
73
|
end
|
115
74
|
|
116
75
|
if @may_contain_runtime_directives
|
117
|
-
|
118
|
-
@document = visitor.visit
|
76
|
+
@document, modified = SkipInclude.render(@document, @variables)
|
119
77
|
|
120
|
-
if
|
78
|
+
if modified
|
121
79
|
@string = nil
|
122
80
|
@digest = nil
|
123
81
|
@operation = nil
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
# Builds hidden selection fields added by stitiching code,
|
6
|
+
# used to request operational data about resolved objects.
|
7
|
+
class SelectionHint
|
8
|
+
HINT_PREFIX = "_STITCH_"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def key?(name)
|
12
|
+
return false unless name
|
13
|
+
|
14
|
+
name.start_with?(HINT_PREFIX)
|
15
|
+
end
|
16
|
+
|
17
|
+
def key(name)
|
18
|
+
"#{HINT_PREFIX}#{name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def key_node(field_name)
|
22
|
+
GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def typename_node
|
26
|
+
@typename_node ||= key_node("__typename")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
|
4
4
|
module GraphQL
|
5
5
|
module Stitching
|
6
|
+
# Shapes the final results payload to the request selection and schema definition.
|
7
|
+
# This eliminates unrequested selection hints and applies null bubbling.
|
6
8
|
class Shaper
|
7
9
|
def initialize(supergraph:, request:)
|
8
10
|
@supergraph = supergraph
|
@@ -19,8 +21,8 @@ module GraphQL
|
|
19
21
|
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
|
20
22
|
return nil if raw_object.nil?
|
21
23
|
|
22
|
-
typename ||= raw_object[
|
23
|
-
raw_object.reject! { |
|
24
|
+
typename ||= raw_object[SelectionHint.typename_node.alias]
|
25
|
+
raw_object.reject! { |key, _v| SelectionHint.key?(key) }
|
24
26
|
|
25
27
|
selections.each do |node|
|
26
28
|
case node
|