graphql-stitching 0.3.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/README.md +3 -3
- data/lib/graphql/stitching/composer/validate_boundaries.rb +7 -6
- data/lib/graphql/stitching/composer/validate_interfaces.rb +40 -12
- data/lib/graphql/stitching/composer.rb +31 -34
- data/lib/graphql/stitching/gateway.rb +2 -2
- data/lib/graphql/stitching/planner.rb +14 -14
- data/lib/graphql/stitching/planner_operation.rb +4 -2
- data/lib/graphql/stitching/shaper.rb +8 -2
- data/lib/graphql/stitching/util.rb +24 -22
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33a2615122207bdf93a7df873fe1efe7a7f3c13050f59dabc6e85becd75cf96e
|
4
|
+
data.tar.gz: 9f203624ab5c26a3cb6ab4710759ef7cb76c1890fff8f4849ab31404875fe79a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad1afa71d6525ea79857106eca6c347f77fab8141dd9a504f3da1a6631f598fe7fb154bc446e228e420fa46dff993140ff568fc3dc94af7a76f0cf29da550896
|
7
|
+
data.tar.gz: 2632af6a1347f1fb6513c79781c3959aacdd879583519cb31e69834a48f2ae70cafdb778d6915713d0d19bf535a67cac55763377cc27da0795b80ed6abc298d4
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -32,7 +32,7 @@ require "graphql/stitching"
|
|
32
32
|
|
33
33
|
## Usage
|
34
34
|
|
35
|
-
The quickest way to start is to use the provided [`Gateway`](./docs/gateway.md) component that
|
35
|
+
The quickest way to start is to use the provided [`Gateway`](./docs/gateway.md) component that wraps a stitched graph in an executable workflow with [caching hooks](./docs/gateway.md#cache-hooks):
|
36
36
|
|
37
37
|
```ruby
|
38
38
|
movies_schema = <<~GRAPHQL
|
@@ -305,13 +305,13 @@ class MyExecutable
|
|
305
305
|
end
|
306
306
|
```
|
307
307
|
|
308
|
-
A [Supergraph](./docs/supergraph.md) is composed with executable
|
308
|
+
A [Supergraph](./docs/supergraph.md) is composed with executable resources provided for each location. Any location that omits the `executable` option will use the provided `schema` as its default executable:
|
309
309
|
|
310
310
|
```ruby
|
311
311
|
supergraph = GraphQL::Stitching::Composer.new.perform({
|
312
312
|
first: {
|
313
313
|
schema: FirstSchema,
|
314
|
-
# executable
|
314
|
+
# executable:^^^^^^ delegates to FirstSchema,
|
315
315
|
},
|
316
316
|
second: {
|
317
317
|
schema: SecondSchema,
|
@@ -33,8 +33,9 @@ module GraphQL
|
|
33
33
|
# only one boundary allowed per type/location/key
|
34
34
|
boundaries_by_location_and_key = boundaries.each_with_object({}) do |boundary, memo|
|
35
35
|
if memo.dig(boundary["location"], boundary["selection"])
|
36
|
-
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["selection"]}`
|
37
|
-
|
36
|
+
raise Composer::ValidationError, "Multiple boundary queries for `#{type.graphql_name}.#{boundary["selection"]}` "\
|
37
|
+
"found in #{boundary["location"]}. Limit one boundary query per type and key in each location. "\
|
38
|
+
"Abstract boundaries provide all possible types."
|
38
39
|
end
|
39
40
|
memo[boundary["location"]] ||= {}
|
40
41
|
memo[boundary["location"]][boundary["selection"]] = boundary
|
@@ -60,8 +61,8 @@ module GraphQL
|
|
60
61
|
remote_locations = bidirectional_access_locations.reject { _1 == location }
|
61
62
|
paths = ctx.route_type_to_locations(type.graphql_name, location, remote_locations)
|
62
63
|
if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
|
63
|
-
raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` boundaries in #{location} to all other locations.
|
64
|
-
|
64
|
+
raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` boundaries in #{location} to all other locations. "\
|
65
|
+
"All locations must provide a boundary accessor that uses a conjoining key."
|
65
66
|
end
|
66
67
|
end
|
67
68
|
end
|
@@ -80,8 +81,8 @@ module GraphQL
|
|
80
81
|
|
81
82
|
subschema_types_by_location.each do |location, subschema_type|
|
82
83
|
if subschema_type.fields.keys.sort != expected_fields
|
83
|
-
raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations,
|
84
|
-
|
84
|
+
raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
|
85
|
+
"or else define boundary queries so that its unique fields may be accessed remotely."
|
85
86
|
end
|
86
87
|
end
|
87
88
|
end
|
@@ -4,19 +4,47 @@ module GraphQL
|
|
4
4
|
module Stitching
|
5
5
|
class Composer::ValidateInterfaces < Composer::BaseValidator
|
6
6
|
|
7
|
+
# For each composed interface, check the interface against each possible type
|
8
|
+
# to assure that intersecting fields have compatible types, structures, and nullability.
|
9
|
+
# Verifies compatibility of types that inherit interface contracts through merging.
|
7
10
|
def perform(supergraph, composer)
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
supergraph.schema.types.each do |type_name, interface_type|
|
12
|
+
next unless interface_type.kind.interface?
|
13
|
+
|
14
|
+
supergraph.schema.possible_types(interface_type).each do |possible_type|
|
15
|
+
interface_type.fields.each do |field_name, interface_field|
|
16
|
+
# graphql-ruby will dynamically apply interface fields on a type implementation,
|
17
|
+
# so check the delegation map to assure that all materialized fields have resolver locations.
|
18
|
+
unless supergraph.locations_by_type_and_field[possible_type.graphql_name][field_name]&.any?
|
19
|
+
raise Composer::ValidationError, "Type #{possible_type.graphql_name} does not implement a `#{field_name}` field in any location, "\
|
20
|
+
"which is required by interface #{interface_type.graphql_name}."
|
21
|
+
end
|
22
|
+
|
23
|
+
intersecting_field = possible_type.fields[field_name]
|
24
|
+
interface_type_structure = Util.flatten_type_structure(interface_field.type)
|
25
|
+
possible_type_structure = Util.flatten_type_structure(intersecting_field.type)
|
26
|
+
|
27
|
+
if possible_type_structure.length != interface_type_structure.length
|
28
|
+
raise Composer::ValidationError, "Incompatible list structures between field #{possible_type.graphql_name}.#{field_name} of type "\
|
29
|
+
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
30
|
+
end
|
31
|
+
|
32
|
+
interface_type_structure.each_with_index do |interface_struct, index|
|
33
|
+
possible_struct = possible_type_structure[index]
|
34
|
+
|
35
|
+
if possible_struct[:name] != interface_struct[:name]
|
36
|
+
raise Composer::ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
|
37
|
+
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
38
|
+
end
|
39
|
+
|
40
|
+
if possible_struct[:null] && !interface_struct[:null]
|
41
|
+
raise Composer::ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
|
42
|
+
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
20
48
|
end
|
21
49
|
|
22
50
|
end
|
@@ -77,7 +77,7 @@ module GraphQL
|
|
77
77
|
schema_types = @subschema_types_by_name_and_location.each_with_object({}) do |(type_name, types_by_location), memo|
|
78
78
|
kinds = types_by_location.values.map { _1.kind.name }.uniq
|
79
79
|
|
80
|
-
|
80
|
+
if kinds.length > 1
|
81
81
|
raise ComposerError, "Cannot merge different kinds for `#{type_name}`. Found: #{kinds.join(", ")}."
|
82
82
|
end
|
83
83
|
|
@@ -96,7 +96,7 @@ module GraphQL
|
|
96
96
|
when "INPUT_OBJECT"
|
97
97
|
build_input_object_type(type_name, types_by_location)
|
98
98
|
else
|
99
|
-
raise ComposerError, "Unexpected kind encountered for `#{type_name}`. Found: #{
|
99
|
+
raise ComposerError, "Unexpected kind encountered for `#{type_name}`. Found: #{kinds.first}."
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
@@ -307,12 +307,13 @@ module GraphQL
|
|
307
307
|
fields_by_name_location.each do |field_name, fields_by_location|
|
308
308
|
value_types = fields_by_location.values.map(&:type)
|
309
309
|
|
310
|
+
type = merge_value_types(type_name, value_types, field_name: field_name)
|
310
311
|
schema_field = owner.field(
|
311
312
|
field_name,
|
312
313
|
description: merge_descriptions(type_name, fields_by_location, field_name: field_name),
|
313
314
|
deprecation_reason: merge_deprecations(type_name, fields_by_location, field_name: field_name),
|
314
|
-
type:
|
315
|
-
null: !
|
315
|
+
type: Util.unwrap_non_null(type),
|
316
|
+
null: !type.non_null?,
|
316
317
|
camelize: false,
|
317
318
|
)
|
318
319
|
|
@@ -345,12 +346,13 @@ module GraphQL
|
|
345
346
|
# Getting double args sometimes... why?
|
346
347
|
return if owner.arguments.any? { _1.first == argument_name }
|
347
348
|
|
349
|
+
type = merge_value_types(type_name, value_types, argument_name: argument_name, field_name: field_name)
|
348
350
|
schema_argument = owner.argument(
|
349
351
|
argument_name,
|
350
352
|
description: merge_descriptions(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
351
353
|
deprecation_reason: merge_deprecations(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
|
352
|
-
type:
|
353
|
-
required:
|
354
|
+
type: Util.unwrap_non_null(type),
|
355
|
+
required: type.non_null?,
|
354
356
|
camelize: false,
|
355
357
|
)
|
356
358
|
|
@@ -401,37 +403,32 @@ module GraphQL
|
|
401
403
|
|
402
404
|
def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
|
403
405
|
path = [type_name, field_name, argument_name].compact.join(".")
|
404
|
-
|
406
|
+
alt_structures = type_candidates.map { Util.flatten_type_structure(_1) }
|
407
|
+
basis_structure = alt_structures.shift
|
405
408
|
|
406
|
-
|
407
|
-
|
408
|
-
end
|
409
|
-
|
410
|
-
type = GraphQL::Schema::BUILT_IN_TYPES.fetch(named_types.first, build_type_binding(named_types.first))
|
411
|
-
list_structures = type_candidates.map { Util.get_list_structure(_1) }
|
412
|
-
|
413
|
-
if list_structures.any?(&:any?)
|
414
|
-
if list_structures.any? { _1.length != list_structures.first.length }
|
409
|
+
alt_structures.each do |alt_structure|
|
410
|
+
if alt_structure.length != basis_structure.length
|
415
411
|
raise ComposerError, "Cannot compose mixed list structures at `#{path}`."
|
416
412
|
end
|
417
413
|
|
418
|
-
|
419
|
-
|
420
|
-
# input arguments use strongest nullability, readonly fields use weakest
|
421
|
-
non_null = list_structures.public_send(argument_name ? :any? : :all?) do |list_structure|
|
422
|
-
list_structure[index].start_with?("non_null")
|
423
|
-
end
|
424
|
-
|
425
|
-
case current
|
426
|
-
when "list", "non_null_list"
|
427
|
-
type = type.to_list_type
|
428
|
-
type = type.to_non_null_type if non_null
|
429
|
-
when "element", "non_null_element"
|
430
|
-
type = type.to_non_null_type if non_null
|
431
|
-
end
|
414
|
+
if alt_structure.last[:name] != basis_structure.last[:name]
|
415
|
+
raise ComposerError, "Cannot compose mixed types at `#{path}`."
|
432
416
|
end
|
433
417
|
end
|
434
418
|
|
419
|
+
type = GraphQL::Schema::BUILT_IN_TYPES.fetch(
|
420
|
+
basis_structure.last[:name],
|
421
|
+
build_type_binding(basis_structure.last[:name])
|
422
|
+
)
|
423
|
+
|
424
|
+
basis_structure.reverse!.each_with_index do |basis, index|
|
425
|
+
rev_index = basis_structure.length - index - 1
|
426
|
+
non_null = alt_structures.each_with_object([!basis[:null]]) { |s, m| m << !s[rev_index][:null] }
|
427
|
+
|
428
|
+
type = type.to_list_type if basis[:list]
|
429
|
+
type = type.to_non_null_type if argument_name ? non_null.any? : non_null.all?
|
430
|
+
end
|
431
|
+
|
435
432
|
type
|
436
433
|
end
|
437
434
|
|
@@ -459,7 +456,7 @@ module GraphQL
|
|
459
456
|
types_by_location.each do |location, type_candidate|
|
460
457
|
type_candidate.fields.each do |field_name, field_candidate|
|
461
458
|
boundary_type_name = field_candidate.type.unwrap.graphql_name
|
462
|
-
|
459
|
+
boundary_structure = Util.flatten_type_structure(field_candidate.type)
|
463
460
|
|
464
461
|
field_candidate.directives.each do |directive|
|
465
462
|
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
|
@@ -482,8 +479,8 @@ module GraphQL
|
|
482
479
|
raise ComposerError, "Invalid boundary argument `#{argument_name}` for #{type_name}.#{field_name}."
|
483
480
|
end
|
484
481
|
|
485
|
-
|
486
|
-
if
|
482
|
+
argument_structure = Util.flatten_type_structure(argument.type)
|
483
|
+
if argument_structure.length != boundary_structure.length
|
487
484
|
raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
|
488
485
|
end
|
489
486
|
|
@@ -493,7 +490,7 @@ module GraphQL
|
|
493
490
|
"selection" => key_selections[0].name,
|
494
491
|
"field" => field_candidate.name,
|
495
492
|
"arg" => argument_name,
|
496
|
-
"list" =>
|
493
|
+
"list" => boundary_structure.first[:list],
|
497
494
|
"type_name" => boundary_type_name,
|
498
495
|
}
|
499
496
|
end
|
@@ -20,13 +20,11 @@ module GraphQL
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def operations
|
23
|
-
|
24
|
-
ops.sort_by!(&:key)
|
25
|
-
ops
|
23
|
+
@operations_by_grouping.values.sort_by!(&:key)
|
26
24
|
end
|
27
25
|
|
28
26
|
def to_h
|
29
|
-
{ "ops" => operations.map(&:to_h) }
|
27
|
+
{ "ops" => operations.map!(&:to_h) }
|
30
28
|
end
|
31
29
|
|
32
30
|
private
|
@@ -53,18 +51,16 @@ module GraphQL
|
|
53
51
|
|
54
52
|
when "mutation"
|
55
53
|
parent_type = @supergraph.schema.mutation
|
56
|
-
location_groups = []
|
57
54
|
|
58
|
-
@request.operation.selections.
|
55
|
+
location_groups = @request.operation.selections.each_with_object([]) do |node, memo|
|
59
56
|
# root fields currently just delegate to the last location that defined them; this should probably be smarter
|
60
57
|
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
|
61
58
|
|
62
|
-
if
|
63
|
-
|
59
|
+
if memo.none? || memo.last[:location] != next_location
|
60
|
+
memo << { location: next_location, selections: [] }
|
64
61
|
end
|
65
62
|
|
66
|
-
|
67
|
-
next_location
|
63
|
+
memo.last[:selections] << node
|
68
64
|
end
|
69
65
|
|
70
66
|
location_groups.reduce(0) do |after_key, group|
|
@@ -144,7 +140,7 @@ module GraphQL
|
|
144
140
|
implements_fragments = false
|
145
141
|
|
146
142
|
if parent_type.kind.interface?
|
147
|
-
expand_interface_selections(current_location, parent_type, input_selections)
|
143
|
+
input_selections = expand_interface_selections(current_location, parent_type, input_selections)
|
148
144
|
end
|
149
145
|
|
150
146
|
input_selections.each do |node|
|
@@ -252,7 +248,7 @@ module GraphQL
|
|
252
248
|
possible_locations = possible_locations_by_field[node.name]
|
253
249
|
preferred_location_score = 0
|
254
250
|
|
255
|
-
# hill
|
251
|
+
# hill-climb to select highest scoring location for each field
|
256
252
|
preferred_location = possible_locations.reduce(possible_locations.first) do |best_location, possible_location|
|
257
253
|
score = selections_by_location[possible_location] ? remote_selections.length : 0
|
258
254
|
score += location_weights.fetch(possible_location, 0)
|
@@ -270,6 +266,8 @@ module GraphQL
|
|
270
266
|
end
|
271
267
|
end
|
272
268
|
|
269
|
+
# route from current location to target locations via boundary queries,
|
270
|
+
# then translate those routes into planner operations
|
273
271
|
routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
|
274
272
|
routes.values.each_with_object({}) do |route, ops_by_location|
|
275
273
|
route.reduce(nil) do |parent_op, boundary|
|
@@ -328,8 +326,8 @@ module GraphQL
|
|
328
326
|
local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
|
329
327
|
|
330
328
|
expanded_selections = nil
|
331
|
-
input_selections.reject
|
332
|
-
if node.is_a?(GraphQL::Language::Nodes::Field) && !local_interface_fields.include?(node.name)
|
329
|
+
input_selections = input_selections.reject do |node|
|
330
|
+
if node.is_a?(GraphQL::Language::Nodes::Field) && node.name != "__typename" && !local_interface_fields.include?(node.name)
|
333
331
|
expanded_selections ||= []
|
334
332
|
expanded_selections << node
|
335
333
|
true
|
@@ -344,6 +342,8 @@ module GraphQL
|
|
344
342
|
input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
|
345
343
|
end
|
346
344
|
end
|
345
|
+
|
346
|
+
input_selections
|
347
347
|
end
|
348
348
|
|
349
349
|
# expand concrete type selections into typed fragments when sending to abstract boundaries
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Stitching
|
5
5
|
class PlannerOperation
|
6
|
+
LANGUAGE_PRINTER = GraphQL::Language::Printer.new
|
7
|
+
|
6
8
|
attr_reader :key, :location, :parent_type, :type_condition, :operation_type, :insertion_path
|
7
9
|
attr_accessor :after_key, :selections, :variables, :boundary
|
8
10
|
|
@@ -32,12 +34,12 @@ module GraphQL
|
|
32
34
|
|
33
35
|
def selection_set
|
34
36
|
op = GraphQL::Language::Nodes::OperationDefinition.new(selections: @selections)
|
35
|
-
|
37
|
+
LANGUAGE_PRINTER.print(op).gsub!(/\s+/, " ").strip!
|
36
38
|
end
|
37
39
|
|
38
40
|
def variable_set
|
39
41
|
@variables.each_with_object({}) do |(variable_name, value_type), memo|
|
40
|
-
memo[variable_name] =
|
42
|
+
memo[variable_name] = LANGUAGE_PRINTER.print(value_type)
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
@@ -42,15 +42,16 @@ module GraphQL
|
|
42
42
|
return nil if raw_object[field_name].nil? && node_type.non_null?
|
43
43
|
|
44
44
|
when GraphQL::Language::Nodes::InlineFragment
|
45
|
-
next unless typename == node.type.name
|
46
45
|
fragment_type = @schema.types[node.type.name]
|
46
|
+
next unless typename_in_type?(typename, fragment_type)
|
47
|
+
|
47
48
|
result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
|
48
49
|
return nil if result.nil?
|
49
50
|
|
50
51
|
when GraphQL::Language::Nodes::FragmentSpread
|
51
52
|
fragment = @request.fragment_definitions[node.name]
|
52
53
|
fragment_type = @schema.types[fragment.type.name]
|
53
|
-
next unless typename
|
54
|
+
next unless typename_in_type?(typename, fragment_type)
|
54
55
|
|
55
56
|
result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
|
56
57
|
return nil if result.nil?
|
@@ -91,6 +92,11 @@ module GraphQL
|
|
91
92
|
|
92
93
|
resolved_list
|
93
94
|
end
|
95
|
+
|
96
|
+
def typename_in_type?(typename, type)
|
97
|
+
return true if type.graphql_name == typename
|
98
|
+
type.kind.abstract? && @schema.possible_types(type).any? { _1.graphql_name == typename }
|
99
|
+
end
|
94
100
|
end
|
95
101
|
end
|
96
102
|
end
|
@@ -10,12 +10,33 @@ module GraphQL
|
|
10
10
|
|
11
11
|
# strips non-null wrappers from a type
|
12
12
|
def self.unwrap_non_null(type)
|
13
|
-
while type.
|
14
|
-
type = type.of_type
|
15
|
-
end
|
13
|
+
type = type.of_type while type.non_null?
|
16
14
|
type
|
17
15
|
end
|
18
16
|
|
17
|
+
# builds a single-dimensional representation of a wrapped type structure
|
18
|
+
def self.flatten_type_structure(type)
|
19
|
+
structure = []
|
20
|
+
|
21
|
+
while type.list?
|
22
|
+
structure << {
|
23
|
+
list: true,
|
24
|
+
null: !type.non_null?,
|
25
|
+
name: nil,
|
26
|
+
}
|
27
|
+
|
28
|
+
type = unwrap_non_null(type).of_type
|
29
|
+
end
|
30
|
+
|
31
|
+
structure << {
|
32
|
+
list: false,
|
33
|
+
null: !type.non_null?,
|
34
|
+
name: type.unwrap.graphql_name,
|
35
|
+
}
|
36
|
+
|
37
|
+
structure
|
38
|
+
end
|
39
|
+
|
19
40
|
# gets a named type for a field node, including hidden root introspections
|
20
41
|
def self.named_type_for_field_node(schema, parent_type, node)
|
21
42
|
if node.name == "__schema" && parent_type == schema.query
|
@@ -40,25 +61,6 @@ module GraphQL
|
|
40
61
|
end
|
41
62
|
result.uniq
|
42
63
|
end
|
43
|
-
|
44
|
-
# gets a deep structural description of a list value type
|
45
|
-
def self.get_list_structure(type)
|
46
|
-
structure = []
|
47
|
-
previous = nil
|
48
|
-
while type.respond_to?(:of_type)
|
49
|
-
if type.is_a?(GraphQL::Schema::List)
|
50
|
-
structure.push(previous.is_a?(GraphQL::Schema::NonNull) ? "non_null_list" : "list")
|
51
|
-
end
|
52
|
-
if structure.any?
|
53
|
-
previous = type
|
54
|
-
if !type.of_type.respond_to?(:of_type)
|
55
|
-
structure.push(previous.is_a?(GraphQL::Schema::NonNull) ? "non_null_element" : "element")
|
56
|
-
end
|
57
|
-
end
|
58
|
-
type = type.of_type
|
59
|
-
end
|
60
|
-
structure
|
61
|
-
end
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
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: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg MacWilliam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|