graphql-stitching 0.1.0 → 0.2.1

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: 5090330cd8b46c9d7dbfc7bbc68f5840e97f8bcbc5ec710de219441237fb61e5
4
- data.tar.gz: 846a15ccc2734024dec751d61d456aa05d4b7c396d6789d629da21e11c182100
3
+ metadata.gz: 05c82abfcbec2db513d097c21424916bea46ebb0e4b434db0dc10a92ddfc2f9b
4
+ data.tar.gz: 79e4184f4ed237e2132f67389bb838fbbf18a8141b1d2ab30f8c5b710f47cb8f
5
5
  SHA512:
6
- metadata.gz: 1c0c16a9bb49b3fad30057958d79fe92737bb659bf7135dd03ef716597f013249c99d9372478e20619f54e1e77f68f3b7f746b92e9e220b80be9ecaf88b656de
7
- data.tar.gz: f370e520552f3e8be141073dd7ca06f68a463824c946b9cd03357b33ad5c9fc928893e846314f08941e2590d24202081450677fbfca480474be9833d341209a1
6
+ metadata.gz: e6c6ccb67c95df19b01bbeea1c0ed731a7153c3e4a184dab7dad6c2c52a22e22183075d123bf2dc78f431e73ff1fb4d31f0a21100072adb4f05bff2813ff9294
7
+ data.tar.gz: 2b9257ed86bdac5cb2403e527fc90893c961637d14bda2c4e5a16be7c8dc23206f860bdb596cc26f35ea704156b7443c5ef013efcc4c8cde251c9a865777e3ad
data/README.md CHANGED
@@ -7,12 +7,12 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
7
7
  **Supports:**
8
8
  - Merged object and interface types.
9
9
  - Multiple keys per merged type.
10
- - Shared objects, enums, and inputs across locations.
10
+ - Shared objects, fields, enums, and inputs across locations.
11
11
  - Combining local and remote schemas.
12
12
 
13
13
  **NOT Supported:**
14
- - Computed fields (ie: federation-style `@requires`)
15
- - Subscriptions
14
+ - Computed fields (ie: federation-style `@requires`).
15
+ - Subscriptions, defer/stream.
16
16
 
17
17
  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 high-throughput API gateway, the opportunity here is for a Ruby application to stitch its local schema onto a remote schema (making itself a superset of the remote) without requiring an additional gateway service.
18
18
 
@@ -70,13 +70,13 @@ result = gateway.execute(
70
70
  )
71
71
  ```
72
72
 
73
- Schemas provided to the `Gateway` constructor 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.
73
+ Schemas provided to the `Gateway` constructor 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) for more information on how schemas get merged.
74
74
 
75
75
  While the [`Gateway`](./docs/gateway.md) constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
76
76
 
77
77
  - [Composer](./docs/composer.md) - merges and validates many schemas into one graph.
78
78
  - [Supergraph](./docs/supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
79
- - [Document](./docs/document.md) - manages a parsed GraphQL request document.
79
+ - [Request](./docs/request.md) - prepares a requested GraphQL document and variables for stitching.
80
80
  - [Planner](./docs/planner.md) - builds a cacheable query plan for a request document.
81
81
  - [Executor](./docs/executor.md) - executes a query plan with given request variables.
82
82
 
@@ -121,7 +121,7 @@ shipping_schema = <<~GRAPHQL
121
121
  }
122
122
  GRAPHQL
123
123
 
124
- supergraph = GraphQL::Stitching::Composer.new({
124
+ supergraph = GraphQL::Stitching::Composer.new(schemas: {
125
125
  "products" => GraphQL::Schema.from_definition(products_schema),
126
126
  "shipping" => GraphQL::Schema.from_definition(shipping_schema),
127
127
  })
@@ -146,7 +146,7 @@ type Query {
146
146
  }
147
147
  ```
148
148
 
149
- * The `@stitch` directive is applied to a root query where the merged type may be accessed. The merged type is inferred from the field return.
149
+ * 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.
150
150
  * 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 later).
151
151
 
152
152
  Each location that provides a unique variant of a type must provide _exactly one_ stitching query per possible key (more on multiple keys later). The exception to this requirement are types that contain only a single key field:
@@ -261,29 +261,29 @@ GraphQL::Stitching.stitch_directive = "merge"
261
261
 
262
262
  ## Executables
263
263
 
264
- A [Supergraph](./docs/supergraph.md) will delegate requests to the individual `GraphQL::Schema` classes that composed it. You may change this behavior by assigning new executables: these may be `GraphQL::Schema` classes, or objects that respond to `.call` with the following arguments...
264
+ An executable resource performs location-specific GraphQL requests. Executables may be `GraphQL::Schema` classes, or objects that respond to `.call` with the following arguments...
265
265
 
266
266
  ```ruby
267
267
  class MyExecutable
268
- def call(location, query_string, variables)
268
+ def call(location, query_string, variables, context)
269
269
  # process a GraphQL request...
270
270
  end
271
271
  end
272
272
  ```
273
273
 
274
- Executables are assigned to a supergraph using `assign_executable`:
274
+ By default, a [Supergraph](./docs/supergraph.md) will use the individual `GraphQL::Schema` classes that composed it as executable resources for each location. You may assign new executables using `assign_executable`:
275
275
 
276
276
  ```ruby
277
277
  supergraph = GraphQL::Stitching::Composer.new(...)
278
278
 
279
279
  supergraph.assign_executable("location1", MyExecutable.new)
280
- supergraph.assign_executable("location2", ->(loc, query, vars) { ... })
281
- supergraph.assign_executable("location3") do |loc, query vars|
280
+ supergraph.assign_executable("location2", ->(loc, query, vars, ctx) { ... })
281
+ supergraph.assign_executable("location3") do |loc, query vars, ctx|
282
282
  # ...
283
283
  end
284
284
  ```
285
285
 
286
- The `GraphQL::Stitching::RemoteClient` class is provided as a simple executable wrapper around `Net::HTTP.post`. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you _must_ manually assign all executables to a `Supergraph` instance when rehydrating it from cache ([see docs](./docs/supergraph.md)).
286
+ The `GraphQL::Stitching::RemoteClient` class is provided as a simple executable wrapper around `Net::HTTP.post`. You should build your own executables to leverage your existing libraries and to add instrumentation. Note that you must manually assign all executables to a `Supergraph` when rehydrating it from cache ([see docs](./docs/supergraph.md)).
287
287
 
