graphql-stitching 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ ## Serving a supergraph
2
+
3
+ Serving a stitched schema should be optimized by environment. In `production` we favor speed and stability over flexibility, while in `development` we favor the reverse. Among the simplest ways to deploy a stitched schema is to compose it locally, write the composed schema as a `.graphql` file in your repo, and then load the pre-composed schema into a stitching client at runtime. This assures that composition always happens before deployment where failures can be detected.
4
+
5
+ ### Exporting a production schema
6
+
7
+ 1. Make a helper class for building your supergraph and exporting it as an SDL string:
8
+
9
+ ```ruby
10
+ class SupergraphHelper
11
+ def self.export
12
+ client = GraphQL::Stitching::Client.new({
13
+ remote: {
14
+ schema: GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
15
+ },
16
+ local: {
17
+ schema: MyLocalSchema
18
+ }
19
+ })
20
+
21
+ client.supergraph.to_definition
22
+ end
23
+ end
24
+ ```
25
+
26
+ 2. Setup a `rake` task for writing the export to a repo file:
27
+
28
+ ```ruby
29
+ task :compose_supergraph do
30
+ File.write("db/schema/supergraph.graphql", SupergraphHelper.export)
31
+ puts "Schema composition was successful."
32
+ end
33
+
34
+ # bundle exec rake compose-supergraph
35
+ ```
36
+
37
+ 3. Also as part of the export Rake task, it's advisable to run a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) across the `main` version and the current compilation to catch breaking change regressions that may arise [during composition](./composing_a_supergraph.md#schema-merge-patterns):
38
+
39
+ ```ruby
40
+ task :compose_supergraph do
41
+ # ...
42
+
43
+ supergraph_file = "db/schema/supergraph.graphql"
44
+ head_commit = %x(git merge-base HEAD origin/main).strip!
45
+ head_source = %x(git show #{head_commit}:#{supergraph_file})
46
+
47
+ old_schema = GraphQL::Schema.from_definition(head_source)
48
+ new_schema = GraphQL::Schema.from_definition(File.read(supergraph_file))
49
+ diff = GraphQL::SchemaComparator.compare(old_schema, new_schema)
50
+ raise "Breaking changes found:\n-#{diff.breaking_changes.join("\n-")}" if diff.breaking?
51
+
52
+ # ...
53
+ end
54
+ ```
55
+
56
+ 4. As a CI safeguard, be sure to write a test that compares the supergraph export against the current repo file. This assures the latest schema is always expored before deploying:
57
+
58
+ ```ruby
59
+ test "supergraph export is up to date." do
60
+ assert_equal SupergraphHelper.export, File.read("db/schema/supergraph.graphql")
61
+ end
62
+ ```
63
+
64
+ ### Supergraph controller
65
+
66
+ Then at runtime, execute requests using a client built for the environment. The `production` client should load the pre-composed export schema, while the `development` client can live reload using runtime composition. Be sure to memoize any static schemas that the development client uses to minimize reloading overhead:
67
+
68
+ ```ruby
69
+ class SupergraphController < ApplicationController
70
+ protect_from_forgery with: :null_session, prepend: true
71
+
72
+ def execute
73
+ # see visibility docs...
74
+ visibility_profile = select_visibility_profile_for_audience(current_user)
75
+
76
+ client.execute(
77
+ query: params[:query],
78
+ variables: params[:variables],
79
+ operation_name: params[:operation_name],
80
+ context: { visibility_profile: visibility_profile },
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ # select which client to use based on the environment...
87
+ def client
88
+ Rails.env.production? ? production_client : development_client
89
+ end
90
+
91
+ # production uses a pre-composed supergraph read from the repo...
92
+ def production_client
93
+ @production_client ||= begin
94
+ supergraph_sdl = File.read("db/schema/supergraph.graphql")
95
+
96
+ GraphQL::Stitching::Client.from_definition(supergraph_sdl, executables: {
97
+ remote: GraphQL::Stitching::HttpExecutable.new("https://api.remote.com/graphql"),
98
+ local: MyLocalSchema,
99
+ }).tap do |client|
100
+ # see performance and error handling docs...
101
+ client.on_cache_read { ... }
102
+ client.on_cache_write { ... }
103
+ client.on_error { ... }
104
+ end
105
+ end
106
+ end
107
+
108
+ # development uses a supergraph composed on the fly...
109
+ def development_client
110
+ GraphQL::Stitching::Client.new(locations: {
111
+ remote: {
112
+ schema: remote_schema,
113
+ executable: GraphQL::Stitching::HttpExecutable.new("https://localhost:3001/graphql"),
114
+ },
115
+ local: {
116
+ schema: MyLocalSchema,
117
+ },
118
+ })
119
+ end
120
+
121
+ # other flat schemas used in development should be
122
+ # cached in memory to avoid as much runtime overhead as possible
123
+ def remote_schema
124
+ @remote_schema ||= GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Client execution
130
+
131
+ The `Client.execute` method provides a mostly drop-in replacement for [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries):
132
+
133
+ ```ruby
134
+ client.execute(
135
+ query: params[:query],
136
+ variables: params[:variables],
137
+ operation_name: params[:operation_name],
138
+ context: { visibility_profile: visibility_profile },
139
+ )
140
+ ```
141
+
142
+ It provides a subset of the standard `execute` arguments:
143
+
144
+ * `query`: a query (or mutation) as a string or parsed AST.
145
+ * `variables`: a hash of variables for the request.
146
+ * `operation_name`: the name of the operation to execute (when multiple are provided).
147
+ * `validate`: true if static validation should run on the supergraph schema before execution.
148
+ * `context`: an object passed through to executable calls and client hooks.
149
+
150
+ ### Production reloading
151
+
152
+ It is possible to "hot" reload a production supergraph (ie: update the graph without a server deployment) using a background process to poll a remote supergraph file for changes and then build it into a new client for the controller at runtime. This works fine as long as locations and their executables don't change. If locations will change, the runtime _must_ be prepared to dynamically generate appropraite location executables.
@@ -1,4 +1,4 @@
1
- ## Stitching subscriptions
1
+ ## Subscriptions
2
2
 
3
3
  Stitching is an interesting prospect for subscriptions because socket-based interactions can be isolated to their own schema/server with very little implementation beyond resolving entity keys. Then, entity data can be stitched onto subscription payloads from other locations.
4
4
 
data/docs/visibility.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Visibility
2
2
 
3
- Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences.
3
+ Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences, and provides a flexible analog to Apollo Federation's `@inaccessible` rule.
4
4
 
5
- Under the hood, this system wraps `GraphQL::Schema::Visibility` (with nil profile support) and requires at least GraphQL Ruby v2.5.3.
5
+ Under the hood, this system wraps [GraphQL visibility](https://graphql-ruby.org/authorization/visibility) (specifically, the newer `GraphQL::Schema::Visibility` with nil profile support) and requires at least GraphQL Ruby v2.5.3.
6
6
 
7
7
  ## Example
8
8
 
@@ -41,7 +41,7 @@ type Query {
41
41
  }
42
42
  ```
