graphql-stitching 0.1.0 → 0.2.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 +17 -17
- data/docs/README.md +1 -1
- data/docs/composer.md +45 -4
- data/docs/executor.md +31 -12
- data/docs/images/library.png +0 -0
- data/docs/planner.md +11 -7
- data/docs/request.md +50 -0
- data/docs/supergraph.md +1 -1
- data/graphql-stitching.gemspec +1 -1
- data/lib/graphql/stitching/composer.rb +98 -16
- data/lib/graphql/stitching/executor.rb +88 -45
- data/lib/graphql/stitching/gateway.rb +18 -13
- data/lib/graphql/stitching/planner.rb +204 -151
- data/lib/graphql/stitching/remote_client.rb +4 -4
- data/lib/graphql/stitching/request.rb +133 -0
- data/lib/graphql/stitching/shaper.rb +7 -7
- data/lib/graphql/stitching/supergraph.rb +44 -10
- data/lib/graphql/stitching/util.rb +28 -35
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +7 -1
- metadata +6 -6
- data/docs/document.md +0 -15
- data/lib/graphql/stitching/document.rb +0 -59
@@ -6,9 +6,9 @@ module GraphQL
|
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
7
|
TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
|
8
8
|
|
9
|
-
def initialize(supergraph:,
|
9
|
+
def initialize(supergraph:, request:)
|
10
10
|
@supergraph = supergraph
|
11
|
-
@
|
11
|
+
@request = request
|
12
12
|
@sequence_key = 0
|
13
13
|
@operations_by_grouping = {}
|
14
14
|
end
|
@@ -31,44 +31,18 @@ module GraphQL
|
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
|
38
|
-
end
|
39
|
-
|
40
|
-
grouping = [after_key, location, parent_type.graphql_name, *insertion_path].join("/")
|
41
|
-
|
42
|
-
if op = @operations_by_grouping[grouping]
|
43
|
-
op.selections += selection_set if selection_set
|
44
|
-
op.variables.merge!(variables) if variables
|
45
|
-
return op
|
46
|
-
end
|
47
|
-
|
48
|
-
type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
|
49
|
-
|
50
|
-
@operations_by_grouping[grouping] = PlannerOperation.new(
|
51
|
-
key: parent_key,
|
52
|
-
after_key: after_key,
|
53
|
-
location: location,
|
54
|
-
parent_type: parent_type,
|
55
|
-
operation_type: operation_type,
|
56
|
-
insertion_path: insertion_path,
|
57
|
-
type_condition: type_conditional ? parent_type.graphql_name : nil,
|
58
|
-
selections: selection_set || [],
|
59
|
-
variables: variables || {},
|
60
|
-
boundary: boundary,
|
61
|
-
)
|
62
|
-
end
|
63
|
-
|
34
|
+
# groups root fields by operational strategy:
|
35
|
+
# - query immedaitely groups all root fields by location for async resolution
|
36
|
+
# - mutation groups sequential root fields by location for serial resolution
|
64
37
|
def build_root_operations
|
65
|
-
case @
|
38
|
+
case @request.operation.operation_type
|
66
39
|
when "query"
|
67
|
-
# plan steps grouping all fields by location for async execution
|
68
40
|
parent_type = @supergraph.schema.query
|
69
41
|
|
70
|
-
selections_by_location = @
|
42
|
+
selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
|
71
43
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
44
|
+
|
45
|
+
# root fields currently just delegate to the last location that defined them; this should probably be smarter
|
72
46
|
memo[locations.last] ||= []
|
73
47
|
memo[locations.last] << node
|
74
48
|
end
|
@@ -78,20 +52,19 @@ module GraphQL
|
|
78
52
|
end
|
79
53
|
|
80
54
|
when "mutation"
|
81
|
-
# plan steps grouping sequential fields by location for serial execution
|
82
55
|
parent_type = @supergraph.schema.mutation
|
83
56
|
location_groups = []
|
84
57
|
|
85
|
-
@
|
86
|
-
location
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
}
|
58
|
+
@request.operation.selections.reduce(nil) do |last_location, node|
|
59
|
+
# root fields currently just delegate to the last location that defined them; this should probably be smarter
|
60
|
+
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
|
61
|
+
|
62
|
+
if next_location != last_location
|
63
|
+
location_groups << { location: next_location, selections: [] }
|
92
64
|
end
|
65
|
+
|
93
66
|
location_groups.last[:selections] << node
|
94
|
-
|
67
|
+
next_location
|
95
68
|
end
|
96
69
|
|
97
70
|
location_groups.reduce(0) do |after_key, group|
|
@@ -109,81 +82,114 @@ module GraphQL
|
|
109
82
|
end
|
110
83
|
end
|
111
84
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
85
|
+
# adds an operation (data access) to the plan which maps a data selection to an insertion point.
|
86
|
+
# note that planned operations are NOT always 1:1 with executed requests, as the executor can
|
87
|
+
# frequently batch different insertion points with the same location into a single request.
|
88
|
+
def add_operation(
|
89
|
+
location:,
|
90
|
+
parent_type:,
|
91
|
+
selections:,
|
92
|
+
insertion_path: [],
|
93
|
+
operation_type: "query",
|
94
|
+
after_key: 0,
|
95
|
+
boundary: nil
|
96
|
+
)
|
97
|
+
parent_key = @sequence_key += 1
|
98
|
+
locale_variables = {}
|
99
|
+
locale_selections = if selections.any?
|
100
|
+
extract_locale_selections(location, parent_type, selections, insertion_path, parent_key, locale_variables)
|
101
|
+
else
|
102
|
+
selections
|
103
|
+
end
|
117
104
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
105
|
+
# groupings coalesce similar operation parameters into a single operation
|
106
|
+
# multiple operations per service may still occur with different insertion points,
|
107
|
+
# but those will get query-batched together during execution.
|
108
|
+
grouping = String.new
|
109
|
+
grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
|
110
|
+
grouping = insertion_path.reduce(grouping) do |memo, segment|
|
111
|
+
memo << "/" << segment
|
112
|
+
end
|
113
|
+
|
114
|
+
if op = @operations_by_grouping[grouping]
|
115
|
+
op.selections.concat(locale_selections)
|
116
|
+
op.variables.merge!(locale_variables)
|
117
|
+
op
|
118
|
+
else
|
119
|
+
# concrete types that are not root Query/Mutation report themselves as a type condition
|
120
|
+
# executor must check the __typename of loaded objects to see if they match subsequent operations
|
121
|
+
# this prevents the executor from taking action on unused fragment selections
|
122
|
+
type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
|
123
|
+
|
124
|
+
@operations_by_grouping[grouping] = PlannerOperation.new(
|
125
|
+
key: parent_key,
|
126
|
+
after_key: after_key,
|
127
|
+
location: location,
|
128
|
+
parent_type: parent_type,
|
129
|
+
operation_type: operation_type,
|
130
|
+
insertion_path: insertion_path,
|
131
|
+
type_condition: type_conditional ? parent_type.graphql_name : nil,
|
132
|
+
selections: locale_selections,
|
133
|
+
variables: locale_variables,
|
134
|
+
boundary: boundary,
|
135
|
+
)
|
136
|
+
end
|
137
|
+
end
|
130
138
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
139
|
+
# extracts a selection tree that can all be fulfilled through the current planning location.
|
140
|
+
# adjoining remote selections will fork new insertion points and extract selections at those locations.
|
141
|
+
def extract_locale_selections(current_location, parent_type, input_selections, insertion_path, after_key, locale_variables)
|
142
|
+
remote_selections = nil
|
143
|
+
locale_selections = []
|
144
|
+
implements_fragments = false
|
136
145
|
|
137
|
-
|
138
|
-
|
139
|
-
end
|
140
|
-
end
|
146
|
+
if parent_type.kind.interface?
|
147
|
+
expand_interface_selections(current_location, parent_type, input_selections)
|
141
148
|
end
|
142
149
|
|
143
150
|
input_selections.each do |node|
|
144
151
|
case node
|
145
152
|
when GraphQL::Language::Nodes::Field
|
146
153
|
if node.name == "__typename"
|
147
|
-
|
154
|
+
locale_selections << node
|
148
155
|
next
|
149
156
|
end
|
150
157
|
|
151
158
|
possible_locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
152
159
|
unless possible_locations.include?(current_location)
|
160
|
+
remote_selections ||= []
|
153
161
|
remote_selections << node
|
154
162
|
next
|
155
163
|
end
|
156
164
|
|
157
|
-
field_type = Util.
|
158
|
-
|
159
|
-
extract_node_variables!(node, variables_result)
|
165
|
+
field_type = Util.named_type_for_field_node(@supergraph.schema, parent_type, node)
|
166
|
+
extract_node_variables(node, locale_variables)
|
160
167
|
|
161
168
|
if Util.is_leaf_type?(field_type)
|
162
|
-
|
169
|
+
locale_selections << node
|
163
170
|
else
|
164
|
-
|
165
|
-
selection_set
|
166
|
-
|
167
|
-
|
171
|
+
insertion_path.push(node.alias || node.name)
|
172
|
+
selection_set = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key, locale_variables)
|
173
|
+
insertion_path.pop
|
174
|
+
|
175
|
+
locale_selections << node.merge(selections: selection_set)
|
168
176
|
end
|
169
177
|
|
170
178
|
when GraphQL::Language::Nodes::InlineFragment
|
171
179
|
next unless @supergraph.locations_by_type[node.type.name].include?(current_location)
|
172
180
|
|
173
181
|
fragment_type = @supergraph.schema.types[node.type.name]
|
174
|
-
selection_set
|
175
|
-
|
176
|
-
variables_result.merge!(variables)
|
182
|
+
selection_set = extract_locale_selections(current_location, fragment_type, node.selections, insertion_path, after_key, locale_variables)
|
183
|
+
locale_selections << node.merge(selections: selection_set)
|
177
184
|
implements_fragments = true
|
178
185
|
|
179
186
|
when GraphQL::Language::Nodes::FragmentSpread
|
180
|
-
fragment = @
|
187
|
+
fragment = @request.fragment_definitions[node.name]
|
181
188
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
182
189
|
|
183
190
|
fragment_type = @supergraph.schema.types[fragment.type.name]
|
184
|
-
selection_set
|
185
|
-
|
186
|
-
variables_result.merge!(variables)
|
191
|
+
selection_set = extract_locale_selections(current_location, fragment_type, fragment.selections, insertion_path, after_key, locale_variables)
|
192
|
+
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
187
193
|
implements_fragments = true
|
188
194
|
|
189
195
|
else
|
@@ -191,25 +197,35 @@ module GraphQL
|
|
191
197
|
end
|
192
198
|
end
|
193
199
|
|
194
|
-
if remote_selections
|
195
|
-
|
196
|
-
|
200
|
+
if remote_selections
|
201
|
+
delegate_remote_selections(
|
202
|
+
current_location,
|
203
|
+
parent_type,
|
204
|
+
locale_selections,
|
205
|
+
remote_selections,
|
206
|
+
insertion_path,
|
207
|
+
after_key
|
208
|
+
)
|
197
209
|
end
|
198
210
|
|
211
|
+
# always include a __typename on abstracts and scopes that implement fragments
|
212
|
+
# this provides type information to inspect while shaping the final result
|
199
213
|
if parent_type.kind.abstract? || implements_fragments
|
200
|
-
|
214
|
+
locale_selections << TYPENAME_NODE
|
201
215
|
end
|
202
216
|
|
203
|
-
|
217
|
+
locale_selections
|
204
218
|
end
|
205
219
|
|
206
|
-
|
207
|
-
|
220
|
+
# distributes remote selections across locations,
|
221
|
+
# while spawning new operations for each new fulfillment.
|
222
|
+
def delegate_remote_selections(current_location, parent_type, locale_selections, remote_selections, insertion_path, after_key)
|
223
|
+
possible_locations_by_field = @supergraph.locations_by_type_and_field[parent_type.graphql_name]
|
208
224
|
selections_by_location = {}
|
209
225
|
|
210
226
|
# distribute unique fields among required locations
|
211
|
-
|
212
|
-
possible_locations =
|
227
|
+
remote_selections.reject! do |node|
|
228
|
+
possible_locations = possible_locations_by_field[node.name]
|
213
229
|
if possible_locations.length == 1
|
214
230
|
selections_by_location[possible_locations.first] ||= []
|
215
231
|
selections_by_location[possible_locations.first] << node
|
@@ -217,31 +233,35 @@ module GraphQL
|
|
217
233
|
end
|
218
234
|
end
|
219
235
|
|
220
|
-
# distribute non-unique fields among available locations, preferring used
|
221
|
-
if
|
222
|
-
# weight locations by number of
|
223
|
-
location_weights =
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
236
|
+
# distribute non-unique fields among available locations, preferring locations already used
|
237
|
+
if remote_selections.any?
|
238
|
+
# weight locations by number of required fields available, preferring greater availability
|
239
|
+
location_weights = if remote_selections.length > 1
|
240
|
+
remote_selections.each_with_object({}) do |node, memo|
|
241
|
+
possible_locations = possible_locations_by_field[node.name]
|
242
|
+
possible_locations.each do |location|
|
243
|
+
memo[location] ||= 0
|
244
|
+
memo[location] += 1
|
245
|
+
end
|
228
246
|
end
|
247
|
+
else
|
248
|
+
GraphQL::Stitching::EMPTY_OBJECT
|
229
249
|
end
|
230
250
|
|
231
|
-
|
232
|
-
possible_locations =
|
233
|
-
|
234
|
-
perfect_location_score = input_selections.length
|
251
|
+
remote_selections.each do |node|
|
252
|
+
possible_locations = possible_locations_by_field[node.name]
|
235
253
|
preferred_location_score = 0
|
236
|
-
|
237
|
-
|
238
|
-
|
254
|
+
|
255
|
+
# hill climbing selects highest scoring locations to use
|
256
|
+
preferred_location = possible_locations.reduce(possible_locations.first) do |best_location, possible_location|
|
257
|
+
score = selections_by_location[location] ? remote_selections.length : 0
|
258
|
+
score += location_weights.fetch(possible_location, 0)
|
239
259
|
|
240
260
|
if score > preferred_location_score
|
241
261
|
preferred_location_score = score
|
242
|
-
|
262
|
+
possible_location
|
243
263
|
else
|
244
|
-
|
264
|
+
best_location
|
245
265
|
end
|
246
266
|
end
|
247
267
|
|
@@ -251,70 +271,103 @@ module GraphQL
|
|
251
271
|
end
|
252
272
|
|
253
273
|
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
|
254
|
-
routes.values.each_with_object({}) do |route,
|
274
|
+
routes.values.each_with_object({}) do |route, ops_by_location|
|
255
275
|
route.reduce(nil) do |parent_op, boundary|
|
256
276
|
location = boundary["location"]
|
257
|
-
|
277
|
+
new_operation = false
|
258
278
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
279
|
+
unless op = ops_by_location[location]
|
280
|
+
new_operation = true
|
281
|
+
op = ops_by_location[location] = add_operation(
|
282
|
+
location: location,
|
283
|
+
# routing locations added as intermediaries have no initial selections,
|
284
|
+
# but will be given foreign keys by subsequent operations
|
285
|
+
selections: selections_by_location[location] || [],
|
286
|
+
parent_type: parent_type,
|
287
|
+
insertion_path: insertion_path.dup,
|
288
|
+
boundary: boundary,
|
289
|
+
after_key: after_key,
|
290
|
+
)
|
291
|
+
end
|
292
|
+
|
293
|
+
foreign_key = "_STITCH_#{boundary["selection"]}"
|
294
|
+
parent_selections = parent_op ? parent_op.selections : locale_selections
|
295
|
+
|
296
|
+
if new_operation || parent_selections.none? { _1.is_a?(GraphQL::Language::Nodes::Field) && _1.alias == foreign_key }
|
297
|
+
foreign_key_node = GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["selection"])
|
298
|
+
parent_selections << foreign_key_node << TYPENAME_NODE
|
277
299
|
end
|
278
300
|
|
279
|
-
|
301
|
+
op
|
280
302
|
end
|
281
303
|
end
|
282
|
-
|
283
|
-
parent_selections_result
|
284
304
|
end
|
285
305
|
|
286
|
-
|
287
|
-
|
306
|
+
# extracts variable definitions used by a node
|
307
|
+
# (each operation tracks the specific variables used in its tree)
|
308
|
+
def extract_node_variables(node_with_args, variable_definitions)
|
309
|
+
node_with_args.arguments.each do |argument|
|
288
310
|
case argument.value
|
289
311
|
when GraphQL::Language::Nodes::InputObject
|
290
|
-
extract_node_variables
|
312
|
+
extract_node_variables(argument.value, variable_definitions)
|
291
313
|
when GraphQL::Language::Nodes::VariableIdentifier
|
292
|
-
|
314
|
+
variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
if node_with_args.respond_to?(:directives)
|
319
|
+
node_with_args.directives.each do |directive|
|
320
|
+
extract_node_variables(directive, variable_definitions)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# fields of a merged interface may not belong to the interface at the local level,
|
326
|
+
# so any non-local interface fields get expanded into typed fragments before planning
|
327
|
+
def expand_interface_selections(current_location, parent_type, input_selections)
|
328
|
+
local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
|
329
|
+
|
330
|
+
expanded_selections = nil
|
331
|
+
input_selections.reject! do |node|
|
332
|
+
if node.is_a?(GraphQL::Language::Nodes::Field) && !local_interface_fields.include?(node.name)
|
333
|
+
expanded_selections ||= []
|
334
|
+
expanded_selections << node
|
335
|
+
true
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
if expanded_selections
|
340
|
+
@supergraph.schema.possible_types(parent_type).each do |possible_type|
|
341
|
+
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
342
|
+
|
343
|
+
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
344
|
+
input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
293
345
|
end
|
294
346
|
end
|
295
347
|
end
|
296
348
|
|
297
349
|
# expand concrete type selections into typed fragments when sending to abstract boundaries
|
350
|
+
# this shifts all loose selection fields into a wrapping concrete type fragment
|
298
351
|
def expand_abstract_boundaries
|
299
352
|
@operations_by_grouping.each do |_grouping, op|
|
300
353
|
next unless op.boundary
|
301
354
|
|
302
|
-
boundary_type = @supergraph.schema.
|
355
|
+
boundary_type = @supergraph.schema.types[op.boundary["type_name"]]
|
303
356
|
next unless boundary_type.kind.abstract?
|
357
|
+
next if boundary_type == op.parent_type
|
304
358
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
end
|
359
|
+
expanded_selections = nil
|
360
|
+
op.selections.reject! do |node|
|
361
|
+
if node.is_a?(GraphQL::Language::Nodes::Field)
|
362
|
+
expanded_selections ||= []
|
363
|
+
expanded_selections << node
|
364
|
+
true
|
312
365
|
end
|
366
|
+
end
|
313
367
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
end
|
368
|
+
if expanded_selections
|
369
|
+
type_name = GraphQL::Language::Nodes::TypeName.new(name: op.parent_type.graphql_name)
|
370
|
+
op.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
318
371
|
end
|
319
372
|
end
|
320
373
|
end
|
@@ -9,14 +9,14 @@ module GraphQL
|
|
9
9
|
class RemoteClient
|
10
10
|
def initialize(url:, headers:{})
|
11
11
|
@url = url
|
12
|
-
@headers = headers
|
12
|
+
@headers = { "Content-Type" => "application/json" }.merge!(headers)
|
13
13
|
end
|
14
14
|
|
15
|
-
def call(
|
15
|
+
def call(_location, document, variables, _context)
|
16
16
|
response = Net::HTTP.post(
|
17
17
|
URI(@url),
|
18
|
-
{ "query" => document, "variables" => variables }
|
19
|
-
|
18
|
+
JSON.generate({ "query" => document, "variables" => variables }),
|
19
|
+
@headers,
|
20
20
|
)
|
21
21
|
JSON.parse(response.body)
|
22
22
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Request
|
6
|
+
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
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
|
+
attr_reader :document, :variables, :operation_name, :context
|
57
|
+
|
58
|
+
def initialize(document, operation_name: nil, variables: nil, context: nil)
|
59
|
+
@may_contain_runtime_directives = true
|
60
|
+
|
61
|
+
@document = if document.is_a?(String)
|
62
|
+
@may_contain_runtime_directives = document.include?("@")
|
63
|
+
GraphQL.parse(document)
|
64
|
+
else
|
65
|
+
document
|
66
|
+
end
|
67
|
+
|
68
|
+
@operation_name = operation_name
|
69
|
+
@variables = variables || {}
|
70
|
+
@context = context || GraphQL::Stitching::EMPTY_OBJECT
|
71
|
+
end
|
72
|
+
|
73
|
+
def string
|
74
|
+
@string ||= @document.to_query_string
|
75
|
+
end
|
76
|
+
|
77
|
+
def digest
|
78
|
+
@digest ||= Digest::SHA2.hexdigest(string)
|
79
|
+
end
|
80
|
+
|
81
|
+
def operation
|
82
|
+
@operation ||= begin
|
83
|
+
operation_defs = @document.definitions.select do |d|
|
84
|
+
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
85
|
+
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
86
|
+
@operation_name ? d.name == @operation_name : true
|
87
|
+
end
|
88
|
+
|
89
|
+
if operation_defs.length < 1
|
90
|
+
raise GraphQL::ExecutionError, "Invalid root operation."
|
91
|
+
elsif operation_defs.length > 1
|
92
|
+
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
|
93
|
+
end
|
94
|
+
|
95
|
+
operation_defs.first
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def variable_definitions
|
100
|
+
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
101
|
+
memo[v.name] = v.type
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def fragment_definitions
|
106
|
+
@fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
|
107
|
+
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare!
|
112
|
+
operation.variables.each do |v|
|
113
|
+
@variables[v.name] ||= v.default_value
|
114
|
+
end
|
115
|
+
|
116
|
+
if @may_contain_runtime_directives
|
117
|
+
visitor = ApplyRuntimeDirectives.new(@document, @variables)
|
118
|
+
@document = visitor.visit
|
119
|
+
|
120
|
+
if visitor.changed?
|
121
|
+
@string = nil
|
122
|
+
@digest = nil
|
123
|
+
@operation = nil
|
124
|
+
@variable_definitions = nil
|
125
|
+
@fragment_definitions = nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
self
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|