288
288
  ## Concurrency
289
289
 
@@ -291,7 +291,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
291
291
 
292
292
  ## Example
293
293
 
294
- This repo includes a working example of several stitched schemas running across Rack servers. Try running it:
294
+ This repo includes a working example of several stitched schemas running across small Rack servers. Try running it:
295
295
 
296
296
  ```shell
297
297
  bundle install
data/docs/README.md CHANGED
@@ -9,6 +9,6 @@ Major components include:
9
9
  - [Gateway](./gateway.md) - an out-of-the-box stitching configuration.
10
10
  - [Composer](./composer.md) - merges and validates many schemas into one graph.
11
11
  - [Supergraph](./supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
12
- - [Document](./document.md) - manages a parsed GraphQL request document.
12
+ - [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
13
13
  - [Planner](./planner.md) - builds a cacheable query plan for a request document.
14
14
  - [Executor](./executor.md) - executes a query plan with given request variables.
data/docs/composer.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## GraphQL::Stitching::Composer
2
2
 
3
- The `Composer` receives many individual `GraphQL:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity. The resulting context provides a combined GraphQL schema and delegation maps used to route incoming requests:
3
+ The `Composer` receives many individual `GraphQL:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity. The resulting supergraph provides a combined GraphQL schema and delegation maps used to route incoming requests:
4
4
 
5
5
  ```ruby
6
6
  storefronts_sdl = <<~GRAPHQL
@@ -33,7 +33,7 @@ products_sdl = <<~GRAPHQL
33
33
  }
34
34
  GRAPHQL
35
35
 
