graphql-stitching 0.1.0 → 0.2.2

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: f67a4b892fba612e2e38552ae0a0f681ed0fdc136c04ecef4d2c01692eed9eb8
4
+ data.tar.gz: 3db164c53dc67d64b7364ba5a17186e3eb612074993b21df5f9a8b059fca9f89
5
5
  SHA512:
6
- metadata.gz: 1c0c16a9bb49b3fad30057958d79fe92737bb659bf7135dd03ef716597f013249c99d9372478e20619f54e1e77f68f3b7f746b92e9e220b80be9ecaf88b656de
7
- data.tar.gz: f370e520552f3e8be141073dd7ca06f68a463824c946b9cd03357b33ad5c9fc928893e846314f08941e2590d24202081450677fbfca480474be9833d341209a1
6
+ metadata.gz: 8d687747a19a25a69b1c998910265a183bdb22338fec5e9787412ca5c1cdfc5e0b838bb49ce4942ce41dec0e000e0ddc35bdf07368a7a772751ffc5342b7ee48
7
+ data.tar.gz: f4419aa525964b37db62ce2343d11914ac56677825c8f1450c5187465b2f28e3a740acf6779309bdb0a5fbd41829054101aed4c9d0eb764882759c50bbf1b641
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:
@@ -214,12 +214,12 @@ type Product {
214
214
  upc: ID!
215
215
  }
216
216
  type Query {
217
- productById(id: ID): Product @stitch(key: "id")
218
- productByUpc(upc: ID): Product @stitch(key: "upc")
217
+ productById(id: ID!): Product @stitch(key: "id")
218
+ productByUpc(upc: ID!): Product @stitch(key: "upc")
219
219
  }
220
220
  ```
221
221
 
222
- The `@stitch` directive is also repeatable, allowing a single query to associate with multiple keys:
222
+ The `@stitch` directive is also repeatable (_requires graphql-ruby v2.0.15_), allowing a single query to associate with multiple keys:
223
223
 
224
224
  ```graphql
