graphql-stitching 1.5.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8fb3561ddb47de2d07bdcdf32e940e1a151dff731081b2b4bffe09489809021
4
- data.tar.gz: 9163e0d0e5116e232ee0594821d02568f85405c7fdc0f80e085e713d2e4d3a71
3
+ metadata.gz: cf60ae0ef85426a3011223bcc146a74a1664762e5488d8266d0afbf1c2143457
4
+ data.tar.gz: d20da5d4817193de7e156441cab88b0855f3fa93aeabe5e760f8fd66ac5b3547
5
5
  SHA512:
6
- metadata.gz: 6823d661a3f812fbf11bbf14edf66f3da27a850399f909061c63d464d3be93777fe7f9c7158ea26cc0d1889aed04749d3ad10cd42c3ca76079604447ab66e3cc
7
- data.tar.gz: 27dee9de11b90e960fb0c17d68129332ede38d237ef37ae1eca4cdca2c83696dec4b6c9f0283ee7f508c4f5076029afc81efa4b58d60e5b74411a6a368092947
6
+ metadata.gz: b89843bfdd353a6b4aec47fce17a6bd6524b29f7d5fa5dc0452570dc1b4bec63691171f7cd3f841c393015df763e20e1be7cf907cc660fcc576cc93e08c3d4dd
7
+ data.tar.gz: a20f68f8bac0a52271fdc76ec4f173347669acda7f49b18e177746c83df1ec42dc878e8d3af37545a99f5456acc6d9f7577600d79c4568925cd23837fe5c2d0a
@@ -13,22 +13,20 @@ jobs:
13
13
  matrix:
14
14
  include:
15
15
  - gemfile: Gemfile
16
+ ruby: 3.3
17
+ - gemfile: gemfiles/graphql_2.3.0.gemfile
16
18
  ruby: 3.2
17
- - gemfile: Gemfile
18
- ruby: 3.1
19
- - gemfile: Gemfile
20
- ruby: 2.7
21
19
  - gemfile: gemfiles/graphql_2.2.0.gemfile
22
20
  ruby: 3.1
23
21
  - gemfile: gemfiles/graphql_2.1.0.gemfile
24
22
  ruby: 3.1
25
23
  - gemfile: gemfiles/graphql_2.0.0.gemfile
26
24
  ruby: 3.1
27
- - gemfile: gemfiles/graphql_1.13.9.gemfile
28
- ruby: 3.1
29
-
25
+ - gemfile: gemfiles/graphql_2.0.0.gemfile
26
+ ruby: 2.7
30
27
  steps:
31
- - uses: actions/checkout@v2
28
+ - run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV
29
+ - uses: actions/checkout@v4
32
30
  - name: Setup Ruby
33
31
  uses: ruby/setup-ruby@v1
34
32
  with:
data/Gemfile CHANGED
@@ -6,3 +6,4 @@ gemspec
6
6
  gem 'pry'
7
7
  gem 'pry-byebug'
8
8
  gem 'warning'
9
+ gem 'minitest-stub-const'
data/README.md CHANGED
@@ -1,14 +1,13 @@
1
1
  ## GraphQL Stitching for Ruby
2
2
 
3
- GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire location graph to be queried through one combined GraphQL surface area.
3
+ GraphQL stitching composes a single schema from multiple underlying GraphQL resources, then smartly proxies portions of incoming requests to their respective locations in dependency order and returns the merged results. This allows an entire graph of locations to be queried through one combined GraphQL surface area.
4
4
 
5
5
  ![Stitched graph](./docs/images/stitching.png)
6
6
 
7
7
  **Supports:**
8
8
  - All operation types: query, mutation, and [subscription](./docs/subscriptions.md).
9
- - Merged object and abstract types.
9
+ - Merged object and abstract types joining though multiple keys.
10
10
  - Shared objects, fields, enums, and inputs across locations.
11
- - Multiple and composite type keys.
12
11
  - Combining local and remote schemas.