36
- supergraph = GraphQL::Stitching::Composer.new({
36
+ supergraph = GraphQL::Stitching::Composer.new(schemas: {
37
37
  "storefronts" => GraphQL::Schema.from_definition(storefronts_sdl),
38
38
  "products" => GraphQL::Schema.from_definition(products_sdl),
39
39
  }).perform
@@ -47,9 +47,9 @@ The individual schemas provided to the composer are assigned a location name bas
47
47
 
48
48
  The strategy used to merge source schemas into the combined schema is based on each element type:
49
49
 
50
- - `Object` and `Interface` types merge their fields together:
50
+ - `Object` and `Interface` types merge their fields and directives together:
51
51
  - Common fields across locations must share a value type, and the weakest nullability is used.
52
- - Field arguments merge using the same rules as `InputObject`.
52
+ - Field and directive arguments merge using the same rules as `InputObject`.
53
53
  - Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
54
54
  - Shared object types without `@stitch` accessors must contain identical fields.
55
55
  - Merged interfaces must remain compatible with all underlying implementations.
@@ -66,4 +66,45 @@ The strategy used to merge source schemas into the combined schema is based on e
66
66
 
67
67
  - `Scalar` types are added for all scalar names across all locations.
68
68
 
69
+ - `Directive` definitions are added for all distinct names across locations:
70
+ - Arguments merge using the same rules as `InputObject`.
71
+ - Stitching directives (both definitions and assignments) are omitted.
72
+
69
73
  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.
74
+
75
+ ### Value merger functions
76
+
77
+ The composer has no way of intelligently merging static data values that are embedded into a schema. These include:
78
+
79
+ - Element descriptions
80
+ - Element deprecations
81
+ - Directive keyword argument values
82
+
83
+ By default, the first non-null value encountered across locations is used to fill these data slots. You may customize this aggregation process by providing value merger functions:
84
+
85
+
86
+ ```ruby
87
+ supergraph = GraphQL::Stitching::Composer.new(
88
+ schemas: { ... },
89
+ description_merger: ->(values_by_location, info) { values_by_location.values.last },
90
+ deprecation_merger: ->(values_by_location, info) { values_by_location.values.last },
91
+ directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
92
+ ).perform
93
+ ```
94
+
95
+ Each merger accepts a `values_by_location` and an `info` argument; these provide the values found across locations and info about where in schema they were encountered:
96
+
97
+ ```ruby
98
+ values_by_location = {
99
+ "storefronts" => "A fabulous data type.",
100
+ "products" => "An excellent data type.",
101
+ }
102
+
103
+ info = {
104
+ type_name: "Product",
105
+ # field_name: ...,
106
+ # argument_name: ...,
107
+ }
108
+ ```
109
+
110
+ The function should then select a value (or compute a new one) and return that for use in the combined schema.
data/docs/executor.md CHANGED
@@ -12,28 +12,29 @@ query = <<~GRAPHQL
12
12
  }
13
13
  GRAPHQL
14
14
 
15
- variables = { "id" => "123" }
16
-
17
- document = GraphQL::Stitching::Document.new(query, operation_name: "MyQuery")
15
+ request = GraphQL::Stitching::Request.new(query, variables: { "id" => "123" }, operation_name: "MyQuery")
18
16
 
19
17
  plan = GraphQL::Stitching::Planner.new(
20
18
  supergraph: supergraph,
21
- document: document,
19
+ request: request,
22
20
  ).perform
23
21
 
24
- # get the raw result without shaping
25
- raw_result = GraphQL::Stitching::Executor.new(
22
+ result = GraphQL::Stitching::Executor.new(
26
23
  supergraph: supergraph,
27
24
  plan: plan.to_h,
28
- variables: variables,
25
+ request: request,
29
26
  ).perform
27
+ ```
28
+
29
+ ### Raw results
30
30
 
31
- # get the final result with shaping
32
- final_result = GraphQL::Stitching::Executor.new(
31
+ By default, execution results are always returned with document shaping (stitching additions removed, missing fields added, null bubbling applied). You may access the raw execution result by calling the `perform` method with a `raw: true` argument:
32
+
33
+ ```ruby
34
+ # get the raw result without shaping
35
+ raw_result = GraphQL::Stitching::Executor.new(
33
36
  supergraph: supergraph,
34
37
  plan: plan.to_h,
35
- variables: variables,
36
- ).perform(document)
38
+ request: request,
39
+ ).perform(raw: true)
37
40
  ```
38
-
39
- Note that an executor's `perform` method accepts a document argument. When provided, the raw execution result will be shaped for delivery to match the document. Without a document, the raw result will be returned with stitching inclusions and no null bubbling applied.
Binary file
data/docs/planner.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ## GraphQL::Stitching::Planner
2
2
 
3
- A `Planner` generates a query plan for a given [`Supergraph`](./supergraph.md) and request [`Document`](./document.md). The generated plan breaks down all the discrete GraphQL operations that must be delegated across locations and their sequencing.
3
+ A `Planner` generates a query plan for a given [`Supergraph`](./supergraph.md) and [`Request`](./request.md). The generated plan breaks down all the discrete GraphQL operations that must be delegated across locations and their sequencing.
4
4
 
5
5
  ```ruby
6
- request = <<~GRAPHQL
6
+ document = <<~GRAPHQL
7
7
  query MyQuery($id: ID!) {
8
8
  product(id:$id) {
9
9
  title
@@ -12,11 +12,11 @@ request = <<~GRAPHQL
12
12
  }
13
13
  GRAPHQL
14
14
 
15
- document = GraphQL::Stitching::Document.new(request, operation_name: "MyQuery")
15
+ request = GraphQL::Stitching::Request.new(document, operation_name: "MyQuery").prepare!
16
16
 
17
17
  plan = GraphQL::Stitching::Planner.new(
18
18
  supergraph: supergraph,
19
- document: document,
19
+ request: request,
20
20
  ).perform
21
21
  ```
22
22
 
@@ -25,17 +25,17 @@ plan = GraphQL::Stitching::Planner.new(
25
25
  Plans are designed to be cacheable. This is very useful for redundant GraphQL documents (commonly sent by frontend clients) where there's no sense in planning every request individually. It's far more efficient to generate a plan once and cache it, then simply retreive the plan and execute it for future requests.
26
26
 
27
27
  ```ruby
28
- cached_plan = $redis.get(document.digest)
28
+ cached_plan = $redis.get(request.digest)
29
29
 
30
30
  plan = if cached_plan
31
31
  JSON.parse(cached_plan)
32
32
  else
33
33
  plan_hash = GraphQL::Stitching::Planner.new(
34
34
  supergraph: supergraph,
35
- document: document,
35
+ request: request,
36
36
  ).perform.to_h
37
37
 
38
- $redis.set(document.digest, JSON.generate(plan_hash))
38
+ $redis.set(request.digest, JSON.generate(plan_hash))
39
39
  plan_hash
40
40
  end
41
41
 
data/docs/request.md ADDED
@@ -0,0 +1,47 @@
1
+ ## GraphQL::Stitching::Request
2
+
3
+ A `Request` contains a parsed GraphQL document and variables, and handles the logistics of extracting the appropriate operation, variable definitions, and fragments. A `Request` should be built once per server request and passed through to other stitching components that utilize request information.
4
+
5
+ ```ruby
6
+ document = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
7
+ request = GraphQL::Stitching::Request.new(document, variables: { "id" => "1" }, operation_name: "FetchMovie")
8
+
9
+ request.document # parsed AST via GraphQL.parse
10
+ request.variables # user-submitted variables
11
+ request.string # normalized printed document string
12
+ request.digest # SHA digest of the normalized document string
13
+
14
+ request.variable_definitions # a mapping of variable names to their type definitions
15
+ request.fragment_definitions # a mapping of fragment names to their fragment definitions
16
+ ```
17
+
18
+ ### Preparing requests
19
+
20
+ A request should be prepared using the `prepare!` method before using it:
21
+
22
+ ```ruby
23
+ document = <<~GRAPHQL
24
+ query FetchMovie($id: ID!, $lang: String = "en", $withShowtimes: Boolean = true) {
25
+ movie(id:$id) {
26
+ id
27
+ title(lang: $lang)
28
+ showtimes @include(if: $withShowtimes) {
29
+ time
30
+ }
31
+ }
32
+ }
33
+ GRAPHQL
34
+
35
+ request = GraphQL::Stitching::Request.new(
36
+ document,
37
+ variables: { "id" => "1" },
38
+ operation_name: "FetchMovie",
39
+ )
40
+
41
+ request.prepare!
42
+ ```
43
+
44
+ Preparing a request will apply several destructive transformations:
45
+
46
+ - Default values from variable definitions will be added to request variables.
47
+ - The document will be pre-shaped based on `@skip` and `@include` directives.
data/docs/supergraph.md CHANGED
@@ -6,7 +6,7 @@ A `Supergraph` is the singuar representation of a stitched graph. `Supergraph` i
6
6
  storefronts_sdl = "type Query { storefront(id: ID!): Storefront } ..."
7
7
  products_sdl = "type Query { product(id: ID!): Product } ..."
8
8
 
9
- supergraph = GraphQL::Stitching::Composer.new({
9
+ supergraph = GraphQL::Stitching::Composer.new(schemas: {
10
10
  "storefronts" => GraphQL::Schema.from_definition(storefronts_sdl),
11
11
  "products" => GraphQL::Schema.from_definition(products_sdl),
12
12
  }).perform
@@ -6,9 +6,9 @@ module GraphQL
6
6
  class ComposerError < StitchingError; end
7
7
  class ValidationError < ComposerError; end
8
8
 
9
- attr_reader :query_name, :mutation_name, :subschema_types_by_name_and_location
9
+ attr_reader :query_name, :mutation_name, :subschema_types_by_name_and_location, :schema_directives
10
10
 
11
- DEFAULT_STRING_MERGER = ->(str_by_location, _info) { str_by_location.values.find { !_1.nil? } }
11
+ DEFAULT_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
12
12
 
13
13
  VALIDATORS = [
14
14
  "ValidateInterfaces",
@@ -20,7 +20,8 @@ module GraphQL
20
20
  query_name: "Query",
21
21
  mutation_name: "Mutation",
22
22
  description_merger: nil,
23
- deprecation_merger: nil
23
+ deprecation_merger: nil,
24
+ directive_kwarg_merger: nil
24
25
  )
25
26
  @schemas = schemas
26
27
  @query_name = query_name
@@ -29,11 +30,27 @@ module GraphQL
29
30
  @boundary_map = {}
30
31
  @mapped_type_names = {}
31
32
 
32
- @description_merger = description_merger || DEFAULT_STRING_MERGER
33
- @deprecation_merger = deprecation_merger || DEFAULT_STRING_MERGER
33
+ @description_merger = description_merger || DEFAULT_VALUE_MERGER
34
+ @deprecation_merger = deprecation_merger || DEFAULT_VALUE_MERGER
35
+ @directive_kwarg_merger = directive_kwarg_merger || DEFAULT_VALUE_MERGER
34
36
  end
35
37
 
36
38
  def perform
39
+ # "directive_name" => "location" => candidate_directive
40
+ @subschema_directives_by_name_and_location = @schemas.each_with_object({}) do |(location, schema), memo|
41
+ (schema.directives.keys - schema.default_directives.keys - GraphQL::Stitching.stitching_directive_names).each do |directive_name|
42
+ memo[directive_name] ||= {}
43
+ memo[directive_name][location] = schema.directives[directive_name]
44
+ end
45
+ end
46
+
47
+ # "Typename" => merged_directive
48
+ @schema_directives = @subschema_directives_by_name_and_location.each_with_object({}) do |(directive_name, directives_by_location), memo|
49
+ memo[directive_name] = build_directive(directive_name, directives_by_location)
50
+ end
51
+
52
+ @schema_directives.merge!(GraphQL::Schema.default_directives)
53
+
37
54
  # "Typename" => "location" => candidate_type
38
55
  @subschema_types_by_name_and_location = @schemas.each_with_object({}) do |(location, schema), memo|
39
56
  raise ComposerError, "Location keys must be strings" unless location.is_a?(String)
@@ -91,6 +108,7 @@ module GraphQL
91
108
  orphan_types schema_types.values
92
109
  query schema_types[builder.query_name]
93
110
  mutation schema_types[builder.mutation_name]
111
+ directives builder.schema_directives.values
94
112
 
95
113
  own_orphan_types.clear
96
114
  end
@@ -112,6 +130,18 @@ module GraphQL
112
130
  supergraph
113
131
  end
114
132
 
133
+ def build_directive(directive_name, directives_by_location)
134
+ builder = self
135
+
136
+ Class.new(GraphQL::Schema::Directive) do
137
+ graphql_name(directive_name)
138
+ description(builder.merge_descriptions(directive_name, directives_by_location))
139
+ repeatable(directives_by_location.values.any?(&:repeatable?))
140
+ locations(*directives_by_location.values.flat_map(&:locations).uniq)
141
+ builder.build_merged_arguments(directive_name, directives_by_location, self)
142
+ end
143
+ end
144
+
115
145
  def build_scalar_type(type_name, types_by_location)
116
146
  built_in_type = GraphQL::Schema::BUILT_IN_TYPES[type_name]
117
147
  return built_in_type if built_in_type
@@ -121,6 +151,7 @@ module GraphQL
121
151
  Class.new(GraphQL::Schema::Scalar) do
122
152
  graphql_name(type_name)
123
153
  description(builder.merge_descriptions(type_name, types_by_location))
154
+ builder.build_merged_directives(type_name, types_by_location, self)
124
155
  end
125
156
  end
126
157
 
@@ -146,13 +177,16 @@ module GraphQL
146
177
  Class.new(GraphQL::Schema::Enum) do
147
178
  graphql_name(type_name)
148
179
  description(builder.merge_descriptions(type_name, types_by_location))
180
+ builder.build_merged_directives(type_name, types_by_location, self)
149
181
 
150
182
  enum_values_by_value_location.each do |value, enum_values_by_location|
151
- value(value,
183
+ enum_value = value(value,
152
184
  value: value,
153
185
  description: builder.merge_descriptions(type_name, enum_values_by_location, enum_value: value),
154
186
  deprecation_reason: builder.merge_deprecations(type_name, enum_values_by_location, enum_value: value),
155
187
  )
188
+
189
+ builder.build_merged_directives(type_name, enum_values_by_location, enum_value, enum_value: value)
156
190
  end
157
191
  end
158
192
  end
@@ -170,6 +204,7 @@ module GraphQL
170
204
  end
171
205
 
172
206
  builder.build_merged_fields(type_name, types_by_location, self)
207
+ builder.build_merged_directives(type_name, types_by_location, self)
173
208
  end
174
209
  end
175
210
 
@@ -187,6 +222,7 @@ module GraphQL
187
222
  end
188
223
 
189
224
  builder.build_merged_fields(type_name, types_by_location, self)
225
+ builder.build_merged_directives(type_name, types_by_location, self)
190
226
  end
191
227
  end
192
228
 
@@ -199,6 +235,7 @@ module GraphQL
199
235
 
200
236
  possible_names = types_by_location.values.flat_map { _1.possible_types.map(&:graphql_name) }.uniq
201
237
  possible_types(*possible_names.map { builder.build_type_binding(_1) })
238
+ builder.build_merged_directives(type_name, types_by_location, self)
202
239
  end
203
240
  end
204
241
 
@@ -209,6 +246,7 @@ module GraphQL
209
246
  graphql_name(type_name)
210
247
  description(builder.merge_descriptions(type_name, types_by_location))
211
248
  builder.build_merged_arguments(type_name, types_by_location, self)
249
+ builder.build_merged_directives(type_name, types_by_location, self)
212
250
  end
213
251
  end
214
252
 
@@ -243,6 +281,7 @@ module GraphQL
243
281
  )
244
282
 
245
283
  build_merged_arguments(type_name, fields_by_location, schema_field, field_name: field_name)
284
+ build_merged_directives(type_name, fields_by_location, schema_field, field_name: field_name)
246
285
  end
247
286
  end
248
287
 
@@ -270,7 +309,7 @@ module GraphQL
270
309
  # Getting double args sometimes... why?
271
310
  return if owner.arguments.any? { _1.first == argument_name }
272
311
 
273
- owner.argument(
312
+ schema_argument = owner.argument(
274
313
  argument_name,
275
314
  description: merge_descriptions(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
276
315
  deprecation_reason: merge_deprecations(type_name, arguments_by_location, argument_name: argument_name, field_name: field_name),
@@ -278,6 +317,49 @@ module GraphQL
278
317
  required: value_types.any?(&:non_null?),
279
318
  camelize: false,
280
319
  )
320
+
321
+ build_merged_directives(type_name, arguments_by_location, schema_argument, field_name: field_name, argument_name: argument_name)
322
+ end
323
+ end
324
+
325
+ def build_merged_directives(type_name, members_by_location, owner, field_name: nil, argument_name: nil, enum_value: nil)
326
+ directives_by_name_location = members_by_location.each_with_object({}) do |(location, member_candidate), memo|
327
+ member_candidate.directives.each do |directive|
328
+ memo[directive.graphql_name] ||= {}
329
+ memo[directive.graphql_name][location] ||= {}
330
+ memo[directive.graphql_name][location] = directive
331
+ end
332
+ end
333
+
334
+ directives_by_name_location.each do |directive_name, directives_by_location|
335
+ directive_class = @schema_directives[directive_name]
336
+ next unless directive_class
337
+
338
+ # handled by deprecation_reason merger...
339
+ next if directive_class.graphql_name == "deprecated"
340
+
341
+ kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
342
+ directive.arguments.keyword_arguments.each do |key, value|
343
+ key = key.to_s
344
+ next unless directive_class.arguments[key]
345
+
346
+ memo[key] ||= {}
347
+ memo[key][location] = value
348
+ end
349
+ end
350
+
351
+ kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
352
+ memo[kwarg_name.to_sym] = @directive_kwarg_merger.call(kwarg_values_by_location, {
353
+ type_name: type_name,
354
+ field_name: field_name,
355
+ argument_name: argument_name,
356
+ enum_value: enum_value,
357
+ directive_name: directive_name,
358
+ kwarg_name: kwarg_name,
359
+ }.compact!)
360
+ end
361
+
362
+ owner.directive(directive_class, **kwargs)
281
363
  end
282
364
  end
283
365
 
@@ -7,16 +7,17 @@ module GraphQL
7
7
  class Executor
8
8
 
9
9
  class RootSource < GraphQL::Dataloader::Source
10
- def initialize(executor)
10
+ def initialize(executor, location)
11
11
  @executor = executor
12
+ @location = location
12
13
  end
13
14
 
14
15
  def fetch(ops)
15
16
  op = ops.first # There should only ever be one per location at a time
16
17
 
17
18
  query_document = build_query(op)
18
- query_variables = @executor.variables.slice(*op["variables"].keys)
19
- result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
19
+ query_variables = @executor.request.variables.slice(*op["variables"].keys)
20
+ result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
20
21
  @executor.query_count += 1
21
22
 
22
23
  @executor.data.merge!(result["data"]) if result["data"]
@@ -24,7 +25,8 @@ module GraphQL
24
25
  result["errors"].each { _1.delete("locations") }
25
26
  @executor.errors.concat(result["errors"])
26
27
  end
27
- op["key"]
28
+
29
+ ops.map { op["key"] }
28
30
  end
29
31
 
30
32
  def build_query(op)
@@ -61,8 +63,8 @@ module GraphQL
61
63
 
62
64
  if origin_sets_by_operation.any?
63
65
  query_document, variable_names = build_query(origin_sets_by_operation)
64
- variables = @executor.variables.slice(*variable_names)
65
- raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
66
+ variables = @executor.request.variables.slice(*variable_names)
67
+ raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
66
68
  @executor.query_count += 1
67
69
 
68
70
  merge_results!(origin_sets_by_operation, raw_result.dig("data"))
@@ -165,23 +167,26 @@ module GraphQL
165
167
 
166
168
  private
167
169
 
168
- # traverses forward through origin data, expanding arrays to follow all paths
170
+ # traverse forward through origin data, expanding arrays to follow all paths
169
171
  # any errors found for an origin object_id have their path prefixed by the object path
170
172
  def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
171
- current_path << forward_path.first
172
- forward_path = forward_path[1..-1]
173
+ current_path.push(forward_path.shift)
173
174
  scope = root[current_path.last]
174
175
 
175
176
  if forward_path.any? && scope.is_a?(Array)
176
177
  scope.each_with_index do |element, index|
177
178
  inner_elements = element.is_a?(Array) ? element.flatten : [element]
178
179
  inner_elements.each do |inner_element|
179
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], inner_element)
180
+ current_path << index
181
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
182
+ current_path.pop
180
183
  end
181
184
  end
182
185
 
183
186
  elsif forward_path.any?
184
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], scope)
187
+ current_path << index
188
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
189
+ current_path.pop
185
190
 
186
191
  elsif scope.is_a?(Array)
187
192
  scope.each_with_index do |element, index|
@@ -196,61 +201,72 @@ module GraphQL
196
201
  errors = pathed_errors_by_object_id[scope.object_id]
197
202
  errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
198
203
  end
204
+
205
+ forward_path.unshift(current_path.pop)
199
206
  end
200
207
  end
201
208
 
202
- attr_reader :supergraph, :data, :errors, :variables
209
+ attr_reader :supergraph, :request, :data, :errors
203
210
  attr_accessor :query_count
204
211
 
205
- def initialize(supergraph:, plan:, variables: {}, nonblocking: false)
212
+ def initialize(supergraph:, request:, plan:, nonblocking: false)
206
213
  @supergraph = supergraph
207
- @variables = variables
214
+ @request = request
208
215
  @queue = plan["ops"]
209
216
  @data = {}
210
217
  @errors = []
211
218
  @query_count = 0
219
+ @exec_cycles = 0
212
220
  @dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
213
221
  end
214
222
 
215
- def perform(document=nil)
223
+ def perform(raw: false)
216
224
  exec!
217
-
218
225
  result = {}
219
- result["data"] = @data if @data && @data.length > 0
220
- result["errors"] = @errors if @errors.length > 0
221
226
 
222
- if document && result["data"]
223
- GraphQL::Stitching::Shaper.new(
227
+ if @data && @data.length > 0
228
+ result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
224
229
  schema: @supergraph.schema,
225
- document: document,
226
- ).perform!(result)
227
- else
228
- result
230
+ request: @request,
231
+ ).perform!(@data)
229
232
  end
233
+
234
+ if @errors.length > 0
235
+ result["errors"] = @errors
236
+ end
237
+
238
+ result
230
239
  end
231
240
 
232
241
  private
233
242
 
234
243
  def exec!(after_keys = [0])
244
+ if @exec_cycles > @queue.length
245
+ # sanity check... if we've exceeded queue size, then something went wrong.
246
+ raise StitchingError, "Too many execution requests attempted."
247
+ end
248
+
235
249
  @dataloader.append_job do
236
- requests = @queue
250
+ tasks = @queue
237
251
  .select { after_keys.include?(_1["after_key"]) }
238
- .group_by { _1["location"] }
239
- .map do |location, ops|
240
- if ops.first["after_key"].zero?
241
- @dataloader.with(RootSource, self).request_all(ops)
252
+ .group_by { [_1["location"], _1["boundary"].nil?] }
253
+ .map do |(location, root_source), ops|
254
+ if root_source
255
+ @dataloader.with(RootSource, self, location).request_all(ops)
242
256
  else
243
257
  @dataloader.with(BoundarySource, self, location).request_all(ops)
244
258
  end
245
259
  end
246
260
 
247
- requests.each(&method(:exec_request))
261
+ tasks.each(&method(:exec_task))
248
262
  end
263
+
264
+ @exec_cycles += 1
249
265
  @dataloader.run
250
266
  end
251
267
 
252
- def exec_request(request)
253
- next_keys = request.load
268
+ def exec_task(task)
269
+ next_keys = task.load
254
270
  next_keys.compact!
255
271
  exec!(next_keys) if next_keys.any?
256
272
  end
@@ -7,8 +7,6 @@ module GraphQL
7
7
  class Gateway
8
8
  class GatewayError < StitchingError; end
9
9
 
10
- EMPTY_CONTEXT = {}.freeze
11
-
12
10
  attr_reader :supergraph
13
11
 
14
12
  def initialize(locations: nil, supergraph: nil)
@@ -25,29 +23,36 @@ module GraphQL
25
23
  end
26
24
  end
27
25
 
28
- def execute(query:, variables: nil, operation_name: nil, context: EMPTY_CONTEXT, validate: true)
29
- document = GraphQL::Stitching::Document.new(query, operation_name: operation_name)
26
+ def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
27
+ request = GraphQL::Stitching::Request.new(
28
+ query,
29
+ operation_name: operation_name,
30
+ variables: variables,
31
+ context: context,
32
+ )
30
33
 
31
34
  if validate
32
- validation_errors = @supergraph.schema.validate(document.ast)
35
+ validation_errors = @supergraph.schema.validate(request.document)
33
36
  return error_result(validation_errors) if validation_errors.any?
34
37
  end
35
38
 
39
+ request.prepare!
40
+
36
41
  begin
37
- plan = fetch_plan(document, context) do
42
+ plan = fetch_plan(request) do
38
43
  GraphQL::Stitching::Planner.new(
39
44
  supergraph: @supergraph,
40
- document: document,
45
+ request: request,
41
46
  ).perform.to_h
42
47
  end
43
48
 
44
49
  GraphQL::Stitching::Executor.new(
45
50
  supergraph: @supergraph,
51
+ request: request,
46
52
  plan: plan,
47
- variables: variables || {},
48
- ).perform(document)
53
+ ).perform
49
54
  rescue StandardError => e
