graphql-stitching 1.0.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|