graphql-stitching 0.1.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,16 +7,17 @@ module GraphQL
7
7
  class Executor
8
8
 
9
9
  class RootSource < GraphQL::Dataloader::Source
10
- def initialize(executor)
10
+ def initialize(executor, location)
11
11
  @executor = executor
12
+ @location = location
12
13
  end
13
14
 
14
15
  def fetch(ops)
15
16
  op = ops.first # There should only ever be one per location at a time
16
17
 
17
- query_document = build_query(op)
18
- query_variables = @executor.variables.slice(*op["variables"].keys)
19
- result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
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)
20
21
  @executor.query_count += 1
21
22
 
22
23
  @executor.data.merge!(result["data"]) if result["data"]
@@ -24,16 +25,27 @@ module GraphQL
24
25
  result["errors"].each { _1.delete("locations") }
25
26
  @executor.errors.concat(result["errors"])
26
27
  end
27
- op["key"]
28
+
29
+ ops.map { op["key"] }
28
30
  end
29
31
 
30
- def build_query(op)
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["key"].to_s
40
+ end
41
+
31
42
  if op["variables"].any?
32
43
  variable_defs = op["variables"].map { |k, v| "$#{k}:#{v}" }.join(",")
33
- "#{op["operation_type"]}(#{variable_defs})#{op["selections"]}"
34
- else
35
- "#{op["operation_type"]}#{op["selections"]}"
44
+ doc << "(" << variable_defs << ")"
36
45
  end
46
+
47
+ doc << op["selections"]
48
+ doc
37
49
  end
38
50
  end
39
51
 
@@ -60,9 +72,9 @@ module GraphQL
60
72
  end
61
73
 
62
74
  if origin_sets_by_operation.any?
63
- query_document, variable_names = build_query(origin_sets_by_operation)
64
- variables = @executor.variables.slice(*variable_names)
65
- raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
75
+ query_document, variable_names = build_document(origin_sets_by_operation, @executor.request.operation_name)
76
+ variables = @executor.request.variables.slice(*variable_names)
77
+ raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
66
78
  @executor.query_count += 1
67
79
 
68
80
  merge_results!(origin_sets_by_operation, raw_result.dig("data"))
@@ -74,7 +86,14 @@ module GraphQL
74
86
  ops.map { origin_sets_by_operation[_1] ? _1["key"] : nil }
75
87
  end
76
88
 
77
- def build_query(origin_sets_by_operation)
89
+ # Builds batched boundary queries
90
+ # "query MyOperation_2_3($var:VarType) {
91
+ # _0_result: list(keys:["a","b","c"]) { boundarySelections... }
92
+ # _1_0_result: item(key:"x") { boundarySelections... }
93
+ # _1_1_result: item(key:"y") { boundarySelections... }
94
+ # _1_2_result: item(key:"z") { boundarySelections... }
95
+ # }"
96
+ def build_document(origin_sets_by_operation, operation_name = nil)
78
97
  variable_defs = {}
79
98
  query_fields = origin_sets_by_operation.map.with_index do |(op, origin_set), batch_index|
80
99
  variable_defs.merge!(op["variables"])
@@ -92,14 +111,24 @@ module GraphQL
92
111
  end
93
112
  end
94
113
 
95
- query_document = if variable_defs.any?
96
- query_variables = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
97
- "query(#{query_variables}){ #{query_fields.join(" ")} }"
98
- else
99
- "query{ #{query_fields.join(" ")} }"
114
+ doc = String.new
115
+ doc << "query" # << boundary fulfillment always uses query
116
+
117
+ if operation_name
118
+ doc << " " << operation_name
119
+ origin_sets_by_operation.each_key do |op|
120
+ doc << "_" << op["key"].to_s
121
+ end
100
122
  end
101
123
 
102
- return query_document, variable_defs.keys
124
+ if variable_defs.any?
125
+ variable_str = variable_defs.map { |k, v| "$#{k}:#{v}" }.join(",")
126
+ doc << "(" << variable_str << ")"
127
+ end
128
+
129
+ doc << "{ " << query_fields.join(" ") << " }"
130
+
131
+ return doc, variable_defs.keys
103
132
  end
104
133
 
105
134
  def merge_results!(origin_sets_by_operation, raw_result)
@@ -165,23 +194,26 @@ module GraphQL
165
194
 
166
195
  private
167
196
 
168
- # traverses forward through origin data, expanding arrays to follow all paths
197
+ # traverse forward through origin data, expanding arrays to follow all paths
169
198
  # any errors found for an origin object_id have their path prefixed by the object path
170
199
  def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
171
- current_path << forward_path.first
172
- forward_path = forward_path[1..-1]
200
+ current_path.push(forward_path.shift)
173
201
  scope = root[current_path.last]
174
202
 
175
203
  if forward_path.any? && scope.is_a?(Array)
176
204
  scope.each_with_index do |element, index|
177
205
  inner_elements = element.is_a?(Array) ? element.flatten : [element]
178
206
  inner_elements.each do |inner_element|
179
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], inner_element)
207
+ current_path << index
208
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
209
+ current_path.pop
180
210
  end
181
211
  end
182
212
 
183
213
  elsif forward_path.any?
184
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], scope)
214
+ current_path << index
215
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
216
+ current_path.pop
185
217
 
186
218
  elsif scope.is_a?(Array)
187
219
  scope.each_with_index do |element, index|
@@ -196,61 +228,72 @@ module GraphQL
196
228
  errors = pathed_errors_by_object_id[scope.object_id]
197
229
  errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
198
230
  end
231
+
232
+ forward_path.unshift(current_path.pop)
199
233
  end
