graphql-stitching 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c1c6ae95fd6ec036a7678356c8bc48191d8cf5cfd9e4b2936e79fdb72b52f48
4
- data.tar.gz: 7f8002d013e26f69df5a7023e147f98b86a46cc3027d2753b6d704bfe75a835b
3
+ metadata.gz: 754e7a61df541f685ef50ec79039df9742e4a2589a30eadf93125d6e4ef47aed
4
+ data.tar.gz: e2ceb89be07b42dbffeaa75416055fa62ffafa001088fd47b2b4f156aee6123a
5
5
  SHA512:
6
- metadata.gz: 734fe2a73f5412f30ccc6d89a20e62c1da376422de65a816a9680f02330cb2df93c39803cc182c5eed1c310624509beccb0a69162c1dad407ccaf6ada69178dd
7
- data.tar.gz: c293a3de403d0888d15047e2de120a8187e4fee598229572f37905dd4e4b3429017ee958ae8ab2a2c16561d25761128b9a6ffb954cbf49b5a310c92aa77f7ffe
6
+ metadata.gz: 561ecdc36e78b31e4f4fd2f85d4ff5e0f1ab23d44570de7d8ffe63c2e46b5856214eb07c25b98eafbd64857602ebd839e187175e9e3ec347caedf4651a00d332
7
+ data.tar.gz: dd7a98e1c97f7bd5a6ca2585c34b6538e94b6dfaaa83edf9b3f1bc287a79d04f62a29ebe2a6b6fae4d06e8af9f08bb20203a89f1b062f24dc622a66973f87abc
data/README.md CHANGED
@@ -16,7 +16,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
16
16
  - Computed fields (ie: federation-style `@requires`).
17
17
  - Subscriptions, defer/stream.
18
18
 
19
- This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-performance API gateway, consider not using Ruby.
19
+ This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-throughput federated reverse proxy, consider not using Ruby.
20
20
 
21
21
  ## Getting started
22
22
 
