graphql-stitching 0.0.1 → 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: a228b0fe5779d625285182f885a7d65b5f66541f275a5313602e68a4c9eb209d
4
- data.tar.gz: f6efcaeafd7c417db07f007641f84c800c0ebb32c99ad19c5004aa2bb9ac8685
3
+ metadata.gz: 05c82abfcbec2db513d097c21424916bea46ebb0e4b434db0dc10a92ddfc2f9b
4
+ data.tar.gz: 79e4184f4ed237e2132f67389bb838fbbf18a8141b1d2ab30f8c5b710f47cb8f
5
5
  SHA512:
6
- metadata.gz: 1d484e39db0ac919e68477e8733fa6f0ca31ffde74b2bfebc4e13ba6b55d7973dc793c6df4251ae7b912c450bdc38ab4478e300c350d7be67555a635687b04a9
7
- data.tar.gz: 6354381e960a922453cd38ae5c513b60f2d2ae4585ec2a53a1ab7b405c2da6ca6d01bce9999794f9c8b6df7fe0d49176a4f944ab0efabfcb8ff252393a60f88e
6
+ metadata.gz: e6c6ccb67c95df19b01bbeea1c0ed731a7153c3e4a184dab7dad6c2c52a22e22183075d123bf2dc78f431e73ff1fb4d31f0a21100072adb4f05bff2813ff9294
7
+ data.tar.gz: 2b9257ed86bdac5cb2403e527fc90893c961637d14bda2c4e5a16be7c8dc23206f860bdb596cc26f35ea704156b7443c5ef013efcc4c8cde251c9a865777e3ad
@@ -11,14 +11,20 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- ruby-version: ['3.1.1']
14
+ include:
15
+ - gemfile: Gemfile
16
+ ruby: 3.2
17
+ - gemfile: Gemfile
18
+ ruby: 3.1
19
+ - gemfile: Gemfile
20
+ ruby: 2.7
15
21
 
16
22
  steps:
17
23
  - uses: actions/checkout@v2
18
24
  - name: Setup Ruby
19
25
  uses: ruby/setup-ruby@v1
20
26
  with:
21
- ruby-version: ${{ matrix.ruby-version }}
27
+ ruby-version: ${{ matrix.ruby }}
22
28
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
23
29
  - name: Run tests
24
30
  run: |
data/.gitignore CHANGED
@@ -10,6 +10,7 @@
10
10
  /test/version_tmp/
11
11
  /tmp/
12
12
  .envrc
13
+ Gemfile.lock
13
14
 
14
15
  # Used by dotenv library to load environment variables.
15
16
  # .env
@@ -17,6 +18,7 @@
17
18
 
18
19
  # Ignore Byebug command history file.
19
20
  .byebug_history
21
+ .ruby-version
20
22
  .DS_Store
21
23
 
22
24
  ## Specific to RubyMotion:
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
 
@@ -46,7 +46,7 @@ showtimes_schema = <<~GRAPHQL
46
46
  GRAPHQL
47
47
 
48
48
  gateway = GraphQL::Stitching::Gateway.new(locations: {
49
- products: {
49
+ movies: {
50
50
  schema: GraphQL::Schema.from_definition(movies_schema),
51
51
  executable: GraphQL::Stitching::RemoteClient.new(url: "http://localhost:3000"),
52
52
  },
@@ -70,16 +70,15 @@ 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
- - [Shaper](./docs/shaper.md) - takes the raw output of the executor and prepares it for delivery.
83
82
 
84
83
  ## Merged types
85
84
 
@@ -122,7 +121,7 @@ shipping_schema = <<~GRAPHQL
122
121
  }
123
122
  GRAPHQL
124
123
 
125
- supergraph = GraphQL::Stitching::Composer.new({
124
+ supergraph = GraphQL::Stitching::Composer.new(schemas: {
126
125
  "products" => GraphQL::Schema.from_definition(products_schema),
127
126
  "shipping" => GraphQL::Schema.from_definition(shipping_schema),
128
127
  })
@@ -147,7 +146,7 @@ type Query {
147
146
  }
148
147
  ```
149
148
 
150
- * 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.
151
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).
152
151
 
153
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:
@@ -262,29 +261,29 @@ GraphQL::Stitching.stitch_directive = "merge"
262
261
 
263
262
  ## Executables
264
263
 
265
- 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...
266
265
 
267
266
  ```ruby
268
267
  class MyExecutable
269
- def call(location, query_string, variables)
268
+ def call(location, query_string, variables, context)
270
269
  # process a GraphQL request...
271
270
  end
272
271
  end
273
272
  ```
274
273
 
275
- 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`:
276
275
 
277
276
  ```ruby
278
277
  supergraph = GraphQL::Stitching::Composer.new(...)
279
278
 
280
279
  supergraph.assign_executable("location1", MyExecutable.new)
281
- supergraph.assign_executable("location2", ->(loc, query, vars) { ... })
282
- 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|
283
282
  # ...
284
283
  end
285
284
  ```
286
285
 
287
- 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)).
288
287
 
289
288
  ## Concurrency
290
289
 
@@ -292,7 +291,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
292
291
 
293
292
  ## Example
294
293
 
295
- 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:
296
295
 
297
296
  ```shell
298
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,18 +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
- raw_result = GraphQL::Stitching::Executor.new(
22
+ result = GraphQL::Stitching::Executor.new(
25
23
  supergraph: supergraph,
26
24
  plan: plan.to_h,
27
- variables: variables,
25
+ request: request,
28
26
  ).perform
29
27
  ```
28
+
29
+ ### Raw results
30
+
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(
36
+ supergraph: supergraph,
37
+ plan: plan.to_h,
38
+ request: request,
39
+ ).perform(raw: true)
40
+ ```
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
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.version = GraphQL::Stitching::VERSION
9
9
  spec.authors = ['Greg MacWilliam']
10
10
  spec.summary = 'GraphQL schema stitching for Ruby'
11
- spec.description = spec.summary
11
+ spec.description = 'Combine GraphQL services into one unified graph'
12
12
  spec.homepage = 'https://github.com/gmac/graphql-stitching-ruby'
13
13
  spec.license = 'MIT'
14
14
 
15
- spec.required_ruby_version = '>= 3.1.1'
15
+ spec.required_ruby_version = '>= 2.7.0'
16
16
 
17
17
  spec.metadata = {
18
18
  'homepage_uri' => 'https://github.com/gmac/graphql-stitching-ruby',
@@ -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