43
43
 
44
- When composing a stitching client, the names of all possible visibility profiles that the supergraph responds to should be specified in composer options:
44
+ When composing a stitching client, the names of all possible visibility profiles that the supergraph should respond to are specified in composer options:
45
45
 
46
46
  ```ruby
47
47
  client = GraphQL::Stitching::Client.new(
@@ -61,20 +61,20 @@ client = GraphQL::Stitching::Client.new(
61
61
  )
62
62
  ```
63
63
 
64
- The client can then execute requests with a `visibility_profile` parameter in context that specifies the name of any profile the supergraph was composed with, or nil:
64
+ The client can then execute requests with a `visibility_profile` parameter in context that specifies one of these names:
65
65
 
66
66
  ```ruby
67
67
  query = %|{
68
68
  featuredProduct {
69
69
  title # always visible
70
70
  price # always visible
71
- msrp # only visible to internal and nil profiles
72
- id # only visible to nil profile
71
+ msrp # only visible to "private" or without profile
72
+ id # only visible without profile
73
73
  }
74
74
  }|
75
75
 
76
76
  result = client.execute(query, context: {
77
- visibility_profile: "public", # << or private, or nil
77
+ visibility_profile: "public", # << or "private"
78
78
  })
79
79
  ```
80
80
 
@@ -82,9 +82,9 @@ The `visibility_profile` parameter will select which visibility distribution to
82
82
 
83
83
  - Using `visibility_profile: "public"` will say the `msrp` field does not exist (because it is restricted to "private").
84
84
  - Using `visibility_profile: "private"` will accesses the `msrp` field as usual.
85
- - Using `visibility_profile: nil` will access the entire graph without any visibility constraints.
85
+ - Providing no profile parameter (or `visibility_profile: nil`) will access the entire graph without any visibility constraints.
86
86
 
87
- The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for the stitching implementation.
87
+ The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for use by the stitching implementation.
88
88
 
