graphql-stitching 1.2.0 → 1.2.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/.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
|