graphql-stitching 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +5 -0
- data/lib/graphql/stitching/composer/base_validator.rb +3 -3
- data/lib/graphql/stitching/composer/validate_boundaries.rb +3 -3
- data/lib/graphql/stitching/composer/validate_interfaces.rb +3 -4
- data/lib/graphql/stitching/composer.rb +66 -9
- data/lib/graphql/stitching/executor/boundary_source.rb +4 -6
- data/lib/graphql/stitching/executor/root_source.rb +4 -4
- data/lib/graphql/stitching/executor.rb +14 -8
- data/lib/graphql/stitching/http_executable.rb +29 -18
- data/lib/graphql/stitching/planner.rb +1 -1
- data/lib/graphql/stitching/request.rb +46 -3
- data/lib/graphql/stitching/shaper.rb +2 -1
- data/lib/graphql/stitching/skip_include.rb +4 -3
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +17 -0
- data/lib/graphql/stitching/supergraph/source_directive.rb +12 -0
- data/lib/graphql/stitching/supergraph.rb +23 -30
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4cb8c0d3b16cda5db8b82d0d0f9eba93a4537c3faa584b1b301dc84b34b5fd0e
|
4
|
+
data.tar.gz: 83f4b436479307ac8575f718ace5a82288024a6e5741f786151e55920d7ef61f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e247e539e223a1fbefcd398c1aeb3940fa91320b81c45caeaa21b3ddca50631ffb6ddbd3963cde07f11609cbaacc1f814a167fefebab8f888979afe929bcb93f
|
7
|
+
data.tar.gz: 66de3bacb73749bd90b4d8e7ade4bf35cc144e0ae60b3ff1c81a5f548b20d74e204454b6727875984da293b4b3853aa9bf6215283666f2b037a7dc148e8aa64b
|
data/.yardopts
ADDED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
5
|
-
class
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Composer
|
5
|
+
class ValidateBoundaries < BaseValidator
|
6
6
|
|
7
7
|
def perform(ctx, composer)
|
8
8
|
ctx.schema.types.each do |type_name, type|
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
5
|
-
class
|
6
|
-
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Composer
|
5
|
+
class ValidateInterfaces < BaseValidator
|
7
6
|
# For each composed interface, check the interface against each possible type
|
8
7
|
# to assure that intersecting fields have compatible types, structures, and nullability.
|
9
8
|
# Verifies compatibility of types that inherit interface contracts through merging.
|
@@ -1,26 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "./composer/base_validator"
|
4
|
+
require_relative "./composer/validate_interfaces"
|
5
|
+
require_relative "./composer/validate_boundaries"
|
6
|
+
|
3
7
|
module GraphQL
|
4
8
|
module Stitching
|
5
9
|
class Composer
|
6
10
|
class ComposerError < StitchingError; end
|
7
11
|
class ValidationError < ComposerError; end
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
|
13
|
+
# @api private
|
14
|
+
NO_DEFAULT_VALUE = begin
|
15
|
+
class T < GraphQL::Schema::Object
|
16
|
+
field(:f, String) do
|
17
|
+
argument(:a, String)
|
18
|
+
end
|
11
19
|
end
|
20
|
+
|
21
|
+
T.get_field("f").get_argument("a").default_value
|
12
22
|
end
|
13
23
|
|
14
|
-
|
24
|
+
# @api private
|
15
25
|
BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
|
26
|
+
|
27
|
+
# @api private
|
16
28
|
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
|
17
29
|
|
30
|
+
# @api private
|
18
31
|
VALIDATORS = [
|
19
32
|
"ValidateInterfaces",
|
20
33
|
"ValidateBoundaries",
|
21
34
|
].freeze
|
22
35
|
|
23
|
-
|
36
|
+
# @return [String] name of the Query type in the composed schema.
|
37
|
+
attr_reader :query_name
|
38
|
+
|
39
|
+
# @return [String] name of the Mutation type in the composed schema.
|
40
|
+
attr_reader :mutation_name
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
attr_reader :candidate_types_by_name_and_location
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
attr_reader :schema_directives
|
24
47
|
|
25
48
|
def initialize(
|
26
49
|
query_name: "Query",
|
@@ -148,6 +171,8 @@ module GraphQL
|
|
148
171
|
supergraph
|
149
172
|
end
|
150
173
|
|
174
|
+
# @!scope class
|
175
|
+
# @!visibility private
|
151
176
|
def prepare_locations_input(locations_input)
|
152
177
|
schemas = {}
|
153
178
|
executables = {}
|
@@ -200,6 +225,8 @@ module GraphQL
|
|
200
225
|
return schemas, executables
|
201
226
|
end
|
202
227
|
|
228
|
+
# @!scope class
|
229
|
+
# @!visibility private
|
203
230
|
def build_directive(directive_name, directives_by_location)
|
204
231
|
builder = self
|
205
232
|
|
@@ -212,6 +239,8 @@ module GraphQL
|
|
212
239
|
end
|
213
240
|
end
|
214
241
|
|
242
|
+
# @!scope class
|
243
|
+
# @!visibility private
|
215
244
|
def build_scalar_type(type_name, types_by_location)
|
216
245
|
built_in_type = GraphQL::Schema::BUILT_IN_TYPES[type_name]
|
217
246
|
return built_in_type if built_in_type
|
@@ -225,6 +254,8 @@ module GraphQL
|
|
225
254
|
end
|
226
255
|
end
|
227
256
|
|
257
|
+
# @!scope class
|
258
|
+
# @!visibility private
|
228
259
|
def build_enum_type(type_name, types_by_location, enum_usage)
|
229
260
|
builder = self
|
230
261
|
|
@@ -261,6 +292,8 @@ module GraphQL
|
|
261
292
|
end
|
262
293
|
end
|
263
294
|
|
295
|
+
# @!scope class
|
296
|
+
# @!visibility private
|
264
297
|
def build_object_type(type_name, types_by_location)
|
265
298
|
builder = self
|
266
299
|
|
@@ -278,6 +311,8 @@ module GraphQL
|
|
278
311
|
end
|
279
312
|
end
|
280
313
|
|
314
|
+
# @!scope class
|
315
|
+
# @!visibility private
|
281
316
|
def build_interface_type(type_name, types_by_location)
|
282
317
|
builder = self
|
283
318
|
|
@@ -296,6 +331,8 @@ module GraphQL
|
|
296
331
|
end
|
297
332
|
end
|
298
333
|
|
334
|
+
# @!scope class
|
335
|
+
# @!visibility private
|
299
336
|
def build_union_type(type_name, types_by_location)
|
300
337
|
builder = self
|
301
338
|
|
@@ -309,6 +346,8 @@ module GraphQL
|
|
309
346
|
end
|
310
347
|
end
|
311
348
|
|
349
|
+
# @!scope class
|
350
|
+
# @!visibility private
|
312
351
|
def build_input_object_type(type_name, types_by_location)
|
313
352
|
builder = self
|
314
353
|
|
@@ -320,10 +359,14 @@ module GraphQL
|
|
320
359
|
end
|
321
360
|
end
|
322
361
|
|
362
|
+
# @!scope class
|
363
|
+
# @!visibility private
|
323
364
|
def build_type_binding(type_name)
|
324
365
|
GraphQL::Schema::LateBoundType.new(@mapped_type_names.fetch(type_name, type_name))
|
325
366
|
end
|
326
367
|
|
368
|
+
# @!scope class
|
369
|
+
# @!visibility private
|
327
370
|
def build_merged_fields(type_name, types_by_location, owner)
|
328
371
|
# "field_name" => "location" => field
|
329
372
|
fields_by_name_location = types_by_location.each_with_object({}) do |(location, type_candidate), memo|
|
@@ -356,6 +399,8 @@ module GraphQL
|
|
356
399
|
end
|
357
400
|
end
|
358
401
|
|
402
|
+
# @!scope class
|
403
|
+
# @!visibility private
|
359
404
|
def build_merged_arguments(type_name, members_by_location, owner, field_name: nil, directive_name: nil)
|
360
405
|
# "argument_name" => "location" => argument
|
361
406
|
args_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
@@ -410,6 +455,8 @@ module GraphQL
|
|
410
455
|
end
|
411
456
|
end
|
412
457
|
|
458
|
+
# @!scope class
|
459
|
+
# @!visibility private
|
413
460
|
def build_merged_directives(type_name, members_by_location, owner, field_name: nil, argument_name: nil, enum_value: nil)
|
414
461
|
directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
|
415
462
|
member_candidate.directives.each do |directive|
|
@@ -451,6 +498,8 @@ module GraphQL
|
|
451
498
|
end
|
452
499
|
end
|
453
500
|
|
501
|
+
# @!scope class
|
502
|
+
# @!visibility private
|
454
503
|
def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
|
455
504
|
path = [type_name, field_name, argument_name].tap(&:compact!).join(".")
|
456
505
|
alt_structures = type_candidates.map { Util.flatten_type_structure(_1) }
|
@@ -482,6 +531,8 @@ module GraphQL
|
|
482
531
|
type
|
483
532
|
end
|
484
533
|
|
534
|
+
# @!scope class
|
535
|
+
# @!visibility private
|
485
536
|
def merge_descriptions(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
|
486
537
|
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.description }
|
487
538
|
@description_merger.call(strings_by_location, {
|
@@ -492,6 +543,8 @@ module GraphQL
|
|
492
543
|
}.tap(&:compact!))
|
493
544
|
end
|
494
545
|
|
546
|
+
# @!scope class
|
547
|
+
# @!visibility private
|
495
548
|
def merge_deprecations(type_name, members_by_location, field_name: nil, argument_name: nil, enum_value: nil)
|
496
549
|
strings_by_location = members_by_location.each_with_object({}) { |(l, m), memo| memo[l] = m.deprecation_reason }
|
497
550
|
@deprecation_merger.call(strings_by_location, {
|
@@ -502,6 +555,8 @@ module GraphQL
|
|
502
555
|
}.tap(&:compact!))
|
503
556
|
end
|
504
557
|
|
558
|
+
# @!scope class
|
559
|
+
# @!visibility private
|
505
560
|
def extract_boundaries(type_name, types_by_location)
|
506
561
|
types_by_location.each do |location, type_candidate|
|
507
562
|
type_candidate.fields.each do |field_name, field_candidate|
|
@@ -554,6 +609,8 @@ module GraphQL
|
|
554
609
|
end
|
555
610
|
end
|
556
611
|
|
612
|
+
# @!scope class
|
613
|
+
# @!visibility private
|
557
614
|
def select_root_field_locations(schema)
|
558
615
|
[schema.query, schema.mutation].tap(&:compact!).each do |root_type|
|
559
616
|
root_type.fields.each do |root_field_name, root_field|
|
@@ -572,6 +629,8 @@ module GraphQL
|
|
572
629
|
end
|
573
630
|
end
|
574
631
|
|
632
|
+
# @!scope class
|
633
|
+
# @!visibility private
|
575
634
|
def expand_abstract_boundaries(schema)
|
576
635
|
@boundary_map.keys.each do |type_name|
|
577
636
|
boundary_type = schema.types[type_name]
|
@@ -585,6 +644,8 @@ module GraphQL
|
|
585
644
|
end
|
586
645
|
end
|
587
646
|
|
647
|
+
# @!scope class
|
648
|
+
# @!visibility private
|
588
649
|
def build_enum_usage_map(schemas)
|
589
650
|
reads = []
|
590
651
|
writes = []
|
@@ -636,7 +697,3 @@ module GraphQL
|
|
636
697
|
end
|
637
698
|
end
|
638
699
|
end
|
639
|
-
|
640
|
-
require_relative "./composer/base_validator"
|
641
|
-
require_relative "./composer/validate_interfaces"
|
642
|
-
require_relative "./composer/validate_boundaries"
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
5
|
-
class
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Executor
|
5
|
+
class BoundarySource < GraphQL::Dataloader::Source
|
6
6
|
def initialize(executor, location)
|
7
7
|
@executor = executor
|
8
8
|
@location = location
|
@@ -29,7 +29,7 @@ module GraphQL
|
|
29
29
|
@executor.request.operation_directives,
|
30
30
|
)
|
31
31
|
variables = @executor.request.variables.slice(*variable_names)
|
32
|
-
raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
|
32
|
+
raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
|
33
33
|
@executor.query_count += 1
|
34
34
|
|
35
35
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
@@ -183,9 +183,7 @@ module GraphQL
|
|
183
183
|
end
|
184
184
|
|
185
185
|
elsif forward_path.any?
|
186
|
-
current_path << index
|
187
186
|
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
188
|
-
current_path.pop
|
189
187
|
|
190
188
|
elsif scope.is_a?(Array)
|
191
189
|
scope.each_with_index do |element, index|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
5
|
-
class
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Executor
|
5
|
+
class RootSource < GraphQL::Dataloader::Source
|
6
6
|
def initialize(executor, location)
|
7
7
|
@executor = executor
|
8
8
|
@location = location
|
@@ -17,7 +17,7 @@ module GraphQL
|
|
17
17
|
@executor.request.operation_directives,
|
18
18
|
)
|
19
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)
|
20
|
+
result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
|
21
21
|
@executor.query_count += 1
|
22
22
|
|
23
23
|
@executor.data.merge!(result["data"]) if result["data"]
|
@@ -1,17 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require_relative "./executor/boundary_source"
|
5
|
+
require_relative "./executor/root_source"
|
4
6
|
|
5
7
|
module GraphQL
|
6
8
|
module Stitching
|
7
9
|
class Executor
|
8
|
-
|
10
|
+
# @return [Request] the stitching request to execute.
|
11
|
+
attr_reader :request
|
12
|
+
|
13
|
+
# @return [Hash] an aggregate data payload to return.
|
14
|
+
attr_reader :data
|
15
|
+
|
16
|
+
# @return [Array<Hash>] aggregate GraphQL errors to return.
|
17
|
+
attr_reader :errors
|
18
|
+
|
19
|
+
# @return [Integer] tally of queries performed while executing.
|
9
20
|
attr_accessor :query_count
|
10
21
|
|
11
22
|
def initialize(request, nonblocking: false)
|
12
23
|
@request = request
|
13
|
-
@supergraph = request.supergraph
|
14
|
-
@plan = request.plan
|
15
24
|
@data = {}
|
16
25
|
@errors = []
|
17
26
|
@query_count = 0
|
@@ -37,13 +46,13 @@ module GraphQL
|
|
37
46
|
private
|
38
47
|
|
39
48
|
def exec!(next_steps = [0])
|
40
|
-
if @exec_cycles > @plan.ops.length
|
49
|
+
if @exec_cycles > @request.plan.ops.length
|
41
50
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
42
51
|
raise StitchingError, "Too many execution requests attempted."
|
43
52
|
end
|
44
53
|
|
45
54
|
@dataloader.append_job do
|
46
|
-
tasks = @plan
|
55
|
+
tasks = @request.plan
|
47
56
|
.ops
|
48
57
|
.select { next_steps.include?(_1.after) }
|
49
58
|
.group_by { [_1.location, _1.boundary.nil?] }
|
@@ -66,6 +75,3 @@ module GraphQL
|
|
66
75
|
end
|
67
76
|
end
|
68
77
|
end
|
69
|
-
|
70
|
-
require_relative "./executor/boundary_source"
|
71
|
-
require_relative "./executor/root_source"
|
@@ -7,27 +7,33 @@ require "json"
|
|
7
7
|
module GraphQL
|
8
8
|
module Stitching
|
9
9
|
class HttpExecutable
|
10
|
-
|
10
|
+
# Builds a new executable for proxying subgraph requests via HTTP.
|
11
|
+
# @param url [String] the url of the remote location to proxy.
|
12
|
+
# @param headers [Hash] headers to include in upstream requests.
|
13
|
+
# @param upload_types [Array<String>, nil] a list of scalar names that represent file uploads. These types extract into multipart forms.
|
14
|
+
def initialize(url:, headers: {}, upload_types: nil)
|
11
15
|
@url = url
|
12
16
|
@headers = { "Content-Type" => "application/json" }.merge!(headers)
|
13
17
|
@upload_types = upload_types
|
14
18
|
end
|
15
19
|
|
16
20
|
def call(request, document, variables)
|
17
|
-
|
18
|
-
extract_multipart_form(request, document, variables)
|
19
|
-
end
|
21
|
+
form_data = extract_multipart_form(request, document, variables)
|
20
22
|
|
21
|
-
response = if
|
22
|
-
|
23
|
+
response = if form_data
|
24
|
+
send_multipart_form(request, form_data)
|
23
25
|
else
|
24
|
-
|
26
|
+
send(request, document, variables)
|
25
27
|
end
|
26
28
|
|
27
29
|
JSON.parse(response.body)
|
28
30
|
end
|
29
31
|
|
30
|
-
|
32
|
+
# Sends a POST request to the remote location.
|
33
|
+
# @param request [Request] the original supergraph request.
|
34
|
+
# @param document [String] the location-specific subgraph document to send.
|
35
|
+
# @param variables [Hash] a hash of variables specific to the subgraph document.
|
36
|
+
def send(_request, document, variables)
|
31
37
|
Net::HTTP.post(
|
32
38
|
URI(@url),
|
33
39
|
JSON.generate({ "query" => document, "variables" => variables }),
|
@@ -35,7 +41,10 @@ module GraphQL
|
|
35
41
|
)
|
36
42
|
end
|
37
43
|
|
38
|
-
|
44
|
+
# Sends a POST request to the remote location with multipart form data.
|
45
|
+
# @param request [Request] the original supergraph request.
|
46
|
+
# @param form_data [Hash] a rendered multipart form with an "operations", "map", and file sections.
|
47
|
+
def send_multipart_form(_request, form_data)
|
39
48
|
uri = URI(@url)
|
40
49
|
req = Net::HTTP::Post.new(uri)
|
41
50
|
@headers.each_pair do |key, value|
|
@@ -48,16 +57,18 @@ module GraphQL
|
|
48
57
|
end
|
49
58
|
end
|
50
59
|
|
51
|
-
#
|
52
|
-
#
|
60
|
+
# Extracts multipart upload forms per the spec:
|
61
|
+
# https://github.com/jaydenseric/graphql-multipart-request-spec
|
62
|
+
# @param request [Request] the original supergraph request.
|
63
|
+
# @param document [String] the location-specific subgraph document to send.
|
64
|
+
# @param variables [Hash] a hash of variables specific to the subgraph document.
|
53
65
|
def extract_multipart_form(request, document, variables)
|
54
|
-
return unless @upload_types
|
66
|
+
return unless @upload_types && request.variable_definitions.any? && variables&.any?
|
55
67
|
|
56
|
-
path = []
|
57
68
|
files_by_path = {}
|
58
69
|
|
59
70
|
# extract all upload scalar values mapped by their input path
|
60
|
-
variables.
|
71
|
+
variables.each_with_object([]) do |(key, value), path|
|
61
72
|
ast_node = request.variable_definitions[key]
|
62
73
|
path << key
|
63
74
|
extract_ast_node(ast_node, value, files_by_path, path, request) if ast_node
|
@@ -70,14 +81,14 @@ module GraphQL
|
|
70
81
|
files = files_by_path.values.tap(&:uniq!)
|
71
82
|
variables_copy = variables.dup
|
72
83
|
|
73
|
-
files_by_path.
|
84
|
+
files_by_path.each_key do |path|
|
74
85
|
orig = variables
|
75
86
|
copy = variables_copy
|
76
87
|
path.each_with_index do |key, i|
|
77
88
|
if i == path.length - 1
|
78
|
-
|
79
|
-
map[
|
80
|
-
map[
|
89
|
+
file_index = files.index(copy[key]).to_s
|
90
|
+
map[file_index] ||= []
|
91
|
+
map[file_index] << "variables.#{path.join(".")}"
|
81
92
|
copy[key] = nil
|
82
93
|
elsif orig[key].object_id == copy[key].object_id
|
83
94
|
copy[key] = copy[key].dup
|
@@ -324,7 +324,7 @@ module GraphQL
|
|
324
324
|
end
|
325
325
|
|
326
326
|
if expanded_selections
|
327
|
-
@
|
327
|
+
@request.warden.possible_types(parent_type).each do |possible_type|
|
328
328
|
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
|
329
329
|
|
330
330
|
type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
|
@@ -6,8 +6,30 @@ module GraphQL
|
|
6
6
|
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
7
|
SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
|
8
8
|
|
9
|
-
|
9
|
+
# @return [Supergraph] supergraph instance that resolves the request.
|
10
|
+
attr_reader :supergraph
|
10
11
|
|
12
|
+
# @return [GraphQL::Language::Nodes::Document] the parsed GraphQL AST document.
|
13
|
+
attr_reader :document
|
14
|
+
|
15
|
+
# @return [Hash] input variables for the request.
|
16
|
+
attr_reader :variables
|
17
|
+
|
18
|
+
# @return [String] operation name selected for the request.
|
19
|
+
attr_reader :operation_name
|
20
|
+
|
21
|
+
# @return [Hash] contextual object passed through resolver flows.
|
22
|
+
attr_reader :context
|
23
|
+
|
24
|
+
# @return [GraphQL::Schema::Warden] a visibility warden for this request.
|
25
|
+
attr_reader :warden
|
26
|
+
|
27
|
+
# Creates a new supergraph request.
|
28
|
+
# @param supergraph [Supergraph] supergraph instance that resolves the request.
|
29
|
+
# @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
|
30
|
+
# @param operation_name [String, nil] operation name selected for the request.
|
31
|
+
# @param variables [Hash, nil] input variables for the request.
|
32
|
+
# @param context [Hash, nil] a contextual object passed through resolver flows.
|
11
33
|
def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
|
12
34
|
@supergraph = supergraph
|
13
35
|
@string = nil
|
@@ -29,25 +51,34 @@ module GraphQL
|
|
29
51
|
|
30
52
|
@operation_name = operation_name
|
31
53
|
@variables = variables || {}
|
32
|
-
|
54
|
+
|
55
|
+
@query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
|
56
|
+
@warden = @query.warden
|
57
|
+
@context = @query.context
|
58
|
+
@context[:request] = self
|
33
59
|
end
|
34
60
|
|
61
|
+
# @return [String] the original document string, or a print of the parsed AST document.
|
35
62
|
def string
|
36
63
|
@string || normalized_string
|
37
64
|
end
|
38
65
|
|
66
|
+
# @return [String] a print of the parsed AST document with consistent whitespace.
|
39
67
|
def normalized_string
|
40
68
|
@normalized_string ||= @document.to_query_string
|
41
69
|
end
|
42
70
|
|
71
|
+
# @return [String] a digest of the original document string. Generally faster but less consistent.
|
43
72
|
def digest
|
44
73
|
@digest ||= Digest::SHA2.hexdigest(string)
|
45
74
|
end
|
46
75
|
|
76
|
+
# @return [String] a digest of the normalized document string. Slower but more consistent.
|
47
77
|
def normalized_digest
|
48
78
|
@normalized_digest ||= Digest::SHA2.hexdigest(normalized_string)
|
49
79
|
end
|
50
80
|
|
81
|
+
# @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
|
51
82
|
def operation
|
52
83
|
@operation ||= begin
|
53
84
|
operation_defs = @document.definitions.select do |d|
|
@@ -66,6 +97,7 @@ module GraphQL
|
|
66
97
|
end
|
67
98
|
end
|
68
99
|
|
100
|
+
# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
|
69
101
|
def operation_directives
|
70
102
|
@operation_directives ||= if operation.directives.any?
|
71
103
|
printer = GraphQL::Language::Printer.new
|
@@ -73,22 +105,27 @@ module GraphQL
|
|
73
105
|
end
|
74
106
|
end
|
75
107
|
|
108
|
+
# @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
|
76
109
|
def variable_definitions
|
77
110
|
@variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
|
78
111
|
memo[v.name] = v.type
|
79
112
|
end
|
80
113
|
end
|
81
114
|
|
115
|
+
# @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
|
82
116
|
def fragment_definitions
|
83
117
|
@fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
|
84
118
|
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
85
119
|
end
|
86
120
|
end
|
87
121
|
|
122
|
+
# Validates the request using the combined supergraph schema.
|
88
123
|
def validate
|
89
|
-
@supergraph.
|
124
|
+
result = @supergraph.static_validator.validate(@query)
|
125
|
+
result[:errors]
|
90
126
|
end
|
91
127
|
|
128
|
+
# Prepares the request for stitching by rendering variable defaults and applying @skip/@include conditionals.
|
92
129
|
def prepare!
|
93
130
|
operation.variables.each do |v|
|
94
131
|
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
|
@@ -106,6 +143,9 @@ module GraphQL
|
|
106
143
|
self
|
107
144
|
end
|
108
145
|
|
146
|
+
# Gets and sets the query plan for the request. Assigned query plans may pull from cache.
|
147
|
+
# @param new_plan [Plan, nil] a cached query plan for the request.
|
148
|
+
# @return [Plan] query plan for the request.
|
109
149
|
def plan(new_plan = nil)
|
110
150
|
if new_plan
|
111
151
|
raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
|
@@ -115,6 +155,9 @@ module GraphQL
|
|
115
155
|
end
|
116
156
|
end
|
117
157
|
|
158
|
+
# Executes the request and returns the rendered response.
|
159
|
+
# @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
|
160
|
+
# @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
|
118
161
|
def execute(raw: false)
|
119
162
|
GraphQL::Stitching::Executor.new(self).perform(raw: raw)
|
120
163
|
end
|
@@ -5,6 +5,7 @@ module GraphQL
|
|
5
5
|
module Stitching
|
6
6
|
# Shapes the final results payload to the request selection and schema definition.
|
7
7
|
# This eliminates unrequested export selections and applies null bubbling.
|
8
|
+
# @api private
|
8
9
|
class Shaper
|
9
10
|
def initialize(request)
|
10
11
|
@request = request
|
@@ -117,7 +118,7 @@ module GraphQL
|
|
117
118
|
def typename_in_type?(typename, type)
|
118
119
|
return true if type.graphql_name == typename
|
119
120
|
|
120
|
-
type.kind.abstract? && @
|
121
|
+
type.kind.abstract? && @request.warden.possible_types(type).any? do |t|
|
121
122
|
t.graphql_name == typename
|
122
123
|
end
|
123
124
|
end
|
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
|
+
# Faster implementation of an AST visitor for prerendering
|
6
|
+
# @skip and @include conditional directives into a document.
|
7
|
+
# This avoids unnecessary planning steps, and prepares result shaping.
|
8
|
+
# @api private
|
5
9
|
class SkipInclude
|
6
10
|
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
11
|
def render(document, variables)
|
11
12
|
changed = false
|
12
13
|
definitions = document.definitions.map do |original_definition|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Supergraph
|
5
|
+
class ResolverDirective < GraphQL::Schema::Directive
|
6
|
+
graphql_name "resolver"
|
7
|
+
locations OBJECT, INTERFACE, UNION
|
8
|
+
argument :location, String, required: true
|
9
|
+
argument :key, String, required: true
|
10
|
+
argument :field, String, required: true
|
11
|
+
argument :arg, String, required: true
|
12
|
+
argument :list, Boolean, required: false
|
13
|
+
argument :federation, Boolean, required: false
|
14
|
+
repeatable true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Supergraph
|
5
|
+
class SourceDirective < GraphQL::Schema::Directive
|
6
|
+
graphql_name "source"
|
7
|
+
locations FIELD_DEFINITION
|
8
|
+
argument :location, String, required: true
|
9
|
+
repeatable true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,29 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "./supergraph/resolver_directive"
|
4
|
+
require_relative "./supergraph/source_directive"
|
5
|
+
|
3
6
|
module GraphQL
|
4
7
|
module Stitching
|
5
8
|
class Supergraph
|
6
9
|
SUPERGRAPH_LOCATION = "__super"
|
7
10
|
|
8
|
-
class ResolverDirective < GraphQL::Schema::Directive
|
9
|
-
graphql_name "resolver"
|
10
|
-
locations OBJECT, INTERFACE, UNION
|
11
|
-
argument :location, String, required: true
|
12
|
-
argument :key, String, required: true
|
13
|
-
argument :field, String, required: true
|
14
|
-
argument :arg, String, required: true
|
15
|
-
argument :list, Boolean, required: false
|
16
|
-
argument :federation, Boolean, required: false
|
17
|
-
repeatable true
|
18
|
-
end
|
19
|
-
|
20
|
-
class SourceDirective < GraphQL::Schema::Directive
|
21
|
-
graphql_name "source"
|
22
|
-
locations FIELD_DEFINITION
|
23
|
-
argument :location, String, required: true
|
24
|
-
repeatable true
|
25
|
-
end
|
26
|
-
|
27
11
|
class << self
|
28
12
|
def validate_executable!(location, executable)
|
29
13
|
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
@@ -88,19 +72,27 @@ module GraphQL
|
|
88
72
|
end
|
89
73
|
end
|
90
74
|
|
91
|
-
|
75
|
+
# @return [GraphQL::Schema] the composed schema for the supergraph.
|
76
|
+
attr_reader :schema
|
77
|
+
|
78
|
+
# @return [Hash<String, Executable>] a map of executable resources by location.
|
79
|
+
attr_reader :executables
|
80
|
+
|
81
|
+
attr_reader :boundaries, :locations_by_type_and_field
|
92
82
|
|
93
83
|
def initialize(schema:, fields: {}, boundaries: {}, executables: {})
|
94
84
|
@schema = schema
|
85
|
+
@schema.use(GraphQL::Schema::AlwaysVisible)
|
86
|
+
|
95
87
|
@boundaries = boundaries
|
96
|
-
@possible_keys_by_type = {}
|
97
|
-
@possible_keys_by_type_and_location = {}
|
98
|
-
@memoized_schema_possible_types = {}
|
99
|
-
@memoized_schema_fields = {}
|
100
|
-
@memoized_introspection_types = nil
|
101
|
-
@memoized_schema_types = nil
|
102
88
|
@fields_by_type_and_location = nil
|
103
89
|
@locations_by_type = nil
|
90
|
+
@memoized_introspection_types = nil
|
91
|
+
@memoized_schema_fields = {}
|
92
|
+
@memoized_schema_types = nil
|
93
|
+
@possible_keys_by_type = {}
|
94
|
+
@possible_keys_by_type_and_location = {}
|
95
|
+
@static_validator = nil
|
104
96
|
|
105
97
|
# add introspection types into the fields mapping
|
106
98
|
@locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
@@ -172,6 +164,11 @@ module GraphQL
|
|
172
164
|
@schema.to_definition
|
173
165
|
end
|
174
166
|
|
167
|
+
# @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
|
168
|
+
def static_validator
|
169
|
+
@static_validator ||= @schema.static_validator
|
170
|
+
end
|
171
|
+
|
175
172
|
def fields
|
176
173
|
@locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
|
177
174
|
end
|
@@ -188,10 +185,6 @@ module GraphQL
|
|
188
185
|
@memoized_schema_types ||= @schema.types
|
189
186
|
end
|
190
187
|
|
191
|
-
def memoized_schema_possible_types(type_name)
|
192
|
-
@memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
|
193
|
-
end
|
194
|
-
|
195
188
|
def memoized_schema_fields(type_name)
|
196
189
|
@memoized_schema_fields[type_name] ||= begin
|
197
190
|
fields = memoized_schema_types[type_name].fields
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-stitching
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -74,6 +74,7 @@ extra_rdoc_files: []
|
|
74
74
|
files:
|
75
75
|
- ".github/workflows/ci.yml"
|
76
76
|
- ".gitignore"
|
77
|
+
- ".yardopts"
|
77
78
|
- Gemfile
|
78
79
|
- LICENSE
|
79
80
|
- README.md
|
@@ -125,6 +126,8 @@ files:
|
|
125
126
|
- lib/graphql/stitching/shaper.rb
|
126
127
|
- lib/graphql/stitching/skip_include.rb
|
127
128
|
- lib/graphql/stitching/supergraph.rb
|
129
|
+
- lib/graphql/stitching/supergraph/resolver_directive.rb
|
130
|
+
- lib/graphql/stitching/supergraph/source_directive.rb
|
128
131
|
- lib/graphql/stitching/util.rb
|
129
132
|
- lib/graphql/stitching/version.rb
|
130
133
|
homepage: https://github.com/gmac/graphql-stitching-ruby
|