200
234
  end
201
235
 
202
- attr_reader :supergraph, :data, :errors, :variables
236
+ attr_reader :supergraph, :request, :data, :errors
203
237
  attr_accessor :query_count
204
238
 
205
- def initialize(supergraph:, plan:, variables: {}, nonblocking: false)
239
+ def initialize(supergraph:, request:, plan:, nonblocking: false)
206
240
  @supergraph = supergraph
207
- @variables = variables
241
+ @request = request
208
242
  @queue = plan["ops"]
209
243
  @data = {}
210
244
  @errors = []
211
245
  @query_count = 0
246
+ @exec_cycles = 0
212
247
  @dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
213
248
  end
214
249
 
215
- def perform(document=nil)
250
+ def perform(raw: false)
216
251
  exec!
217
-
218
252
  result = {}
219
- result["data"] = @data if @data && @data.length > 0
220
- result["errors"] = @errors if @errors.length > 0
221
253
 
222
- if document && result["data"]
223
- GraphQL::Stitching::Shaper.new(
254
+ if @data && @data.length > 0
255
+ result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
224
256
  schema: @supergraph.schema,
225
- document: document,
226
- ).perform!(result)
227
- else
228
- result
257
+ request: @request,
258
+ ).perform!(@data)
229
259
  end
260
+
261
+ if @errors.length > 0
262
+ result["errors"] = @errors
263
+ end
264
+
265
+ result
230
266
  end
231
267
 
232
268
  private
233
269
 
234
270
  def exec!(after_keys = [0])
271
+ if @exec_cycles > @queue.length
272
+ # sanity check... if we've exceeded queue size, then something went wrong.
273
+ raise StitchingError, "Too many execution requests attempted."
274
+ end
275
+
235
276
  @dataloader.append_job do
236
- requests = @queue
277
+ tasks = @queue
237
278
  .select { after_keys.include?(_1["after_key"]) }
238
- .group_by { _1["location"] }
239
- .map do |location, ops|
240
- if ops.first["after_key"].zero?
241
- @dataloader.with(RootSource, self).request_all(ops)
279
+ .group_by { [_1["location"], _1["boundary"].nil?] }
280
+ .map do |(location, root_source), ops|
281
+ if root_source
282
+ @dataloader.with(RootSource, self, location).request_all(ops)
242
283
  else
243
284
  @dataloader.with(BoundarySource, self, location).request_all(ops)
244
285
  end
245
286
  end
246
287
 
247
- requests.each(&method(:exec_request))
288
+ tasks.each(&method(:exec_task))
248
289
  end
290
+
291
+ @exec_cycles += 1
249
292
  @dataloader.run
250
293
  end
251
294
 
252
- def exec_request(request)
253
- next_keys = request.load
295
+ def exec_task(task)
296
+ next_keys = task.load
254
297
  next_keys.compact!
255
298
  exec!(next_keys) if next_keys.any?
256
299
  end
@@ -7,8 +7,6 @@ module GraphQL
7
7
  class Gateway
8
8
  class GatewayError < StitchingError; end
9
9
 
10
- EMPTY_CONTEXT = {}.freeze
11
-
12
10
  attr_reader :supergraph
13
11
 
14
12
  def initialize(locations: nil, supergraph: nil)
@@ -25,29 +23,36 @@ module GraphQL
25
23
  end
26
24
  end
27
25
 
28
- def execute(query:, variables: nil, operation_name: nil, context: EMPTY_CONTEXT, validate: true)
29
- document = GraphQL::Stitching::Document.new(query, operation_name: operation_name)
26
+ def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
27
+ request = GraphQL::Stitching::Request.new(
28
+ query,
29
+ operation_name: operation_name,
30
+ variables: variables,
31
+ context: context,
32
+ )
30
33
 
31
34
  if validate
32
- validation_errors = @supergraph.schema.validate(document.ast)
35
+ validation_errors = @supergraph.schema.validate(request.document)
33
36
  return error_result(validation_errors) if validation_errors.any?
34
37
  end
35
38
 
39
+ request.prepare!
40
+
36
41
  begin
37
- plan = fetch_plan(document, context) do
42
+ plan = fetch_plan(request) do
38
43
  GraphQL::Stitching::Planner.new(
39
44
  supergraph: @supergraph,
40
- document: document,
45
+ request: request,
41
46
  ).perform.to_h
42
47
  end
43
48
 
44
49
  GraphQL::Stitching::Executor.new(
45
50
  supergraph: @supergraph,
51
+ request: request,
46
52
  plan: plan,
47
- variables: variables || {},
48
- ).perform(document)
53
+ ).perform
49
54
  rescue StandardError => e
50
- custom_message = @on_error.call(e, context) if @on_error
55
+ custom_message = @on_error.call(e, request.context) if @on_error
51
56
  error_result([{ "message" => custom_message || "An unexpected error occured." }])
52
57
  end
53
58
  end
@@ -91,16 +96,16 @@ module GraphQL
91
96
  supergraph
92
97
  end
93
98
 
94
- def fetch_plan(document, context)
99
+ def fetch_plan(request)
95
100
  if @on_cache_read
96
- cached_plan = @on_cache_read.call(document.digest, context)
101
+ cached_plan = @on_cache_read.call(request.digest, request.context)
97
102
  return JSON.parse(cached_plan) if cached_plan
98
103
  end
99
104
 
100
105
  plan_json = yield
101
106
 
102
107
  if @on_cache_write
103
- @on_cache_write.call(document.digest, JSON.generate(plan_json), context)
108
+ @on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
104
109
  end
105
110
 
106
111
  plan_json