graphql-stitching 1.2.2 → 1.2.4

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: 6753e205aac88d3dd3be1c906b743cc49ae0de84ff8ab56c7e9be6e889429aa3
4
- data.tar.gz: 59440f9d70cb365ef124604c42012bf655934555dfb76905328368353f5e310b
3
+ metadata.gz: 5c1c6ae95fd6ec036a7678356c8bc48191d8cf5cfd9e4b2936e79fdb72b52f48
4
+ data.tar.gz: 7f8002d013e26f69df5a7023e147f98b86a46cc3027d2753b6d704bfe75a835b
5
5
  SHA512:
6
- metadata.gz: ac218d2218c14b8debff552ffb11e83e00293dfebb404d25296ed9e408232e488c44575c701ee3fd933331d84755062517f2c2da8df2db98ac7c5e8594595846
7
- data.tar.gz: ec4f5051ebd511e72536c8e3246302079b2eb65ab5edab37c7c7d7de5488cdc7581318c69a0ea54047bd79ff42bf9e9c5af3434a27fc5667cf029340527c8117
6
+ metadata.gz: 734fe2a73f5412f30ccc6d89a20e62c1da376422de65a816a9680f02330cb2df93c39803cc182c5eed1c310624509beccb0a69162c1dad407ccaf6ada69178dd
7
+ data.tar.gz: c293a3de403d0888d15047e2de120a8187e4fee598229572f37905dd4e4b3429017ee958ae8ab2a2c16561d25761128b9a6ffb954cbf49b5a310c92aa77f7ffe
@@ -16,10 +16,16 @@ jobs:
16
16
  ruby: 3.2
17
17
  - gemfile: Gemfile
18
18
  ruby: 3.1
19
- - gemfile: gemfiles/graphql_1.13.9.gemfile
20
- ruby: 3.1
21
19
  - gemfile: Gemfile
22
20
  ruby: 2.7
21
+ - gemfile: gemfiles/graphql_2.2.0.gemfile
22
+ ruby: 3.1
23
+ - gemfile: gemfiles/graphql_2.1.0.gemfile
24
+ ruby: 3.1
25
+ - gemfile: gemfiles/graphql_2.0.0.gemfile
26
+ ruby: 3.1
27
+ - gemfile: gemfiles/graphql_1.13.9.gemfile
28
+ ruby: 3.1
23
29
 
24
30
  steps:
25
31
  - uses: actions/checkout@v2
data/README.md CHANGED
@@ -9,14 +9,14 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
9
9
  - Multiple keys per merged type.
10
10
  - Shared objects, fields, enums, and inputs across locations.
11
11
  - Combining local and remote schemas.
12
- - Type merging via arbitrary queries or federation `_entities` protocol.
13
12
  - File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
13
+ - Tested with all minor versions of `graphql-ruby`.
14
14
 
15
15
  **NOT Supported:**
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. While Ruby is not the fastest language for a purely high-throughput API gateway, 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.
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.
20
20
 
21
21
  ## Getting started
22
22
 
@@ -34,7 +34,7 @@ require "graphql/stitching"
34
34
 
35
35
  ## Usage
36
36
 