13
12
  - [File uploads](./docs/http_executable.md) via multipart forms.
14
13
  - Tested with all minor versions of `graphql-ruby`.
@@ -17,7 +16,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
17
16
  - Computed fields (ie: federation-style `@requires`).
18
17
  - Defer/stream.
19
18
 
20
- 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.
19
+ This Ruby implementation is designed as a generic library to join basic spec-compliant GraphQL schemas using their existing types and fields in a [DIY](https://dictionary.cambridge.org/us/dictionary/english/diy) capacity. 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 a purely high-throughput federation gateway with managed schema deployments, consider more opinionated frameworks such as [Apollo Federation](https://www.apollographql.com/docs/federation/).
21
20
 
22
21
  ## Getting started
23
22
 
@@ -88,17 +87,59 @@ While `Client` is sufficient for most usecases, the library offers several discr
88
87
 
89
88
  ![Merging types](./docs/images/merging.png)
90
89
 
91
- 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 [type 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).
90
+ To facilitate this, schemas should be designed around [merged type keys](#merged-type-keys) that stitching can cross-reference and fetch across locations using [type 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
+
92
+ ### Merged type keys
93
+
94
+ Foreign keys in a GraphQL schema frequently look like the `Product.imageId` field here:
95
+
96
+ ```graphql
97
+ # -- Products schema:
98
+
99
+ type Product {
100
+ id: ID!
101
+ imageId: ID!
102
+ }
103
+
104
+ # -- Images schema:
105
+
106
+ type Image {
107
+ id: ID!
108
+ url: String!
109
+ }
110
+ ```
111
+
112
+ However, this design does not lend itself to merging types across locations. A simple schema refactor makes this foreign key more expressive as an entity type, and turns the key into an _object_ that will merge with analogous objects in other locations:
113
+
114
+ ```graphql
115
+ # -- Products schema:
116
+
117
+ type Product {
118
+ id: ID!
119
+ image: Image!
120
+ }
121
+
122
+ type Image {
123
+ id: ID!
124
+ }
125
+
126
+ # -- Images schema:
127
+
128
+ type Image {
129
+ id: ID!
130
+ url: String!
131
+ }
132
+ ```
92
133
 
93
134
  ### Merged type resolver queries
94
135
 
95
- Types merge through resolver queries identified by a `@stitch` directive:
136
+ Each location that provides a unique variant of a type must provide at least one _resolver query_ for accessing it. Type resolvers are root queries identified by a `@stitch` directive:
96
137
 
97
138
  ```graphql
98
139
  directive @stitch(key: String!, arguments: String) repeatable on FIELD_DEFINITION
99
140
  ```
100
141
 
101
- 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.
142
+ This directive tells stitching how to cross-reference and fetch types from across locations, for example:
102
143
 
103
144
  ```ruby
104
145
  products_schema = <<~GRAPHQL
@@ -151,10 +192,10 @@ type Query {
151
192
  }
152
193
  ```
153
194
 
154
- * 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.
155
- * 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).
195
+ * The `@stitch` directive marks a root query where the merged type may be accessed. The merged type identity is inferred from the field return. This identifier can also be provided as [static configuration](#sdl-based-schemas).
196
+ * The `key: "id"` parameter indicates that an `{ id }` must be selected from prior locations so it can 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).
156
197
 
157
- 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:
198
+ Merged types must have a resolver query in each of their possible locations. The one exception to this requirement are [outbound-only types](./docs/mechanics.md#outbound-only-merged-types) that contain no exclusive data, such as foreign keys:
158
199
 
159
200
  ```graphql
160
201
  type Product {
@@ -162,7 +203,7 @@ type Product {
162
203
  }
163
204
  ```
164
205
 
165
- 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.
206
+ The above type contains nothing but a key field that is available in other locations. Therefore, this variant will never require an inbound request to fetch it, and its resolver query may be omitted from this location.
166
207
 
167
208
  #### List queries
168
209
 
@@ -258,7 +299,6 @@ Resolver keys may make composite selections for multiple key fields and/or neste
258
299
  ```graphql
259
300
  interface FieldOwner {
260
301
  id: ID!
261
- type: String!
262
302
  }
263
303
  type CustomField {
264
304
  owner: FieldOwner!
@@ -273,8 +313,8 @@ input CustomFieldLookup {
273
313
 
274
314
  type Query {
275
315
  customFields(lookups: [CustomFieldLookup!]!): [CustomField]! @stitch(
276
- key: "owner { id type } key",
277
- arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.type, key: $.key }"
316
+ key: "owner { id __typename } key",
317
+ arguments: "lookups: { ownerId: $.owner.id, ownerType: $.owner.__typename, key: $.key }"
278
318
  )
279
319
  }
280
320
  ```
@@ -443,7 +483,6 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
443
483
 
444
484
  ## Additional topics
445
485
 
446
- - [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
447
486
  - [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
448
487
  - [Schema composition merge patterns](./docs/composer.md#merge-patterns)
449
488
  - [Subscriptions tutorial](./docs/subscriptions.md)
data/docs/composer.md CHANGED
@@ -10,6 +10,7 @@ A `Composer` may be constructed with optional settings that tune how it builds a
10
10
  composer = GraphQL::Stitching::Composer.new(
11
11
  query_name: "Query",
12
12
  mutation_name: "Mutation",
13
+ subscription_name: "Subscription",
13
14
  description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
14
15
  deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
15
16
  default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
@@ -24,6 +25,8 @@ Constructor arguments:
24
25
 
25
26
  - **`mutation_name:`** _optional_, the name of the root mutation type in the composed schema; `Mutation` by default. The root mutation types from all location schemas will be merged into this type, regardless of their local names.
26
27
 
28
+ - **`subscription_name:`** _optional_, the name of the root subscription type in the composed schema; `Subscription` by default. The root subscription types from all location schemas will be merged into this type, regardless of their local names.
29
+
27
30
  - **`description_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
28
31
 
29
32
  - **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
@@ -98,17 +101,16 @@ Location settings have top-level keys that specify arbitrary location names, eac
98
101
 
99
102
  The strategy used to merge source schemas into the combined schema is based on each element type:
100
103
 
104
+ - Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
105
+ - Arguments must share a value type, and the strictest nullability across locations is used.
106
+ - Composition fails if argument intersection would eliminate a non-null argument.
107
+
101
108
  - `Object` and `Interface` types merge their fields and directives together:
102
109
  - Common fields across locations must share a value type, and the weakest nullability is used.
103
- - Field and directive arguments merge using the same rules as `InputObject`.
104
110
  - Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
105
111
  - Shared object types without `@stitch` accessors must contain identical fields.
106
112
  - Merged interfaces must remain compatible with all underlying implementations.
107
113
 
108
- - `InputObject` types intersect arguments from across locations (arguments must appear in all locations):
109
- - Arguments must share a value type, and the strictest nullability across locations is used.
110
- - Composition fails if argument intersection would eliminate a non-null argument.
111
-
112
114
  - `Enum` types merge their values based on how the enum is used:
113
115
  - Enums used anywhere as an argument will intersect their values (common values across all locations).
114
116
  - Enums used exclusively in read contexts will provide a union of values (all values across all locations).
@@ -118,7 +120,6 @@ The strategy used to merge source schemas into the combined schema is based on e
118
120
  - `Scalar` types are added for all scalar names across all locations.
119
121
 
120
122
  - `Directive` definitions are added for all distinct names across locations:
121
- - Arguments merge using the same rules as `InputObject`.
122
123
  - Stitching directives (both definitions and assignments) are omitted.
123
124
 
124
125
  Note that the structure of a composed schema may change based on new schema additions and/or element usage (ie: changing input object arguments in one service may cause the intersection of arguments to change). Therefore, it's highly recommended that you use a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) to flag regressions across composed schema versions.
@@ -65,6 +65,6 @@ It's perfectly fine to mix and match schemas that implement an `_entities` query
65
65
 
66
66
  ### Federation features that will most definitly break
67
67
 
68
- - `@external` fields will confuse the stitching query planner.
68
+ - `@external` fields will confuse the stitching query planner (as the fields aren't natively resolvable at the location).
69
69
  - `@requires` fields will not be sent any dependencies.
70
70
  - No support for Apollo composition directives.
data/docs/mechanics.md CHANGED
@@ -1,47 +1,5 @@
1
1
  ## Schema Stitching, mechanics
2
2
 
3
- ### Modeling foreign keys for stitching
4
-
5
- Foreign keys in a GraphQL schema typically look like the `Product.imageId` field here:
6
-
7
- ```graphql
8
- # -- Products schema:
9
-
10
- type Product {
11
- id: ID!
12
- imageId: ID!
13
- }
14
-
15
- # -- Images schema:
16
-
17
- type Image {
18
- id: ID!
19
- url: String!
20
- }
21
- ```
22
-
23
- However, this design does not lend itself to stitching where types need to _merge_ across locations. A simple schema refactor makes this foreign key more expressive as an entity type, and turns the key into an _object_ that will merge with analogous object types in other locations:
24
-
25
- ```graphql
26
- # -- Products schema:
27
-
28
- type Product {
29
- id: ID!
30
- image: Image!
31
- }
32
-
33
- type Image {
34
- id: ID!
35
- }
36
-
37
- # -- Images schema:
38
-
39
- type Image {
40
- id: ID!
41
- url: String!
42
- }
43
- ```
44
-
45
3
  ### Deploying a stitched schema
46
4
 
47
5
  Among the simplest and most effective ways to manage a stitched schema is to compose it locally, write the composed SDL as a `.graphql` file in your repo, and then load the composed schema into a stitching client at runtime. For example, setup a `rake` task that loads/fetches subgraph schemas, composes them, and then writes the composed schema definition as a file committed to the repo:
@@ -94,7 +52,7 @@ class GraphQlController
94
52
  end
95
53
  ```
96
54
 
97
- This process assures that composition always happens before deployment where failures can be detected. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
55
+ This process assures that composition always happens before deployment where failures can be detected. Use CI to verify that the repo's supergraph output is always up to date. Hot reloading of the supergraph can also be accommodated by uploading the composed schema to a sync location (cloud storage, etc) that is polled by the application runtime. When the schema changes, load it into a new stitching client and swap that into the application.
98
56
 
99
57
  ### Field selection routing
100
58
 
data/docs/request.md CHANGED
@@ -15,15 +15,15 @@ request = GraphQL::Stitching::Request.new(
15
15
 
16
16
  A `Request` provides the following information:
17
17
 
18
- - `req.document`: parsed AST of the GraphQL source
19
- - `req.variables`: a hash of user-submitted variables
20
- - `req.string`: the original GraphQL source string, or printed document
21
- - `req.digest`: a SHA2 of the request string
22
- - `req.normalized_string`: printed document string with consistent whitespace
23
- - `req.normalized_digest`: a SHA2 of the normalized string
24
- - `req.operation`: the operation definition selected for the request
25
- - `req.variable_definitions`: a mapping of variable names to their type definitions
26
- - `req.fragment_definitions`: a mapping of fragment names to their fragment definitions
18
+ - `req.document`: parsed AST of the GraphQL source.
19
+ - `req.variables`: a hash of user-submitted variables.
20
+ - `req.string`: the original GraphQL source string, or printed document.
21
+ - `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
22
+ - `req.normalized_string`: printed document string with consistent whitespace.
23
+ - `req.normalized_digest`: a digest of the normalized string, hashed by the `Stitching.digest` implementation.
24
+ - `req.operation`: the operation definition selected for the request.
25
+ - `req.variable_definitions`: a mapping of variable names to their type definitions.
26
+ - `req.fragment_definitions`: a mapping of fragment names to their fragment definitions.
27
27
 
28
28
  ### Request lifecycle
29
29
 
@@ -32,5 +32,5 @@ component, or you may invoke them manually:
32
32
 
33
33
  1. `request.validate`: runs static validations on the request using the combined schema.
34
34
  2. `request.prepare!`: inserts variable defaults and pre-renders skip/include conditional shaping.
35
- 3. `request.plan`: builds a plan for the request. May act as a setting for plans pulled from cache.
35
+ 3. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
36
36
  4. `request.execute`: executes the request, and returns the resulting data.
@@ -4,7 +4,7 @@ Stitching is an interesting prospect for subscriptions because socket-based inte
4
4
 
5
5
  ### Composing a subscriptions schema
6
6
 
7
- For simplicity, subscription resolvers should exist together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:
7
+ For simplicity, subscription resolvers are best kept together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:
8
8
 
9
9
  ```ruby
10
10
  class SubscriptionSchema < GraphQL::Schema
@@ -122,7 +122,7 @@ class GraphqlController < ApplicationController
122
122
  def execute
123
123
  result = StitchedSchema.execute(
124
124
  params[:query],
125
- context: {},
125
+ context: {},
126
126
  variables: params[:variables],
127
127
  operation_name: params[:operationName],
128
128
  )
@@ -164,7 +164,7 @@ class GraphqlChannel < ApplicationCable::Channel
164
164
 
165
165
  def unsubscribed
166
166
  @subscription_ids.each { |sid|
167
- # Go directly through the subscriptions subschema
167
+ # Go directly through the subscriptions subschema
168
168
  # when managing/triggering subscriptions:
169
169
  SubscriptionSchema.subscriptions.delete_subscription(sid)
170
170
  }
@@ -172,7 +172,7 @@ class GraphqlChannel < ApplicationCable::Channel
172
172
  end
173
173
  ```
174
174
 
175
- What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back out through the stitching client.
175
+ What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back out through the stitching client. The `unsubscribed` method works directly with the subschema where subscriptions are managed.
176
176
 
177
177
  #### Plugin
178
178
 
@@ -188,7 +188,7 @@ class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubs
188
188
  end
189
189
 
190
190
  class SubscriptionSchema < GraphQL::Schema
191
- # switch the plugin on the subscriptions schema to use the patched class...
191
+ # switch the plugin on the subscriptions schema to use the patched class...
192
192
  use StitchedActionCableSubscriptions
193
193
  end
194
194
  ```
@@ -200,7 +200,7 @@ Subscription update events are triggered as normal directly through the subscrip
200
200
  ```ruby
201
201
  class Comment < ApplicationRecord
202
202
  after_create :trigger_subscriptions
203
-
203
+
204
204
  def trigger_subscriptions
205
205
  SubscriptionsSchema.subscriptions.trigger(:comment_added_to_post, { post_id: post_id }, self)
206
206
  end
data/docs/supergraph.md CHANGED
@@ -9,17 +9,17 @@ A Supergraph is designed to be composed, cached, and restored. Calling `to_defin
9
9
  ```ruby
10
10
  supergraph_sdl = supergraph.to_definition
11
11
 
12
- # stash this composed schema in a cache...
13
- $cache.set("cached_supergraph_sdl", supergraph_sdl)
14
-
15
- # or, write the composed schema as a file into your repo...
12
+ # write the composed schema as a file into your repo...
16
13
  File.write("supergraph/schema.graphql", supergraph_sdl)
14
+
15
+ # or, stash this composed schema in a cache...
16
+ $cache.set("cached_supergraph_sdl", supergraph_sdl)
17
17
  ```
18
18
 
19
19
  To restore a Supergraph, call `from_definition` providing the cached SDL string and a hash of executables keyed by their location names:
20
20
 
21
21
  ```ruby
22
- supergraph_sdl = $cache.get("cached_supergraph_sdl")
22
+ supergraph_sdl = File.read("supergraph/schema.graphql")
23
23
 
24
24
  supergraph = GraphQL::Stitching::Supergraph.from_definition(
25
25
  supergraph_sdl,
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- gemspec
5
4
 
6
5
  gem 'graphql', '~> 2.0.0'
6
+ gem 'warning'
7
+ gem 'minitest-stub-const'
8
+
9
+ gemspec path: "../"
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- gemspec
5
4
 
6
5
  gem 'graphql', '~> 2.1.0'
6
+ gem 'warning'
7
+ gem 'minitest-stub-const'
8
+
9
+ gemspec path: "../"
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- gemspec
5
4
 
6
5
  gem 'graphql', '~> 2.2.0'
6
+ gem 'warning'
7
+ gem 'minitest-stub-const'
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'graphql', '~> 2.3.0'
6
+ gem 'warning'
7
+ gem 'minitest-stub-const'
8
+
9
+ gemspec path: "../"
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  end
27
27
  spec.require_paths = ['lib']
28
28
 
29
- spec.add_runtime_dependency 'graphql', '>= 1.13.9'
29
+ spec.add_runtime_dependency 'graphql', '>= 2.0'
30
30
 
31
31
  spec.add_development_dependency 'bundler', '~> 2.0'
32
32
  spec.add_development_dependency 'rake', '~> 12.0'
@@ -7,8 +7,6 @@ module GraphQL
7
7
  # Client is an out-of-the-box helper that assembles all
8
8
  # stitching components into a workflow that executes requests.
9
9
  class Client
10
- class ClientError < StitchingError; end
11
-
12
10
  # @return [Supergraph] composed supergraph that services incoming requests.
13
11
  attr_reader :supergraph
14
12
 
@@ -18,9 +16,9 @@ module GraphQL
18
16
  # @param composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
19
17
  def initialize(locations: nil, supergraph: nil, composer: nil)
20
18
  @supergraph = if locations && supergraph
21
- raise ClientError, "Cannot provide both locations and a supergraph."
19
+ raise ArgumentError, "Cannot provide both locations and a supergraph."
22
20
  elsif supergraph && !supergraph.is_a?(Supergraph)
23
- raise ClientError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
21
+ raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
24
22
  elsif supergraph
25
23
  supergraph
26
24
  else
@@ -58,17 +56,17 @@ module GraphQL
58
56
  end
59
57
 
60
58
  def on_cache_read(&block)
61
- raise ClientError, "A cache read block is required." unless block_given?
59
+ raise ArgumentError, "A cache read block is required." unless block_given?
62
60
  @on_cache_read = block
63
61
  end
64
62
 
65
63
  def on_cache_write(&block)
66
- raise ClientError, "A cache write block is required." unless block_given?
64
+ raise ArgumentError, "A cache write block is required." unless block_given?
67
65
  @on_cache_write = block
68
66
  end
69
67
 
70
68
  def on_error(&block)
71
- raise ClientError, "An error handler block is required." unless block_given?
69
+ raise ArgumentError, "An error handler block is required." unless block_given?
72
70
  @on_error = block
73
71
  end
74
72
 
@@ -403,8 +403,13 @@ module GraphQL
403
403
  next
404
404
  end
405
405
 
406
- # Getting double args sometimes on auto-generated connection types... why?
407
- next if owner.arguments.any? { _1.first == argument_name }
406
+ # Getting double args sometimes... why?
407
+ begin
408
+ next if owner.arguments(GraphQL::Query::NullContext.instance, false).key?(argument_name)
409
+ rescue ArgumentError
410
+ # pre- graphql v2.4.5
411
+ next if owner.arguments.key?(argument_name)
412
+ end
408
413
 
409
414
  kwargs = {}
410
415
  default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
@@ -64,7 +64,7 @@ module GraphQL::Stitching
64
64
  return nil if result.nil?
65
65
 
66
66
  else
67
- raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
67
+ raise DocumentError.new("selection node type")
68
68
  end
69
69
  end
70
70
 
@@ -118,7 +118,7 @@ module GraphQL::Stitching
118
118
  def typename_in_type?(typename, type)
119
119
  return true if type.graphql_name == typename
120
120
 
121
- type.kind.abstract? && @request.warden.possible_types(type).any? do |t|
121
+ type.kind.abstract? && @supergraph.schema.possible_types(type).any? do |t|
122
122
  t.graphql_name == typename
123
123
  end
124
124
  end
@@ -10,7 +10,7 @@ module GraphQL::Stitching
10
10
  end
11
11
 
12
12
  def fetch(ops)
13
- origin_sets_by_operation = ops.each_with_object({}) do |op, memo|
13
+ origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
14
14
  origin_set = op.path.reduce([@executor.data]) do |set, path_segment|
15
15
  set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
16
16
  end
@@ -86,7 +86,7 @@ module GraphQL::Stitching
86
86
  end
87
87
  end
88
88
 
89
- doc = String.new("query") # << resolver fulfillment always uses query
89
+ doc = String.new(QUERY_OP) # << resolver fulfillment always uses query
90
90
 
91
91
  if operation_name
92
92
  doc << " #{operation_name}"
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # Builds a new executor.
28
28
  # @param request [Request] the stitching request to execute.
29
29
  # @param nonblocking [Boolean] specifies if the dataloader should use async concurrency.
30
- def initialize(request, data: {}, errors: [], after: 0, nonblocking: false)
30
+ def initialize(request, data: {}, errors: [], after: Planner::ROOT_INDEX, nonblocking: false)
31
31
  @request = request
32
32
  @data = data
33
33
  @errors = errors
@@ -38,7 +38,7 @@ module GraphQL
38
38
  end
39
39
 
40
40
  def perform(raw: false)
41
- exec!
41
+ exec!([@after])
42
42
  result = {}
43
43
 
44
44
  if @data && @data.length > 0
@@ -54,7 +54,7 @@ module GraphQL
54
54
 
55
55
  private
56
56
 
57
- def exec!(next_steps = [@after])
57
+ def exec!(next_steps)
58
58
  if @exec_cycles > @request.plan.ops.length
59
59
  # sanity check... if we've exceeded queue size, then something went wrong.
60
60
  raise StitchingError, "Too many execution requests attempted."
@@ -20,7 +20,7 @@ module GraphQL
20
20
  def perform
21
21
  build_root_entrypoints
22
22
  expand_abstract_resolvers
23
- Plan.new(ops: steps.map(&:to_plan_op))
23
+ Plan.new(ops: steps.map!(&:to_plan_op))
24
24
  end
25
25
 
26
26
  def steps
@@ -76,9 +76,7 @@ module GraphQL
76
76
  resolver: nil
77
77
  )
78
78
  # coalesce repeat parameters into a single entrypoint
79
- entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key&.to_definition}/#")
80
- path.each { entrypoint << "/#{_1}" }
81
-
79
+ entrypoint = [parent_index, location, parent_type.graphql_name, resolver&.key&.to_definition, "#", *path].join("/")
82
80
  step = @steps_by_entrypoint[entrypoint]
83
81
  next_index = step ? parent_index : @planning_index += 1
84
82
 
@@ -156,7 +154,7 @@ module GraphQL
156
154
  when SUBSCRIPTION_OP
157
155
  # A.3) Permit exactly one subscription field.
158
156
  each_field_in_scope(parent_type, @request.operation.selections) do |node|
159
- raise StitchingError, "Too many root fields for subscription." unless @steps_by_entrypoint.empty?
157
+ raise DocumentError.new("root field") unless @steps_by_entrypoint.empty?
160
158
 
161
159
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
162
160
  add_step(
@@ -169,7 +167,7 @@ module GraphQL
169
167
  end
170
168
 
171
169
  else
172
- raise StitchingError, "Invalid operation type."
170
+ raise DocumentError.new("operation type")
173
171
  end
174
172
  end
175
173
 
@@ -189,7 +187,7 @@ module GraphQL
189
187
  each_field_in_scope(parent_type, fragment.selections, &block)
190
188
 
191
189
  else
192
- raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
190
+ raise DocumentError.new("selection node type")
193
191
  end
194
192
  end
195
193
  end
@@ -271,7 +269,7 @@ module GraphQL
271
269
  end
272
270
 
273
271
  else
274
- raise StitchingError, "Unexpected node of type #{node.class.name} in selection set."
272
+ raise DocumentError.new("selection node type")
275
273
  end
276
274
  end
277
275
 
@@ -333,7 +331,7 @@ module GraphQL
333
331
  end
334
332
 
335
333
  if expanded_selections
336
- @request.warden.possible_types(parent_type).each do |possible_type|
334
+ @supergraph.schema.possible_types(parent_type).each do |possible_type|
337
335
  next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
338
336
 
339
337
  type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
@@ -26,9 +26,6 @@ module GraphQL
26
26
  # @return [Hash] contextual object passed through resolver flows.
27
27
  attr_reader :context
28
28
 
29
- # @return [GraphQL::Schema::Warden] a visibility warden for this request.
30
- attr_reader :warden
31
-
32
29
  # Creates a new supergraph request.
33
30
  # @param supergraph [Supergraph] supergraph instance that resolves the request.
34
31
  # @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
@@ -58,7 +55,6 @@ module GraphQL
58
55
  @variables = variables || {}
59
56
 
60
57
  @query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
61
- @warden = @query.warden
62
58
  @context = @query.context
63
59
  @context[:request] = self
64
60
  end
@@ -141,7 +137,7 @@ module GraphQL
141
137
  # Validates the request using the combined supergraph schema.
142
138
  # @return [Array<GraphQL::ExecutionError>] an array of static validation errors
143
139
  def validate
144
- result = @supergraph.static_validator.validate(@query)
140
+ result = @supergraph.schema.static_validator.validate(@query)
145
141
  result[:errors]
146
142
  end
147
143
 
@@ -50,11 +50,6 @@ module GraphQL
50
50
  end.freeze
51
51
  end
52
52
 
53
- # @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
54
- def static_validator
55
- @static_validator ||= @schema.static_validator
56
- end
57
-
58
53
  def resolvers_by_version
59
54
  @resolvers_by_version ||= resolvers.values.tap(&:flatten!).each_with_object({}) do |resolver, memo|
60
55
  memo[resolver.version] = resolver
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.5.1"
5
+ VERSION = "1.5.2"
6
6
  end
7
7
  end
@@ -25,6 +25,11 @@ module GraphQL
25
25
  class StitchingError < StandardError; end
26
26
  class CompositionError < StitchingError; end
27
27
  class ValidationError < CompositionError; end
28
+ class DocumentError < StandardError
29
+ def initialize(element)
30
+ super("Invalid #{element} encountered in document")
31
+ end
32
+ end
28
33
 
29
34
  class << self
30
35
  attr_writer :stitch_directive
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.5.1
4
+ version: 1.5.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: 2024-09-19 00:00:00.000000000 Z
11
+ date: 2025-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.13.9
19
+ version: '2.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.13.9
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -151,10 +151,10 @@ files:
151
151
  - examples/subscriptions/public/apple-touch-icon.png
152
152
  - examples/subscriptions/public/favicon.ico
153
153
  - examples/subscriptions/public/robots.txt
154
- - gemfiles/graphql_1.13.9.gemfile
155
154
  - gemfiles/graphql_2.0.0.gemfile
156
155
  - gemfiles/graphql_2.1.0.gemfile
157
156
  - gemfiles/graphql_2.2.0.gemfile
157
+ - gemfiles/graphql_2.3.0.gemfile
158
158
  - graphql-stitching.gemspec
159
159
  - lib/graphql/stitching.rb
160
160
  - lib/graphql/stitching/client.rb
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
- gemspec
5
-
6
- gem 'graphql', '~> 1.13.9'