50
- custom_message = @on_error.call(e, context) if @on_error
55
+ custom_message = @on_error.call(e, request.context) if @on_error
51
56
  error_result([{ "message" => custom_message || "An unexpected error occured." }])
52
57
  end
53
58
  end
@@ -91,16 +96,16 @@ module GraphQL
91
96
  supergraph
92
97
  end
93
98
 
94
- def fetch_plan(document, context)
99
+ def fetch_plan(request)
95
100
  if @on_cache_read
96
- cached_plan = @on_cache_read.call(document.digest, context)
101
+ cached_plan = @on_cache_read.call(request.digest, request.context)
97
102
  return JSON.parse(cached_plan) if cached_plan
98
103
  end
99
104
 
100
105
  plan_json = yield
101
106
 
102
107
  if @on_cache_write
103
- @on_cache_write.call(document.digest, JSON.generate(plan_json), context)
108
+ @on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
104
109
  end
105
110
 
106
111
  plan_json
@@ -6,9 +6,9 @@ module GraphQL
6
6
  SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
7
7
  TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
8
8
 
9
- def initialize(supergraph:, document:)
9
+ def initialize(supergraph:, request:)
10
10
  @supergraph = supergraph
11
- @document = document
11
+ @request = request
12
12
  @sequence_key = 0
