graphql-stitching 1.0.0 → 1.0.1
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 +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
|