graphql-stitching 1.6.1 → 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 +111 -15
- data/lib/graphql/stitching/directives.rb +51 -0
- data/lib/graphql/stitching/supergraph/from_definition.rb +95 -0
- data/lib/graphql/stitching/supergraph/types.rb +74 -0
- data/lib/graphql/stitching/supergraph.rb +22 -16
- data/lib/graphql/stitching/type_resolver.rb +4 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +19 -6
- metadata +6 -6
- data/lib/graphql/stitching/supergraph/key_directive.rb +0 -13
- data/lib/graphql/stitching/supergraph/resolver_directive.rb +0 -18
- data/lib/graphql/stitching/supergraph/source_directive.rb +0 -12
- data/lib/graphql/stitching/supergraph/to_definition.rb +0 -164
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
|
|
@@ -22,6 +22,9 @@ module GraphQL
|
|
22
22
|
|
23
23
|
# @api private
|
24
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(:&) }
|
25
28
|
|
26
29
|
# @api private
|
27
30
|
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
|
@@ -51,6 +54,7 @@ module GraphQL
|
|
51
54
|
query_name: "Query",
|
52
55
|
mutation_name: "Mutation",
|
53
56
|
subscription_name: "Subscription",
|
57
|
+
visibility_profiles: [],
|
54
58
|
description_merger: nil,
|
55
59
|
deprecation_merger: nil,
|
56
60
|
default_value_merger: nil,
|
@@ -70,6 +74,7 @@ module GraphQL
|
|
70
74
|
@resolver_map = {}
|
71
75
|
@resolver_configs = {}
|
72
76
|
@mapped_type_names = {}
|
77
|
+
@visibility_profiles = Set.new(visibility_profiles)
|
73
78
|
@subgraph_directives_by_name_and_location = nil
|
74
79
|
@subgraph_types_by_name_and_location = nil
|
75
80
|
@schema_directives = nil
|
@@ -82,9 +87,16 @@ module GraphQL
|
|
82
87
|
|
83
88
|
schemas, executables = prepare_locations_input(locations_input)
|
84
89
|
|
90
|
+
directives_to_omit = [
|
91
|
+
GraphQL::Stitching.stitch_directive,
|
92
|
+
Directives::SupergraphKey.graphql_name,
|
93
|
+
Directives::SupergraphResolver.graphql_name,
|
94
|
+
Directives::SupergraphSource.graphql_name,
|
95
|
+
]
|
96
|
+
|
85
97
|
# "directive_name" => "location" => subgraph_directive
|
86
98
|
@subgraph_directives_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
|
87
|
-
(schema.directives.keys - schema.default_directives.keys -
|
99
|
+
(schema.directives.keys - schema.default_directives.keys - directives_to_omit).each do |directive_name|
|
88
100
|
memo[directive_name] ||= {}
|
89
101
|
memo[directive_name][location] = schema.directives[directive_name]
|
90
102
|
end
|
@@ -154,25 +166,30 @@ module GraphQL
|
|
154
166
|
|
155
167
|
builder = self
|
156
168
|
schema = Class.new(GraphQL::Schema) do
|
169
|
+
object_types = schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? }
|
157
170
|
add_type_and_traverse(schema_types.values, root: false)
|
158
|
-
orphan_types(
|
171
|
+
orphan_types(object_types)
|
159
172
|
query schema_types[builder.query_name]
|
160
173
|
mutation schema_types[builder.mutation_name]
|
161
174
|
subscription schema_types[builder.subscription_name]
|
162
175
|
directives builder.schema_directives.values
|
163
176
|
|
177
|
+
object_types.each do |t|
|
178
|
+
t.interfaces.each { _1.orphan_types(t) }
|
179
|
+
end
|
180
|
+
|
164
181
|
own_orphan_types.clear
|
165
182
|
end
|
166
183
|
|
167
184
|
select_root_field_locations(schema)
|
168
185
|
expand_abstract_resolvers(schema, schemas)
|
186
|
+
apply_supergraph_directives(schema, @resolver_map, @field_map)
|
169
187
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
)
|
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
|
+
|
192
|
+
supergraph = Supergraph.from_definition(schema, executables: executables)
|
176
193
|
|
177
194
|
COMPOSITION_VALIDATORS.each do |validator_class|
|
178
195
|
validator_class.new.perform(supergraph, self)
|
@@ -228,7 +245,7 @@ module GraphQL
|
|
228
245
|
|
229
246
|
builder = self
|
230
247
|
|
231
|
-
Class.new(GraphQL::
|
248
|
+
Class.new(GraphQL::Stitching::Supergraph::ScalarType) do
|
232
249
|
graphql_name(type_name)
|
233
250
|
description(builder.merge_descriptions(type_name, types_by_location))
|
234
251
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -255,7 +272,7 @@ module GraphQL
|
|
255
272
|
end
|
256
273
|
end
|
257
274
|
|
258
|
-
Class.new(GraphQL::
|
275
|
+
Class.new(GraphQL::Stitching::Supergraph::EnumType) do
|
259
276
|
graphql_name(type_name)
|
260
277
|
description(builder.merge_descriptions(type_name, types_by_location))
|
261
278
|
builder.build_merged_directives(type_name, types_by_location, self)
|
@@ -277,7 +294,7 @@ module GraphQL
|
|
277
294
|
def build_object_type(type_name, types_by_location)
|
278
295
|
builder = self
|
279
296
|
|
280
|
-
Class.new(GraphQL::
|
297
|
+
Class.new(GraphQL::Stitching::Supergraph::ObjectType) do
|
281
298
|
graphql_name(type_name)
|
282
299
|
description(builder.merge_descriptions(type_name, types_by_location))
|
283
300
|
|
@@ -297,7 +314,7 @@ module GraphQL
|
|
297
314
|
builder = self
|
298
315
|
|
299
316
|
Module.new do
|
300
|
-
include GraphQL::
|
317
|
+
include GraphQL::Stitching::Supergraph::InterfaceType
|
301
318
|
graphql_name(type_name)
|
302
319
|
description(builder.merge_descriptions(type_name, types_by_location))
|
303
320
|
|
@@ -316,7 +333,7 @@ module GraphQL
|
|
316
333
|
def build_union_type(type_name, types_by_location)
|
317
334
|
builder = self
|
318
335
|
|
319
|
-
Class.new(GraphQL::
|
336
|
+
Class.new(GraphQL::Stitching::Supergraph::UnionType) do
|
320
337
|
graphql_name(type_name)
|
321
338
|
description(builder.merge_descriptions(type_name, types_by_location))
|
322
339
|
|
@@ -331,7 +348,7 @@ module GraphQL
|
|
331
348
|
def build_input_object_type(type_name, types_by_location)
|
332
349
|
builder = self
|
333
350
|
|
334
|
-
Class.new(GraphQL::
|
351
|
+
Class.new(GraphQL::Stitching::Supergraph::InputObjectType) do
|
335
352
|
graphql_name(type_name)
|
336
353
|
description(builder.merge_descriptions(type_name, types_by_location))
|
337
354
|
builder.build_merged_arguments(type_name, types_by_location, self)
|
@@ -442,6 +459,7 @@ module GraphQL
|
|
442
459
|
end
|
443
460
|
|
444
461
|
directives_by_name_location.each do |directive_name, directives_by_location|
|
462
|
+
kwarg_merger = @directive_kwarg_merger
|
445
463
|
directive_class = @schema_directives[directive_name]
|
446
464
|
next unless directive_class
|
447
465
|
|
@@ -458,8 +476,20 @@ module GraphQL
|
|
458
476
|
end
|
459
477
|
end
|
460
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
|
+
|
461
491
|
kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
|
462
|
-
memo[kwarg_name.to_sym] =
|
492
|
+
memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
|
463
493
|
type_name: type_name,
|
464
494
|
field_name: field_name,
|
465
495
|
argument_name: argument_name,
|
@@ -670,6 +700,72 @@ module GraphQL
|
|
670
700
|
memo[enum_name] << :write
|
671
701
|
end
|
672
702
|
end
|
703
|
+
|
704
|
+
def apply_supergraph_directives(schema, resolvers_by_type_name, locations_by_type_and_field)
|
705
|
+
schema_directives = {}
|
706
|
+
schema.types.each do |type_name, type|
|
707
|
+
if resolvers_for_type = resolvers_by_type_name.dig(type_name)
|
708
|
+
# Apply key directives for each unique type/key/location
|
709
|
+
# (this allows keys to be composite selections and/or omitted from the supergraph schema)
|
710
|
+
keys_for_type = resolvers_for_type.each_with_object({}) do |resolver, memo|
|
711
|
+
memo[resolver.key.to_definition] ||= Set.new
|
712
|
+
memo[resolver.key.to_definition].merge(resolver.key.locations)
|
713
|
+
end
|
714
|
+
|
715
|
+
keys_for_type.each do |key, locations|
|
716
|
+
locations.each do |location|
|
717
|
+
schema_directives[Directives::SupergraphKey.graphql_name] ||= Directives::SupergraphKey
|
718
|
+
type.directive(Directives::SupergraphKey, key: key, location: location)
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
# Apply resolver directives for each unique query resolver
|
723
|
+
resolvers_for_type.each do |resolver|
|
724
|
+
params = {
|
725
|
+
location: resolver.location,
|
726
|
+
field: resolver.field,
|
727
|
+
list: resolver.list? || nil,
|
728
|
+
key: resolver.key.to_definition,
|
729
|
+
arguments: resolver.arguments.map(&:to_definition).join(", "),
|
730
|
+
argument_types: resolver.arguments.map(&:to_type_definition).join(", "),
|
731
|
+
type_name: (resolver.type_name if resolver.type_name != type_name),
|
732
|
+
}
|
733
|
+
|
734
|
+
schema_directives[Directives::SupergraphResolver.graphql_name] ||= Directives::SupergraphResolver
|
735
|
+
type.directive(Directives::SupergraphResolver, **params.tap(&:compact!))
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
next unless type.kind.fields? && !type.introspection?
|
740
|
+
|
741
|
+
type.fields.each do |field_name, field|
|
742
|
+
if field.owner != type
|
743
|
+
# make a local copy of fields inherited from an interface
|
744
|
+
# to assure that source attributions reflect the object, not the interface.
|
745
|
+
field = type.field(
|
746
|
+
field.graphql_name,
|
747
|
+
description: field.description,
|
748
|
+
deprecation_reason: field.deprecation_reason,
|
749
|
+
type: Util.unwrap_non_null(field.type),
|
750
|
+
null: !field.type.non_null?,
|
751
|
+
connection: false,
|
752
|
+
camelize: false,
|
753
|
+
)
|
754
|
+
end
|
755
|
+
|
756
|
+
locations_for_field = locations_by_type_and_field.dig(type_name, field_name)
|
757
|
+
next if locations_for_field.nil?
|
758
|
+
|
759
|
+
# Apply source directives to annotate the possible locations of each field
|
760
|
+
locations_for_field.each do |location|
|
761
|
+
schema_directives[Directives::SupergraphSource.graphql_name] ||= Directives::SupergraphSource
|
762
|
+
field.directive(Directives::SupergraphSource, location: location)
|
763
|
+
end
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
schema_directives.each_value { |directive_class| schema.directive(directive_class) }
|
768
|
+
end
|
673
769
|
end
|
674
770
|
end
|
675
771
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
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
|
24
|
+
graphql_name "key"
|
25
|
+
locations OBJECT, INTERFACE, UNION
|
26
|
+
argument :key, String, required: true
|
27
|
+
argument :location, String, required: true
|
28
|
+
repeatable true
|
29
|
+
end
|
30
|
+
|
31
|
+
class SupergraphResolver < GraphQL::Schema::Directive
|
32
|
+
graphql_name "resolver"
|
33
|
+
locations OBJECT, INTERFACE, UNION
|
34
|
+
argument :location, String, required: true
|
35
|
+
argument :list, Boolean, required: false
|
36
|
+
argument :key, String, required: true
|
37
|
+
argument :field, String, required: true
|
38
|
+
argument :arguments, String, required: true
|
39
|
+
argument :argument_types, String, required: true
|
40
|
+
argument :type_name, String, required: false
|
41
|
+
repeatable true
|
42
|
+
end
|
43
|
+
|
44
|
+
class SupergraphSource < GraphQL::Schema::Directive
|
45
|
+
graphql_name "source"
|
46
|
+
locations FIELD_DEFINITION
|
47
|
+
argument :location, String, required: true
|
48
|
+
repeatable true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Supergraph
|
5
|
+
class << self
|
6
|
+
def validate_executable!(location, executable)
|
7
|
+
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
8
|
+
return true if executable && executable.respond_to?(:call)
|
9
|
+
raise StitchingError, "Invalid executable provided for location `#{location}`."
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_definition(schema, executables:)
|
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
|
+
|
21
|
+
field_map = {}
|
22
|
+
resolver_map = {}
|
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
|
29
|
+
|
30
|
+
schema.types.each do |type_name, type|
|
31
|
+
next if type.introspection?
|
32
|
+
|
33
|
+
# Collect/build key definitions for each type
|
34
|
+
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
|
35
|
+
next unless directive.graphql_name == Directives::SupergraphKey.graphql_name
|
36
|
+
|
37
|
+
kwargs = directive.arguments.keyword_arguments
|
38
|
+
memo[kwargs[:key]] ||= []
|
39
|
+
memo[kwargs[:key]] << kwargs[:location]
|
40
|
+
end
|
41
|
+
|
42
|
+
key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
|
43
|
+
memo[key] = TypeResolver.parse_key(key, locations)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Collect/build resolver definitions for each type
|
47
|
+
type.directives.each do |d|
|
48
|
+
next unless d.graphql_name == Directives::SupergraphResolver.graphql_name
|
49
|
+
|
50
|
+
kwargs = d.arguments.keyword_arguments
|
51
|
+
resolver_map[type_name] ||= []
|
52
|
+
resolver_map[type_name] << TypeResolver.new(
|
53
|
+
location: kwargs[:location],
|
54
|
+
type_name: kwargs.fetch(:type_name, type_name),
|
55
|
+
field: kwargs[:field],
|
56
|
+
list: kwargs[:list] || false,
|
57
|
+
key: key_definitions[kwargs[:key]],
|
58
|
+
arguments: TypeResolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
next unless type.kind.fields?
|
63
|
+
|
64
|
+
type.fields.each do |field_name, field|
|
65
|
+
# Collection locations for each field definition
|
66
|
+
field.directives.each do |d|
|
67
|
+
next unless d.graphql_name == Directives::SupergraphSource.graphql_name
|
68
|
+
|
69
|
+
location = d.arguments.keyword_arguments[:location]
|
70
|
+
field_map[type_name] ||= {}
|
71
|
+
field_map[type_name][field_name] ||= []
|
72
|
+
field_map[type_name][field_name] << location
|
73
|
+
possible_locations[location] = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
executables = possible_locations.keys.each_with_object({}) do |location, memo|
|
79
|
+
executable = executables[location] || executables[location.to_sym]
|
80
|
+
if validate_executable!(location, executable)
|
81
|
+
memo[location] = executable
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
new(
|
86
|
+
schema: schema,
|
87
|
+
fields: field_map,
|
88
|
+
resolvers: resolver_map,
|
89
|
+
visibility_profiles: visibility_profiles,
|
90
|
+
executables: executables,
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
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,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "supergraph/
|
3
|
+
require_relative "supergraph/types"
|
4
|
+
require_relative "supergraph/from_definition"
|
4
5
|
|
5
6
|
module GraphQL
|
6
7
|
module Stitching
|
@@ -16,23 +17,25 @@ module GraphQL
|
|
16
17
|
# @return [Hash<String, Executable>] a map of executable resources by location.
|
17
18
|
attr_reader :executables
|
18
19
|
|
19
|
-
attr_reader :resolvers
|
20
|
+
attr_reader :resolvers
|
21
|
+
attr_reader :memoized_schema_types
|
22
|
+
attr_reader :memoized_introspection_types
|
23
|
+
attr_reader :locations_by_type_and_field
|
20
24
|
|
21
|
-
def initialize(schema:, fields: {}, resolvers: {}, executables: {})
|
25
|
+
def initialize(schema:, fields: {}, resolvers: {}, visibility_profiles: [], executables: {})
|
22
26
|
@schema = schema
|
23
27
|
@resolvers = resolvers
|
24
28
|
@resolvers_by_version = nil
|
25
29
|
@fields_by_type_and_location = nil
|
26
30
|
@locations_by_type = nil
|
27
|
-
@memoized_introspection_types =
|
31
|
+
@memoized_introspection_types = @schema.introspection_system.types
|
32
|
+
@memoized_schema_types = @schema.types
|
28
33
|
@memoized_schema_fields = {}
|
29
|
-
@memoized_schema_types = nil
|
30
34
|
@possible_keys_by_type = {}
|
31
35
|
@possible_keys_by_type_and_location = {}
|
32
|
-
@static_validator = nil
|
33
36
|
|
34
37
|
# add introspection types into the fields mapping
|
35
|
-
@locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
38
|
+
@locations_by_type_and_field = @memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
|
36
39
|
next unless type.kind.fields?
|
37
40
|
|
38
41
|
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
|
@@ -46,6 +49,17 @@ module GraphQL
|
|
46
49
|
memo[location.to_s] = executable
|
47
50
|
end
|
48
51
|
end.freeze
|
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
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_definition
|
62
|
+
@schema.to_definition
|
49
63
|
end
|
50
64
|
|
51
65
|
def resolvers_by_version
|
@@ -62,17 +76,9 @@ module GraphQL
|
|
62
76
|
@executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
|
63
77
|
end
|
64
78
|
|
65
|
-
def memoized_introspection_types
|
66
|
-
@memoized_introspection_types ||= schema.introspection_system.types
|
67
|
-
end
|
68
|
-
|
69
|
-
def memoized_schema_types
|
70
|
-
@memoized_schema_types ||= @schema.types
|
71
|
-
end
|
72
|
-
|
73
79
|
def memoized_schema_fields(type_name)
|
74
80
|
@memoized_schema_fields[type_name] ||= begin
|
75
|
-
fields = memoized_schema_types[type_name].fields
|
81
|
+
fields = @memoized_schema_types[type_name].fields
|
76
82
|
@schema.introspection_system.dynamic_fields.each do |field|
|
77
83
|
fields[field.name] ||= field # adds __typename
|
78
84
|
end
|
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)
|
@@ -50,15 +48,30 @@ module GraphQL
|
|
50
48
|
@stitch_directive ||= "stitch"
|
51
49
|
end
|
52
50
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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)
|
57
69
|
end
|
58
70
|
end
|
59
71
|
end
|
60
72
|
end
|
61
73
|
|
74
|
+
require_relative "stitching/directives"
|
62
75
|
require_relative "stitching/supergraph"
|
63
76
|
require_relative "stitching/client"
|
64
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
|
@@ -164,6 +165,7 @@ files:
|
|
164
165
|
- lib/graphql/stitching/composer/type_resolver_config.rb
|
165
166
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
166
167
|
- lib/graphql/stitching/composer/validate_type_resolvers.rb
|
168
|
+
- lib/graphql/stitching/directives.rb
|
167
169
|
- lib/graphql/stitching/executor.rb
|
168
170
|
- lib/graphql/stitching/executor/root_source.rb
|
169
171
|
- lib/graphql/stitching/executor/shaper.rb
|
@@ -175,10 +177,8 @@ files:
|
|
175
177
|
- lib/graphql/stitching/request.rb
|
176
178
|
- lib/graphql/stitching/request/skip_include.rb
|
177
179
|
- lib/graphql/stitching/supergraph.rb
|
178
|
-
- lib/graphql/stitching/supergraph/
|
179
|
-
- lib/graphql/stitching/supergraph/
|
180
|
-
- lib/graphql/stitching/supergraph/source_directive.rb
|
181
|
-
- lib/graphql/stitching/supergraph/to_definition.rb
|
180
|
+
- lib/graphql/stitching/supergraph/from_definition.rb
|
181
|
+
- lib/graphql/stitching/supergraph/types.rb
|
182
182
|
- lib/graphql/stitching/type_resolver.rb
|
183
183
|
- lib/graphql/stitching/type_resolver/arguments.rb
|
184
184
|
- lib/graphql/stitching/type_resolver/keys.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL::Stitching
|
4
|
-
class Supergraph
|
5
|
-
class KeyDirective < GraphQL::Schema::Directive
|
6
|
-
graphql_name "key"
|
7
|
-
locations OBJECT, INTERFACE, UNION
|
8
|
-
argument :key, String, required: true
|
9
|
-
argument :location, String, required: true
|
10
|
-
repeatable true
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL::Stitching
|
4
|
-
class Supergraph
|
5
|
-
class ResolverDirective < GraphQL::Schema::Directive
|
6
|
-
graphql_name "resolver"
|
7
|
-
locations OBJECT, INTERFACE, UNION
|
8
|
-
argument :location, String, required: true
|
9
|
-
argument :list, Boolean, required: false
|
10
|
-
argument :key, String, required: true
|
11
|
-
argument :field, String, required: true
|
12
|
-
argument :arguments, String, required: true
|
13
|
-
argument :argument_types, String, required: true
|
14
|
-
argument :type_name, String, required: false
|
15
|
-
repeatable true
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL::Stitching
|
4
|
-
class Supergraph
|
5
|
-
class SourceDirective < GraphQL::Schema::Directive
|
6
|
-
graphql_name "source"
|
7
|
-
locations FIELD_DEFINITION
|
8
|
-
argument :location, String, required: true
|
9
|
-
repeatable true
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
@@ -1,164 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require_relative "./key_directive"
|
3
|
-
require_relative "./resolver_directive"
|
4
|
-
require_relative "./source_directive"
|
5
|
-
|
6
|
-
module GraphQL::Stitching
|
7
|
-
class Supergraph
|
8
|
-
class << self
|
9
|
-
def validate_executable!(location, executable)
|
10
|
-
return true if executable.is_a?(Class) && executable <= GraphQL::Schema
|
11
|
-
return true if executable && executable.respond_to?(:call)
|
12
|
-
raise StitchingError, "Invalid executable provided for location `#{location}`."
|
13
|
-
end
|
14
|
-
|
15
|
-
def from_definition(schema, executables:)
|
16
|
-
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
|
17
|
-
field_map = {}
|
18
|
-
resolver_map = {}
|
19
|
-
possible_locations = {}
|
20
|
-
|
21
|
-
schema.types.each do |type_name, type|
|
22
|
-
next if type.introspection?
|
23
|
-
|
24
|
-
# Collect/build key definitions for each type
|
25
|
-
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
|
26
|
-
next unless directive.graphql_name == KeyDirective.graphql_name
|
27
|
-
|
28
|
-
kwargs = directive.arguments.keyword_arguments
|
29
|
-
memo[kwargs[:key]] ||= []
|
30
|
-
memo[kwargs[:key]] << kwargs[:location]
|
31
|
-
end
|
32
|
-
|
33
|
-
key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
|
34
|
-
memo[key] = TypeResolver.parse_key(key, locations)
|
35
|
-
end
|
36
|
-
|
37
|
-
# Collect/build resolver definitions for each type
|
38
|
-
type.directives.each do |directive|
|
39
|
-
next unless directive.graphql_name == ResolverDirective.graphql_name
|
40
|
-
|
41
|
-
kwargs = directive.arguments.keyword_arguments
|
42
|
-
resolver_map[type_name] ||= []
|
43
|
-
resolver_map[type_name] << TypeResolver.new(
|
44
|
-
location: kwargs[:location],
|
45
|
-
type_name: kwargs.fetch(:type_name, type_name),
|
46
|
-
field: kwargs[:field],
|
47
|
-
list: kwargs[:list] || false,
|
48
|
-
key: key_definitions[kwargs[:key]],
|
49
|
-
arguments: TypeResolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
|
50
|
-
)
|
51
|
-
end
|
52
|
-
|
53
|
-
next unless type.kind.fields?
|
54
|
-
|
55
|
-
type.fields.each do |field_name, field|
|
56
|
-
# Collection locations for each field definition
|
57
|
-
field.directives.each do |d|
|
58
|
-
next unless d.graphql_name == SourceDirective.graphql_name
|
59
|
-
|
60
|
-
location = d.arguments.keyword_arguments[:location]
|
61
|
-
field_map[type_name] ||= {}
|
62
|
-
field_map[type_name][field_name] ||= []
|
63
|
-
field_map[type_name][field_name] << location
|
64
|
-
possible_locations[location] = true
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
executables = possible_locations.keys.each_with_object({}) do |location, memo|
|
70
|
-
executable = executables[location] || executables[location.to_sym]
|
71
|
-
if validate_executable!(location, executable)
|
72
|
-
memo[location] = executable
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
new(
|
77
|
-
schema: schema,
|
78
|
-
fields: field_map,
|
79
|
-
resolvers: resolver_map,
|
80
|
-
executables: executables,
|
81
|
-
)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def to_definition
|
86
|
-
if @schema.directives[KeyDirective.graphql_name].nil?
|
87
|
-
@schema.directive(KeyDirective)
|
88
|
-
end
|
89
|
-
if @schema.directives[ResolverDirective.graphql_name].nil?
|
90
|
-
@schema.directive(ResolverDirective)
|
91
|
-
end
|
92
|
-
if @schema.directives[SourceDirective.graphql_name].nil?
|
93
|
-
@schema.directive(SourceDirective)
|
94
|
-
end
|
95
|
-
|
96
|
-
@schema.types.each do |type_name, type|
|
97
|
-
if resolvers_for_type = @resolvers.dig(type_name)
|
98
|
-
# Apply key directives for each unique type/key/location
|
99
|
-
# (this allows keys to be composite selections and/or omitted from the supergraph schema)
|
100
|
-
keys_for_type = resolvers_for_type.each_with_object({}) do |resolver, memo|
|
101
|
-
memo[resolver.key.to_definition] ||= Set.new
|
102
|
-
memo[resolver.key.to_definition].merge(resolver.key.locations)
|
103
|
-
end
|
104
|
-
|
105
|
-
keys_for_type.each do |key, locations|
|
106
|
-
locations.each do |location|
|
107
|
-
params = { key: key, location: location }
|
108
|
-
|
109
|
-
unless has_directive?(type, KeyDirective.graphql_name, params)
|
110
|
-
type.directive(KeyDirective, **params)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
# Apply resolver directives for each unique query resolver
|
116
|
-
resolvers_for_type.each do |resolver|
|
117
|
-
params = {
|
118
|
-
location: resolver.location,
|
119
|
-
field: resolver.field,
|
120
|
-
list: resolver.list? || nil,
|
121
|
-
key: resolver.key.to_definition,
|
122
|
-
arguments: resolver.arguments.map(&:to_definition).join(", "),
|
123
|
-
argument_types: resolver.arguments.map(&:to_type_definition).join(", "),
|
124
|
-
type_name: (resolver.type_name if resolver.type_name != type_name),
|
125
|
-
}
|
126
|
-
|
127
|
-
unless has_directive?(type, ResolverDirective.graphql_name, params)
|
128
|
-
type.directive(ResolverDirective, **params.tap(&:compact!))
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
next unless type.kind.fields?
|
134
|
-
|
135
|
-
type.fields.each do |field_name, field|
|
136
|
-
locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
|
137
|
-
next if locations_for_field.nil?
|
138
|
-
|
139
|
-
# Apply source directives to annotate the possible locations of each field
|
140
|
-
locations_for_field.each do |location|
|
141
|
-
params = { location: location }
|
142
|
-
|
143
|
-
unless has_directive?(field, SourceDirective.graphql_name, params)
|
144
|
-
field.directive(SourceDirective, **params)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
@schema.to_definition
|
151
|
-
end
|
152
|
-
|
153
|
-
private
|
154
|
-
|
155
|
-
def has_directive?(element, directive_name, params)
|
156
|
-
existing = element.directives.find do |d|
|
157
|
-
kwargs = d.arguments.keyword_arguments
|
158
|
-
d.graphql_name == directive_name && params.all? { |k, v| kwargs[k] == v }
|
159
|
-
end
|
160
|
-
|
161
|
-
!existing.nil?
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|