13
13
  @operations_by_grouping = {}
14
14
  end
@@ -37,7 +37,11 @@ module GraphQL
37
37
  extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
38
38
  end
39
39
 
40
- grouping = [after_key, location, parent_type.graphql_name, *insertion_path].join("/")
40
+ grouping = String.new
41
+ grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
42
+ grouping = insertion_path.reduce(grouping) do |memo, segment|
43
+ memo << "/" << segment
44
+ end
41
45
 
42
46
  if op = @operations_by_grouping[grouping]
43
47
  op.selections += selection_set if selection_set
@@ -62,12 +66,12 @@ module GraphQL
62
66
  end
63
67
 
64
68
  def build_root_operations
65
- case @document.operation.operation_type
69
+ case @request.operation.operation_type
66
70
  when "query"
67
71
  # plan steps grouping all fields by location for async execution
68
72
  parent_type = @supergraph.schema.query
69
73
 
70
- selections_by_location = @document.operation.selections.each_with_object({}) do |node, memo|
74
+ selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
71
75
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
72
76
  memo[locations.last] ||= []
73
77
  memo[locations.last] << node
@@ -82,7 +86,7 @@ module GraphQL
82
86
  parent_type = @supergraph.schema.mutation
83
87
  location_groups = []
84
88
 