89
89
  ## Adding visibility directives
90
90
 
@@ -105,7 +105,7 @@ end
105
105
 
106
106
  ## Merging visibilities
107
107
 
108
- Visibility directives merge across schemas into the narrowest constraint possible. Profile sets for an element will intersect into its supergraph constraint:
108
+ Visibility directives merge across schemas into the narrowest constraint possible. Profiles for an element will intersect into its merged supergraph constraint:
109
109
 
110
110
  ```graphql
111
111
  # location 1
@@ -165,4 +165,14 @@ type Query {
165
165
  }
166
166
  ```
167
167
 
168
- In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it.
168
+ In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it. You can review materialized visibility profiles by printing their respective schemas:
169
+
170
+ ```ruby
171
+ public_schema = client.supergraph.to_definition(visibility_profile: "public")
172
+ File.write("schemas/supergraph_public.graphql", public_schema)
173
+
174
+ private_schema = client.supergraph.to_definition(visibility_profile: "private")
175
+ File.write("schemas/supergraph_private.graphql", private_schema)
176
+ ```
177
+
178
+ It's helpful to commit these outputs to your repo where you can monitor their diffs during the PR process.
@@ -7,6 +7,12 @@ module GraphQL
7
7
  # Client is an out-of-the-box helper that assembles all
8
8
  # stitching components into a workflow that executes requests.
9
9
  class Client
10
+ class << self
11
+ def from_definition(schema, executables:)
12
+ new(supergraph: Supergraph.from_definition(schema, executables: executables))
13
+ end
14
+ end
15
+
10
16
  # @return [Supergraph] composed supergraph that services incoming requests.
11
17
  attr_reader :supergraph
12
18
 
@@ -26,9 +26,6 @@ module GraphQL
26
26
  # @api private
27
27
  VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
28
28
 
29
- # @api private
30
- BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
31
-
32
29
  # @api private
33
30
  COMPOSITION_VALIDATORS = [
34
31
  ValidateInterfaces,
@@ -59,7 +56,8 @@ module GraphQL
59
56
  deprecation_merger: nil,
60
57
  default_value_merger: nil,
61
58
  directive_kwarg_merger: nil,
62
- root_field_location_selector: nil
59
+ root_field_location_selector: nil,
60
+ root_entrypoints: nil
63
61
  )
64
62
  @query_name = query_name
65
63
  @mutation_name = mutation_name
@@ -68,7 +66,8 @@ module GraphQL
68
66
  @deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
69
67
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
70
68
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
71
- @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
69
+ @root_field_location_selector = root_field_location_selector
70
+ @root_entrypoints = root_entrypoints || {}
72
71
 
73
72
  @field_map = {}
74
73
  @resolver_map = {}
@@ -631,11 +630,20 @@ module GraphQL
631
630
  root_field_locations = @field_map[root_type.graphql_name][root_field_name]
632
631
  next unless root_field_locations.length > 1
633
632
 
634
- target_location = @root_field_location_selector.call(root_field_locations, {
635
- type_name: root_type.graphql_name,
636
- field_name: root_field_name,
637
- })
638
- next unless root_field_locations.include?(target_location)
633
+ root_field_path = "#{root_type.graphql_name}.#{root_field_name}"
634
+ target_location = if @root_field_location_selector && @root_entrypoints.empty?
635
+ Warning.warn("Composer option `root_field_location_selector` is deprecated and will be removed.")
636
+ @root_field_location_selector.call(root_field_locations, {
637
+ type_name: root_type.graphql_name,
638
+ field_name: root_field_name,
639
+ })
640
+ else
641
+ @root_entrypoints[root_field_path] || root_field_locations.last
642
+ end
643
+
644
+ unless root_field_locations.include?(target_location)
645
+ raise CompositionError, "Invalid `root_entrypoints` configuration: `#{root_field_path}` has no `#{target_location}` location."
646
+ end
639
647
 
640
648
  root_field_locations.reject! { _1 == target_location }
641
649
  root_field_locations.unshift(target_location)
@@ -57,6 +57,10 @@ module GraphQL
57
57
  @context[:request] = self
58
58
  end
59
59
 
60
+ def original_document
61
+ @query.document
62
+ end
63
+
60
64
  # @return [String] the original document string, or a print of the parsed AST document.
61
65
  def string
62
66
  with_prepared_document { @string || normalized_string }
@@ -58,8 +58,10 @@ module GraphQL
58
58
  end
59
59
  end
60
60
 
