graphql-stitching 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)
@@ -16,7 +16,7 @@ module GraphQL::Stitching
16
16
  @executor.request.operation_name,
17
17
  @executor.request.operation_directives,
18
18
  )
19
- query_variables = @executor.request.variables.slice(*op.variables.keys)
19
+ query_variables = @executor.request.variables.slice(*op.variables.each_key)
20
20
  result = @executor.request.supergraph.execute_at_location(op.location, query_document, query_variables, @executor.request)
21
21
  @executor.query_count += 1
22
22
 
@@ -32,7 +32,7 @@ module GraphQL::Stitching
32
32
  field_name = node.alias || node.name
33
33
 
34
34
  if @request.query.get_field(parent_type, node.name).introspection?
35
- if node.name == TYPENAME && parent_type == @root_type
35
+ if node.name == TYPENAME && parent_type == @root_type && node != TypeResolver::TYPENAME_EXPORT_NODE
36
36
  raw_object[field_name] = @root_type.graphql_name
37
37
  end
38
38
  next
@@ -165,7 +165,7 @@ module GraphQL::Stitching
165
165
  if pathed_errors_by_op_index_and_object_id.any?
166
166
  pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
167
167
  repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
168
- errors_result.concat(pathed_errors_by_object_id.values)
168
+ errors_result.push(*pathed_errors_by_object_id.each_value)
169
169
  end
170
170
  end
171
171
 
@@ -213,7 +213,7 @@ module GraphQL
213
213
  input_selections.each do |node|
214
214
  case node
215
215
  when GraphQL::Language::Nodes::Field
216
- if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX)
216
+ if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node != TypeResolver::TYPENAME_EXPORT_NODE
217
217
  raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
218
218
  elsif node.name == TYPENAME
219
219
  locale_selections << node
@@ -284,7 +284,7 @@ module GraphQL
284
284
  remote_selections_by_location = delegate_remote_selections(parent_type, remote_selections)
285
285
 
286
286
  # D) Create paths routing to new entrypoint locations via resolver queries.
287
- routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.keys)
287
+ routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, remote_selections_by_location.each_key)
288
288
 
289
289
  # E) Translate resolver pathways into new entrypoints.
290
290
  routes.each_value do |route|
@@ -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 }
@@ -143,6 +147,11 @@ module GraphQL
143
147
  @query.static_errors
144
148
  end
145
149
 
150
+ # @return [Boolean] is the request valid?
151
+ def valid?
152
+ validate.empty?
153
+ end
154
+
146
155
  # Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
147
156
  # which is useful for redundant GraphQL documents (commonly sent by frontend clients).
148
157
  # ```ruby
@@ -21,11 +21,8 @@ module GraphQL::Stitching
21
21
  field_map = {}
22
22
  resolver_map = {}
23
23
  possible_locations = {}
24
- visibility_profiles = if (visibility_def = schema.directives[GraphQL::Stitching.visibility_directive])
25
- visibility_def.get_argument("profiles").default_value
26
- else
27
- []
28
- end
24
+ visibility_definition = schema.directives[GraphQL::Stitching.visibility_directive]
25
+ visibility_profiles = visibility_definition&.get_argument("profiles")&.default_value || EMPTY_ARRAY
29
26
 
30
27
  schema.types.each do |type_name, type|
31
28
  next if type.introspection?
@@ -75,7 +72,7 @@ module GraphQL::Stitching
75
72
  end
76
73
  end
77
74
 
78
- executables = possible_locations.keys.each_with_object({}) do |location, memo|
75
+ executables = possible_locations.each_key.each_with_object({}) do |location, memo|
79
76
  executable = executables[location] || executables[location.to_sym]
80
77
  if validate_executable!(location, executable)
81
78
  memo[location] = executable
@@ -38,7 +38,7 @@ module GraphQL
38
38
  @locations_by_type_and_field = @memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
39
39
  next unless type.kind.fields?
40
40
 
41
- memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
41
+ memo[type_name] = type.fields.each_key.each_with_object({}) do |field_name, m|
42
42
  m[field_name] = [SUPERGRAPH_LOCATION]
43
43
  end
44
44
  end.freeze
@@ -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
@@ -73,7 +75,7 @@ module GraphQL
73
75
  end
74
76
 
75
77
  def locations
76
- @executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
78
+ @executables.each_key.reject { _1 == SUPERGRAPH_LOCATION }
77
79
  end
78
80
 
79
81
  def memoized_schema_fields(type_name)
@@ -128,7 +130,7 @@ module GraphQL
128
130
  # "Type" => ["location1", "location2", ...]
129
131
  def locations_by_type
130
132
  @locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
131
- memo[type_name] = fields.values.flatten.uniq
133
+ memo[type_name] = fields.values.tap(&:flatten!).tap(&:uniq!)
132
134
  end
133
135
  end
134
136
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.7.0"
5
+ VERSION = "1.7.2"
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-01 00:00:00.000000000 Z
11
+ date: 2025-05-19 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
- ```