85
- @document.operation.selections.reduce(nil) do |last_location, node|
89
+ @request.operation.selections.reduce(nil) do |last_location, node|
86
90
  location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
87
91
  if location != last_location
88
92
  location_groups << {
@@ -161,8 +165,10 @@ module GraphQL
161
165
  if Util.is_leaf_type?(field_type)
162
166
  selections_result << node
163
167
  else
164
- expanded_path = [*insertion_path, node.alias || node.name]
165
- selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, expanded_path, after_key)
168
+ insertion_path.push(node.alias || node.name)
169
+ selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key)
170
+ insertion_path.pop
171
+
166
172
  selections_result << node.merge(selections: selection_set)
167
173
  variables_result.merge!(variables)
168
174
  end
@@ -177,7 +183,7 @@ module GraphQL
177
183
  implements_fragments = true
178
184
 
179
185
  when GraphQL::Language::Nodes::FragmentSpread
180
- fragment = @document.fragment_definitions[node.name]
186
+ fragment = @request.fragment_definitions[node.name]
181
187
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
182
188
 
183
189
  fragment_type = @supergraph.schema.types[fragment.type.name]
@@ -260,7 +266,7 @@ module GraphQL
260
266
  location: location,
261
267
  selections: selections_by_location[location],
262
268
  parent_type: parent_type,