61
- def to_definition
62
- @schema.to_definition
61
+ def to_definition(visibility_profile: nil)
62
+ @schema.to_definition(context: {
63
+ visibility_profile: visibility_profile,
64
+ }.tap(&:compact!))
63
65
  end
64
66
 
65
67
  def resolvers_by_version
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.7.0"
5
+ VERSION = "1.7.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-stitching
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.7.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: 2025-05-01 00:00:00.000000000 Z
11
+ date: 2025-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -79,19 +79,19 @@ files:
79
79
  - LICENSE
80
80
  - README.md
81
81
  - Rakefile
82
- - docs/README.md
83
- - docs/client.md
84
- - docs/composer.md
85
- - docs/federation_entities.md
86
- - docs/http_executable.md
82
+ - docs/composing_a_supergraph.md
83
+ - docs/error_handling.md
84
+ - docs/executables.md
87
85
  - docs/images/library.png
88
86
  - docs/images/merging.png
89
87
  - docs/images/stitching.png
90
- - docs/mechanics.md
91
- - docs/request.md
88
+ - docs/introduction.md
89
+ - docs/merged_types.md
90
+ - docs/merged_types_apollo.md
91
+ - docs/performance.md
92
+ - docs/query_planning.md
93
+ - docs/serving_a_supergraph.md
92
94
  - docs/subscriptions.md
93
- - docs/supergraph.md
94
- - docs/type_resolver.md
95
95
  - docs/visibility.md
96
96
  - examples/file_uploads/Gemfile
97
97
  - examples/file_uploads/Procfile
