graphql-stitching 1.1.1 → 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/Gemfile +0 -3
- data/README.md +10 -32
- data/docs/README.md +1 -0
- data/docs/client.md +2 -2
- data/docs/composer.md +1 -1
- data/docs/executor.md +8 -15
- data/docs/http_executable.md +51 -0
- data/docs/planner.md +12 -14
- data/docs/request.md +2 -0
- data/docs/supergraph.md +2 -2
- data/examples/file_uploads/Gemfile +9 -0
- data/examples/file_uploads/Procfile +2 -0
- data/examples/file_uploads/README.md +37 -0
- data/examples/file_uploads/file.txt +1 -0
- data/examples/file_uploads/gateway.rb +37 -0
- data/examples/file_uploads/helpers.rb +62 -0
- data/examples/file_uploads/remote.rb +21 -0
- data/examples/merged_types/Gemfile +8 -0
- data/examples/merged_types/Procfile +3 -0
- data/examples/merged_types/README.md +33 -0
- data/{example → examples/merged_types}/gateway.rb +4 -5
- data/examples/merged_types/remote1.rb +22 -0
- data/examples/merged_types/remote2.rb +22 -0
- data/lib/graphql/stitching/client.rb +9 -19
- 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 +16 -13
- data/lib/graphql/stitching/export_selection.rb +6 -1
- data/lib/graphql/stitching/http_executable.rb +145 -4
- data/lib/graphql/stitching/planner.rb +3 -3
- data/lib/graphql/stitching/request.rb +66 -4
- data/lib/graphql/stitching/shaper.rb +4 -3
- 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 +27 -34
- data/lib/graphql/stitching/util.rb +0 -9
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +20 -7
- data/Procfile +0 -3
- data/example/remote1.rb +0 -26
- data/example/remote2.rb +0 -26
- /data/{example → examples/merged_types}/graphiql.html +0 -0
@@ -28,6 +28,7 @@ module GraphQL
|
|
28
28
|
|
29
29
|
def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
|
30
30
|
request = GraphQL::Stitching::Request.new(
|
31
|
+
@supergraph,
|
31
32
|
query,
|
32
33
|
operation_name: operation_name,
|
33
34
|
variables: variables,
|
@@ -35,24 +36,13 @@ module GraphQL
|
|
35
36
|
)
|
36
37
|
|
37
38
|
if validate
|
38
|
-
validation_errors =
|
39
|
+
validation_errors = request.validate
|
39
40
|
return error_result(validation_errors) if validation_errors.any?
|
40
41
|
end
|
41
42
|
|
42
43
|
request.prepare!
|
43
|
-
|
44
|
-
|
45
|
-
GraphQL::Stitching::Planner.new(
|
46
|
-
supergraph: @supergraph,
|
47
|
-
request: request,
|
48
|
-
).perform
|
49
|
-
end
|
50
|
-
|
51
|
-
GraphQL::Stitching::Executor.new(
|
52
|
-
supergraph: @supergraph,
|
53
|
-
request: request,
|
54
|
-
plan: plan,
|
55
|
-
).perform
|
44
|
+
load_plan(request)
|
45
|
+
request.execute
|
56
46
|
rescue GraphQL::ParseError, GraphQL::ExecutionError => e
|
57
47
|
error_result([e])
|
58
48
|
rescue StandardError => e
|
@@ -77,13 +67,13 @@ module GraphQL
|
|
77
67
|
|
78
68
|
private
|
79
69
|
|
80
|
-
def
|
81
|
-
if @on_cache_read
|
82
|
-
|
83
|
-
return
|
70
|
+
def load_plan(request)
|
71
|
+
if @on_cache_read && plan_json = @on_cache_read.call(request)
|
72
|
+
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
|
73
|
+
return request.plan(plan)
|
84
74
|
end
|
85
75
|
|
86
|
-
plan =
|
76
|
+
plan = request.plan
|
87
77
|
|
88
78
|
if @on_cache_write
|
89
79
|
@on_cache_write.call(request, JSON.generate(plan.as_json))
|
@@ -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
|
-
def initialize(
|
12
|
-
@supergraph = supergraph
|
22
|
+
def initialize(request, nonblocking: false)
|
13
23
|
@request = request
|
14
|
-
@plan = plan
|
15
24
|
@data = {}
|
16
25
|
@errors = []
|
17
26
|
@query_count = 0
|
@@ -24,10 +33,7 @@ module GraphQL
|
|
24
33
|
result = {}
|
25
34
|
|
26
35
|
if @data && @data.length > 0
|
27
|
-
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
|
28
|
-
supergraph: @supergraph,
|
29
|
-
request: @request,
|
30
|
-
).perform!(@data)
|
36
|
+
result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(@request).perform!(@data)
|
31
37
|
end
|
32
38
|
|
33
39
|
if @errors.length > 0
|
@@ -40,13 +46,13 @@ module GraphQL
|
|
40
46
|
private
|
41
47
|
|
42
48
|
def exec!(next_steps = [0])
|
43
|
-
if @exec_cycles > @plan.ops.length
|
49
|
+
if @exec_cycles > @request.plan.ops.length
|
44
50
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
45
51
|
raise StitchingError, "Too many execution requests attempted."
|
46
52
|
end
|
47
53
|
|
48
54
|
@dataloader.append_job do
|
49
|
-
tasks = @plan
|
55
|
+
tasks = @request.plan
|
50
56
|
.ops
|
51
57
|
.select { next_steps.include?(_1.after) }
|
52
58
|
.group_by { [_1.location, _1.boundary.nil?] }
|
@@ -69,6 +75,3 @@ module GraphQL
|
|
69
75
|
end
|
70
76
|
end
|
71
77
|
end
|
72
|
-
|
73
|
-
require_relative "./executor/boundary_source"
|
74
|
-
require_relative "./executor/root_source"
|
@@ -20,8 +20,13 @@ module GraphQL
|
|
20
20
|
"#{EXPORT_PREFIX}#{name}"
|
21
21
|
end
|
22
22
|
|
23
|
+
# The argument assigning Field.alias changed from
|
24
|
+
# a generic `alias` hash key to a structured `field_alias` kwarg.
|
25
|
+
# See https://github.com/rmosolgo/graphql-ruby/pull/4718
|
26
|
+
FIELD_ALIAS_KWARG = !GraphQL::Language::Nodes::Field.new(field_alias: "a").alias.nil?
|
27
|
+
|
23
28
|
def key_node(field_name)
|
24
|
-
if
|
29
|
+
if FIELD_ALIAS_KWARG
|
25
30
|
GraphQL::Language::Nodes::Field.new(field_alias: key(field_name), name: field_name)
|
26
31
|
else
|
27
32
|
GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
|
@@ -7,18 +7,159 @@ 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)
|
17
|
+
@upload_types = upload_types
|
13
18
|
end
|
14
19
|
|
15
|
-
def call(
|
16
|
-
|
20
|
+
def call(request, document, variables)
|
21
|
+
form_data = extract_multipart_form(request, document, variables)
|
22
|
+
|
23
|
+
response = if form_data
|
24
|
+
send_multipart_form(request, form_data)
|
25
|
+
else
|
26
|
+
send(request, document, variables)
|
27
|
+
end
|
28
|
+
|
29
|
+
JSON.parse(response.body)
|
30
|
+
end
|
31
|
+
|
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)
|
37
|
+
Net::HTTP.post(
|
17
38
|
URI(@url),
|
18
39
|
JSON.generate({ "query" => document, "variables" => variables }),
|
19
40
|
@headers,
|
20
41
|
)
|
21
|
-
|
42
|
+
end
|
43
|
+
|
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)
|
48
|
+
uri = URI(@url)
|
49
|
+
req = Net::HTTP::Post.new(uri)
|
50
|
+
@headers.each_pair do |key, value|
|
51
|
+
req[key] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
req.set_form(form_data.to_a, "multipart/form-data")
|
55
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
56
|
+
http.request(req)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
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.
|
65
|
+
def extract_multipart_form(request, document, variables)
|
66
|
+
return unless @upload_types && request.variable_definitions.any? && variables&.any?
|
67
|
+
|
68
|
+
files_by_path = {}
|
69
|
+
|
70
|
+
# extract all upload scalar values mapped by their input path
|
71
|
+
variables.each_with_object([]) do |(key, value), path|
|
72
|
+
ast_node = request.variable_definitions[key]
|
73
|
+
path << key
|
74
|
+
extract_ast_node(ast_node, value, files_by_path, path, request) if ast_node
|
75
|
+
path.pop
|
76
|
+
end
|
77
|
+
|
78
|
+
return if files_by_path.none?
|
79
|
+
|
80
|
+
map = {}
|
81
|
+
files = files_by_path.values.tap(&:uniq!)
|
82
|
+
variables_copy = variables.dup
|
83
|
+
|
84
|
+
files_by_path.each_key do |path|
|
85
|
+
orig = variables
|
86
|
+
copy = variables_copy
|
87
|
+
path.each_with_index do |key, i|
|
88
|
+
if i == path.length - 1
|
89
|
+
file_index = files.index(copy[key]).to_s
|
90
|
+
map[file_index] ||= []
|
91
|
+
map[file_index] << "variables.#{path.join(".")}"
|
92
|
+
copy[key] = nil
|
93
|
+
elsif orig[key].object_id == copy[key].object_id
|
94
|
+
copy[key] = copy[key].dup
|
95
|
+
end
|
96
|
+
orig = orig[key]
|
97
|
+
copy = copy[key]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
form = {
|
102
|
+
"operations" => JSON.generate({
|
103
|
+
"query" => document,
|
104
|
+
"variables" => variables_copy,
|
105
|
+
}),
|
106
|
+
"map" => JSON.generate(map),
|
107
|
+
}
|
108
|
+
|
109
|
+
files.each_with_object(form).with_index do |(file, memo), index|
|
110
|
+
memo[index.to_s] = file.respond_to?(:tempfile) ? file.tempfile : file
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def extract_ast_node(ast_node, value, files_by_path, path, request)
|
117
|
+
return unless value
|
118
|
+
|
119
|
+
ast_node = ast_node.of_type while ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
|
120
|
+
|
121
|
+
if ast_node.is_a?(GraphQL::Language::Nodes::ListType)
|
122
|
+
if value.is_a?(Array)
|
123
|
+
value.each_with_index do |val, index|
|
124
|
+
path << index
|
125
|
+
extract_ast_node(ast_node.of_type, val, files_by_path, path, request)
|
126
|
+
path.pop
|
127
|
+
end
|
128
|
+
end
|
129
|
+
elsif @upload_types.include?(ast_node.name)
|
130
|
+
files_by_path[path.dup] = value
|
131
|
+
else
|
132
|
+
type_def = request.supergraph.schema.get_type(ast_node.name)
|
133
|
+
extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def extract_type_node(parent_type, value, files_by_path, path)
|
138
|
+
return unless value
|
139
|
+
|
140
|
+
parent_type = Util.unwrap_non_null(parent_type)
|
141
|
+
|
142
|
+
if parent_type.list?
|
143
|
+
if value.is_a?(Array)
|
144
|
+
value.each_with_index do |val, index|
|
145
|
+
path << index
|
146
|
+
extract_type_node(parent_type.of_type, val, files_by_path, path)
|
147
|
+
path.pop
|
148
|
+
end
|
149
|
+
end
|
150
|
+
elsif parent_type.kind.input_object?
|
151
|
+
if value.is_a?(Enumerable)
|
152
|
+
arguments = parent_type.arguments
|
153
|
+
value.each do |key, val|
|
154
|
+
arg_type = arguments[key]&.type
|
155
|
+
path << key
|
156
|
+
extract_type_node(arg_type, val, files_by_path, path) if arg_type
|
157
|
+
path.pop
|
158
|
+
end
|
159
|
+
end
|
160
|
+
elsif @upload_types.include?(parent_type.graphql_name)
|
161
|
+
files_by_path[path.dup] = value
|
162
|
+
end
|
22
163
|
end
|
23
164
|
end
|
24
165
|
end
|
@@ -9,9 +9,9 @@ module GraphQL
|
|
9
9
|
MUTATION_OP = "mutation"
|
10
10
|
ROOT_INDEX = 0
|
11
11
|
|
12
|
-
def initialize(
|
13
|
-
@supergraph = supergraph
|
12
|
+
def initialize(request)
|
14
13
|
@request = request
|
14
|
+
@supergraph = request.supergraph
|
15
15
|
@planning_index = ROOT_INDEX
|
16
16
|
@steps_by_entrypoint = {}
|
17
17
|
end
|
@@ -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)
|