263
- insertion_path: insertion_path,
269
+ insertion_path: insertion_path.dup,
264
270
  boundary: boundary,
265
271
  after_key: after_key,
266
272
  )
@@ -289,7 +295,7 @@ module GraphQL
289
295
  when GraphQL::Language::Nodes::InputObject
290
296
  extract_node_variables!(argument.value, memo)
291
297
  when GraphQL::Language::Nodes::VariableIdentifier
292
- memo[argument.value.name] ||= @document.variable_definitions[argument.value.name]
298
+ memo[argument.value.name] ||= @request.variable_definitions[argument.value.name]
293
299
  end
294
300
  end
295
301
  end
@@ -9,14 +9,14 @@ module GraphQL
9
9
  class RemoteClient
10
10
  def initialize(url:, headers:{})
11
11
  @url = url
12
- @headers = headers
12
+ @headers = { "Content-Type" => "application/json" }.merge!(headers)
13
13
  end
14
14
 
15
- def call(location, document, variables)
15
+ def call(_location, document, variables, _context)
16
16
  response = Net::HTTP.post(
17
17
  URI(@url),
18
- { "query" => document, "variables" => variables }.to_json,
19
- { "Content-Type" => "application/json" }.merge!(@headers)
18
+ JSON.generate({ "query" => document, "variables" => variables }),
19
+ @headers,
20
20
  )
21
21
  JSON.parse(response.body)
22
22
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ class Request
6
+ SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
+ EMPTY_CONTEXT = {}.freeze
8
+
9
+ class ApplyRuntimeDirectives < GraphQL::Language::Visitor
10
+ def initialize(document, variables)
11
+ @changed = false
12
+ @variables = variables
13
+ super(document)
14
+ end
15
+
16
+ def changed?
17
+ @changed
18
+ end
19
+
20
+ def on_field(node, parent)
21
+ delete_node = false
22
+ filtered_directives = if node.directives.any?
23
+ node.directives.select do |directive|
24
+ if directive.name == "skip"
25
+ delete_node = assess_argument_value(directive.arguments.first)
26
+ false
27
+ elsif directive.name == "include"
28
+ delete_node = !assess_argument_value(directive.arguments.first)
29
+ false
30
+ else
31
+ true
32
+ end
33
+ end
34
+ end
35
+
36
+ if delete_node
37
+ @changed = true
38
+ super(DELETE_NODE, parent)
39
+ elsif filtered_directives && filtered_directives.length != node.directives.length
40
+ @changed = true
41
+ super(node.merge(directives: filtered_directives), parent)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def assess_argument_value(arg)
50
+ if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
51
+ return @variables[arg.value.name]
52
+ end
53
+ arg.value
54
+ end
55
+ end
56
+
57
+ attr_reader :document, :variables, :operation_name, :context
58
+
59
+ def initialize(document, operation_name: nil, variables: nil, context: nil)
60
+ @may_contain_runtime_directives = true
61
+
62
+ @document = if document.is_a?(String)
63
+ @may_contain_runtime_directives = document.include?("@")
64
+ GraphQL.parse(document)
65
+ else
66
+ document
67
+ end
68
+
69
+ @operation_name = operation_name
70
+ @variables = variables || {}
71
+ @context = context || EMPTY_CONTEXT
72
+ end
73
+
74
+ def string
75
+ @string ||= @document.to_query_string
76
+ end
77
+
78
+ def digest
79
+ @digest ||= Digest::SHA2.hexdigest(string)
80
+ end
81
+
82
+ def operation
83
+ @operation ||= begin
84
+ operation_defs = @document.definitions.select do |d|
85
+ next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
86
+ next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
87
+ @operation_name ? d.name == @operation_name : true
88
+ end
89
+
90
+ if operation_defs.length < 1
91
+ raise GraphQL::ExecutionError, "Invalid root operation."
92
+ elsif operation_defs.length > 1
93
+ raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
94
+ end
95
+
96
+ operation_defs.first
97
+ end
98
+ end
99
+
100
+ def variable_definitions
101
+ @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
102
+ memo[v.name] = v.type
103
+ end
104
+ end
105
+
106
+ def fragment_definitions
107
+ @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
108
+ memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
109
+ end
110
+ end
111
+
112
+ def prepare!
113
+ operation.variables.each do |v|
114
+ @variables[v.name] ||= v.default_value
115
+ end
116
+
117
+ return self unless @may_contain_runtime_directives
118
+
119
+ visitor = ApplyRuntimeDirectives.new(@document, @variables)
120
+ @document = visitor.visit
121
+
122
+ if visitor.changed?
123
+ @string = nil
124
+ @digest = nil
125
+ @operation = nil
126
+ @variable_definitions = nil
127
+ @fragment_definitions = nil
128
+ end
129
+ self
130
+ end
131
+ end
132
+ end
133
+ end
@@ -4,14 +4,14 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  class Shaper
7
- def initialize(schema:, document:)
7
+ def initialize(schema:, request:)
8
8
  @schema = schema
9
- @document = document
9
+ @request = request
10
10
  end
11
11
 
12
12
  def perform!(raw)
13
- raw["data"] = resolve_object_scope(raw["data"], @schema.query, @document.operation.selections)
14
- raw
13
+ root_type = @schema.public_send(@request.operation.operation_type)
14
+ resolve_object_scope(raw, root_type, @request.operation.selections)
15
15
  end
16
16
 
17
17
  private
@@ -48,7 +48,7 @@ module GraphQL
48
48
  return nil if result.nil?
