graphql-stitching 1.6.2 → 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.
- checksums.yaml +4 -4
- data/README.md +57 -429
- data/docs/composing_a_supergraph.md +215 -0
- data/docs/error_handling.md +69 -0
- data/docs/executables.md +112 -0
- data/docs/introduction.md +17 -0
- data/docs/merged_types.md +457 -0
- data/docs/{federation_entities.md → merged_types_apollo.md} +1 -1
- data/docs/performance.md +71 -0
- data/docs/query_planning.md +102 -0
- data/docs/serving_a_supergraph.md +152 -0
- data/docs/subscriptions.md +1 -1
- data/docs/visibility.md +178 -0
- data/lib/graphql/stitching/client.rb +11 -3
- data/lib/graphql/stitching/composer.rb +55 -26
- data/lib/graphql/stitching/{composer/supergraph_directives.rb → directives.rb} +22 -4
- data/lib/graphql/stitching/request.rb +4 -0
- data/lib/graphql/stitching/supergraph/from_definition.rb +20 -7
- data/lib/graphql/stitching/supergraph/types.rb +74 -0
- data/lib/graphql/stitching/supergraph.rb +12 -4
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +21 -2
- metadata +14 -12
- data/docs/README.md +0 -19
- data/docs/client.md +0 -107
- data/docs/composer.md +0 -125
- data/docs/http_executable.md +0 -51
- data/docs/mechanics.md +0 -306
- data/docs/request.md +0 -34
- data/docs/supergraph.md +0 -31
- data/docs/type_resolver.md +0 -101
@@ -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.
|
data/docs/subscriptions.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
##
|
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
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# Visibility
|
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, and provides a flexible analog to Apollo Federation's `@inaccessible` rule.
|
4
|
+
|
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
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
Schemas may include a `@visibility` directive that defines element _profiles_. A profile is just a label describing an API distribution (public, private, etc). When a request is assigned a visibility profile, it can only access elements belonging to that profile. Elements without an explicit `@visibility` constraint belong to all profiles. For example:
|
10
|
+
|
11
|
+
_schemas/product_info.graphql_
|
12
|
+
```graphql
|
13
|
+
directive @stitch(key: String!) on FIELD_DEFINITION
|
14
|
+
directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
|
15
|
+
|
16
|
+
type Product {
|
17
|
+
id: ID!
|
18
|
+
title: String!
|
19
|
+
description: String!
|
20
|
+
}
|
21
|
+
|
22
|
+
type Query {
|
23
|
+
featuredProduct: Product
|
24
|
+
product(id: ID!): Product @stitch(key: "id") @visibility(profiles: ["private"])
|
25
|
+
}
|
26
|
+
```
|
27
|
+
|
28
|
+
_schemas/product_prices.graphql_
|
29
|
+
```graphql
|
30
|
+
directive @stitch(key: String!) on FIELD_DEFINITION
|
31
|
+
directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
|
32
|
+
|
33
|
+
type Product {
|
34
|
+
id: ID! @visibility(profiles: [])
|
35
|
+
msrp: Float! @visibility(profiles: ["private"])
|
36
|
+
price: Float!
|
37
|
+
}
|
38
|
+
|
39
|
+
type Query {
|
40
|
+
products(ids: [ID!]!): [Product]! @stitch(key: "id") @visibility(profiles: ["private"])
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
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
|
+
|
46
|
+
```ruby
|
47
|
+
client = GraphQL::Stitching::Client.new(
|
48
|
+
composer_options: {
|
49
|
+
visibility_profiles: ["public", "private"],
|
50
|
+
},
|
51
|
+
locations: {
|
52
|
+
info: {
|
53
|
+
schema: GraphQL::Schema.from_definition(File.read("schemas/product_info.graphql")),
|
54
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
55
|
+
},
|
56
|
+
prices: {
|
57
|
+
schema: GraphQL::Schema.from_definition(File.read("schemas/product_prices.graphql")),
|
58
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
59
|
+
},
|
60
|
+
}
|
61
|
+
)
|
62
|
+
```
|
63
|
+
|
64
|
+
The client can then execute requests with a `visibility_profile` parameter in context that specifies one of these names:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
query = %|{
|
68
|
+
featuredProduct {
|
69
|
+
title # always visible
|
70
|
+
price # always visible
|
71
|
+
msrp # only visible to "private" or without profile
|
72
|
+
id # only visible without profile
|
73
|
+
}
|
74
|
+
}|
|
75
|
+
|
76
|
+
result = client.execute(query, context: {
|
77
|
+
visibility_profile: "public", # << or "private"
|
78
|
+
})
|
79
|
+
```
|
80
|
+
|
81
|
+
The `visibility_profile` parameter will select which visibility distribution to use while introspecting and validating the request. For example:
|
82
|
+
|
83
|
+
- Using `visibility_profile: "public"` will say the `msrp` field does not exist (because it is restricted to "private").
|
84
|
+
- Using `visibility_profile: "private"` will accesses the `msrp` field as usual.
|
85
|
+
- Providing no profile parameter (or `visibility_profile: nil`) will access the entire graph without any visibility constraints.
|
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 use by the stitching implementation.
|
88
|
+
|
89
|
+
## Adding visibility directives
|
90
|
+
|
91
|
+
Add the `@visibility` directive into schemas using the library definition:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class QueryType < GraphQL::Schema::Object
|
95
|
+
field :my_field, String, null: true do |f|
|
96
|
+
f.directive(GraphQL::Stitching::Directives::Visibility, profiles: ["private"])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class MySchema < GraphQL::Schema
|
101
|
+
directive(GraphQL::Stitching::Directives::Visibility)
|
102
|
+
query(QueryType)
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
## Merging visibilities
|
107
|
+
|
108
|
+
Visibility directives merge across schemas into the narrowest constraint possible. Profiles for an element will intersect into its merged supergraph constraint:
|
109
|
+
|
110
|
+
```graphql
|
111
|
+
# location 1
|
112
|
+
myField: String @visibility(profiles: ["a", "c"])
|
113
|
+
|
114
|
+
# location 2
|
115
|
+
myField: String @visibility(profiles: ["b", "c"])
|
116
|
+
|
117
|
+
# merged supergraph
|
118
|
+
myField: String @visibility(profiles: ["c"])
|
119
|
+
```
|
120
|
+
|
121
|
+
This may cause an element's profiles to intersect into an empty set, which means the element belongs to no profiles and will be hidden from all named distributions:
|
122
|
+
|
123
|
+
```graphql
|
124
|
+
# location 1
|
125
|
+
myField: String @visibility(profiles: ["a"])
|
126
|
+
|
127
|
+
# location 2
|
128
|
+
myField: String @visibility(profiles: ["b"])
|
129
|
+
|
130
|
+
# merged supergraph
|
131
|
+
myField: String @visibility(profiles: [])
|
132
|
+
```
|
133
|
+
|
134
|
+
Locations may omit visibility information to give other locations full control. Remember that elements without a `@visibility` constraint belong to all profiles, which also applies while merging:
|
135
|
+
|
136
|
+
```graphql
|
137
|
+
# location 1
|
138
|
+
myField: String
|
139
|
+
|
140
|
+
# location 2
|
141
|
+
myField: String @visibility(profiles: ["b"])
|
142
|
+
|
143
|
+
# merged supergraph
|
144
|
+
myField: String @visibility(profiles: ["b"])
|
145
|
+
```
|
146
|
+
|
147
|
+
## Type controls
|
148
|
+
|
149
|
+
Visibility controls can be applied to almost all GraphQL schema elements, including:
|
150
|
+
|
151
|
+
- Types (Object, Interface, Union, Enum, Scalar, InputObject)
|
152
|
+
- Fields (of Object and Interface)
|
153
|
+
- Arguments (of Field and InputObject)
|
154
|
+
- Enum values
|
155
|
+
|
156
|
+
While the visibility of type members (fields, arguments, and enum values) are pretty intuitive, the visibility of parent types is far more nuanced as constraints start to cascade:
|
157
|
+
|
158
|
+
```graphql
|
159
|
+
type Widget @visibility(profiles: ["private"]) {
|
160
|
+
title: String
|
161
|
+
}
|
162
|
+
|
163
|
+
type Query {
|
164
|
+
widget: Widget # << GETS HIDDEN
|
165
|
+
}
|
166
|
+
```
|
167
|
+
|
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,22 +7,30 @@ 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
|
|
13
19
|
# Builds a new client instance. Either `supergraph` or `locations` configuration is required.
|
14
20
|
# @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
|
15
21
|
# @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
|
16
|
-
# @param
|
17
|
-
def initialize(locations: nil, supergraph: nil,
|
22
|
+
# @param composer_options [Hash] optional, composer options for configuring composition.
|
23
|
+
def initialize(locations: nil, supergraph: nil, composer_options: {})
|
18
24
|
@supergraph = if locations && supergraph
|
19
25
|
raise ArgumentError, "Cannot provide both locations and a supergraph."
|
20
26
|
elsif supergraph && !supergraph.is_a?(Supergraph)
|
21
27
|
raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
28
|
+
elsif supergraph && composer_options.any?
|
29
|
+
raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
|
22
30
|
elsif supergraph
|
23
31
|
supergraph
|
24
32
|
else
|
25
|
-
composer
|
33
|
+
composer = Composer.new(**composer_options)
|
26
34
|
composer.perform(locations)
|
27
35
|
end
|
28
36
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "composer/base_validator"
|
4
|
-
require_relative "composer/supergraph_directives"
|
5
4
|
require_relative "composer/validate_interfaces"
|
6
5
|
require_relative "composer/validate_type_resolvers"
|
7
6
|
require_relative "composer/type_resolver_config"
|
@@ -23,9 +22,9 @@ module GraphQL
|
|
23
22
|
|
24
23
|
# @api private
|
25
24
|
BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
|
26
|
-
|
25
|
+
|
27
26
|
# @api private
|
28
|
-
|
27
|
+
VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
|
29
28
|
|
30
29
|
# @api private
|
31
30
|
COMPOSITION_VALIDATORS = [
|
@@ -52,11 +51,13 @@ module GraphQL
|
|
52
51
|
query_name: "Query",
|
53
52
|
mutation_name: "Mutation",
|
54
53
|
subscription_name: "Subscription",
|
54
|
+
visibility_profiles: [],
|
55
55
|
description_merger: nil,
|
56
56
|
deprecation_merger: nil,
|
57
57
|
default_value_merger: nil,
|
58
58
|
directive_kwarg_merger: nil,
|
59
|
-
root_field_location_selector: nil
|
59
|
+
root_field_location_selector: nil,
|
60
|
+
root_entrypoints: nil
|
60
61
|
)
|
61
62
|
@query_name = query_name
|
62
63
|
@mutation_name = mutation_name
|
@@ -65,12 +66,14 @@ module GraphQL
|
|
65
66
|
@deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
|
66
67
|
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
|
67
68
|
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
|
68
|
-
@root_field_location_selector = root_field_location_selector
|
69
|
+
@root_field_location_selector = root_field_location_selector
|
70
|
+
@root_entrypoints = root_entrypoints || {}
|
69
71
|
|
70
72
|
@field_map = {}
|
71
73
|
@resolver_map = {}
|
72
74
|
@resolver_configs = {}
|
73
75
|
@mapped_type_names = {}
|
76
|
+
@visibility_profiles = Set.new(visibility_profiles)
|
74
77
|
@subgraph_directives_by_name_and_location = nil
|
75
78
|
@subgraph_types_by_name_and_location = nil
|
76
79
|
@schema_directives = nil
|
@@ -85,9 +88,9 @@ module GraphQL
|
|
85
88
|
|
86
89
|
directives_to_omit = [
|
87
90
|
GraphQL::Stitching.stitch_directive,
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
+
Directives::SupergraphKey.graphql_name,
|
92
|
+
Directives::SupergraphResolver.graphql_name,
|
93
|
+
Directives::SupergraphSource.graphql_name,
|
91
94
|
]
|
92
95
|
|
93
96
|
# "directive_name" => "location" => subgraph_directive
|
@@ -181,6 +184,10 @@ module GraphQL
|
|
181
184
|
expand_abstract_resolvers(schema, schemas)
|
182
185
|
apply_supergraph_directives(schema, @resolver_map, @field_map)
|
183
186
|
|
187
|
+
if (visibility_def = schema.directives[GraphQL::Stitching.visibility_directive])
|
188
|
+
visibility_def.get_argument("profiles").default_value(@visibility_profiles.to_a.sort)
|
189
|
+
end
|
190
|
+
|
184
191
|
supergraph = Supergraph.from_definition(schema, executables: executables)
|
185
192
|
|
186
193
|
COMPOSITION_VALIDATORS.each do |validator_class|
|
@@ -237,7 +244,7 @@ module GraphQL
|
|
237
244
|
|
238
245
|
builder = self
|
239
246
|
|
240
|
-
Class.new(GraphQL::
|
247
|
+
Class.new(GraphQL::Stitching::Supergraph::ScalarType) do
|
241
248
|
graphql_name(type_name)
|
242
249
|
description(builder.merge_descriptions(type_name, types_by_location))
|
243
250
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -264,7 +271,7 @@ module GraphQL
|
|
264
271
|
end
|
265
272
|
end
|
266
273
|
|
267
|
-
Class.new(GraphQL::
|
274
|
+
Class.new(GraphQL::Stitching::Supergraph::EnumType) do
|
268
275
|
graphql_name(type_name)
|
269
276
|
description(builder.merge_descriptions(type_name, types_by_location))
|
270
277
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -286,7 +293,7 @@ module GraphQL
|
|
286
293
|
def build_object_type(type_name, types_by_location)
|
287
294
|
builder = self
|
288
295
|
|
289
|
-
Class.new(GraphQL::
|
296
|
+
Class.new(GraphQL::Stitching::Supergraph::ObjectType) do
|
290
297
|
graphql_name(type_name)
|
291
298
|
description(builder.merge_descriptions(type_name, types_by_location))
|
292
299
|
|
@@ -306,7 +313,7 @@ module GraphQL
|
|
306
313
|
builder = self
|
307
314
|
|
308
315
|
Module.new do
|
309
|
-
include GraphQL::
|
316
|
+
include GraphQL::Stitching::Supergraph::InterfaceType
|
310
317
|
graphql_name(type_name)
|
311
318
|
description(builder.merge_descriptions(type_name, types_by_location))
|
312
319
|
|
@@ -325,7 +332,7 @@ module GraphQL
|
|
325
332
|
def build_union_type(type_name, types_by_location)
|
326
333
|
builder = self
|
327
334
|
|
328
|
-
Class.new(GraphQL::
|
335
|
+
Class.new(GraphQL::Stitching::Supergraph::UnionType) do
|
329
336
|
graphql_name(type_name)
|
330
337
|
description(builder.merge_descriptions(type_name, types_by_location))
|
331
338
|
|
@@ -340,7 +347,7 @@ module GraphQL
|
|
340
347
|
def build_input_object_type(type_name, types_by_location)
|
341
348
|
builder = self
|
342
349
|
|
343
|
-
Class.new(GraphQL::
|
350
|
+
Class.new(GraphQL::Stitching::Supergraph::InputObjectType) do
|
344
351
|
graphql_name(type_name)
|
345
352
|
description(builder.merge_descriptions(type_name, types_by_location))
|
346
353
|
builder.build_merged_arguments(type_name, types_by_location, self)
|
@@ -451,6 +458,7 @@ module GraphQL
|
|
451
458
|
end
|
452
459
|
|
453
460
|
directives_by_name_location.each do |directive_name, directives_by_location|
|
461
|
+
kwarg_merger = @directive_kwarg_merger
|
454
462
|
directive_class = @schema_directives[directive_name]
|
455
463
|
next unless directive_class
|
456
464
|
|
@@ -467,8 +475,20 @@ module GraphQL
|
|
467
475
|
end
|
468
476
|
end
|
469
477
|
|
478
|
+
if directive_class.graphql_name == GraphQL::Stitching.visibility_directive
|
479
|
+
unless GraphQL::Stitching.supports_visibility?
|
480
|
+
raise CompositionError, "Using `@#{GraphQL::Stitching.visibility_directive}` directive " \
|
481
|
+
"for schema visibility controls requires GraphQL Ruby v#{GraphQL::Stitching::MIN_VISIBILITY_VERSION} or later."
|
482
|
+
end
|
483
|
+
|
484
|
+
if (profiles = kwarg_values_by_name_location["profiles"])
|
485
|
+
@visibility_profiles.merge(profiles.each_value.reduce(&:|))
|
486
|
+
kwarg_merger = VISIBILITY_PROFILES_MERGER
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
470
490
|
kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
|
471
|
-
memo[kwarg_name.to_sym] =
|
491
|
+
memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
|
472
492
|
type_name: type_name,
|
473
493
|
field_name: field_name,
|
474
494
|
argument_name: argument_name,
|
@@ -610,11 +630,20 @@ module GraphQL
|
|
610
630
|
root_field_locations = @field_map[root_type.graphql_name][root_field_name]
|
611
631
|
next unless root_field_locations.length > 1
|
612
632
|
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
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
|
618
647
|
|
619
648
|
root_field_locations.reject! { _1 == target_location }
|
620
649
|
root_field_locations.unshift(target_location)
|
@@ -693,8 +722,8 @@ module GraphQL
|
|
693
722
|
|
694
723
|
keys_for_type.each do |key, locations|
|
695
724
|
locations.each do |location|
|
696
|
-
schema_directives[
|
697
|
-
type.directive(
|
725
|
+
schema_directives[Directives::SupergraphKey.graphql_name] ||= Directives::SupergraphKey
|
726
|
+
type.directive(Directives::SupergraphKey, key: key, location: location)
|
698
727
|
end
|
699
728
|
end
|
700
729
|
|
@@ -710,8 +739,8 @@ module GraphQL
|
|
710
739
|
type_name: (resolver.type_name if resolver.type_name != type_name),
|
711
740
|
}
|
712
741
|
|
713
|
-
schema_directives[
|
714
|
-
type.directive(
|
742
|
+
schema_directives[Directives::SupergraphResolver.graphql_name] ||= Directives::SupergraphResolver
|
743
|
+
type.directive(Directives::SupergraphResolver, **params.tap(&:compact!))
|
715
744
|
end
|
716
745
|
end
|
717
746
|
|
@@ -737,8 +766,8 @@ module GraphQL
|
|
737
766
|
|
738
767
|
# Apply source directives to annotate the possible locations of each field
|
739
768
|
locations_for_field.each do |location|
|
740
|
-
schema_directives[
|
741
|
-
field.directive(
|
769
|
+
schema_directives[Directives::SupergraphSource.graphql_name] ||= Directives::SupergraphSource
|
770
|
+
field.directive(Directives::SupergraphSource, location: location)
|
742
771
|
end
|
743
772
|
end
|
744
773
|
end
|
@@ -1,8 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module GraphQL::Stitching
|
4
|
-
|
5
|
-
class
|
4
|
+
module Directives
|
5
|
+
class Stitch < GraphQL::Schema::Directive
|
6
|
+
graphql_name "stitch"
|
7
|
+
locations FIELD_DEFINITION
|
8
|
+
argument :key, String, required: true
|
9
|
+
argument :arguments, String, required: false
|
10
|
+
argument :type_name, String, required: false
|
11
|
+
repeatable true
|
12
|
+
end
|
13
|
+
|
14
|
+
class Visibility < GraphQL::Schema::Directive
|
15
|
+
graphql_name "visibility"
|
16
|
+
locations(
|
17
|
+
OBJECT, INTERFACE, UNION, INPUT_OBJECT, ENUM, SCALAR,
|
18
|
+
FIELD_DEFINITION, ARGUMENT_DEFINITION, INPUT_FIELD_DEFINITION, ENUM_VALUE
|
19
|
+
)
|
20
|
+
argument :profiles, [String, null: false], required: true
|
21
|
+
end
|
22
|
+
|
23
|
+
class SupergraphKey < GraphQL::Schema::Directive
|
6
24
|
graphql_name "key"
|
7
25
|
locations OBJECT, INTERFACE, UNION
|
8
26
|
argument :key, String, required: true
|
@@ -10,7 +28,7 @@ module GraphQL::Stitching
|
|
10
28
|
repeatable true
|
11
29
|
end
|
12
30
|
|
13
|
-
class
|
31
|
+
class SupergraphResolver < GraphQL::Schema::Directive
|
14
32
|
graphql_name "resolver"
|
15
33
|
locations OBJECT, INTERFACE, UNION
|
16
34
|
argument :location, String, required: true
|
@@ -23,7 +41,7 @@ module GraphQL::Stitching
|
|
23
41
|
repeatable true
|
24
42
|
end
|
25
43
|
|
26
|
-
class
|
44
|
+
class SupergraphSource < GraphQL::Schema::Directive
|
27
45
|
graphql_name "source"
|
28
46
|
locations FIELD_DEFINITION
|
29
47
|
argument :location, String, required: true
|
@@ -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 }
|