graphql-stitching 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/graphql/stitching/boundary.rb +28 -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 +14 -14
- data/lib/graphql/stitching/executor/root_source.rb +8 -8
- data/lib/graphql/stitching/executor.rb +3 -3
- data/lib/graphql/stitching/plan.rb +65 -0
- data/lib/graphql/stitching/planner.rb +70 -89
- data/lib/graphql/stitching/planner_step.rb +63 -0
- data/lib/graphql/stitching/request.rb +2 -51
- data/lib/graphql/stitching/selection_hint.rb +29 -0
- data/lib/graphql/stitching/shaper.rb +2 -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,25 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Planner
|
6
6
|
SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
|
7
|
-
|
8
|
-
|
7
|
+
QUERY_OP = "query"
|
8
|
+
MUTATION_OP = "mutation"
|
9
|
+
ROOT_INDEX = 0
|
9
10
|
|
10
11
|
def initialize(supergraph:, request:)
|
11
12
|
@supergraph = supergraph
|
12
13
|
@request = request
|
13
|
-
@
|
14
|
-
@
|
14
|
+
@planning_index = ROOT_INDEX
|
15
|
+
@steps_by_entrypoint = {}
|
15
16
|
end
|
16
17
|
|
17
18
|
def perform
|
18
19
|
build_root_entrypoints
|
19
20
|
expand_abstract_boundaries
|
20
|
-
|
21
|
+
Plan.new(ops: steps.map(&:to_plan_op))
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
@
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
{ "ops" => operations.map!(&:to_h) }
|
24
|
+
def steps
|
25
|
+
@steps_by_entrypoint.values.sort_by!(&:index)
|
29
26
|
end
|
30
27
|
|
31
28
|
private
|
@@ -48,7 +45,7 @@ module GraphQL
|
|
48
45
|
# B.3) Collect all variable definitions used within the filtered selection.
|
49
46
|
# These specify which request variables to pass along with the selection.
|
50
47
|
#
|
51
|
-
# B.4) Add a `__typename` selection to concrete types
|
48
|
+
# B.4) Add a `__typename` selection to abstracts and concrete types that implement
|
52
49
|
# fragments. This provides resolved type information used during execution.
|
53
50
|
#
|
54
51
|
# C) Delegate adjoining selections to new entrypoint locations.
|
@@ -57,7 +54,7 @@ module GraphQL
|
|
57
54
|
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
58
55
|
#
|
59
56
|
# D) Create paths routing to new entrypoint locations via boundary queries.
|
60
|
-
# D.1) Types joining through multiple keys route using
|
57
|
+
# D.1) Types joining through multiple keys route using A* search.
|
61
58
|
# D.2) Types joining through a single key route via quick location match.
|
62
59
|
# (D.2 is an optional optimization of D.1)
|
63
60
|
#
|
@@ -68,38 +65,38 @@ module GraphQL
|
|
68
65
|
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
69
66
|
# **
|
70
67
|
|
71
|
-
# adds
|
72
|
-
def
|
68
|
+
# adds a planning step for fetching and inserting data into the aggregate result.
|
69
|
+
def add_step(
|
73
70
|
location:,
|
74
|
-
|
71
|
+
parent_index:,
|
75
72
|
parent_type:,
|
76
73
|
selections:,
|
77
74
|
variables: {},
|
78
75
|
path: [],
|
79
|
-
operation_type:
|
76
|
+
operation_type: QUERY_OP,
|
80
77
|
boundary: nil
|
81
78
|
)
|
82
79
|
# coalesce repeat parameters into a single entrypoint
|
83
|
-
boundary_key = boundary ? boundary
|
84
|
-
entrypoint = String.new("#{
|
80
|
+
boundary_key = boundary ? boundary.key : "_"
|
81
|
+
entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{boundary_key}")
|
85
82
|
path.each { entrypoint << "/#{_1}" }
|
86
83
|
|
87
|
-
|
88
|
-
|
84
|
+
step = @steps_by_entrypoint[entrypoint]
|
85
|
+
next_index = step ? parent_index : @planning_index += 1
|
89
86
|
|
90
87
|
if selections.any?
|
91
|
-
selections = extract_locale_selections(location, parent_type,
|
88
|
+
selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
|
92
89
|
end
|
93
90
|
|
94
|
-
if
|
91
|
+
if step.nil?
|
95
92
|
# concrete types that are not root Query/Mutation report themselves as a type condition
|
96
93
|
# executor must check the __typename of loaded objects to see if they match subsequent operations
|
97
94
|
# this prevents the executor from taking action on unused fragment selections
|
98
95
|
conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.root_type_for_operation(operation_type)
|
99
96
|
|
100
|
-
@
|
101
|
-
|
102
|
-
after:
|
97
|
+
@steps_by_entrypoint[entrypoint] = PlannerStep.new(
|
98
|
+
index: next_index,
|
99
|
+
after: parent_index,
|
103
100
|
location: location,
|
104
101
|
parent_type: parent_type,
|
105
102
|
operation_type: operation_type,
|
@@ -110,57 +107,59 @@ module GraphQL
|
|
110
107
|
boundary: boundary,
|
111
108
|
)
|
112
109
|
else
|
113
|
-
|
114
|
-
|
110
|
+
step.selections.concat(selections)
|
111
|
+
step
|
115
112
|
end
|
116
113
|
end
|
117
114
|
|
115
|
+
ScopePartition = Struct.new(:location, :selections, keyword_init: true)
|
116
|
+
|
118
117
|
# A) Group all root selections by their preferred entrypoint locations.
|
119
118
|
def build_root_entrypoints
|
120
119
|
case @request.operation.operation_type
|
121
|
-
when
|
120
|
+
when QUERY_OP
|
122
121
|
# A.1) Group query fields by location for parallel execution.
|
123
122
|
parent_type = @supergraph.schema.query
|
124
123
|
|
125
124
|
selections_by_location = {}
|
126
|
-
|
125
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
127
126
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
128
127
|
selections_by_location[locations.first] ||= []
|
129
128
|
selections_by_location[locations.first] << node
|
130
129
|
end
|
131
130
|
|
132
131
|
selections_by_location.each do |location, selections|
|
133
|
-
|
132
|
+
add_step(
|
134
133
|
location: location,
|
135
|
-
|
134
|
+
parent_index: ROOT_INDEX,
|
136
135
|
parent_type: parent_type,
|
137
136
|
selections: selections,
|
138
137
|
)
|
139
138
|
end
|
140
139
|
|
141
|
-
when
|
140
|
+
when MUTATION_OP
|
142
141
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
143
142
|
parent_type = @supergraph.schema.mutation
|
144
143
|
|
145
144
|
partitions = []
|
146
|
-
|
145
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
147
146
|
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
|
148
147
|
|
149
|
-
if partitions.none? || partitions.last
|
150
|
-
partitions <<
|
148
|
+
if partitions.none? || partitions.last.location != next_location
|
149
|
+
partitions << ScopePartition.new(location: next_location, selections: [])
|
151
150
|
end
|
152
151
|
|
153
|
-
partitions.last
|
152
|
+
partitions.last.selections << node
|
154
153
|
end
|
155
154
|
|
156
|
-
partitions.reduce(
|
157
|
-
|
158
|
-
location: partition
|
159
|
-
|
155
|
+
partitions.reduce(ROOT_INDEX) do |parent_index, partition|
|
156
|
+
add_step(
|
157
|
+
location: partition.location,
|
158
|
+
parent_index: parent_index,
|
160
159
|
parent_type: parent_type,
|
161
|
-
selections: partition
|
162
|
-
operation_type:
|
163
|
-
).
|
160
|
+
selections: partition.selections,
|
161
|
+
operation_type: MUTATION_OP,
|
162
|
+
).index
|
164
163
|
end
|
165
164
|
|
166
165
|
else
|
@@ -168,7 +167,7 @@ module GraphQL
|
|
168
167
|
end
|
169
168
|
end
|
170
169
|
|
171
|
-
def
|
170
|
+
def each_field_in_scope(parent_type, input_selections, &block)
|
172
171
|
input_selections.each do |node|
|
173
172
|
case node
|
174
173
|
when GraphQL::Language::Nodes::Field
|
@@ -176,12 +175,12 @@ module GraphQL
|
|
176
175
|
|
177
176
|
when GraphQL::Language::Nodes::InlineFragment
|
178
177
|
next unless node.type.nil? || parent_type.graphql_name == node.type.name
|
179
|
-
|
178
|
+
each_field_in_scope(parent_type, node.selections, &block)
|
180
179
|
|
181
180
|
when GraphQL::Language::Nodes::FragmentSpread
|
182
181
|
fragment = @request.fragment_definitions[node.name]
|
183
182
|
next unless parent_type.graphql_name == fragment.type.name
|
184
|
-
|
183
|
+
each_field_in_scope(parent_type, fragment.selections, &block)
|
185
184
|
|
186
185
|
else
|
187
186
|
raise "Unexpected node of type #{node.class.name} in selection set."
|
@@ -193,7 +192,7 @@ module GraphQL
|
|
193
192
|
def extract_locale_selections(
|
194
193
|
current_location,
|
195
194
|
parent_type,
|
196
|
-
|
195
|
+
parent_index,
|
197
196
|
input_selections,
|
198
197
|
path,
|
199
198
|
locale_variables,
|
@@ -230,7 +229,7 @@ module GraphQL
|
|
230
229
|
locale_selections << node
|
231
230
|
else
|
232
231
|
path.push(node.alias || node.name)
|
233
|
-
selection_set = extract_locale_selections(current_location, field_type,
|
232
|
+
selection_set = extract_locale_selections(current_location, field_type, parent_index, node.selections, path, locale_variables)
|
234
233
|
path.pop
|
235
234
|
|
236
235
|
locale_selections << node.merge(selections: selection_set)
|
@@ -242,7 +241,7 @@ module GraphQL
|
|
242
241
|
|
243
242
|
is_same_scope = fragment_type == parent_type
|
244
243
|
selection_set = is_same_scope ? locale_selections : []
|
245
|
-
extract_locale_selections(current_location, fragment_type,
|
244
|
+
extract_locale_selections(current_location, fragment_type, parent_index, node.selections, path, locale_variables, selection_set)
|
246
245
|
|
247
246
|
unless is_same_scope
|
248
247
|
locale_selections << node.merge(selections: selection_set)
|
@@ -256,7 +255,7 @@ module GraphQL
|
|
256
255
|
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
257
256
|
is_same_scope = fragment_type == parent_type
|
258
257
|
selection_set = is_same_scope ? locale_selections : []
|
259
|
-
extract_locale_selections(current_location, fragment_type,
|
258
|
+
extract_locale_selections(current_location, fragment_type, parent_index, fragment.selections, path, locale_variables, selection_set)
|
260
259
|
|
261
260
|
unless is_same_scope
|
262
261
|
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
@@ -268,10 +267,10 @@ module GraphQL
|
|
268
267
|
end
|
269
268
|
end
|
270
269
|
|
271
|
-
# B.4) Add a `__typename` selection to concrete types
|
270
|
+
# B.4) Add a `__typename` selection to abstracts and concrete types that implement
|
272
271
|
# fragments so that resolved type information is available during execution.
|
273
272
|
if requires_typename
|
274
|
-
locale_selections <<
|
273
|
+
locale_selections << SelectionHint.typename_node
|
275
274
|
end
|
276
275
|
|
277
276
|
if remote_selections
|
@@ -285,28 +284,24 @@ module GraphQL
|
|
285
284
|
routes.each_value do |route|
|
286
285
|
route.reduce(locale_selections) do |parent_selections, boundary|
|
287
286
|
# E.1) Add the key of each boundary query into the prior location's selection set.
|
288
|
-
foreign_key =
|
287
|
+
foreign_key = SelectionHint.key(boundary.key)
|
289
288
|
has_key = false
|
290
289
|
has_typename = false
|
291
290
|
|
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
|
291
|
+
parent_selections.each do |node|
|
292
|
+
next unless node.is_a?(GraphQL::Language::Nodes::Field)
|
293
|
+
has_key ||= node.alias == foreign_key
|
294
|
+
has_typename ||= node.alias == SelectionHint.typename_node.alias
|
300
295
|
end
|
301
296
|
|
302
|
-
parent_selections <<
|
303
|
-
parent_selections <<
|
297
|
+
parent_selections << SelectionHint.key_node(boundary.key) unless has_key
|
298
|
+
parent_selections << SelectionHint.typename_node unless has_typename
|
304
299
|
|
305
300
|
# E.2) Add a planner operation for each new entrypoint location.
|
306
|
-
location = boundary
|
307
|
-
|
301
|
+
location = boundary.location
|
302
|
+
add_step(
|
308
303
|
location: location,
|
309
|
-
|
304
|
+
parent_index: parent_index,
|
310
305
|
parent_type: parent_type,
|
311
306
|
selections: remote_selections_by_location[location] || [],
|
312
307
|
path: path.dup,
|
@@ -328,27 +323,13 @@ module GraphQL
|
|
328
323
|
|
329
324
|
expanded_selections = nil
|
330
325
|
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
|
-
|
326
|
+
if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
|
327
|
+
expanded_selections ||= []
|
328
|
+
expanded_selections << node
|
329
|
+
nil
|
330
|
+
else
|
331
|
+
node
|
350
332
|
end
|
351
|
-
node
|
352
333
|
end
|
353
334
|
|
354
335
|
if expanded_selections
|
@@ -446,10 +427,10 @@ module GraphQL
|
|
446
427
|
|
447
428
|
# F) Wrap concrete selections targeting abstract boundaries in typed fragments.
|
448
429
|
def expand_abstract_boundaries
|
449
|
-
@
|
430
|
+
@steps_by_entrypoint.each_value do |op|
|
450
431
|
next unless op.boundary
|
451
432
|
|
452
|
-
boundary_type = @supergraph.memoized_schema_types[op.boundary
|
433
|
+
boundary_type = @supergraph.memoized_schema_types[op.boundary.type_name]
|
453
434
|
next unless boundary_type.kind.abstract?
|
454
435
|
next if boundary_type == op.parent_type
|
455
436
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class PlannerStep
|
6
|
+
GRAPHQL_PRINTER = GraphQL::Language::Printer.new
|
7
|
+
|
8
|
+
attr_reader :index, :location, :parent_type, :if_type, :operation_type, :path
|
9
|
+
attr_accessor :after, :selections, :variables, :boundary
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
location:,
|
13
|
+
parent_type:,
|
14
|
+
index:,
|
15
|
+
after: nil,
|
16
|
+
operation_type: "query",
|
17
|
+
selections: [],
|
18
|
+
variables: {},
|
19
|
+
path: [],
|
20
|
+
if_type: nil,
|
21
|
+
boundary: nil
|
22
|
+
)
|
23
|
+
@location = location
|
24
|
+
@parent_type = parent_type
|
25
|
+
@index = index
|
26
|
+
@after = after
|
27
|
+
@operation_type = operation_type
|
28
|
+
@selections = selections
|
29
|
+
@variables = variables
|
30
|
+
@path = path
|
31
|
+
@if_type = if_type
|
32
|
+
@boundary = boundary
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_plan_op
|
36
|
+
GraphQL::Stitching::Plan::Op.new(
|
37
|
+
step: @index,
|
38
|
+
after: @after,
|
39
|
+
location: @location,
|
40
|
+
operation_type: @operation_type,
|
41
|
+
selections: rendered_selections,
|
42
|
+
variables: rendered_variables,
|
43
|
+
path: @path,
|
44
|
+
if_type: @if_type,
|
45
|
+
boundary: @boundary,
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def rendered_selections
|
52
|
+
op = GraphQL::Language::Nodes::OperationDefinition.new(operation_type: "", selections: @selections)
|
53
|
+
GRAPHQL_PRINTER.print(op).gsub!(/\s+/, " ").strip!
|
54
|
+
end
|
55
|
+
|
56
|
+
def rendered_variables
|
57
|
+
@variables.each_with_object({}) do |(variable_name, value_type), memo|
|
58
|
+
memo[variable_name] = GRAPHQL_PRINTER.print(value_type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
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)
|
@@ -114,10 +66,9 @@ module GraphQL
|
|
114
66
|
end
|
115
67
|
|
116
68
|
if @may_contain_runtime_directives
|
117
|
-
|
118
|
-
@document = visitor.visit
|
69
|
+
@document, modified = SkipInclude.render(@document, @variables)
|
119
70
|
|
120
|
-
if
|
71
|
+
if modified
|
121
72
|
@string = nil
|
122
73
|
@digest = nil
|
123
74
|
@operation = nil
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class SelectionHint
|
6
|
+
HINT_PREFIX = "_STITCH_"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def key?(name)
|
10
|
+
return false unless name
|
11
|
+
|
12
|
+
name.start_with?(HINT_PREFIX)
|
13
|
+
end
|
14
|
+
|
15
|
+
def key(name)
|
16
|
+
"#{HINT_PREFIX}#{name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def key_node(field_name)
|
20
|
+
GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def typename_node
|
24
|
+
@typename_node ||= key_node("__typename")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -19,8 +19,8 @@ module GraphQL
|
|
19
19
|
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
|
20
20
|
return nil if raw_object.nil?
|
21
21
|
|
22
|
-
typename ||= raw_object[
|
23
|
-
raw_object.reject! { |
|
22
|
+
typename ||= raw_object[SelectionHint.typename_node.alias]
|
23
|
+
raw_object.reject! { |key, _v| SelectionHint.key?(key) }
|
24
24
|
|
25
25
|
selections.each do |node|
|
26
26
|
case node
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class SkipInclude
|
6
|
+
class << self
|
7
|
+
# Faster implementation of an AST visitor for prerendering
|
8
|
+
# @skip and @include conditional directives into a document.
|
9
|
+
# This avoids unnecessary planning steps, and prepares result shaping.
|
10
|
+
def render(document, variables)
|
11
|
+
changed = false
|
12
|
+
definitions = document.definitions.map do |original_definition|
|
13
|
+
definition = render_node(original_definition, variables)
|
14
|
+
changed ||= definition.object_id != original_definition.object_id
|
15
|
+
definition
|
16
|
+
end
|
17
|
+
|
18
|
+
return document.merge(definitions: definitions), changed
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def render_node(parent_node, variables)
|
24
|
+
changed = false
|
25
|
+
filtered_selections = parent_node.selections.filter_map do |original_node|
|
26
|
+
node = prune_node(original_node, variables)
|
27
|
+
if node.nil?
|
28
|
+
changed = true
|
29
|
+
next nil
|
30
|
+
end
|
31
|
+
|
32
|
+
node = render_node(node, variables) if node.selections.any?
|
33
|
+
changed ||= node.object_id != original_node.object_id
|
34
|
+
node
|
35
|
+
end
|
36
|
+
|
37
|
+
if filtered_selections.none?
|
38
|
+
filtered_selections << SelectionHint.typename_node
|
39
|
+
end
|
40
|
+
|
41
|
+
if changed
|
42
|
+
parent_node.merge(selections: filtered_selections)
|
43
|
+
else
|
44
|
+
parent_node
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def prune_node(node, variables)
|
49
|
+
return node unless node.directives.any?
|
50
|
+
|
51
|
+
delete_node = false
|
52
|
+
filtered_directives = node.directives.reject do |directive|
|
53
|
+
if directive.name == "skip"
|
54
|
+
delete_node = assess_condition(directive.arguments.first, variables)
|
55
|
+
true
|
56
|
+
elsif directive.name == "include"
|
57
|
+
delete_node = !assess_condition(directive.arguments.first, variables)
|
58
|
+
true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if delete_node
|
63
|
+
nil
|
64
|
+
elsif filtered_directives.length != node.directives.length
|
65
|
+
node.merge(directives: filtered_directives)
|
66
|
+
else
|
67
|
+
node
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def assess_condition(arg, variables)
|
72
|
+
if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
73
|
+
variables[arg.value.name] || variables[arg.value.name.to_sym]
|
74
|
+
else
|
75
|
+
arg.value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|