graphql-stitching 0.3.6 → 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 +77 -12
- data/docs/README.md +1 -1
- data/docs/client.md +103 -0
- data/docs/composer.md +2 -2
- data/docs/supergraph.md +1 -1
- data/example/gateway.rb +4 -4
- data/lib/graphql/stitching/boundary.rb +28 -0
- data/lib/graphql/stitching/{gateway.rb → client.rb} +12 -12
- 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 +49 -29
- data/lib/graphql/stitching/executor/boundary_source.rb +199 -0
- data/lib/graphql/stitching/executor/root_source.rb +48 -0
- data/lib/graphql/stitching/executor.rb +6 -229
- data/lib/graphql/stitching/{remote_client.rb → http_executable.rb} +1 -1
- 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 +8 -4
- metadata +12 -6
- data/docs/gateway.md +0 -103
- data/lib/graphql/stitching/planner_operation.rb +0 -63
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Executor::BoundarySource < GraphQL::Dataloader::Source
|
6
|
+
def initialize(executor, location)
|
7
|
+
@executor = executor
|
8
|
+
@location = location
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch(ops)
|
12
|
+
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
|
13
|
+
origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
|
14
|
+
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
15
|
+
end
|
16
|
+
|
17
|
+
if op.if_type
|
18
|
+
# operations planned around unused fragment conditions should not trigger requests
|
19
|
+
origin_set.select! { _1[SelectionHint.typename_node.alias] == op.if_type }
|
20
|
+
end
|
21
|
+
|
22
|
+
memo[op] = origin_set if origin_set.any?
|
23
|
+
end
|
24
|
+
|
25
|
+
if origin_sets_by_operation.any?
|
26
|
+
query_document, variable_names = build_document(origin_sets_by_operation, @executor.request.operation_name)
|
27
|
+
variables = @executor.request.variables.slice(*variable_names)
|
28
|
+
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
|
29
|
+
@executor.query_count += 1
|
30
|
+
|
31
|
+
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
32
|
+
|
33
|
+
errors = raw_result.dig("errors")
|
34
|
+
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Builds batched boundary queries
|
41
|
+
# "query MyOperation_2_3($var:VarType) {
|
42
|
+
# _0_result: list(keys:["a","b","c"]) { boundarySelections... }
|
43
|
+
# _1_0_result: item(key:"x") { boundarySelections... }
|
44
|
+
# _1_1_result: item(key:"y") { boundarySelections... }
|
45
|
+
# _1_2_result: item(key:"z") { boundarySelections... }
|
46
|
+
# }"
|
47
|
+
def build_document(origin_sets_by_operation, operation_name = nil)
|
48
|
+
variable_defs = {}
|
49
|
+
query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
|
50
|
+
variable_defs.merge!(op.variables)
|
51
|
+
boundary = op.boundary
|
52
|
+
|
53
|
+
if boundary.list
|
54
|
+
input = origin_set.each_with_index.reduce(String.new) do |memo, (origin_obj, index)|
|
55
|
+
memo << "," if index > 0
|
56
|
+
memo << build_key(boundary.key, origin_obj, federation: boundary.federation)
|
57
|
+
memo
|
58
|
+
end
|
59
|
+
|
60
|
+
"_#{batch_index}_result: #{boundary.field}(#{boundary.arg}:[#{input}]) #{op.selections}"
|
61
|
+
else
|
62
|
+
origin_set.map.with_index do |origin_obj, index|
|
63
|
+
input = build_key(boundary.key, origin_obj, federation: boundary.federation)
|
64
|
+
"_#{batch_index}_#{index}_result: #{boundary.field}(#{boundary.arg}:#{input}) #{op.selections}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
doc = String.new("query") # << boundary fulfillment always uses query
|
70
|
+
|
71
|
+
if operation_name
|
72
|
+
doc << " #{operation_name}"
|
73
|
+
origin_sets_by_operation.each_key do |op|
|
74
|
+
doc << "_#{op.step}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if variable_defs.any?
|
79
|
+
variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
|
80
|
+
doc << "(#{variable_str})"
|
81
|
+
end
|
82
|
+
|
83
|
+
doc << "{ #{query_fields.join(" ")} }"
|
84
|
+
|
85
|
+
return doc, variable_defs.keys
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_key(key, origin_obj, federation: false)
|
89
|
+
key_value = JSON.generate(origin_obj[SelectionHint.key(key)])
|
90
|
+
if federation
|
91
|
+
"{ __typename: \"#{origin_obj[SelectionHint.typename_node.alias]}\", #{key}: #{key_value} }"
|
92
|
+
else
|
93
|
+
key_value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def merge_results!(origin_sets_by_operation, raw_result)
|
98
|
+
return unless raw_result
|
99
|
+
|
100
|
+
origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
|
101
|
+
results = if op.dig("boundary", "list")
|
102
|
+
raw_result["_#{batch_index}_result"]
|
103
|
+
else
|
104
|
+
origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
|
105
|
+
end
|
106
|
+
|
107
|
+
next unless results&.any?
|
108
|
+
|
109
|
+
origin_set.each_with_index do |origin_obj, index|
|
110
|
+
origin_obj.merge!(results[index]) if results[index]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# https://spec.graphql.org/June2018/#sec-Errors
|
116
|
+
def extract_errors!(origin_sets_by_operation, errors)
|
117
|
+
ops = origin_sets_by_operation.keys
|
118
|
+
origin_sets = origin_sets_by_operation.values
|
119
|
+
pathed_errors_by_op_index_and_object_id = {}
|
120
|
+
|
121
|
+
errors_result = errors.each_with_object([]) do |err, memo|
|
122
|
+
err.delete("locations")
|
123
|
+
path = err["path"]
|
124
|
+
|
125
|
+
if path && path.length > 0
|
126
|
+
result_alias = /^_(\d+)(?:_(\d+))?_result$/.match(path.first.to_s)
|
127
|
+
|
128
|
+
if result_alias
|
129
|
+
path = err["path"] = path[1..-1]
|
130
|
+
|
131
|
+
origin_obj = if result_alias[2]
|
132
|
+
origin_sets.dig(result_alias[1].to_i, result_alias[2].to_i)
|
133
|
+
elsif path[0].is_a?(Integer) || /\d+/.match?(path[0].to_s)
|
134
|
+
origin_sets.dig(result_alias[1].to_i, path.shift.to_i)
|
135
|
+
end
|
136
|
+
|
137
|
+
if origin_obj
|
138
|
+
by_op_index = pathed_errors_by_op_index_and_object_id[result_alias[1].to_i] ||= {}
|
139
|
+
by_object_id = by_op_index[origin_obj.object_id] ||= []
|
140
|
+
by_object_id << err
|
141
|
+
next
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
memo << err
|
147
|
+
end
|
148
|
+
|
149
|
+
if pathed_errors_by_op_index_and_object_id.any?
|
150
|
+
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
151
|
+
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
|
152
|
+
errors_result.concat(pathed_errors_by_object_id.values)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
errors_result.flatten!
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# traverse forward through origin data, expanding arrays to follow all paths
|
161
|
+
# any errors found for an origin object_id have their path prefixed by the object path
|
162
|
+
def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
|
163
|
+
current_path.push(forward_path.shift)
|
164
|
+
scope = root[current_path.last]
|
165
|
+
|
166
|
+
if forward_path.any? && scope.is_a?(Array)
|
167
|
+
scope.each_with_index do |element, index|
|
168
|
+
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
169
|
+
inner_elements.each do |inner_element|
|
170
|
+
current_path << index
|
171
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
172
|
+
current_path.pop
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
elsif forward_path.any?
|
177
|
+
current_path << index
|
178
|
+
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
179
|
+
current_path.pop
|
180
|
+
|
181
|
+
elsif scope.is_a?(Array)
|
182
|
+
scope.each_with_index do |element, index|
|
183
|
+
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
184
|
+
inner_elements.each do |inner_element|
|
185
|
+
errors = pathed_errors_by_object_id[inner_element.object_id]
|
186
|
+
errors.each { _1["path"] = [*current_path, index, *_1["path"]] } if errors
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
else
|
191
|
+
errors = pathed_errors_by_object_id[scope.object_id]
|
192
|
+
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
193
|
+
end
|
194
|
+
|
195
|
+
forward_path.unshift(current_path.pop)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Executor::RootSource < GraphQL::Dataloader::Source
|
6
|
+
def initialize(executor, location)
|
7
|
+
@executor = executor
|
8
|
+
@location = location
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch(ops)
|
12
|
+
op = ops.first # There should only ever be one per location at a time
|
13
|
+
|
14
|
+
query_document = build_document(op, @executor.request.operation_name)
|
15
|
+
query_variables = @executor.request.variables.slice(*op.variables.keys)
|
16
|
+
result = @executor.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request.context)
|
17
|
+
@executor.query_count += 1
|
18
|
+
|
19
|
+
@executor.data.merge!(result["data"]) if result["data"]
|
20
|
+
if result["errors"]&.any?
|
21
|
+
result["errors"].each { _1.delete("locations") }
|
22
|
+
@executor.errors.concat(result["errors"])
|
23
|
+
end
|
24
|
+
|
25
|
+
ops.map(&:step)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Builds root source documents
|
29
|
+
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
30
|
+
def build_document(op, operation_name = nil)
|
31
|
+
doc = String.new
|
32
|
+
doc << op.operation_type
|
33
|
+
|
34
|
+
if operation_name
|
35
|
+
doc << " #{operation_name}_#{op.step}"
|
36
|
+
end
|
37
|
+
|
38
|
+
if op.variables.any?
|
39
|
+
variable_defs = op.variables.map { |k, v| "$#{k}:#{v}" }.join(",")
|
40
|
+
doc << "(#{variable_defs})"
|
41
|
+
end
|
42
|
+
|
43
|
+
doc << op.selections
|
44
|
+
doc
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -5,239 +5,13 @@ require "json"
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
7
|
class Executor
|
8
|
-
|
9
|
-
class RootSource < GraphQL::Dataloader::Source
|
10
|
-
def initialize(executor, location)
|
11
|
-
@executor = executor
|
12
|
-
@location = location
|
13
|
-
end
|
14
|
-
|
15
|
-
def fetch(ops)
|
16
|
-
op = ops.first # There should only ever be one per location at a time
|
17
|
-
|
18
|
-
query_document = build_document(op, @executor.request.operation_name)
|
19
|
-
query_variables = @executor.request.variables.slice(*op["variables"].keys)
|
20
|
-
result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
|
21
|
-
@executor.query_count += 1
|
22
|
-
|
23
|
-
@executor.data.merge!(result["data"]) if result["data"]
|
24
|
-
if result["errors"]&.any?
|
25
|
-
result["errors"].each { _1.delete("locations") }
|
26
|
-
@executor.errors.concat(result["errors"])
|
27
|
-
end
|
28
|
-
|
29
|
-
ops.map { op["order"] }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Builds root source documents
|
33
|
-
# "query MyOperation_1($var:VarType) { rootSelections ... }"
|
34
|
-
def build_document(op, operation_name = nil)
|
35
|
-
doc = String.new
|
36
|
-
doc << op["operation_type"]
|
37
|
-
|
38
|
-
if operation_name
|
39
|
-
doc << " #{operation_name}_#{op["order"]}"
|
40
|
-
end
|
41
|
-
|
42
|
-
if op["variables"].any?
|
43
|
-
variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
|
44
|
-
doc << "(#{variable_defs})"
|
45
|
-
end
|
46
|
-
|
47
|
-
doc << op["selections"]
|
48
|
-
doc
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class BoundarySource < GraphQL::Dataloader::Source
|
53
|
-
def initialize(executor, location)
|
54
|
-
@executor = executor
|
55
|
-
@location = location
|
56
|
-
end
|
57
|
-
|
58
|
-
def fetch(ops)
|
59
|
-
origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
|
60
|
-
origin_set = op["path"].reduce([@executor.data]) do |set, path_segment|
|
61
|
-
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
62
|
-
end
|
63
|
-
|
64
|
-
if op["if_type"]
|
65
|
-
# operations planned around unused fragment conditions should not trigger requests
|
66
|
-
origin_set.select! { _1["_STITCH_typename"] == op["if_type"] }
|
67
|
-
end
|
68
|
-
|
69
|
-
memo[op] = origin_set if origin_set.any?
|
70
|
-
end
|
71
|
-
|
72
|
-
if origin_sets_by_operation.any?
|
73
|
-
query_document, variable_names = build_document(origin_sets_by_operation, @executor.request.operation_name)
|
74
|
-
variables = @executor.request.variables.slice(*variable_names)
|
75
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
|
76
|
-
@executor.query_count += 1
|
77
|
-
|
78
|
-
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
79
|
-
|
80
|
-
errors = raw_result.dig("errors")
|
81
|
-
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors&.any?
|
82
|
-
end
|
83
|
-
|
84
|
-
ops.map { origin_sets_by_operation[_1] ? _1["order"] : nil }
|
85
|
-
end
|
86
|
-
|
87
|
-
# Builds batched boundary queries
|
88
|
-
# "query MyOperation_2_3($var:VarType) {
|
89
|
-
# _0_result: list(keys:["a","b","c"]) { boundarySelections... }
|
90
|
-
# _1_0_result: item(key:"x") { boundarySelections... }
|
91
|
-
# _1_1_result: item(key:"y") { boundarySelections... }
|
92
|
-
# _1_2_result: item(key:"z") { boundarySelections... }
|
93
|
-
# }"
|
94
|
-
def build_document(origin_sets_by_operation, operation_name = nil)
|
95
|
-
variable_defs = {}
|
96
|
-
query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
|
97
|
-
variable_defs.merge!(op["variables"])
|
98
|
-
boundary = op["boundary"]
|
99
|
-
key_selection = "_STITCH_#{boundary["key"]}"
|
100
|
-
|
101
|
-
if boundary["list"]
|
102
|
-
input = JSON.generate(origin_set.map { _1[key_selection] })
|
103
|
-
"_#{batch_index}_result: #{boundary["field"]}(#{boundary["arg"]}:#{input}) #{op["selections"]}"
|
104
|
-
else
|
105
|
-
origin_set.map.with_index do |origin_obj, index|
|
106
|
-
input = JSON.generate(origin_obj[key_selection])
|
107
|
-
"_#{batch_index}_#{index}_result: #{boundary["field"]}(#{boundary["arg"]}:#{input}) #{op["selections"]}"
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
doc = String.new
|
113
|
-
doc << "query" # << boundary fulfillment always uses query
|
114
|
-
|
115
|
-
if operation_name
|
116
|
-
doc << " #{operation_name}"
|
117
|
-
origin_sets_by_operation.each_key do |op|
|
118
|
-
doc << "_#{op["order"]}"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
if variable_defs.any?
|
123
|
-
variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
|
124
|
-
doc << "(#{variable_str})"
|
125
|
-
end
|
126
|
-
|
127
|
-
doc << "{ #{query_fields.join(" ")} }"
|
128
|
-
|
129
|
-
return doc, variable_defs.keys
|
130
|
-
end
|
131
|
-
|
132
|
-
def merge_results!(origin_sets_by_operation, raw_result)
|
133
|
-
return unless raw_result
|
134
|
-
|
135
|
-
origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
|
136
|
-
results = if op.dig("boundary", "list")
|
137
|
-
raw_result["_#{batch_index}_result"]
|
138
|
-
else
|
139
|
-
origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
|
140
|
-
end
|
141
|
-
|
142
|
-
next unless results&.any?
|
143
|
-
|
144
|
-
origin_set.each_with_index do |origin_obj, index|
|
145
|
-
origin_obj.merge!(results[index]) if results[index]
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
# https://spec.graphql.org/June2018/#sec-Errors
|
151
|
-
def extract_errors!(origin_sets_by_operation, errors)
|
152
|
-
ops = origin_sets_by_operation.keys
|
153
|
-
origin_sets = origin_sets_by_operation.values
|
154
|
-
pathed_errors_by_op_index_and_object_id = {}
|
155
|
-
|
156
|
-
errors_result = errors.each_with_object([]) do |err, memo|
|
157
|
-
err.delete("locations")
|
158
|
-
path = err["path"]
|
159
|
-
|
160
|
-
if path && path.length > 0
|
161
|
-
result_alias = /^_(\d+)(?:_(\d+))?_result$/.match(path.first.to_s)
|
162
|
-
|
163
|
-
if result_alias
|
164
|
-
path = err["path"] = path[1..-1]
|
165
|
-
|
166
|
-
origin_obj = if result_alias[2]
|
167
|
-
origin_sets.dig(result_alias[1].to_i, result_alias[2].to_i)
|
168
|
-
elsif path[0].is_a?(Integer) || /\d+/.match?(path[0].to_s)
|
169
|
-
origin_sets.dig(result_alias[1].to_i, path.shift.to_i)
|
170
|
-
end
|
171
|
-
|
172
|
-
if origin_obj
|
173
|
-
by_op_index = pathed_errors_by_op_index_and_object_id[result_alias[1].to_i] ||= {}
|
174
|
-
by_object_id = by_op_index[origin_obj.object_id] ||= []
|
175
|
-
by_object_id << err
|
176
|
-
next
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
memo << err
|
182
|
-
end
|
183
|
-
|
184
|
-
if pathed_errors_by_op_index_and_object_id.any?
|
185
|
-
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
186
|
-
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
|
187
|
-
errors_result.concat(pathed_errors_by_object_id.values)
|
188
|
-
end
|
189
|
-
end
|
190
|
-
errors_result.flatten!
|
191
|
-
end
|
192
|
-
|
193
|
-
private
|
194
|
-
|
195
|
-
# traverse forward through origin data, expanding arrays to follow all paths
|
196
|
-
# any errors found for an origin object_id have their path prefixed by the object path
|
197
|
-
def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
|
198
|
-
current_path.push(forward_path.shift)
|
199
|
-
scope = root[current_path.last]
|
200
|
-
|
201
|
-
if forward_path.any? && scope.is_a?(Array)
|
202
|
-
scope.each_with_index do |element, index|
|
203
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
204
|
-
inner_elements.each do |inner_element|
|
205
|
-
current_path << index
|
206
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
207
|
-
current_path.pop
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
elsif forward_path.any?
|
212
|
-
current_path << index
|
213
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
214
|
-
current_path.pop
|
215
|
-
|
216
|
-
elsif scope.is_a?(Array)
|
217
|
-
scope.each_with_index do |element, index|
|
218
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
219
|
-
inner_elements.each do |inner_element|
|
220
|
-
errors = pathed_errors_by_object_id[inner_element.object_id]
|
221
|
-
errors.each { _1["path"] = [*current_path, index, *_1["path"]] } if errors
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
else
|
226
|
-
errors = pathed_errors_by_object_id[scope.object_id]
|
227
|
-
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
228
|
-
end
|
229
|
-
|
230
|
-
forward_path.unshift(current_path.pop)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
8
|
attr_reader :supergraph, :request, :data, :errors
|
235
9
|
attr_accessor :query_count
|
236
10
|
|
237
11
|
def initialize(supergraph:, request:, plan:, nonblocking: false)
|
238
12
|
@supergraph = supergraph
|
239
13
|
@request = request
|
240
|
-
@queue = plan
|
14
|
+
@queue = plan.ops
|
241
15
|
@data = {}
|
242
16
|
@errors = []
|
243
17
|
@query_count = 0
|
@@ -273,8 +47,8 @@ module GraphQL
|
|
273
47
|
|
274
48
|
@dataloader.append_job do
|
275
49
|
tasks = @queue
|
276
|
-
.select { next_ordinals.include?(_1
|
277
|
-
.group_by { [_1
|
50
|
+
.select { next_ordinals.include?(_1.after) }
|
51
|
+
.group_by { [_1.location, _1.boundary.nil?] }
|
278
52
|
.map do |(location, root_source), ops|
|
279
53
|
if root_source
|
280
54
|
@dataloader.with(RootSource, self, location).request_all(ops)
|
@@ -297,3 +71,6 @@ module GraphQL
|
|
297
71
|
end
|
298
72
|
end
|
299
73
|
end
|
74
|
+
|
75
|
+
require_relative "./executor/boundary_source"
|
76
|
+
require_relative "./executor/root_source"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Plan
|
6
|
+
Op = Struct.new(
|
7
|
+
:step,
|
8
|
+
:after,
|
9
|
+
:location,
|
10
|
+
:operation_type,
|
11
|
+
:selections,
|
12
|
+
:variables,
|
13
|
+
:path,
|
14
|
+
:if_type,
|
15
|
+
:boundary,
|
16
|
+
keyword_init: true
|
17
|
+
) do
|
18
|
+
def as_json
|
19
|
+
{
|
20
|
+
step: step,
|
21
|
+
after: after,
|
22
|
+
location: location,
|
23
|
+
operation_type: operation_type,
|
24
|
+
selections: selections,
|
25
|
+
variables: variables,
|
26
|
+
path: path,
|
27
|
+
if_type: if_type,
|
28
|
+
boundary: boundary&.as_json
|
29
|
+
}.tap(&:compact!)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def from_json(json)
|
35
|
+
ops = json["ops"]
|
36
|
+
ops = ops.map do |op|
|
37
|
+
boundary = op["boundary"]
|
38
|
+
Op.new(
|
39
|
+
step: op["step"],
|
40
|
+
after: op["after"],
|
41
|
+
location: op["location"],
|
42
|
+
operation_type: op["operation_type"],
|
43
|
+
selections: op["selections"],
|
44
|
+
variables: op["variables"],
|
45
|
+
path: op["path"],
|
46
|
+
if_type: op["if_type"],
|
47
|
+
boundary: boundary ? GraphQL::Stitching::Boundary.new(**boundary) : nil,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
new(ops: ops)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :ops
|
55
|
+
|
56
|
+
def initialize(ops: [])
|
57
|
+
@ops = ops
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json
|
61
|
+
{ ops: @ops.map(&:as_json) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|