225
225
  type Product {
@@ -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,47 @@ 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(
16
+ query,
17
+ variables: { "id" => "123" },
18
+ operation_name: "MyQuery",
19
+ )
18
20
 
19
21
  plan = GraphQL::Stitching::Planner.new(
20
22
  supergraph: supergraph,
21
- document: document,
23
+ request: request,
22
24
  ).perform
23
25
 
24
- # get the raw result without shaping
25
- raw_result = GraphQL::Stitching::Executor.new(
26
+ result = GraphQL::Stitching::Executor.new(
26
27
  supergraph: supergraph,
28
+ request: request,
27
29
  plan: plan.to_h,
28
- variables: variables,
29
30
  ).perform
31
+ ```
30
32
 
31
- # get the final result with shaping
32
- final_result = GraphQL::Stitching::Executor.new(
33
+ ### Raw results
34
+
35
+ 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:
36
+
37
+ ```ruby
38
+ # get the raw result without shaping
39
+ raw_result = GraphQL::Stitching::Executor.new(
33
40
  supergraph: supergraph,
41
+ request: request,
34
42
  plan: plan.to_h,
35
- variables: variables,
36
- ).perform(document)
43
+ ).perform(raw: true)
44
+ ```
45
+
46
+ ### Batching
47
+
48
+ The Executor batches together as many requests as possible to a given location at a given time. Batched queries are written with the operation name suffixed by all operation keys in the batch, and root stitching fields are each prefixed by their batch index and collection index (for non-list fields):
49
+
50
+ ```graphql
51
+ query MyOperation_2_3($lang:String!,$currency:Currency!){
52
+ _0_result: storefronts(ids:["7","8"]) { name(lang:$lang) }
53
+ _1_0_result: product(upc:"abc") { price(currency:$currency) }
54
+ _1_1_result: product(upc:"xyz") { price(currency:$currency) }
55
+ }
37
56
  ```
38
57
 
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.
58
+ All told, the executor will make one request per location per generation of data. Generations started on separate forks of the resolution tree will be resolved independently.
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,15 @@ 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(
16
+ document,
17
+ variables: { "id" => "1" },
18
+ operation_name: "MyQuery",
19
+ ).prepare!
16
20
 
17
21
  plan = GraphQL::Stitching::Planner.new(
18
22
  supergraph: supergraph,
19
- document: document,
23
+ request: request,
20
24
  ).perform
21
25
  ```
22
26
 
@@ -25,17 +29,17 @@ plan = GraphQL::Stitching::Planner.new(
25
29
  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
30
 
27
31
  ```ruby
28
- cached_plan = $redis.get(document.digest)
32
+ cached_plan = $redis.get(request.digest)
29
33
 
30
34
  plan = if cached_plan
31
35
  JSON.parse(cached_plan)
32
36
  else
33
37
  plan_hash = GraphQL::Stitching::Planner.new(
34
38
  supergraph: supergraph,
35
- document: document,
39
+ request: request,
36
40
  ).perform.to_h
37
41
 
38
- $redis.set(document.digest, JSON.generate(plan_hash))
42
+ $redis.set(request.digest, JSON.generate(plan_hash))
39
43
  plan_hash
40
44
  end
41
45
 
data/docs/request.md ADDED
@@ -0,0 +1,50 @@
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 for stitching using the `prepare!` method _after_ validations have been run:
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
+ errors = MySchema.validate(request.document)
42
+ # return early with any static validation errors...
43
+
44
+ request.prepare!
45
+ ```
46
+
47
+ Preparing a request will apply several destructive transformations:
48
+
49
+ - Default values from variable definitions will be added to request variables.
50
+ - 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
@@ -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', '~> 2.0.16'
29
+ spec.add_runtime_dependency 'graphql', '~> 2.0.3'
30
30
 
31
31
  spec.add_development_dependency 'bundler', '~> 2.0'
32
32
  spec.add_development_dependency 'rake', '~> 12.0'
@@ -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,12 +317,55 @@ 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
 
284
366
  def merge_value_types(type_name, type_candidates, field_name: nil, argument_name: nil)
285
367
  path = [type_name, field_name, argument_name].compact.join(".")
286
- named_types = type_candidates.map { Util.get_named_type(_1).graphql_name }.uniq
368
+ named_types = type_candidates.map { _1.unwrap.graphql_name }.uniq
287
369
 
288
370
  unless named_types.all? { _1 == named_types.first }
289
371
  raise ComposerError, "Cannot compose mixed types at `#{path}`. Found: #{named_types.join(", ")}."
@@ -340,7 +422,7 @@ module GraphQL
340
422
  def extract_boundaries(type_name, types_by_location)
341
423
  types_by_location.each do |location, type_candidate|
342
424
  type_candidate.fields.each do |field_name, field_candidate|
343
- boundary_type_name = Util.get_named_type(field_candidate.type).graphql_name
425
+ boundary_type_name = field_candidate.type.unwrap.graphql_name
344
426
  boundary_list = Util.get_list_structure(field_candidate.type)
345
427
 
346
428
  field_candidate.directives.each do |directive|
@@ -388,10 +470,10 @@ module GraphQL
388
470
  boundary_type = schema.types[type_name]
389
471
  next unless boundary_type.kind.abstract?
390
472
 
391
- possible_types = Util.get_possible_types(schema, boundary_type)
392
- possible_types.select { @subschema_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |possible_type|
393
- @boundary_map[possible_type.graphql_name] ||= []
394
- @boundary_map[possible_type.graphql_name].push(*@boundary_map[type_name])
473
+ expanded_types = Util.expand_abstract_type(schema, boundary_type)
474
+ expanded_types.select { @subschema_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
475
+ @boundary_map[expanded_type.graphql_name] ||= []
476
+ @boundary_map[expanded_type.graphql_name].push(*@boundary_map[type_name])
395
477
  end
396
478
  end
397
479
  end
@@ -406,18 +488,18 @@ module GraphQL
406
488
 
407
489
  if type.kind.object? || type.kind.interface?
408
490
  type.fields.values.each do |field|
409
- field_type = Util.get_named_type(field.type)
491
+ field_type = field.type.unwrap
410
492
  reads << field_type.graphql_name if field_type.kind.enum?
411
493
 
412
494
  field.arguments.values.each do |argument|
413
- argument_type = Util.get_named_type(argument.type)
495
+ argument_type = argument.type.unwrap
414
496
  writes << argument_type.graphql_name if argument_type.kind.enum?
415
497
  end
416
498
  end
417
499
 
418
500
  elsif type.kind.input_object?
419
501
  type.arguments.values.each do |argument|
420
- argument_type = Util.get_named_type(argument.type)
502
+ argument_type = argument.type.unwrap
421
503
  writes << argument_type.graphql_name if argument_type.kind.enum?
422
504
  end
423
505
  end