data/docs/README.md DELETED
@@ -1,19 +0,0 @@
1
- ## GraphQL::Stitching
2
-
3
- This module provides a collection of components that may be composed into a stitched schema.
4
-
5
- ![Library flow](./images/library.png)
6
-
7
- Major components include:
8
-
9
- - [Client](./client.md) - an out-of-the-box setup for performing stitched requests.
10
- - [Composer](./composer.md) - merges and validates many schemas into one graph.
11
- - [Supergraph](./supergraph.md) - manages the combined schema and location routing maps. Can be exported, cached, and rehydrated.
12
- - [Request](./request.md) - prepares a requested GraphQL document and variables for stitching.
13
- - [HttpExecutable](./http_executable.md) - proxies requests to remotes with multipart file upload support.
14
-
15
- Additional topics:
16
-
17
- - [Stitching mechanics](./mechanics.md) - more about building for stitching and how it operates.
18
- - [Subscriptions](./subscriptions.md) - explore how to stitch realtime event subscriptions.
19
- - [Federation entities](./federation_entities.md) - more about Apollo Federation compatibility.
data/docs/client.md DELETED
@@ -1,107 +0,0 @@
1
- ## GraphQL::Stitching::Client
2
-
3
- The `Client` is an out-of-the-box convenience with all stitching components assembled into a default workflow. A client is designed to work for most common needs, though you're welcome to assemble the component parts into your own configuration (see the [client source](../lib/graphql/stitching/client.rb) for an example). A client is constructed with the same [location settings](./composer.md#performing-composition) used to perform supergraph composition:
4
-
5
- ```ruby
6
- movies_schema = "type Query { ..."
7
- showtimes_schema = "type Query { ..."
8
-
9
- client = GraphQL::Stitching::Client.new(locations: {
10
- products: {
11
- schema: GraphQL::Schema.from_definition(movies_schema),
12
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3000"),
13
- stitch: [{ field_name: "products", key: "id" }],
14
- },
15
- showtimes: {
16
- schema: GraphQL::Schema.from_definition(showtimes_schema),
17
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
18
- },
19
- my_local: {
20
- schema: MyLocal::GraphQL::Schema,
21
- },
22
- })
23
- ```
24
-
25
- Alternatively, you may pass a prebuilt `Supergraph` instance to the `Client` constructor. This is useful when [exporting and rehydrating](./supergraph.md#export-and-caching) supergraph instances, which bypasses the need for runtime composition:
26
-
27
- ```ruby
28
- supergraph_sdl = File.read("precomposed_schema.graphql")
29
- supergraph = GraphQL::Stitching::Supergraph.from_definition(
30
- supergraph_sdl,
31
- executables: { ... },
32
- )
33
-
34
- client = GraphQL::Stitching::Client.new(supergraph: supergraph)
35
- ```
36
-
37
- ### Execution
38
-
39
- A client provides an `execute` method with a subset of arguments provided by [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries). Executing requests on a stitching client becomes mostly a drop-in replacement to executing on a `GraphQL::Schema` instance:
40
-
41
- ```ruby
42
- result = client.execute(
43
- query: "query MyProduct($id: ID!) { product(id: $id) { name } }",
44
- variables: { "id" => "1" },
45
- operation_name: "MyProduct",
46
- )
47
- ```
48
-
49
- Arguments for the `execute` method include:
50
-
51
- * `query`: a query (or mutation) as a string or parsed AST.
52
- * `variables`: a hash of variables for the request.
53
- * `operation_name`: the name of the operation to execute (when multiple are provided).
54
- * `validate`: true if static validation should run on the supergraph schema before execution.
55
- * `context`: an object passed through to executable calls and client hooks.
56
-
57
- ### Cache hooks
58
-
59
- The client provides cache hooks to enable caching query plans across requests. Without caching, every request made to the client will be planned individually. With caching, a query may be planned once, cached, and then executed from cache for subsequent requests. Cache keys are a normalized digest of each query string.
60
-
61
- ```ruby
62
- client.on_cache_read do |request|
63
- $cache.get(request.digest) # << 3P code
64
- end
65
-
66
- client.on_cache_write do |request, payload|
67
- $cache.set(request.digest, payload) # << 3P code
68
- end
69
- ```
70
-
71
- All request digests use SHA2 by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base scoping by reconfiguring the stitching library:
72
-
73
- ```ruby
74
- GraphQL::Stitching.digest { |str| Digest::MD5.hexdigest("v2/#{str}") }
75
- ```
76
-
77
- Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
78
-
79
- ```graphql
80
- query {
81
- product(id: "1") { name }
82
- }
83
- ```
84
-
85
- Instead, leverage query variables so that the document body remains consistent across requests:
86
-
87
- ```graphql
88
- query($id: ID!) {
89
- product(id: $id) { name }
90
- }
91
-
92
- # variables: { "id" => "1" }
93
- ```
94
-
95
- ### Error hooks
96
-
97
- The client also provides an error hook. Any program errors rescued during execution will be passed to the `on_error` handler, which can report on the error as needed and return a formatted error message for the client to add to the [GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) result.
98
-
99
- ```ruby
100
- client.on_error do |request, err|
101
- # log the error
102
- Bugsnag.notify(err)
103
-
104
- # return a formatted message for the public response
105
- "Whoops, please contact support abount request '#{request.context[:request_id]}'"
106
- end
107
- ```
data/docs/composer.md DELETED
@@ -1,125 +0,0 @@
1
- ## GraphQL::Stitching::Composer
2
-
3
- A `Composer` receives many individual `GraphQL::Schema` instances representing various graph locations and _composes_ them into one combined [`Supergraph`](./supergraph.md) that is validated for integrity.
4
-
5
- ### Configuring composition
6
-
7
- A `Composer` may be constructed with optional settings that tune how it builds a schema:
8
-
9
- ```ruby
10
- composer = GraphQL::Stitching::Composer.new(
11
- query_name: "Query",
12
- mutation_name: "Mutation",
13
- subscription_name: "Subscription",
14
- description_merger: ->(values_by_location, info) { values_by_location.values.join("\n") },
15
- deprecation_merger: ->(values_by_location, info) { values_by_location.values.first },
16
- default_value_merger: ->(values_by_location, info) { values_by_location.values.first },
17
- directive_kwarg_merger: ->(values_by_location, info) { values_by_location.values.last },
18
- root_field_location_selector: ->(locations, info) { locations.last },
19
- )
20
- ```
21
-
22
- Constructor arguments:
23
-
24
- - **`query_name:`** _optional_, the name of the root query type in the composed schema; `Query` by default. The root query types from all location schemas will be merged into this type, regardless of their local names.
25
-
26
- - **`mutation_name:`** _optional_, the name of the root mutation type in the composed schema; `Mutation` by default. The root mutation types from all location schemas will be merged into this type, regardless of their local names.
27
-
28
- - **`subscription_name:`** _optional_, the name of the root subscription type in the composed schema; `Subscription` by default. The root subscription types from all location schemas will be merged into this type, regardless of their local names.
29
-
30
- - **`description_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element description strings from across locations.
31
-
32
- - **`deprecation_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging element deprecation strings from across locations.
33
-
34
- - **`default_value_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging argument default values from across locations.
35
-
36
- - **`directive_kwarg_merger:`** _optional_, a [value merger function](#value-merger-functions) for merging directive keyword arguments from across locations.
37
-
38
- - **`root_field_location_selector:`** _optional_, selects a default routing location for root fields with multiple locations. Use this to prioritize sending root fields to their primary data sources (only applies while routing the root operation scope). This handler receives an array of possible locations and an info object with field information, and should return the prioritized location. The last location is used by default.
39
-
40
- #### Value merger functions
41
-
42
- Static data values such as element descriptions and directive arguments must also merge across locations. By default, the first non-null value encountered for a given element attribute is used. A value merger function may customize this process by selecting a different value or computing a new one:
43
-
44
- ```ruby
45
- composer = GraphQL::Stitching::Composer.new(
46
- description_merger: ->(values_by_location, info) { values_by_location.values.compact.join("\n") },
47
- )
48
- ```
49
-
50
- A merger function receives `values_by_location` and `info` arguments; these provide possible values keyed by location and info about where in the schema these values were encountered:
51
-
52
- ```ruby
53
- values_by_location = {
54
- "storefronts" => "A fabulous data type.",
55
- "products" => "An excellent data type.",
56
- }
57
-
58
- info = {
59
- type_name: "Product",
60
- # field_name: ...,
61
- # argument_name: ...,
62
- # directive_name: ...,
63
- }
64
- ```
65
-
66
- ### Performing composition
67
-
68
- Construct a `Composer` and call its `perform` method with location settings to compose a supergraph:
69
-
70
- ```ruby
71
- storefronts_sdl = "type Query { ..."
72
- products_sdl = "type Query { ..."
73
-
74
- supergraph = GraphQL::Stitching::Composer.new.perform({
75
- storefronts: {
76
- schema: GraphQL::Schema.from_definition(storefronts_sdl),
77
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
78
- stitch: [{ field_name: "storefront", key: "id" }],
79
- },
80
- products: {
81
- schema: GraphQL::Schema.from_definition(products_sdl),
82
- executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
83
- },
84
- my_local: {
85
- schema: MyLocalSchema,
86
- },
87
- })
88
-
89
- combined_schema = supergraph.schema
90
- ```
91
-
92
- Location settings have top-level keys that specify arbitrary location names, each of which provide:
93
-
94
- - **`schema:`** _required_, provides a `GraphQL::Schema` class for the location. This may be a class-based schema that inherits from `GraphQL::Schema`, or built from SDL (Schema Definition Language) string using `GraphQL::Schema.from_definition` and mapped to a remote location. The provided schema is only used for type reference and does not require any real data resolvers (unless it is also used as the location's executable, see below).
95
-
96
- - **`executable:`** _optional_, provides an executable resource to be called when delegating a request to this location. Executables are `GraphQL::Schema` classes or any object with a `.call(request, source, variables)` method that returns a GraphQL response. Omitting the executable option will use the location's provided `schema` as the executable resource.
97
-
98
- - **`stitch:`** _optional_, an array of configs used to dynamically apply `@stitch` directives to select root fields prior to composing. This is useful when you can't easily render stitching directives into a location's source schema.
99
-
100
- ### Merge patterns
101
-
102
- The strategy used to merge source schemas into the combined schema is based on each element type:
103
-
104
- - Arguments of fields, directives, and `InputObject` types intersect for each parent element across locations (an element's arguments must appear in all locations):
105
- - Arguments must share a value type, and the strictest nullability across locations is used.
106
- - Composition fails if argument intersection would eliminate a non-null argument.
107
-
108
- - `Object` and `Interface` types merge their fields and directives together:
109
- - Common fields across locations must share a value type, and the weakest nullability is used.
110
- - Objects with unique fields across locations must implement [`@stitch` accessors](../README.md#merged-types).
111
- - Shared object types without `@stitch` accessors must contain identical fields.
112
- - Merged interfaces must remain compatible with all underlying implementations.
113
-
114
- - `Enum` types merge their values based on how the enum is used:
115
- - Enums used anywhere as an argument will intersect their values (common values across all locations).
116
- - Enums used exclusively in read contexts will provide a union of values (all values across all locations).
117
-
118
- - `Union` types merge all possible types from across all locations.
119
-
120
- - `Scalar` types are added for all scalar names across all locations.
121
-
122
- - `Directive` definitions are added for all distinct names across locations:
123
- - Stitching directives (both definitions and assignments) are omitted.
124
-
125
- Note that the structure of a composed schema may change based on new schema additions and/or element usage (ie: changing input object arguments in one service may cause the intersection of arguments to change). Therefore, it's highly recommended that you use a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) to flag regressions across composed schema versions.