49
49
 
50
50
  when GraphQL::Language::Nodes::FragmentSpread
51
- fragment = @document.fragment_definitions[node.name]
51
+ fragment = @request.fragment_definitions[node.name]
52
52
  fragment_type = @schema.types[fragment.type.name]
53
53
  next unless typename == fragment_type.graphql_name
54
54
 
@@ -57,27 +57,28 @@ module GraphQL
57
57
  def assign_executable(location, executable = nil, &block)
58
58
  executable ||= block
59
59
  unless executable.is_a?(Class) && executable <= GraphQL::Schema
60
- raise "A client or block handler must be provided." unless executable
61
- raise "A client must be callable" unless executable.respond_to?(:call)
60
+ raise StitchingError, "A client or block handler must be provided." unless executable
61
+ raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
62
62
  end
63
63
  @executables[location] = executable
64
64
  end
65
65
 
66
- def execute_at_location(location, query, variables)
66
+ def execute_at_location(location, query, variables, context)
67
67
  executable = executables[location]
68
68
 
69
69
  if executable.nil?
70
- raise "No executable assigned for #{location} location."
70
+ raise StitchingError, "No executable assigned for #{location} location."
71
71
  elsif executable.is_a?(Class) && executable <= GraphQL::Schema
72
72
  executable.execute(
73
73
  query: query,
74
74
  variables: variables,
75
+ context: context,
75
76
  validate: false,
76
77
  )
77
78
  elsif executable.respond_to?(:call)
78
- executable.call(location, query, variables)
79
+ executable.call(location, query, variables, context)
79
80
  else
80
- raise "Missing valid executable for #{location} location."
81
+ raise StitchingError, "Missing valid executable for #{location} location."
81
82
  end
82
83
  end
83
84
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
@@ -13,6 +13,10 @@ module GraphQL
13
13
  end
14
14
 
15
15
  attr_writer :stitch_directive
16
+
17
+ def stitching_directive_names
18
+ [stitch_directive]
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -20,11 +24,11 @@ end
20
24
  require_relative "stitching/gateway"
21
25
  require_relative "stitching/supergraph"
22
26
  require_relative "stitching/composer"
23
- require_relative "stitching/document"
24
27
  require_relative "stitching/executor"
25
28
  require_relative "stitching/planner_operation"
26
29
  require_relative "stitching/planner"
27
30
  require_relative "stitching/remote_client"
31
+ require_relative "stitching/request"
28
32
  require_relative "stitching/shaper"
29
33
  require_relative "stitching/util"
30
34
  require_relative "stitching/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-stitching
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-11 00:00:00.000000000 Z
11
+ date: 2023-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -81,13 +81,13 @@ files:
81
81
  - Rakefile
82
82
  - docs/README.md
83
83
  - docs/composer.md
84
- - docs/document.md
85
84
  - docs/executor.md
86
85
  - docs/gateway.md
87
86
  - docs/images/library.png
88
87
  - docs/images/merging.png
89
88
  - docs/images/stitching.png
90
89
  - docs/planner.md
90
+ - docs/request.md
91
91
  - docs/supergraph.md
92
92
  - example/gateway.rb
93
93
  - example/graphiql.html
@@ -99,12 +99,12 @@ files:
99
99
  - lib/graphql/stitching/composer/base_validator.rb
100
100
  - lib/graphql/stitching/composer/validate_boundaries.rb
101
101
  - lib/graphql/stitching/composer/validate_interfaces.rb
102
- - lib/graphql/stitching/document.rb
103
102
  - lib/graphql/stitching/executor.rb
104
103
  - lib/graphql/stitching/gateway.rb
105
104
  - lib/graphql/stitching/planner.rb
106
105
  - lib/graphql/stitching/planner_operation.rb
107
106
  - lib/graphql/stitching/remote_client.rb
107
+ - lib/graphql/stitching/request.rb
108
108
  - lib/graphql/stitching/shaper.rb
109
109
  - lib/graphql/stitching/supergraph.rb
110
110
  - lib/graphql/stitching/util.rb
data/docs/document.md DELETED
@@ -1,15 +0,0 @@
1
- ## GraphQL::Stitching::Document
2
-
3
- A `Document` wraps a parsed GraphQL request, and handles the logistics of extracting its appropriate operation, variable definitions, and fragments. A `Document` should be built once for a request and passed through to other stitching components that utilize document information.
4
-
5
- ```ruby
6
- query = "query FetchMovie($id: ID!) { movie(id:$id) { id genre } }"
7
- document = GraphQL::Stitching::Document.new(query, operation_name: "FetchMovie")
8
-
9
- document.ast # parsed AST via GraphQL.parse
10
- document.string # normalized printed string
11
- document.digest # SHA digest of the normalized string
12
-
13
- document.variables # mapping of variable names to type definitions
14
- document.fragments # mapping of fragment names to fragment definitions
15
- ```
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- class Document
6
- SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
-
8
- attr_reader :ast, :operation_name
9
-
10
- def initialize(string_or_ast, operation_name: nil)
11
- @ast = if string_or_ast.is_a?(String)
12
- GraphQL.parse(string_or_ast)
13
- else
14
- string_or_ast
15
- end
16
-
17
- @operation_name = operation_name
18
- end
19
-
20
- def string
21
- @string ||= GraphQL::Language::Printer.new.print(@ast)
22
- end
23
-
24
- def digest
25
- @digest ||= Digest::SHA2.hexdigest(string)
26
- end
27
-
28
- def operation
29
- @operation ||= begin
30
- operation_defs = @ast.definitions.select do |d|
31
- next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
32
- next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
33
- @operation_name ? d.name == @operation_name : true
34
- end
35
-
36
- if operation_defs.length < 1
37
- raise GraphQL::ExecutionError, "Invalid root operation."
38
- elsif operation_defs.length > 1
39
- raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
40
- end
41
-
42
- operation_defs.first
43
- end
44
- end
45
-
46
- def variable_definitions
47
- @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
48
- memo[v.name] = v.type
49
- end
50
- end
51
-
52
- def fragment_definitions
53
- @fragment_definitions ||= @ast.definitions.each_with_object({}) do |d, memo|
54
- memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
55
- end
56
- end
57
- end
58
- end
59
- end