@@ -153,7 +153,7 @@ type Query {
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
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](#multiple-query-arguments) later).
155
155
 
156
- Each location that provides a unique variant of a type must provide at least one stitching query for the type. The exception to this requirement are [foreign key types](./docs/mechanics.md##modeling-foreign-keys-for-stitching) that contain only a single key field:
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
 
158
158
  ```graphql
159
159
  type Product {
@@ -161,11 +161,11 @@ type Product {
161
161
  }
162
162
  ```
163
163
 
164
- The above representation of a `Product` type provides no unique data beyond a key that is available in other locations. Thus, this representation will never require an inbound request to fetch it, and its stitching query may be omitted.
164
+ The above representation of a `Product` type contains nothing but a key that is available in other locations. Therefore, this representation will never require an inbound request to fetch it, and its resolver query may be omitted.
165
165
 
166
166
  #### List queries
167
167
 
168
- It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a stitching query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
168
+ It's okay ([even preferable](#batching) in most circumstances) to provide a list accessor as a resolver query. The only requirement is that both the field argument and return type must be lists, and the query results are expected to be a mapped set with `null` holding the position of missing results.
169
169
 
170
170
  ```graphql
171
171
  type Query {
@@ -180,7 +180,7 @@ See [error handling](./docs/mechanics.md#stitched-errors) tips for list queries.
180
180
 
181
181
  #### Abstract queries
182
182
 
183
- It's okay for stitching queries to be implemented through abstract types. An abstract query will provide access to all of its possible types by default, each of which must implement the key.
183
+ It's okay for resolver queries to be implemented through abstract types. An abstract query will provide access to all of its possible types by default, each of which must implement the key.
184
184
 
185
185
  ```graphql
186
186
  interface Node {
@@ -214,14 +214,14 @@ type Query {
214
214
 
215
215
  #### Multiple query arguments
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 queries that accept multiple arguments with unmatched names, the key may provide an argument mapping specified as `"<arg>:<key>"`.
217
+ Stitching infers which argument to use for queries with a single argument, or when the key name matches its intended argument. For queries that accept multiple arguments with unmatched names, the key should provide an argument alias specified as `"<arg>:<key>"`.
218
218
 
219
219
  ```graphql
220
220
  type Product {
221
221
  id: ID!
222
222
  }
223
223
  type Query {
224
- product(by_id: ID, by_sku: ID): Product @stitch(key: "by_id:id")
224
+ product(byId: ID, bySku: ID): Product @stitch(key: "byId:id")
225
225
  }
226
226
  ```
227
227
 
@@ -235,7 +235,7 @@ type Product { id:ID! sku:ID! } # products location
235
235
  type Product { sku:ID! } # catelog location
236
236
  ```
237
237
 
238
- In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides stitching queries for each possible key:
238
+ In the above graph, the `storefronts` and `catelog` locations have different keys that join through an intermediary. This pattern is perfectly valid and resolvable as long as the intermediary provides resolver queries for each possible key:
239
239
 
240
240
  ```graphql
241
241
  type Product {
@@ -244,7 +244,7 @@ type Product {
244
244
  }
245
245
  type Query {
246
246
  productById(id: ID!): Product @stitch(key: "id")
247
- productByUpc(sku: ID!): Product @stitch(key: "sku")
247
+ productBySku(sku: ID!): Product @stitch(key: "sku")
248
248
  }
249
249
  ```
250
250
 
@@ -294,7 +294,7 @@ sdl_string = <<~GRAPHQL
294
294
  }
295
295
  type Query {
296
296
  productById(id: ID!): Product
297
- productByUpc(sku: ID!): Product
297
+ productBySku(sku: ID!): Product
298
298
  }
299
299
  GRAPHQL
300
300
 
@@ -304,7 +304,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
304
304
  executable: ->() { ... },
305
305
  stitch: [
306
306
  { field_name: "productById", key: "id" },
307
- { field_name: "productByUpc", key: "sku" },
307
+ { field_name: "productBySku", key: "sku" },
308
308
  ]
309
309
  },
310
310
  # ...
data/docs/mechanics.md CHANGED
@@ -303,3 +303,45 @@ And produces this result:
303
303
  ```
304
304
 
305
305
  Location B is allowed to return `null` here because its one unique field, `rating`, is nullable (the `id` field can be provided by Location A). If `rating` were non-null, then null bubbling would invalidate the response data.
306
+
307
+ ### Outbound-only merged types
308
+
309
+ Merged types do not always require a resolver query. For example:
310
+
311
+ ```graphql
312
+ # -- Location A
313
+
314
+ type Widget {
315
+ id: ID!
316
+ name: String
317
+ }
318
+
319
+ type Query {
320
+ widgetA(id: ID!): Widget @stitch(key: "id")
321
+ }
322
+
323
+ # -- Location B
324
+
325
+ type Widget {
326
+ id: ID!
327
+ size: Float
328
+ }
329
+
330
+ type Query {
331
+ widgetB(id: ID!): Widget @stitch(key: "id")
332
+ }
333
+
334
+ # -- Location C
335
+
336
+ type Widget {
337
+ id: ID!
338
+ name: String
339
+ size: Float
340
+ }
341
+
342
+ type Query {
343
+ featuredWidget: Widget
344
+ }
345
+ ```
346
+
347
+ In this graph, `Widget` is a merged type without a resolver query in location C. This works because all of its fields are resolvable in other locations; that means location C can provide outbound representations of this type without ever needing to resolve inbound requests for it. Outbound types do still require a key field (such as `id` above) that allow them to join with data in other resolver locations.
@@ -4,29 +4,29 @@ module GraphQL::Stitching
4
4
  class Composer
5
5
  class ValidateBoundaries < BaseValidator
6
6
 
7
- def perform(ctx, composer)
8
- ctx.schema.types.each do |type_name, type|
7
+ def perform(supergraph, composer)
8
+ supergraph.schema.types.each do |type_name, type|
9
9
  # objects and interfaces that are not the root operation types
10
10
  next unless type.kind.object? || type.kind.interface?
11
- next if ctx.schema.query == type || ctx.schema.mutation == type
11
+ next if supergraph.schema.query == type || supergraph.schema.mutation == type
12
12
  next if type.graphql_name.start_with?("__")
13
13
 
14
14
  # multiple subschemas implement the type
15
15
  candidate_types_by_location = composer.candidate_types_by_name_and_location[type_name]
16
16
  next unless candidate_types_by_location.length > 1
17
17
 
18
- boundaries = ctx.boundaries[type_name]
18
+ boundaries = supergraph.boundaries[type_name]
19
19
  if boundaries&.any?
20
- validate_as_boundary(ctx, type, candidate_types_by_location, boundaries)
20
+ validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
21
21
  elsif type.kind.object?
22
- validate_as_shared(ctx, type, candidate_types_by_location)
22
+ validate_as_shared(supergraph, type, candidate_types_by_location)
23
23
  end
24
24
  end
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def validate_as_boundary(ctx, type, candidate_types_by_location, boundaries)
29
+ def validate_as_boundary(supergraph, type, candidate_types_by_location, boundaries)
30
30
  # abstract boundaries are expanded with their concrete implementations, which each get validated. Ignore the abstract itself.
31
31
  return if type.kind.abstract?
32
32
 
@@ -41,25 +41,30 @@ module GraphQL::Stitching
41
41
  memo[boundary.location][boundary.key] = boundary
42
42
  end
43
43
 
44
- boundary_keys = boundaries.map { _1.key }.uniq
45
- key_only_types_by_location = candidate_types_by_location.select do |location, subschema_type|
46
- subschema_type.fields.keys.length == 1 && boundary_keys.include?(subschema_type.fields.keys.first)
47
- end
44
+ boundary_keys = boundaries.map(&:key).to_set
45
+
46
+ # All non-key fields must be resolvable in at least one boundary location
47
+ supergraph.locations_by_type_and_field[type.graphql_name].each do |field_name, locations|
48
+ next if boundary_keys.include?(field_name)
48
49
 
49
- # all locations have a boundary, or else are key-only
50
- candidate_types_by_location.each do |location, subschema_type|
51
- unless boundaries_by_location_and_key[location] || key_only_types_by_location[location]
52
- raise Composer::ValidationError, "A boundary query is required for `#{type.graphql_name}` in #{location} because it provides unique fields."
50
+ if locations.none? { boundaries_by_location_and_key[_1] }
51
+ where = locations.length > 1 ? "one of #{locations.join(", ")} locations" : locations.first
52
+ raise Composer::ValidationError, "A boundary query is required for `#{type.graphql_name}` in #{where} to resolve field `#{field_name}`."
53
53
  end
54
54
  end
55
55
 
56
- outbound_access_locations = key_only_types_by_location.keys
57
- bidirectional_access_locations = candidate_types_by_location.keys - outbound_access_locations
56
+ # All locations of a boundary type must include at least one key field
57
+ supergraph.fields_by_type_and_location[type.graphql_name].each do |location, field_names|
58
+ if field_names.none? { boundary_keys.include?(_1) }
59
+ raise Composer::ValidationError, "A boundary key is required for `#{type.graphql_name}` in #{location} to join with other locations."
60
+ end
61
+ end
58
62
 
59
63
  # verify that all outbound locations can access all inbound locations
60
- (outbound_access_locations + bidirectional_access_locations).each do |location|
61
- remote_locations = bidirectional_access_locations.reject { _1 == location }
62
- paths = ctx.route_type_to_locations(type.graphql_name, location, remote_locations)
64
+ resolver_locations = boundaries_by_location_and_key.keys
65
+ candidate_types_by_location.each_key do |location|
66
+ remote_locations = resolver_locations.reject { _1 == location }
67
+ paths = supergraph.route_type_to_locations(type.graphql_name, location, remote_locations)
63
68
  if paths.length != remote_locations.length || paths.any? { |_loc, path| path.nil? }
64
69
  raise Composer::ValidationError, "Cannot route `#{type.graphql_name}` boundaries in #{location} to all other locations. "\
65
70
  "All locations must provide a boundary accessor that uses a conjoining key."
@@ -67,7 +72,7 @@ module GraphQL::Stitching
67
72
  end
68
73
  end
69
74
 
70
- def validate_as_shared(ctx, type, candidate_types_by_location)
75
+ def validate_as_shared(supergraph, type, candidate_types_by_location)
71
76
  expected_fields = begin
72
77
  type.fields.keys.sort
73
78
  rescue StandardError => e
@@ -79,8 +84,8 @@ module GraphQL::Stitching
79
84
  end
80
85
  end
81
86
 
82
- candidate_types_by_location.each do |location, subschema_type|
83
- if subschema_type.fields.keys.sort != expected_fields
87
+ candidate_types_by_location.each do |location, candidate_type|
88
+ if candidate_type.fields.keys.sort != expected_fields
84
89
  raise Composer::ValidationError, "Shared type `#{type.graphql_name}` must have consistent fields across locations, "\
85
90
  "or else define boundary queries so that its unique fields may be accessed remotely."
86
91
  end
@@ -68,6 +68,7 @@ module GraphQL
68
68
  @boundary_map = nil
69
69
  @mapped_type_names = nil
70
70
  @candidate_directives_by_name_and_location = nil
71
+ @candidate_types_by_name_and_location = nil
71
72
  @schema_directives = nil
72
73
  end
73
74
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.2.4"
5
+ VERSION = "1.2.5"
6
6
  end
7
7
  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: 1.2.4
4
+ version: 1.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-07 00:00:00.000000000 Z
11
+ date: 2024-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql