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.
@@ -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