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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6b2cc734796d7455701bcc5b376a8efb49534c43b7a75de53bc1f2e686bf54c
4
- data.tar.gz: 449c09b94257de6ae720b4b7938c13b8881da9f1f8dc7684aabdc4c493b2c6d2
3
+ metadata.gz: 63b137317e57972e09c9926447c53ab26ac99a146557258e94b62abff305d096
4
+ data.tar.gz: e3a2217f5c04cc1a8a8412f15bb0ee5e6c4340869f945758735468c0903840f6
5
5
  SHA512:
6
- metadata.gz: 94345a14cc9bcee462854188b543a170b3dbd65cc948e55773e1a07357f47026f41018ea35eedbacc8647d8a92634be1f143d86543f0ea092f48ddb1281a86f5
7
- data.tar.gz: ae7cb67b6f36ca209f327d43bd293fa6afc4e6a93e8e91e83f4acf2893b744831836959402cd236e789ade5602857bb39d484025ea58c7e16d19b9ec5eb69d67
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: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
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: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
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 class:
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 StitchingResolver, key: "id"
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)
@@ -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 composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
17
- def initialize(locations: nil, supergraph: nil, composer: 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 ||= Composer.new
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
- KeyDirective.graphql_name,
89
- ResolverDirective.graphql_name,
90
- SourceDirective.graphql_name,
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::Schema::Scalar) do
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::Schema::Enum) do
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::Schema::Object) do
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::Schema::Interface
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::Schema::Union) do
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::Schema::InputObject) do
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] = @directive_kwarg_merger.call(kwarg_values_by_location, {
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[KeyDirective.graphql_name] ||= KeyDirective
697
- type.directive(KeyDirective, key: key, location: location)
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[ResolverDirective.graphql_name] ||= ResolverDirective
714
- type.directive(ResolverDirective, **params.tap(&:compact!))
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[SourceDirective.graphql_name] ||= SourceDirective
741
- field.directive(SourceDirective, location: location)
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
- class Composer
5
- class KeyDirective < GraphQL::Schema::Directive
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 ResolverDirective < GraphQL::Schema::Directive
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 SourceDirective < GraphQL::Schema::Directive
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
- schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
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 == Composer::KeyDirective.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 |directive|
36
- next unless directive.graphql_name == Composer::ResolverDirective.graphql_name
47
+ type.directives.each do |d|
48
+ next unless d.graphql_name == Directives::SupergraphResolver.graphql_name
37
49
 
38
- kwargs = directive.arguments.keyword_arguments
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 == Composer::SourceDirective.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
- @schema.use(GraphQL::Schema::AlwaysVisible)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.6.2"
5
+ VERSION = "1.7.0"
6
6
  end
7
7
  end
@@ -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.6.2
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-04-26 00:00:00.000000000 Z
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