graphql-stitching 1.6.2 → 1.7.0
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 +6 -12
- data/docs/visibility.md +168 -0
- data/lib/graphql/stitching/client.rb +5 -3
- data/lib/graphql/stitching/composer.rb +38 -17
- data/lib/graphql/stitching/{composer/supergraph_directives.rb → directives.rb} +22 -4
- 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 +8 -2
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +21 -2
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63b137317e57972e09c9926447c53ab26ac99a146557258e94b62abff305d096
|
4
|
+
data.tar.gz: e3a2217f5c04cc1a8a8412f15bb0ee5e6c4340869f945758735468c0903840f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63ccfdcf28c02490946d2b7b9fef5dbe726cc47a214887010dc7dfa7f066d266bb7b2e5d899b13ad705060b8f7981904f47029a119c73c1aa582181d97ec6bec
|
7
|
+
data.tar.gz: d72baafd4419199658afbdba8a70a109a84da1e48e712344f568a5d4f75f5d0b2da3f3549a50370e528357fd003d9f8cbae825a490b2b0eed86c84528471855f
|
data/README.md
CHANGED
@@ -9,6 +9,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
9
9
|
- Merged object and abstract types joining though multiple keys.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
11
|
- Combining local and remote schemas.
|
12
|
+
- [Visibility controls](./docs/visibility.md) for hiding schema elements.
|
12
13
|
- [File uploads](./docs/http_executable.md) via multipart forms.
|
13
14
|
- Tested with all minor versions of `graphql-ruby`.
|
14
15
|
|
@@ -171,11 +172,11 @@ GRAPHQL
|
|
171
172
|
client = GraphQL::Stitching::Client.new(locations: {
|
172
173
|
products: {
|
173
174
|
schema: GraphQL::Schema.from_definition(products_schema),
|
174
|
-
executable:
|
175
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
|
175
176
|
},
|
176
177
|
catalog: {
|
177
178
|
schema: GraphQL::Schema.from_definition(catalog_schema),
|
178
|
-
executable:
|
179
|
+
executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
|
179
180
|
},
|
180
181
|
})
|
181
182
|
```
|
@@ -358,20 +359,12 @@ type Query {
|
|
358
359
|
|
359
360
|
#### Class-based schemas
|
360
361
|
|
361
|
-
The `@stitch` directive can be added to class-based schemas with a directive
|
362
|
+
The `@stitch` directive can be added to class-based schemas with a directive definition provided by the library:
|
362
363
|
|
363
364
|
```ruby
|
364
|
-
class StitchingResolver < GraphQL::Schema::Directive
|
365
|
-
graphql_name "stitch"
|
366
|
-
locations FIELD_DEFINITION
|
367
|
-
repeatable true
|
368
|
-
argument :key, String, required: true
|
369
|
-
argument :arguments, String, required: false
|
370
|
-
end
|
371
|
-
|
372
365
|
class Query < GraphQL::Schema::Object
|
373
366
|
field :product, Product, null: false do
|
374
|
-
directive
|
367
|
+
directive GraphQL::Stitching::Directives::Stitch, key: "id"
|
375
368
|
argument :id, ID, required: true
|
376
369
|
end
|
377
370
|
end
|
@@ -485,6 +478,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
485
478
|
|
486
479
|
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
487
480
|
- [Schema composition merge patterns](./docs/composer.md#merge-patterns)
|
481
|
+
- [Visibility controls](./docs/visibility.md)
|
488
482
|
- [Subscriptions tutorial](./docs/subscriptions.md)
|
489
483
|
- [Field selection routing](./docs/mechanics.md#field-selection-routing)
|
490
484
|
- [Root selection routing](./docs/mechanics.md#root-selection-routing)
|
data/docs/visibility.md
ADDED
@@ -0,0 +1,168 @@
|
|
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.
|
4
|
+
|
5
|
+
Under the hood, this system wraps `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 responds to should be 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 the name of any profile the supergraph was composed with, or nil:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
query = %|{
|
68
|
+
featuredProduct {
|
69
|
+
title # always visible
|
70
|
+
price # always visible
|
71
|
+
msrp # only visible to internal and nil profiles
|
72
|
+
id # only visible to nil profile
|
73
|
+
}
|
74
|
+
}|
|
75
|
+
|
76
|
+
result = client.execute(query, context: {
|
77
|
+
visibility_profile: "public", # << or private, or nil
|
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
|
+
- Using `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 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. Profile sets for an element will intersect into its 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.
|
@@ -13,16 +13,18 @@ module GraphQL
|
|
13
13
|
# Builds a new client instance. Either `supergraph` or `locations` configuration is required.
|
14
14
|
# @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
|
15
15
|
# @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
|
16
|
-
# @param
|
17
|
-
def initialize(locations: nil, supergraph: nil,
|
16
|
+
# @param composer_options [Hash] optional, composer options for configuring composition.
|
17
|
+
def initialize(locations: nil, supergraph: nil, composer_options: {})
|
18
18
|
@supergraph = if locations && supergraph
|
19
19
|
raise ArgumentError, "Cannot provide both locations and a supergraph."
|
20
20
|
elsif supergraph && !supergraph.is_a?(Supergraph)
|
21
21
|
raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
|
22
|
+
elsif supergraph && composer_options.any?
|
23
|
+
raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
|
22
24
|
elsif supergraph
|
23
25
|
supergraph
|
24
26
|
else
|
25
|
-
composer
|
27
|
+
composer = Composer.new(**composer_options)
|
26
28
|
composer.perform(locations)
|
27
29
|
end
|
28
30
|
|
@@ -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,6 +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? } }
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
|
26
28
|
|
27
29
|
# @api private
|
28
30
|
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
|
@@ -52,6 +54,7 @@ module GraphQL
|
|
52
54
|
query_name: "Query",
|
53
55
|
mutation_name: "Mutation",
|
54
56
|
subscription_name: "Subscription",
|
57
|
+
visibility_profiles: [],
|
55
58
|
description_merger: nil,
|
56
59
|
deprecation_merger: nil,
|
57
60
|
default_value_merger: nil,
|
@@ -71,6 +74,7 @@ module GraphQL
|
|
71
74
|
@resolver_map = {}
|
72
75
|
@resolver_configs = {}
|
73
76
|
@mapped_type_names = {}
|
77
|
+
@visibility_profiles = Set.new(visibility_profiles)
|
74
78
|
@subgraph_directives_by_name_and_location = nil
|
75
79
|
@subgraph_types_by_name_and_location = nil
|
76
80
|
@schema_directives = nil
|
@@ -85,9 +89,9 @@ module GraphQL
|
|
85
89
|
|
86
90
|
directives_to_omit = [
|
87
91
|
GraphQL::Stitching.stitch_directive,
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
Directives::SupergraphKey.graphql_name,
|
93
|
+
Directives::SupergraphResolver.graphql_name,
|
94
|
+
Directives::SupergraphSource.graphql_name,
|
91
95
|
]
|
92
96
|
|
93
97
|
# "directive_name" => "location" => subgraph_directive
|
@@ -181,6 +185,10 @@ module GraphQL
|
|
181
185
|
expand_abstract_resolvers(schema, schemas)
|
182
186
|
apply_supergraph_directives(schema, @resolver_map, @field_map)
|
183
187
|
|
188
|
+
if (visibility_def = schema.directives[GraphQL::Stitching.visibility_directive])
|
189
|
+
visibility_def.get_argument("profiles").default_value(@visibility_profiles.to_a.sort)
|
190
|
+
end
|
191
|
+
|
184
192
|
supergraph = Supergraph.from_definition(schema, executables: executables)
|
185
193
|
|
186
194
|
COMPOSITION_VALIDATORS.each do |validator_class|
|
@@ -237,7 +245,7 @@ module GraphQL
|
|
237
245
|
|
238
246
|
builder = self
|
239
247
|
|
240
|
-
Class.new(GraphQL::
|
248
|
+
Class.new(GraphQL::Stitching::Supergraph::ScalarType) do
|
241
249
|
graphql_name(type_name)
|
242
250
|
description(builder.merge_descriptions(type_name, types_by_location))
|
243
251
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -264,7 +272,7 @@ module GraphQL
|
|
264
272
|
end
|
265
273
|
end
|
266
274
|
|
267
|
-
Class.new(GraphQL::
|
275
|
+
Class.new(GraphQL::Stitching::Supergraph::EnumType) do
|
268
276
|
graphql_name(type_name)
|
269
277
|
description(builder.merge_descriptions(type_name, types_by_location))
|
270
278
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -286,7 +294,7 @@ module GraphQL
|
|
286
294
|
def build_object_type(type_name, types_by_location)
|
287
295
|
builder = self
|
288
296
|
|
289
|
-
Class.new(GraphQL::
|
297
|
+
Class.new(GraphQL::Stitching::Supergraph::ObjectType) do
|
290
298
|
graphql_name(type_name)
|
291
299
|
description(builder.merge_descriptions(type_name, types_by_location))
|
292
300
|
|
@@ -306,7 +314,7 @@ module GraphQL
|
|
306
314
|
builder = self
|
307
315
|
|
308
316
|
Module.new do
|
309
|
-
include GraphQL::
|
317
|
+
include GraphQL::Stitching::Supergraph::InterfaceType
|
310
318
|
graphql_name(type_name)
|
311
319
|
description(builder.merge_descriptions(type_name, types_by_location))
|
312
320
|
|
@@ -325,7 +333,7 @@ module GraphQL
|
|
325
333
|
def build_union_type(type_name, types_by_location)
|
326
334
|
builder = self
|
327
335
|
|
328
|
-
Class.new(GraphQL::
|
336
|
+
Class.new(GraphQL::Stitching::Supergraph::UnionType) do
|
329
337
|
graphql_name(type_name)
|
330
338
|
description(builder.merge_descriptions(type_name, types_by_location))
|
331
339
|
|
@@ -340,7 +348,7 @@ module GraphQL
|
|
340
348
|
def build_input_object_type(type_name, types_by_location)
|
341
349
|
builder = self
|
342
350
|
|
343
|
-
Class.new(GraphQL::
|
351
|
+
Class.new(GraphQL::Stitching::Supergraph::InputObjectType) do
|
344
352
|
graphql_name(type_name)
|
345
353
|
description(builder.merge_descriptions(type_name, types_by_location))
|
346
354
|
builder.build_merged_arguments(type_name, types_by_location, self)
|
@@ -451,6 +459,7 @@ module GraphQL
|
|
451
459
|
end
|
452
460
|
|
453
461
|
directives_by_name_location.each do |directive_name, directives_by_location|
|
462
|
+
kwarg_merger = @directive_kwarg_merger
|
454
463
|
directive_class = @schema_directives[directive_name]
|
455
464
|
next unless directive_class
|
456
465
|
|
@@ -467,8 +476,20 @@ module GraphQL
|
|
467
476
|
end
|
468
477
|
end
|
469
478
|
|
479
|
+
if directive_class.graphql_name == GraphQL::Stitching.visibility_directive
|
480
|
+
unless GraphQL::Stitching.supports_visibility?
|
481
|
+
raise CompositionError, "Using `@#{GraphQL::Stitching.visibility_directive}` directive " \
|
482
|
+
"for schema visibility controls requires GraphQL Ruby v#{GraphQL::Stitching::MIN_VISIBILITY_VERSION} or later."
|
483
|
+
end
|
484
|
+
|
485
|
+
if (profiles = kwarg_values_by_name_location["profiles"])
|
486
|
+
@visibility_profiles.merge(profiles.each_value.reduce(&:|))
|
487
|
+
kwarg_merger = VISIBILITY_PROFILES_MERGER
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
470
491
|
kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
|
471
|
-
memo[kwarg_name.to_sym] =
|
492
|
+
memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
|
472
493
|
type_name: type_name,
|
473
494
|
field_name: field_name,
|
474
495
|
argument_name: argument_name,
|
@@ -693,8 +714,8 @@ module GraphQL
|
|
693
714
|
|
694
715
|
keys_for_type.each do |key, locations|
|
695
716
|
locations.each do |location|
|
696
|
-
schema_directives[
|
697
|
-
type.directive(
|
717
|
+
schema_directives[Directives::SupergraphKey.graphql_name] ||= Directives::SupergraphKey
|
718
|
+
type.directive(Directives::SupergraphKey, key: key, location: location)
|
698
719
|
end
|
699
720
|
end
|
700
721
|
|
@@ -710,8 +731,8 @@ module GraphQL
|
|
710
731
|
type_name: (resolver.type_name if resolver.type_name != type_name),
|
711
732
|
}
|
712
733
|
|
713
|
-
schema_directives[
|
714
|
-
type.directive(
|
734
|
+
schema_directives[Directives::SupergraphResolver.graphql_name] ||= Directives::SupergraphResolver
|
735
|
+
type.directive(Directives::SupergraphResolver, **params.tap(&:compact!))
|
715
736
|
end
|
716
737
|
end
|
717
738
|
|
@@ -737,8 +758,8 @@ module GraphQL
|
|
737
758
|
|
738
759
|
# Apply source directives to annotate the possible locations of each field
|
739
760
|
locations_for_field.each do |location|
|
740
|
-
schema_directives[
|
741
|
-
field.directive(
|
761
|
+
schema_directives[Directives::SupergraphSource.graphql_name] ||= Directives::SupergraphSource
|
762
|
+
field.directive(Directives::SupergraphSource, location: location)
|
742
763
|
end
|
743
764
|
end
|
744
765
|
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
|
@@ -10,17 +10,29 @@ module GraphQL::Stitching
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def from_definition(schema, executables:)
|
13
|
-
|
13
|
+
if schema.is_a?(String)
|
14
|
+
schema = if GraphQL::Stitching.supports_visibility?
|
15
|
+
GraphQL::Schema.from_definition(schema, base_types: BASE_TYPES)
|
16
|
+
else
|
17
|
+
GraphQL::Schema.from_definition(schema)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
14
21
|
field_map = {}
|
15
22
|
resolver_map = {}
|
16
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
|
17
29
|
|
18
30
|
schema.types.each do |type_name, type|
|
19
31
|
next if type.introspection?
|
20
32
|
|
21
33
|
# Collect/build key definitions for each type
|
22
34
|
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
|
23
|
-
next unless directive.graphql_name ==
|
35
|
+
next unless directive.graphql_name == Directives::SupergraphKey.graphql_name
|
24
36
|
|
25
37
|
kwargs = directive.arguments.keyword_arguments
|
26
38
|
memo[kwargs[:key]] ||= []
|
@@ -32,10 +44,10 @@ module GraphQL::Stitching
|
|
32
44
|
end
|
33
45
|
|
34
46
|
# Collect/build resolver definitions for each type
|
35
|
-
type.directives.each do |
|
36
|
-
next unless
|
47
|
+
type.directives.each do |d|
|
48
|
+
next unless d.graphql_name == Directives::SupergraphResolver.graphql_name
|
37
49
|
|
38
|
-
kwargs =
|
50
|
+
kwargs = d.arguments.keyword_arguments
|
39
51
|
resolver_map[type_name] ||= []
|
40
52
|
resolver_map[type_name] << TypeResolver.new(
|
41
53
|
location: kwargs[:location],
|
@@ -52,8 +64,8 @@ module GraphQL::Stitching
|
|
52
64
|
type.fields.each do |field_name, field|
|
53
65
|
# Collection locations for each field definition
|
54
66
|
field.directives.each do |d|
|
55
|
-
next unless d.graphql_name ==
|
56
|
-
|
67
|
+
next unless d.graphql_name == Directives::SupergraphSource.graphql_name
|
68
|
+
|
57
69
|
location = d.arguments.keyword_arguments[:location]
|
58
70
|
field_map[type_name] ||= {}
|
59
71
|
field_map[type_name][field_name] ||= []
|
@@ -74,6 +86,7 @@ module GraphQL::Stitching
|
|
74
86
|
schema: schema,
|
75
87
|
fields: field_map,
|
76
88
|
resolvers: resolver_map,
|
89
|
+
visibility_profiles: visibility_profiles,
|
77
90
|
executables: executables,
|
78
91
|
)
|
79
92
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Supergraph
|
5
|
+
module Visibility
|
6
|
+
def visible?(ctx)
|
7
|
+
profile = ctx[:visibility_profile]
|
8
|
+
return true if profile.nil?
|
9
|
+
|
10
|
+
directive = directives.find { _1.graphql_name == GraphQL::Stitching.visibility_directive }
|
11
|
+
return true if directive.nil?
|
12
|
+
|
13
|
+
profiles = directive.arguments.keyword_arguments[:profiles]
|
14
|
+
return true if profiles.nil?
|
15
|
+
|
16
|
+
profiles.include?(profile)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ArgumentType < GraphQL::Schema::Argument
|
21
|
+
include Visibility
|
22
|
+
end
|
23
|
+
|
24
|
+
class FieldType < GraphQL::Schema::Field
|
25
|
+
include Visibility
|
26
|
+
argument_class(ArgumentType)
|
27
|
+
end
|
28
|
+
|
29
|
+
class InputObjectType < GraphQL::Schema::InputObject
|
30
|
+
extend Visibility
|
31
|
+
argument_class(ArgumentType)
|
32
|
+
end
|
33
|
+
|
34
|
+
module InterfaceType
|
35
|
+
include GraphQL::Schema::Interface
|
36
|
+
field_class(FieldType)
|
37
|
+
|
38
|
+
definition_methods do
|
39
|
+
include Visibility
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ObjectType < GraphQL::Schema::Object
|
44
|
+
extend Visibility
|
45
|
+
field_class(FieldType)
|
46
|
+
end
|
47
|
+
|
48
|
+
class EnumValueType < GraphQL::Schema::EnumValue
|
49
|
+
include Visibility
|
50
|
+
end
|
51
|
+
|
52
|
+
class EnumType < GraphQL::Schema::Enum
|
53
|
+
extend Visibility
|
54
|
+
enum_value_class(EnumValueType)
|
55
|
+
end
|
56
|
+
|
57
|
+
class ScalarType < GraphQL::Schema::Scalar
|
58
|
+
extend Visibility
|
59
|
+
end
|
60
|
+
|
61
|
+
class UnionType < GraphQL::Schema::Union
|
62
|
+
extend Visibility
|
63
|
+
end
|
64
|
+
|
65
|
+
BASE_TYPES = {
|
66
|
+
enum: EnumType,
|
67
|
+
input_object: InputObjectType,
|
68
|
+
interface: InterfaceType,
|
69
|
+
object: ObjectType,
|
70
|
+
scalar: ScalarType,
|
71
|
+
union: UnionType,
|
72
|
+
}.freeze
|
73
|
+
end
|
74
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "supergraph/types"
|
3
4
|
require_relative "supergraph/from_definition"
|
4
5
|
|
5
6
|
module GraphQL
|
@@ -21,7 +22,7 @@ module GraphQL
|
|
21
22
|
attr_reader :memoized_introspection_types
|
22
23
|
attr_reader :locations_by_type_and_field
|
23
24
|
|
24
|
-
def initialize(schema:, fields: {}, resolvers: {}, executables: {})
|
25
|
+
def initialize(schema:, fields: {}, resolvers: {}, visibility_profiles: [], executables: {})
|
25
26
|
@schema = schema
|
26
27
|
@resolvers = resolvers
|
27
28
|
@resolvers_by_version = nil
|
@@ -49,7 +50,12 @@ module GraphQL
|
|
49
50
|
end
|
50
51
|
end.freeze
|
51
52
|
|
52
|
-
|
53
|
+
if visibility_profiles.any?
|
54
|
+
profiles = visibility_profiles.each_with_object({ nil => {} }) { |p, m| m[p.to_s] = {} }
|
55
|
+
@schema.use(GraphQL::Schema::Visibility, profiles: profiles)
|
56
|
+
else
|
57
|
+
@schema.use(GraphQL::Schema::AlwaysVisible)
|
58
|
+
end
|
53
59
|
end
|
54
60
|
|
55
61
|
def to_definition
|
data/lib/graphql/stitching.rb
CHANGED
@@ -32,8 +32,6 @@ module GraphQL
|
|
32
32
|
end
|
33
33
|
|
34
34
|
class << self
|
35
|
-
attr_writer :stitch_directive
|
36
|
-
|
37
35
|
# Proc used to compute digests; uses SHA2 by default.
|
38
36
|
# @returns [Proc] proc used to compute digests.
|
39
37
|
def digest(&block)
|
@@ -49,10 +47,31 @@ module GraphQL
|
|
49
47
|
def stitch_directive
|
50
48
|
@stitch_directive ||= "stitch"
|
51
49
|
end
|
50
|
+
|
51
|
+
attr_writer :stitch_directive
|
52
|
+
|
53
|
+
# Name of the directive used to denote member visibilities.
|
54
|
+
# @returns [String] name of the visibility directive.
|
55
|
+
def visibility_directive
|
56
|
+
@visibility_directive ||= "visibility"
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_writer :visibility_directive
|
60
|
+
|
61
|
+
MIN_VISIBILITY_VERSION = "2.5.3"
|
62
|
+
|
63
|
+
# @returns Boolean true if GraphQL::Schema::Visibility is fully supported
|
64
|
+
def supports_visibility?
|
65
|
+
return @supports_visibility if defined?(@supports_visibility)
|
66
|
+
|
67
|
+
# Requires `Visibility` (v2.4) with nil profile support (v2.5.3)
|
68
|
+
@supports_visibility = Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new(MIN_VISIBILITY_VERSION)
|
69
|
+
end
|
52
70
|
end
|
53
71
|
end
|
54
72
|
end
|
55
73
|
|
74
|
+
require_relative "stitching/directives"
|
56
75
|
require_relative "stitching/supergraph"
|
57
76
|
require_relative "stitching/client"
|
58
77
|
require_relative "stitching/composer"
|
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.
|
4
|
+
version: 1.7.0
|
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-
|
11
|
+
date: 2025-05-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -92,6 +92,7 @@ files:
|
|
92
92
|
- docs/subscriptions.md
|
93
93
|
- docs/supergraph.md
|
94
94
|
- docs/type_resolver.md
|
95
|
+
- docs/visibility.md
|
95
96
|
- examples/file_uploads/Gemfile
|
96
97
|
- examples/file_uploads/Procfile
|
97
98
|
- examples/file_uploads/README.md
|
@@ -161,10 +162,10 @@ files:
|
|
161
162
|
- lib/graphql/stitching/client.rb
|
162
163
|
- lib/graphql/stitching/composer.rb
|
163
164
|
- lib/graphql/stitching/composer/base_validator.rb
|
164
|
-
- lib/graphql/stitching/composer/supergraph_directives.rb
|
165
165
|
- lib/graphql/stitching/composer/type_resolver_config.rb
|
166
166
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
167
167
|
- lib/graphql/stitching/composer/validate_type_resolvers.rb
|
168
|
+
- lib/graphql/stitching/directives.rb
|
168
169
|
- lib/graphql/stitching/executor.rb
|
169
170
|
- lib/graphql/stitching/executor/root_source.rb
|
170
171
|
- lib/graphql/stitching/executor/shaper.rb
|
@@ -177,6 +178,7 @@ files:
|
|
177
178
|
- lib/graphql/stitching/request/skip_include.rb
|
178
179
|
- lib/graphql/stitching/supergraph.rb
|
179
180
|
- lib/graphql/stitching/supergraph/from_definition.rb
|
181
|
+
- lib/graphql/stitching/supergraph/types.rb
|
180
182
|
- lib/graphql/stitching/type_resolver.rb
|
181
183
|
- lib/graphql/stitching/type_resolver/arguments.rb
|
182
184
|
- lib/graphql/stitching/type_resolver/keys.rb
|