37
- The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow with [caching hooks](./docs/client.md#cache-hooks):
37
+ The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow (with optional query plan caching hooks):
38
38
 
39
39
  ```ruby
40
40
  movies_schema = <<~GRAPHQL
@@ -72,7 +72,7 @@ result = client.execute(
72
72
  )
73
73
  ```
74
74
 
75
- Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations. See [composer docs](./docs/composer.md#merge-patterns) for more information on how schemas get merged.
75
+ Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
76
76
 
77
77
  While the `Client` constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
78
78
 
@@ -87,11 +87,11 @@ While the `Client` constructor is an easy quick start, the library also has seve
87
87
 
88
88
  ![Merging types](./docs/images/merging.png)
89
89
 
90
- To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location. This can be done using [arbitrary queries](#merged-types-via-arbitrary-queries) or [federation entities](#merged-types-via-federation-entities).
90
+ To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
91
91
 
92
- ### Merged types via arbitrary queries
92
+ ### Merged type resolver queries
93
93
 
94
- Types can merge through arbitrary queries using the `@stitch` directive:
94
+ Types merge through resolver queries identified by a `@stitch` directive:
95
95
 
96
96
  ```graphql
97
97
  directive @stitch(key: String!) repeatable on FIELD_DEFINITION
@@ -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. For interfaces, the key selection should match a field within the interface. For unions, all possible types must implement the key selection individually.
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.
184
184
 
185
185
  ```graphql
186
186
  interface Node {
@@ -195,13 +195,33 @@ type Query {
195
195
  }
196
196
  ```
197
197
 
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
+
200
+ ```graphql
201
+ directive @stitch(key: String!, typeName: String) repeatable on FIELD_DEFINITION
202
+
203
+ type Product { sku: ID! }
204
+ type Order { id: ID! }
205
+ type Customer { id: ID! } # << not stitched
206
+ union Entity = Product | Order | Customer
207
+
208
+ type Query {
209
+ entity(key: ID!): Entity
210
+ @stitch(key: "sku", typeName: "Product")
211
+ @stitch(key: "id", typeName: "Order")
212
+ }
213
+ ```
214
+
198
215
  #### Multiple query arguments
199
216
 
200
- Stitching infers which argument to use for queries with a single argument. For queries that accept multiple arguments, the key must provide an argument mapping specified as `"<arg>:<key>"`. Note the `"id:id"` 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 may provide an argument mapping specified as `"<arg>:<key>"`.
201
218
 
202
219
  ```graphql
220
+ type Product {
221
+ id: ID!
222
+ }
203
223
  type Query {
204
- product(id: ID, upc: ID): Product @stitch(key: "id:id")
224
+ product(by_id: ID, by_sku: ID): Product @stitch(key: "by_id:id")
205
225
  }
206
226
  ```
207
227
 
@@ -211,8 +231,8 @@ A type may exist in multiple locations across the graph using different keys, fo
211
231
 
212
232
  ```graphql
213
233
  type Product { id:ID! } # storefronts location
214
- type Product { id:ID! upc:ID! } # products location
215
- type Product { upc:ID! } # catelog location
234
+ type Product { id:ID! sku:ID! } # products location
235
+ type Product { sku:ID! } # catelog location
216
236
  ```
217
237
 
218
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:
@@ -220,11 +240,11 @@ In the above graph, the `storefronts` and `catelog` locations have different key
220
240
  ```graphql
221
241
  type Product {
222
242
  id: ID!
223
- upc: ID!
243
+ sku: ID!
224
244
  }
225
245
  type Query {
226
246
  productById(id: ID!): Product @stitch(key: "id")
227
- productByUpc(upc: ID!): Product @stitch(key: "upc")
247
+ productByUpc(sku: ID!): Product @stitch(key: "sku")
228
248
  }
229
249
  ```
230
250
 
@@ -233,10 +253,10 @@ The `@stitch` directive is also repeatable, allowing a single query to associate
233
253
  ```graphql
234
254
  type Product {
235
255
  id: ID!
236
- upc: ID!
256
+ sku: ID!
237
257
  }
238
258
  type Query {
239
- product(id: ID, upc: ID): Product @stitch(key: "id:id") @stitch(key: "upc:upc")
259
+ product(id: ID, sku: ID): Product @stitch(key: "id") @stitch(key: "sku")
240
260
  }
241
261
  ```
242
262
 
@@ -270,11 +290,11 @@ A clean SDL string may also have stitching directives applied via static configu
270
290
  sdl_string = <<~GRAPHQL
271
291
  type Product {
272
292
  id: ID!
273
- upc: ID!
293
+ sku: ID!
274
294
  }
275
295
  type Query {
276
296
  productById(id: ID!): Product
277
- productByUpc(upc: ID!): Product
297
+ productByUpc(sku: ID!): Product
278
298
  }
279
299
  GRAPHQL
280
300
 
@@ -284,7 +304,7 @@ supergraph = GraphQL::Stitching::Composer.new.perform({
284
304
  executable: ->() { ... },
285
305
  stitch: [
286
306
  { field_name: "productById", key: "id" },
287
- { field_name: "productByUpc", key: "upc" },
307
+ { field_name: "productByUpc", key: "sku" },
288
308
  ]
289
309
  },
290
310
  # ...
@@ -299,65 +319,6 @@ The library is configured to use a `@stitch` directive by default. You may custo
299
319
  GraphQL::Stitching.stitch_directive = "merge"
300
320
  ```
301
321
 
302
- ### Merged types via Federation entities
303
-
304
- The [Apollo Federation specification](https://www.apollographql.com/docs/federation/subgraph-spec/) defines a standard interface for accessing merged type variants across locations. Stitching can utilize a _subset_ of this interface to facilitate basic type merging. The following spec is supported:
305
-
306
- - `@key(fields: "id")` (repeatable) specifies a key field for an object type. The key `fields` argument may only contain one field selection.
307
- - `_Entity` is a union type that must contain all types that implement a `@key`.
308
- - `_Any` is a scalar that recieves raw JSON objects; each object representation contains a `__typename` and the type's key field.
309
- - `_entities(representations: [_Any!]!): [_Entity]!` is a root query for local entity types.
310
-
311
- The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
312
-
313
- ```ruby
314
- products_schema = <<~GRAPHQL
315
- directive @key(fields: String!) repeatable on OBJECT
316
-
317
- type Product @key(fields: "id") {
318
- id: ID!
319
- name: String!
320
- }
321
-
322
- union _Entity = Product
323
- scalar _Any
324
-
325
- type Query {
326
- user(id: ID!): User
327
- _entities(representations: [_Any!]!): [_Entity]!
328
- }
329
- GRAPHQL
330
-
331
- catalog_schema = <<~GRAPHQL
332
- directive @key(fields: String!) repeatable on OBJECT
333
-
334
- type Product @key(fields: "id") {
335
- id: ID!
336
- price: Float!
337
- }
338
-
339
- union _Entity = Product
340
- scalar _Any
341
-
342
- type Query {
343
- _entities(representations: [_Any!]!): [_Entity]!
344
- }
345
- GRAPHQL
346
-
347
- client = GraphQL::Stitching::Client.new(locations: {
348
- products: {
349
- schema: GraphQL::Schema.from_definition(products_schema),
350
- executable: ...,
351
- },
352
- catalog: {
353
- schema: GraphQL::Schema.from_definition(catalog_schema),
354
- executable: ...,
355
- },
356
- })
357
- ```
358
-
359
- It's perfectly fine to mix and match schemas that implement an `_entities` query with schemas that implement `@stitch` directives; the protocols achieve the same result. Note that stitching is much simpler than Apollo Federation by design, and that Federation's advanced routing features (such as the `@requires` and `@external` directives) will not work with stitching.
360
-
361
322
  ## Executables
362
323
 
363
324
  An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or any object that responds to `.call(request, source, variables)` and returns a raw GraphQL response:
@@ -427,6 +388,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
427
388
 
428
389
  - [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
429
390
  - [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
391
+ - [Schema composition merge patterns](./docs/composer.md#merge-patterns)
430
392
  - [Field selection routing](./docs/mechanics.md#field-selection-routing)
431
393
  - [Root selection routing](./docs/mechanics.md#root-selection-routing)
432
394
  - [Stitched errors](./docs/mechanics.md#stitched-errors)
@@ -0,0 +1,70 @@
1
+ ## Merged types via Apollo Federation `_entities`
2
+
3
+ The [Apollo Federation specification](https://www.apollographql.com/docs/federation/subgraph-spec/) defines a standard interface for accessing merged type variants across locations. Stitching can utilize a _subset_ of this interface to facilitate basic type merging; the full spec is NOT supported and therefore is not fully interchangable with an Apollo Gateway.
4
+
5
+ To avoid confusion, using [basic resolver queries](../README.md#merged-type-resolver-queries) is recommended unless you specifically need to interact with a service built for an Apollo ecosystem. Even then, be wary that it does not exceed the supported spec by [using features that will not work](#federation-features-that-will-most-definitly-break).
6
+
7
+ ### Supported spec
8
+
9
+ The following subset of the federation spec is supported:
10
+
11
+ - `@key(fields: "id")` (repeatable) specifies a key field for an object type. The key `fields` argument may only contain one field selection.
12
+ - `_Entity` is a union type that must contain all types that implement a `@key`.
13
+ - `_Any` is a scalar that recieves raw JSON objects; each object representation contains a `__typename` and the type's key field.
14
+ - `_entities(representations: [_Any!]!): [_Entity]!` is a root query for local entity types.
15
+
16
+ The composer will automatcially detect and stitch schemas with an `_entities` query, for example:
17
+
18
+ ```ruby
19
+ products_schema = <<~GRAPHQL
20
+ directive @key(fields: String!) repeatable on OBJECT
21
+
22
+ type Product @key(fields: "id") {
23
+ id: ID!
24
+ name: String!
25
+ }
26
+
27
+ union _Entity = Product
28
+ scalar _Any
29
+
30
+ type Query {
31
+ user(id: ID!): User
32
+ _entities(representations: [_Any!]!): [_Entity]!
33
+ }
34
+ GRAPHQL
35
+
36
+ catalog_schema = <<~GRAPHQL
37
+ directive @key(fields: String!) repeatable on OBJECT
38
+
39
+ type Product @key(fields: "id") {
40
+ id: ID!
41
+ price: Float!
42
+ }
43
+
44
+ union _Entity = Product
45
+ scalar _Any
46
+
47
+ type Query {
48
+ _entities(representations: [_Any!]!): [_Entity]!
49
+ }
50
+ GRAPHQL
51
+
52
+ client = GraphQL::Stitching::Client.new(locations: {
53
+ products: {
54
+ schema: GraphQL::Schema.from_definition(products_schema),
55
+ executable: ...,
56
+ },
57
+ catalog: {
58
+ schema: GraphQL::Schema.from_definition(catalog_schema),
59
+ executable: ...,
60
+ },
61
+ })
62
+ ```
63
+
64
+ It's perfectly fine to mix and match schemas that implement an `_entities` query with schemas that implement `@stitch` directives; the protocols achieve the same result.
65
+
66
+ ### Federation features that will most definitly break
67
+
68
+ - `@external` fields will confuse the stitching query planner.
69
+ - `@requires` fields will not be sent any dependencies.
70
+ - No support for Apollo composition directives.
@@ -3,4 +3,4 @@
3
3
  source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
- gem 'graphql', '1.13.9'
6
+ gem 'graphql', '~> 1.13.9'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'graphql', '~> 2.0.0'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'graphql', '~> 2.1.0'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'graphql', '~> 2.2.0'
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL::Stitching
4
+ class Composer
5
+ class BoundaryConfig
6
+ ENTITY_TYPENAME = "_Entity"
7
+ ENTITIES_QUERY = "_entities"
8
+
9
+ class << self
10
+ def extract_directive_assignments(schema, location, assignments)
11
+ return EMPTY_OBJECT unless assignments && assignments.any?
12
+
13
+ assignments.each_with_object({}) do |kwargs, memo|
14
+ type = kwargs[:parent_type_name] ? schema.get_type(kwargs[:parent_type_name]) : schema.query
15
+ raise ComposerError, "Invalid stitch directive type `#{kwargs[:parent_type_name]}`" unless type
16
+
17
+ field = type.get_field(kwargs[:field_name])
18
+ raise ComposerError, "Invalid stitch directive field `#{kwargs[:field_name]}`" unless field
19
+
20
+ field_path = "#{location}.#{field.name}"
21
+ memo[field_path] ||= []
22
+ memo[field_path] << from_kwargs(kwargs)
23
+ end
24
+ end
25
+
26
+ def extract_federation_entities(schema, location)
27
+ return EMPTY_OBJECT unless federation_entities_schema?(schema)
28
+
29
+ schema.possible_types(schema.get_type(ENTITY_TYPENAME)).each_with_object({}) do |entity_type, memo|
30
+ entity_type.directives.each do |directive|
31
+ next unless directive.graphql_name == "key"
32
+
33
+ key = directive.arguments.keyword_arguments.fetch(:fields).strip
34
+ raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
35
+
36
+ field_path = "#{location}._entities"
37
+ memo[field_path] ||= []
38
+ memo[field_path] << new(
39
+ key: key,
40
+ type_name: entity_type.graphql_name,
41
+ federation: true,
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ def from_kwargs(kwargs)
48
+ new(
49
+ key: kwargs[:key],
50
+ type_name: kwargs[:type_name] || kwargs[:typeName],
51
+ federation: kwargs[:federation] || false,
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def federation_entities_schema?(schema)
58
+ entity_type = schema.get_type(ENTITY_TYPENAME)
59
+ entities_query = schema.query.get_field(ENTITIES_QUERY)
60
+ entity_type && entity_type.kind.union? && entities_query && entities_query.type.unwrap == entity_type
61
+ end
62
+ end
63
+
64
+ attr_reader :key, :type_name, :federation
65
+
66
+ def initialize(key:, type_name:, federation: false)
67
+ @key = key
68
+ @type_name = type_name
69
+ @federation = federation
70
+ end
71
+ end
72
+ end
73
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "./composer/base_validator"
4
4
  require_relative "./composer/validate_interfaces"
5
5
  require_relative "./composer/validate_boundaries"
6
+ require_relative "./composer/boundary_config"
6
7
 
7
8
  module GraphQL
8
9
  module Stitching
@@ -61,7 +62,7 @@ module GraphQL
61
62
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
62
63
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
63
64
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
64
- @stitch_directives = {}
65
+ @boundary_configs = {}
65
66
 
66
67
  @field_map = nil
67
68
  @boundary_map = nil
@@ -145,7 +146,8 @@ module GraphQL
145
146
 
146
147
  builder = self
147
148
  schema = Class.new(GraphQL::Schema) do
148
- orphan_types schema_types.values
149
+ add_type_and_traverse(schema_types.values, root: false)
150
+ orphan_types(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
149
151
  query schema_types[builder.query_name]
150
152
  mutation schema_types[builder.mutation_name]
151
153
  directives builder.schema_directives.values
@@ -186,37 +188,8 @@ module GraphQL
186
188
  raise ComposerError, "The schema for `#{location}` location must be a GraphQL::Schema class."
187
189
  end
188
190
 
189
- input.fetch(:stitch, GraphQL::Stitching::EMPTY_ARRAY).each do |dir|
190
- type = dir[:parent_type_name] ? schema.types[dir[:parent_type_name]] : schema.query
191
- raise ComposerError, "Invalid stitch directive type `#{dir[:parent_type_name]}`" unless type
192
-
193
- field = type.fields[dir[:field_name]]
194
- raise ComposerError, "Invalid stitch directive field `#{dir[:field_name]}`" unless field
195
-
196
- field_path = "#{location}.#{field.name}"
197
- @stitch_directives[field_path] ||= []
198
- @stitch_directives[field_path] << dir.slice(:key, :type_name)
199
- end
200
-
201
- federation_entity_type = schema.types["_Entity"]
202
- if federation_entity_type && federation_entity_type.kind.union? && schema.query.fields["_entities"]&.type&.unwrap == federation_entity_type
203
- schema.possible_types(federation_entity_type).each do |entity_type|
204
- entity_type.directives.each do |directive|
205
- next unless directive.graphql_name == "key"
206
-
207
- key = directive.arguments.keyword_arguments.fetch(:fields).strip
208
- raise ComposerError, "Composite federation keys are not supported." unless /^\w+$/.match?(key)
209
-
210
- field_path = "#{location}._entities"
211
- @stitch_directives[field_path] ||= []
212
- @stitch_directives[field_path] << {
213
- key: key,
214
- type_name: entity_type.graphql_name,
215
- federation: true,
216
- }
217
- end
218
- end
219
- end
191
+ @boundary_configs.merge!(BoundaryConfig.extract_directive_assignments(schema, location, input[:stitch]))
192
+ @boundary_configs.merge!(BoundaryConfig.extract_federation_entities(schema, location))
220
193
 
221
194
  schemas[location.to_s] = schema
222
195
  executables[location.to_s] = input[:executable] || schema
@@ -556,19 +529,17 @@ module GraphQL
556
529
  def extract_boundaries(type_name, types_by_location)
557
530
  types_by_location.each do |location, type_candidate|
558
531
  type_candidate.fields.each do |field_name, field_candidate|
559
- boundary_type_name = field_candidate.type.unwrap.graphql_name
532
+ boundary_type = field_candidate.type.unwrap
560
533
  boundary_structure = Util.flatten_type_structure(field_candidate.type)
561
- boundary_kwargs = @stitch_directives["#{location}.#{field_name}"] || []
534
+ boundary_configs = @boundary_configs.fetch("#{location}.#{field_name}", [])
562
535
 
563
536
  field_candidate.directives.each do |directive|
564
537
  next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
565
- boundary_kwargs << directive.arguments.keyword_arguments
538
+ boundary_configs << BoundaryConfig.from_kwargs(directive.arguments.keyword_arguments)
566
539
  end
567
540
 
568
- boundary_kwargs.each do |kwargs|
569
- key = kwargs.fetch(:key)
570
- impl_type_name = kwargs.fetch(:type_name, boundary_type_name)
571
- key_selections = GraphQL.parse("{ #{key} }").definitions[0].selections
541
+ boundary_configs.each do |config|
542
+ key_selections = GraphQL.parse("{ #{config.key} }").definitions[0].selections
572
543
 
573
544
  if key_selections.length != 1
574
545
  raise ComposerError, "Boundary key at #{type_name}.#{field_name} must specify exactly one key."
@@ -577,6 +548,8 @@ module GraphQL
577
548
  argument_name = key_selections[0].alias
578
549
  argument_name ||= if field_candidate.arguments.size == 1
579
550
  field_candidate.arguments.keys.first
551
+ elsif field_candidate.arguments[config.key]
552
+ config.key
580
553
  end
581
554
 
582
555
  argument = field_candidate.arguments[argument_name]
@@ -590,15 +563,26 @@ module GraphQL
590
563
  raise ComposerError, "Mismatched input/output for #{type_name}.#{field_name}.#{argument_name} boundary. Arguments must map directly to results."
591
564
  end
592
565
 
593
- @boundary_map[impl_type_name] ||= []
594
- @boundary_map[impl_type_name] << Boundary.new(
566
+ boundary_type_name = if config.type_name
567
+ if !boundary_type.kind.abstract?
568
+ raise ComposerError, "Resolver config may only specify a type name for abstract resolvers."
569
+ elsif !boundary_type.possible_types.find { _1.graphql_name == config.type_name }
570
+ raise ComposerError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
571
+ end
572
+ config.type_name
573
+ else
574
+ boundary_type.graphql_name
575
+ end
576
+
577
+ @boundary_map[boundary_type_name] ||= []
578
+ @boundary_map[boundary_type_name] << Boundary.new(
595
579
  location: location,
596
- type_name: impl_type_name,
580
+ type_name: boundary_type_name,
597
581
  key: key_selections[0].name,
598
582
  field: field_candidate.name,
599
583
  arg: argument_name,
600
584
  list: boundary_structure.first.list?,
601
- federation: kwargs[:federation] || false,
585
+ federation: config.federation,
602
586
  )
603
587
  end
604
588
  end
@@ -5,6 +5,7 @@ module GraphQL::Stitching
5
5
  class ResolverDirective < GraphQL::Schema::Directive
6
6
  graphql_name "resolver"
7
7
  locations OBJECT, INTERFACE, UNION
8
+ argument :type_name, String, required: false
8
9
  argument :location, String, required: true
9
10
  argument :key, String, required: true
10
11
  argument :field, String, required: true
@@ -31,7 +31,7 @@ module GraphQL
31
31
  kwargs = directive.arguments.keyword_arguments
32
32
  boundary_map[type_name] ||= []
33
33
  boundary_map[type_name] << Boundary.new(
34
- type_name: type_name,
34
+ type_name: kwargs.fetch(:type_name, type_name),
35
35
  location: kwargs[:location],
36
36
  key: kwargs[:key],
37
37
  field: kwargs[:field],
@@ -134,6 +134,7 @@ module GraphQL
134
134
  end
135
135
 
136
136
  type.directive(ResolverDirective, **{
137
+ type_name: (boundary.type_name if boundary.type_name != type_name),
137
138
  location: boundary.location,
138
139
  key: boundary.key,
139
140
  field: boundary.field,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.2.2"
5
+ VERSION = "1.2.4"
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.2
4
+ version: 1.2.4
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-02-11 00:00:00.000000000 Z
11
+ date: 2024-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -82,6 +82,7 @@ files:
82
82
  - docs/README.md
83
83
  - docs/client.md
84
84
  - docs/composer.md
85
+ - docs/federation_entities.md
85
86
  - docs/http_executable.md
86
87
  - docs/images/library.png
87
88
  - docs/images/merging.png
@@ -104,12 +105,16 @@ files:
104
105
  - examples/merged_types/remote1.rb
105
106
  - examples/merged_types/remote2.rb
106
107
  - gemfiles/graphql_1.13.9.gemfile
108
+ - gemfiles/graphql_2.0.0.gemfile
109
+ - gemfiles/graphql_2.1.0.gemfile
110
+ - gemfiles/graphql_2.2.0.gemfile
107
111
  - graphql-stitching.gemspec
108
112
  - lib/graphql/stitching.rb
109
113
  - lib/graphql/stitching/boundary.rb
110
114
  - lib/graphql/stitching/client.rb
111
115
  - lib/graphql/stitching/composer.rb
112
116
  - lib/graphql/stitching/composer/base_validator.rb
117
+ - lib/graphql/stitching/composer/boundary_config.rb
113
118
  - lib/graphql/stitching/composer/validate_boundaries.rb
114
119
  - lib/graphql/stitching/composer/validate_interfaces.rb
115
120
  - lib/graphql/stitching/executor.rb