graphql-stitching 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +57 -7
- data/docs/resolver.md +101 -0
- data/lib/graphql/stitching/client.rb +5 -1
- data/lib/graphql/stitching/composer/resolver_config.rb +17 -12
- data/lib/graphql/stitching/composer/validate_interfaces.rb +4 -4
- data/lib/graphql/stitching/composer/validate_resolvers.rb +23 -22
- data/lib/graphql/stitching/composer.rb +77 -83
- data/lib/graphql/stitching/executor/resolver_source.rb +25 -26
- data/lib/graphql/stitching/plan.rb +2 -3
- data/lib/graphql/stitching/planner.rb +10 -21
- data/lib/graphql/stitching/planner_step.rb +1 -1
- data/lib/graphql/stitching/resolver/arguments.rb +284 -0
- data/lib/graphql/stitching/resolver/keys.rb +206 -0
- data/lib/graphql/stitching/resolver.rb +44 -23
- data/lib/graphql/stitching/shaper.rb +3 -3
- data/lib/graphql/stitching/skip_include.rb +1 -1
- data/lib/graphql/stitching/supergraph/key_directive.rb +13 -0
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +4 -5
- data/lib/graphql/stitching/supergraph/to_definition.rb +165 -0
- data/lib/graphql/stitching/supergraph.rb +13 -128
- data/lib/graphql/stitching/util.rb +28 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +2 -1
- metadata +7 -3
- data/lib/graphql/stitching/export_selection.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 448ca61527e02c6f232fa811e7e99ba1aa243bc58685f313ca5613a6724ea306
|
4
|
+
data.tar.gz: a0af76ea7eb429d60f5a7e4df4c39103539a01a69136c769f027cef52a108cab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cea8a8674d67a421059e32778ffc3d1bbd699fa307efe2aead4571925ed77d8da7852431ef99c5809e0106ff16af212c26a6372c211b2d287d3a2c4cef5eb80
|
7
|
+
data.tar.gz: 972f74c60afef0c615d8dc6a3069bb7ddfbe88a84e22a8940d496037ab67f23491c639e8cc3e145b98697c921eb6c20577babec4be2a8e6a3221264e925cdc01
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
6
6
|
|
7
7
|
**Supports:**
|
8
8
|
- Merged object and abstract types.
|
9
|
-
- Multiple keys per merged type.
|
9
|
+
- Multiple and composite keys per merged type.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
12
|
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
@@ -94,7 +94,7 @@ To facilitate this merging of types, stitching must know how to cross-reference
|
|
94
94
|
Types merge through resolver queries identified by a `@stitch` directive:
|
95
95
|
|
96
96
|
```graphql
|
97
|
-
directive @stitch(key: String
|
97
|
+
directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
|
98
98
|
```
|
99
99
|
|
100
100
|
This directive (or [static configuration](#sdl-based-schemas)) is applied to root queries where a merged type may be accessed in each location, and a `key` argument specifies a field needed from other locations to be used as a query argument.
|
@@ -151,7 +151,7 @@ type Query {
|
|
151
151
|
```
|
152
152
|
|
153
153
|
* The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type identity is inferred from the field return.
|
154
|
-
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#
|
154
|
+
* The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it may be submitted as an argument to this query. The query argument used to send the key is inferred when possible ([more on arguments](#argument-shapes) later).
|
155
155
|
|
156
156
|
Each location that provides a unique variant of a type must provide at least one resolver query for the type. The exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) and/or [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain no exclusive data:
|
157
157
|
|
@@ -198,7 +198,7 @@ type Query {
|
|
198
198
|
To customize which types an abstract query provides and their respective keys, you may extend the `@stitch` directive with a `typeName` constraint. This can be repeated to select multiple types.
|
199
199
|
|
200
200
|
```graphql
|
201
|
-
directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
|
201
|
+
directive @stitch(key: String!, arguments: String, typeName: String) repeatable on FIELD_DEFINITION
|
202
202
|
|
203
203
|
type Product { sku: ID! }
|
204
204
|
type Order { id: ID! }
|
@@ -212,19 +212,69 @@ type Query {
|
|
212
212
|
}
|
213
213
|
```
|
214
214
|
|
215
|
-
####
|
215
|
+
#### Argument shapes
|
216
216
|
|
217
|
-
Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For
|
217
|
+
Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For custom mappings, the `arguments` option may specify a template of GraphQL arguments that insert key selections:
|
218
218
|
|
219
219
|
```graphql
|
220
220
|
type Product {
|
221
221
|
id: ID!
|
222
222
|
}
|
223
223
|
type Query {
|
224
|
-
product(byId: ID, bySku: ID): Product
|
224
|
+
product(byId: ID, bySku: ID): Product
|
225
|
+
@stitch(key: "id", arguments: "byId: $.id")
|
225
226
|
}
|
226
227
|
```
|
227
228
|
|
229
|
+
Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`. This syntax allows sending multiple arguments that intermix stitching keys with complex input shapes and other static values:
|
230
|
+
|
231
|
+
```graphql
|
232
|
+
type Product {
|
233
|
+
id: ID!
|
234
|
+
}
|
235
|
+
union Entity = Product
|
236
|
+
input EntityKey {
|
237
|
+
id: ID!
|
238
|
+
type: String!
|
239
|
+
}
|
240
|
+
|
241
|
+
type Query {
|
242
|
+
entities(keys: [EntityKey!]!, source: String="database"): [Entity]!
|
243
|
+
@stitch(key: "id", arguments: "keys: { id: $.id, type: $.__typename }, source: 'cache'")
|
244
|
+
}
|
245
|
+
```
|
246
|
+
|
247
|
+
See [resolver arguments](./docs/resolver.md#arguments) for full documentation on shaping input.
|
248
|
+
|
249
|
+
#### Composite type keys
|
250
|
+
|
251
|
+
Resolver keys may make composite selections for multiple key fields and/or nested scopes, for example:
|
252
|
+
|
253
|
+
```graphql
|
254
|
+
interface FieldOwner {
|
255
|
+
id: ID!
|
256
|
+
type: String!
|
257
|
+
}
|
258
|
+
type CustomField {
|
259
|
+
owner: FieldOwner!
|
260
|
+
key: String!
|
261
|
+
value: String
|
262
|
+
}
|
263
|
+
input CustomFieldLookup {
|
264
|
+
ownerId: ID!
|
265
|
+
ownerType: String!
|
266
|
+
key: String!
|
267
|
+
}
|
268
|
+
type Query {
|
269
|
+
customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
|
270
|
+
key: "owner { id type } key",
|
271
|
+
arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.type, key: $.key }"
|
272
|
+
)
|
273
|
+
}
|
274
|
+
```
|
275
|
+
|
276
|
+
Note that composite key selections may _not_ be distributed across locations. The complete selection criteria must be available in each location that provides the key.
|
277
|
+
|
228
278
|
#### Multiple type keys
|
229
279
|
|
230
280
|
A type may exist in multiple locations across the graph using different keys, for example:
|
data/docs/resolver.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
## GraphQL::Stitching::Resolver
|
2
|
+
|
3
|
+
A `Resolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
|
4
|
+
|
5
|
+
### Arguments
|
6
|
+
|
7
|
+
Resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
|
8
|
+
|
9
|
+
#### Key insertions
|
10
|
+
|
11
|
+
Key values fetched from previous locations may be inserted into arguments. Key insertions are prefixed by `$` and specify a dot-notation path to any selections made by the resolver `key`, or `__typename`.
|
12
|
+
|
13
|
+
```graphql
|
14
|
+
type Query {
|
15
|
+
entity(id: ID!, type: String!): [Entity]!
|
16
|
+
@stitch(key: "owner { id }", arguments: "id: $.owner.id, type: $.__typename")
|
17
|
+
}
|
18
|
+
```
|
19
|
+
|
20
|
+
Key insertions are _not_ quoted to differentiate them from other literal values.
|
21
|
+
|
22
|
+
#### Lists
|
23
|
+
|
24
|
+
List arguments may specify input just like non-list arguments, and [GraphQL list input coercion](https://spec.graphql.org/October2021/#sec-List.Input-Coercion) will assume the shape represents a list item:
|
25
|
+
|
26
|
+
```graphql
|
27
|
+
type Query {
|
28
|
+
product(ids: [ID!]!, source: DataSource!): [Product]!
|
29
|
+
@stitch(key: "id", arguments: "ids: $.id, source: CACHE")
|
30
|
+
}
|
31
|
+
```
|
32
|
+
|
33
|
+
List resolvers (that return list types) may _only_ insert keys into repeatable list arguments, while non-list arguments may only contain static values. Nested list inputs are neither common nor practical, so are not supported.
|
34
|
+
|
35
|
+
#### Built-in scalars
|
36
|
+
|
37
|
+
Built-in scalars are written as normal literal values. For convenience, string literals may be enclosed in single quotes rather than escaped double-quotes:
|
38
|
+
|
39
|
+
```graphql
|
40
|
+
type Query {
|
41
|
+
product(id: ID!, source: String!): Product
|
42
|
+
@stitch(key: "id", arguments: "id: $.id, source: 'cache'")
|
43
|
+
|
44
|
+
variant(id: ID!, limit: Int!): Variant
|
45
|
+
@stitch(key: "id", arguments: "id: $.id, limit: 100")
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
All scalar usage must be legal to the resolver field's arguments schema.
|
50
|
+
|
51
|
+
#### Enums
|
52
|
+
|
53
|
+
Enum literals may be provided anywhere in the input structure. They are _not_ quoted:
|
54
|
+
|
55
|
+
```graphql
|
56
|
+
enum DataSource {
|
57
|
+
CACHE
|
58
|
+
}
|
59
|
+
type Query {
|
60
|
+
product(id: ID!, source: DataSource!): [Product]!
|
61
|
+
@stitch(key: "id", arguments: "id: $.id, source: CACHE")
|
62
|
+
}
|
63
|
+
```
|
64
|
+
|
65
|
+
All enum usage must be legal to the resolver field's arguments schema.
|
66
|
+
|
67
|
+
#### Input Objects
|
68
|
+
|
69
|
+
Input objects may be provided anywhere in the input, even as nested structures. The stitching resolver will build the specified object shape:
|
70
|
+
|
71
|
+
```graphql
|
72
|
+
input ComplexKey {
|
73
|
+
id: ID
|
74
|
+
nested: ComplexKey
|
75
|
+
}
|
76
|
+
type Query {
|
77
|
+
product(key: ComplexKey!): [Product]!
|
78
|
+
@stitch(key: "id", arguments: "key: { nested: { id: $.id } }")
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
Input object shapes must conform to their respective schema definitions based on their placement within resolver arguments.
|
83
|
+
|
84
|
+
#### Custom scalars
|
85
|
+
|
86
|
+
Custom scalar keys allow any input shape to be submitted, from primitive scalars to complex object structures. These values will be sent and recieved as untyped JSON input:
|
87
|
+
|
88
|
+
```graphql
|
89
|
+
type Product {
|
90
|
+
id: ID!
|
91
|
+
}
|
92
|
+
union Entity = Product
|
93
|
+
scalar Key
|
94
|
+
|
95
|
+
type Query {
|
96
|
+
entities(representations: [Key!]!): [Entity]!
|
97
|
+
@stitch(key: "id", arguments: "representations: { id: $.id, __typename: $.__typename }")
|
98
|
+
}
|
99
|
+
```
|
100
|
+
|
101
|
+
Custom scalar arguments have no structured schema definition to validate against. This makes them flexible but quite lax, for better or worse.
|
@@ -70,7 +70,11 @@ module GraphQL
|
|
70
70
|
def load_plan(request)
|
71
71
|
if @on_cache_read && plan_json = @on_cache_read.call(request)
|
72
72
|
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
|
73
|
-
|
73
|
+
|
74
|
+
# only use plans referencing current resolver versions
|
75
|
+
if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
|
76
|
+
return request.plan(plan)
|
77
|
+
end
|
74
78
|
end
|
75
79
|
|
76
80
|
plan = request.plan
|
@@ -12,10 +12,10 @@ module GraphQL::Stitching
|
|
12
12
|
|
13
13
|
assignments.each_with_object({}) do |kwargs, memo|
|
14
14
|
type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
|
15
|
-
raise
|
15
|
+
raise CompositionError, "Invalid stitch directive type `#{kwargs[:parent_type_name]}`" unless type
|
16
16
|
|
17
17
|
field = type.get_field(kwargs[:field_name])
|
18
|
-
raise
|
18
|
+
raise CompositionError, "Invalid stitch directive field `#{kwargs[:field_name]}`" unless field
|
19
19
|
|
20
20
|
field_path = "#{location}.#{field.name}"
|
21
21
|
memo[field_path] ||= []
|
@@ -30,15 +30,15 @@ module GraphQL::Stitching
|
|
30
30
|
entity_type.directives.each do |directive|
|
31
31
|
next unless directive.graphql_name == "key"
|
32
32
|
|
33
|
-
key = directive.arguments.keyword_arguments.fetch(:fields)
|
34
|
-
|
35
|
-
|
33
|
+
key = Resolver.parse_key(directive.arguments.keyword_arguments.fetch(:fields))
|
34
|
+
key_fields = key.map { "#{_1.name}: $.#{_1.name}" }
|
36
35
|
field_path = "#{location}._entities"
|
36
|
+
|
37
37
|
memo[field_path] ||= []
|
38
38
|
memo[field_path] << new(
|
39
|
-
key: key,
|
39
|
+
key: key.to_definition,
|
40
40
|
type_name: entity_type.graphql_name,
|
41
|
-
representations:
|
41
|
+
arguments: "representations: { #{key_fields.join(", ")}, __typename: $.__typename }",
|
42
42
|
)
|
43
43
|
end
|
44
44
|
end
|
@@ -48,7 +48,7 @@ module GraphQL::Stitching
|
|
48
48
|
new(
|
49
49
|
key: kwargs[:key],
|
50
50
|
type_name: kwargs[:type_name] || kwargs[:typeName],
|
51
|
-
|
51
|
+
arguments: kwargs[:arguments],
|
52
52
|
)
|
53
53
|
end
|
54
54
|
|
@@ -57,16 +57,21 @@ module GraphQL::Stitching
|
|
57
57
|
def federation_entities_schema?(schema)
|
58
58
|
entity_type = schema.get_type(ENTITY_TYPENAME)
|
59
59
|
entities_query = schema.query.get_field(ENTITIES_QUERY)
|
60
|
-
entity_type &&
|
60
|
+
entity_type &&
|
61
|
+
entity_type.kind.union? &&
|
62
|
+
entities_query &&
|
63
|
+
entities_query.arguments["representations"] &&
|
64
|
+
entities_query.type.list? &&
|
65
|
+
entities_query.type.unwrap == entity_type
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
64
|
-
attr_reader :key, :type_name, :
|
69
|
+
attr_reader :key, :type_name, :arguments
|
65
70
|
|
66
|
-
def initialize(key:, type_name:,
|
71
|
+
def initialize(key:, type_name:, arguments: nil)
|
67
72
|
@key = key
|
68
73
|
@type_name = type_name
|
69
|
-
@
|
74
|
+
@arguments = arguments
|
70
75
|
end
|
71
76
|
end
|
72
77
|
end
|
@@ -15,7 +15,7 @@ module GraphQL::Stitching
|
|
15
15
|
# graphql-ruby will dynamically apply interface fields on a type implementation,
|
16
16
|
# so check the delegation map to assure that all materialized fields have resolver locations.
|
17
17
|
unless supergraph.locations_by_type_and_field[possible_type.graphql_name][field_name]&.any?
|
18
|
-
raise
|
18
|
+
raise ValidationError, "Type #{possible_type.graphql_name} does not implement a `#{field_name}` field in any location, "\
|
19
19
|
"which is required by interface #{interface_type.graphql_name}."
|
20
20
|
end
|
21
21
|
|
@@ -24,7 +24,7 @@ module GraphQL::Stitching
|
|
24
24
|
possible_type_structure = Util.flatten_type_structure(intersecting_field.type)
|
25
25
|
|
26
26
|
if possible_type_structure.length != interface_type_structure.length
|
27
|
-
raise
|
27
|
+
raise ValidationError, "Incompatible list structures between field #{possible_type.graphql_name}.#{field_name} of type "\
|
28
28
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
29
29
|
end
|
30
30
|
|
@@ -32,12 +32,12 @@ module GraphQL::Stitching
|
|
32
32
|
possible_struct = possible_type_structure[index]
|
33
33
|
|
34
34
|
if possible_struct.name != interface_struct.name
|
35
|
-
raise
|
35
|
+
raise ValidationError, "Incompatible named types between field #{possible_type.graphql_name}.#{field_name} of type "\
|
36
36
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
37
37
|
end
|
38
38
|
|
39
39
|
if possible_struct.null? && interface_struct.non_null?
|
40
|
-
raise
|
40
|
+
raise ValidationError, "Incompatible nullability between field #{possible_type.graphql_name}.#{field_name} of type "\
|
41
41
|
"#{intersecting_field.type.to_type_signature} and interface #{interface_type.graphql_name}.#{field_name} of type #{interface_field.type.to_type_signature}."
|
42
42
|
end
|
43
43
|
end
|
@@ -12,81 +12,82 @@ module GraphQL::Stitching
|
|
12
12
|
next if type.graphql_name.start_with?("__")
|
13
13
|
|
14
14
|
# multiple subschemas implement the type
|
15
|
-
|
16
|
-
next unless
|
15
|
+
subgraph_types_by_location = composer.subgraph_types_by_name_and_location[type_name]
|
16
|
+
next unless subgraph_types_by_location.length > 1
|
17
17
|
|
18
18
|
resolvers = supergraph.resolvers[type_name]
|
19
19
|
if resolvers&.any?
|
20
|
-
validate_as_resolver(supergraph, type,
|
20
|
+
validate_as_resolver(supergraph, type, subgraph_types_by_location, resolvers)
|
21
21
|
elsif type.kind.object?
|
22
|
-
validate_as_shared(supergraph, type,
|
22
|
+
validate_as_shared(supergraph, type, subgraph_types_by_location)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
-
def validate_as_resolver(supergraph, type,
|
29
|
+
def validate_as_resolver(supergraph, type, subgraph_types_by_location, resolvers)
|
30
30
|
# abstract resolvers are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
|
31
31
|
return if type.kind.abstract?
|
32
32
|
|
33
33
|
# only one resolver allowed per type/location/key
|
34
34
|
resolvers_by_location_and_key = resolvers.each_with_object({}) do |resolver, memo|
|
35
|
-
if memo.dig(resolver.location, resolver.key)
|
36
|
-
raise
|
35
|
+
if memo.dig(resolver.location, resolver.key.to_definition)
|
36
|
+
raise ValidationError, "Multiple resolver queries for `#{type.graphql_name}.#{resolver.key}` "\
|
37
37
|
"found in #{resolver.location}. Limit one resolver query per type and key in each location. "\
|
38
38
|
"Abstract resolvers provide all possible types."
|
39
39
|
end
|
40
40
|
memo[resolver.location] ||= {}
|
41
|
-
memo[resolver.location][resolver.key] = resolver
|
41
|
+
memo[resolver.location][resolver.key.to_definition] = resolver
|
42
42
|
end
|
43
43
|
|
44
|
-
resolver_keys = resolvers.map(&:key)
|
44
|
+
resolver_keys = resolvers.map(&:key)
|
45
|
+
resolver_key_strs = resolver_keys.map(&:to_definition).to_set
|
45
46
|
|
46
47
|
# All non-key fields must be resolvable in at least one resolver location
|
47
48
|
supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
|
48
|
-
next if
|
49
|
+
next if resolver_key_strs.include?(field_name)
|
49
50
|
|
50
51
|
if locations.none? { resolvers_by_location_and_key[_1] }
|
51
52
|
where = locations.length > 1 ? "one of #{locations.join(", ")} locations" : locations.first
|
52
|
-
raise
|
53
|
+
raise ValidationError, "A resolver query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
56
|
-
# All locations of a
|
57
|
-
supergraph.fields_by_type_and_location[type.graphql_name].
|
58
|
-
if
|
59
|
-
raise
|
57
|
+
# All locations of a merged type must include at least one resolver key
|
58
|
+
supergraph.fields_by_type_and_location[type.graphql_name].each_key do |location|
|
59
|
+
if resolver_keys.none? { _1.locations.include?(location) }
|
60
|
+
raise ValidationError, "A resolver key is required for `#{type.graphql_name}` in #{location} to join with other locations."
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
64
|
# verify that all outbound locations can access all inbound locations
|
64
65
|
resolver_locations = resolvers_by_location_and_key.keys
|
65
|
-
|
66
|
+
subgraph_types_by_location.each_key do |location|
|
66
67
|
remote_locations = resolver_locations.reject { _1 == location }
|
67
68
|
paths = supergraph.route_type_to_locations(type.graphql_name, location, remote_locations)
|
68
69
|
if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
|
69
|
-
raise
|
70
|
+
raise ValidationError, "Cannot route `#{type.graphql_name}` resolvers in #{location} to all other locations. "\
|
70
71
|
"All locations must provide a resolver query with a joining key."
|
71
72
|
end
|
72
73
|
end
|
73
74
|
end
|
74
75
|
|
75
|
-
def validate_as_shared(supergraph, type,
|
76
|
+
def validate_as_shared(supergraph, type, subgraph_types_by_location)
|
76
77
|
expected_fields = begin
|
77
78
|
type.fields.keys.sort
|
78
79
|
rescue StandardError => e
|
79
80
|
# bug with inherited interfaces in older versions of GraphQL
|
80
81
|
if type.interfaces.any? { _1.is_a?(GraphQL::Schema::LateBoundType) }
|
81
|
-
raise
|
82
|
+
raise CompositionError, "Merged interface inheritance requires GraphQL >= v2.0.3"
|
82
83
|
else
|
83
84
|
raise e
|
84
85
|
end
|
85
86
|
end
|
86
87
|
|
87
|
-
|
88
|
-
if
|
89
|
-
raise
|
88
|
+
subgraph_types_by_location.each do |location, subgraph_type|
|
89
|
+
if subgraph_type.fields.keys.sort != expected_fields
|
90
|
+
raise ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
|
90
91
|
"or else define resolver queries so that its unique fields may be accessed remotely."
|
91
92
|
end
|
92
93
|
end
|