Skip to content

Commit f66d344

Browse files
committed
supergraph visibility controls.
1 parent 5fefd60 commit f66d344

File tree

17 files changed

+1113
-157
lines changed

17 files changed

+1113
-157
lines changed

lib/graphql/stitching.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ def initialize(element)
3232
end
3333

3434
class << self
35-
attr_writer :stitch_directive
36-
3735
# Proc used to compute digests; uses SHA2 by default.
3836
# @returns [Proc] proc used to compute digests.
3937
def digest(&block)
@@ -49,6 +47,25 @@ def digest(&block)
4947
def stitch_directive
5048
@stitch_directive ||= "stitch"
5149
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+
# @returns Boolean true if GraphQL::Schema::Visibility is fully supported
62+
def supports_visibility?
63+
return @supports_visibility if defined?(@supports_visibility)
64+
65+
# Requires `Visibility` (v2.4) and `Schema.from_definition(base_types:)` (v2.4.15)
66+
@supports_visibility = Object.const_defined?("GraphQL::Schema::Visibility") &&
67+
GraphQL::Schema.method(:from_definition).parameters.any? { _1.last == :base_types }
68+
end
5269
end
5370
end
5471
end

lib/graphql/stitching/client.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ class Client
1313
# Builds a new client instance. Either `supergraph` or `locations` configuration is required.
1414
# @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
1515
# @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: {})
1818
@supergraph = if locations && supergraph
1919
raise ArgumentError, "Cannot provide both locations and a supergraph."
2020
elsif supergraph && !supergraph.is_a?(Supergraph)
2121
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."
2224
elsif supergraph
2325
supergraph
2426
else
25-
composer ||= Composer.new
27+
composer = Composer.new(**composer_options)
2628
composer.perform(locations)
2729
end
2830

lib/graphql/stitching/composer.rb

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require_relative "composer/base_validator"
4-
require_relative "composer/supergraph_directives"
54
require_relative "composer/validate_interfaces"
65
require_relative "composer/validate_type_resolvers"
76
require_relative "composer/type_resolver_config"
@@ -23,6 +22,9 @@ class Composer
2322

2423
# @api private
2524
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(:&) }
2628

2729
# @api private
2830
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
@@ -52,6 +54,7 @@ def initialize(
5254
query_name: "Query",
5355
mutation_name: "Mutation",
5456
subscription_name: "Subscription",
57+
visibility_profiles: [],
5558
description_merger: nil,
5659
deprecation_merger: nil,
5760
default_value_merger: nil,
@@ -71,6 +74,7 @@ def initialize(
7174
@resolver_map = {}
7275
@resolver_configs = {}
7376
@mapped_type_names = {}
77+
@visibility_profiles = Set.new(visibility_profiles)
7478
@subgraph_directives_by_name_and_location = nil
7579
@subgraph_types_by_name_and_location = nil
7680
@schema_directives = nil
@@ -85,9 +89,9 @@ def perform(locations_input)
8589

8690
directives_to_omit = [
8791
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,
9195
]
9296

9397
# "directive_name" => "location" => subgraph_directive
@@ -181,6 +185,10 @@ def perform(locations_input)
181185
expand_abstract_resolvers(schema, schemas)
182186
apply_supergraph_directives(schema, @resolver_map, @field_map)
183187

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+
184192
supergraph = Supergraph.from_definition(schema, executables: executables)
185193

186194
COMPOSITION_VALIDATORS.each do |validator_class|
@@ -237,7 +245,7 @@ def build_scalar_type(type_name, types_by_location)
237245

238246
builder = self
239247

240-
Class.new(GraphQL::Schema::Scalar) do
248+
Class.new(GraphQL::Stitching::Supergraph::BaseScalar) do
241249
graphql_name(type_name)
242250
description(builder.merge_descriptions(type_name, types_by_location))
243251
builder.build_merged_directives(type_name, types_by_location, self)
@@ -264,7 +272,7 @@ def build_enum_type(type_name, types_by_location, enum_usage)
264272
end
265273
end
266274

267-
Class.new(GraphQL::Schema::Enum) do
275+
Class.new(GraphQL::Stitching::Supergraph::BaseEnum) do
268276
graphql_name(type_name)
269277
description(builder.merge_descriptions(type_name, types_by_location))
270278
builder.build_merged_directives(type_name, types_by_location, self)
@@ -286,7 +294,7 @@ def build_enum_type(type_name, types_by_location, enum_usage)
286294
def build_object_type(type_name, types_by_location)
287295
builder = self
288296

289-
Class.new(GraphQL::Schema::Object) do
297+
Class.new(GraphQL::Stitching::Supergraph::BaseObject) do
290298
graphql_name(type_name)
291299
description(builder.merge_descriptions(type_name, types_by_location))
292300

@@ -306,7 +314,7 @@ def build_interface_type(type_name, types_by_location)
306314
builder = self
307315

308316
Module.new do
309-
include GraphQL::Schema::Interface
317+
include GraphQL::Stitching::Supergraph::BaseInterface
310318
graphql_name(type_name)
311319
description(builder.merge_descriptions(type_name, types_by_location))
312320

@@ -325,7 +333,7 @@ def build_interface_type(type_name, types_by_location)
325333
def build_union_type(type_name, types_by_location)
326334
builder = self
327335

328-
Class.new(GraphQL::Schema::Union) do
336+
Class.new(GraphQL::Stitching::Supergraph::BaseUnion) do
329337
graphql_name(type_name)
330338
description(builder.merge_descriptions(type_name, types_by_location))
331339

@@ -340,7 +348,7 @@ def build_union_type(type_name, types_by_location)
340348
def build_input_object_type(type_name, types_by_location)
341349
builder = self
342350

343-
Class.new(GraphQL::Schema::InputObject) do
351+
Class.new(GraphQL::Stitching::Supergraph::BaseInputObject) do
344352
graphql_name(type_name)
345353
description(builder.merge_descriptions(type_name, types_by_location))
346354
builder.build_merged_arguments(type_name, types_by_location, self)
@@ -451,6 +459,7 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
451459
end
452460

453461
directives_by_name_location.each do |directive_name, directives_by_location|
462+
kwarg_merger = @directive_kwarg_merger
454463
directive_class = @schema_directives[directive_name]
455464
next unless directive_class
456465

@@ -467,8 +476,20 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
467476
end
468477
end
469478

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 v2.4.15 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+
470491
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, {
472493
type_name: type_name,
473494
field_name: field_name,
474495
argument_name: argument_name,
@@ -693,8 +714,8 @@ def apply_supergraph_directives(schema, resolvers_by_type_name, locations_by_typ
693714

694715
keys_for_type.each do |key, locations|
695716
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)
698719
end
699720
end
700721

@@ -710,8 +731,8 @@ def apply_supergraph_directives(schema, resolvers_by_type_name, locations_by_typ
710731
type_name: (resolver.type_name if resolver.type_name != type_name),
711732
}
712733

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!))
715736
end
716737
end
717738

@@ -737,8 +758,8 @@ def apply_supergraph_directives(schema, resolvers_by_type_name, locations_by_typ
737758

738759
# Apply source directives to annotate the possible locations of each field
739760
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)
742763
end
743764
end
744765
end

lib/graphql/stitching/composer/supergraph_directives.rb

Lines changed: 0 additions & 33 deletions
This file was deleted.

lib/graphql/stitching/directives.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,42 @@ class Stitch < GraphQL::Schema::Directive
1010
argument :type_name, String, required: false
1111
repeatable true
1212
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
1350
end
1451
end

lib/graphql/stitching/supergraph.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative "supergraph/base_types"
34
require_relative "supergraph/from_definition"
45

56
module GraphQL
@@ -21,7 +22,7 @@ class Supergraph
2122
attr_reader :memoized_introspection_types
2223
attr_reader :locations_by_type_and_field
2324

24-
def initialize(schema:, fields: {}, resolvers: {}, executables: {})
25+
def initialize(schema:, fields: {}, resolvers: {}, visibility_profiles: [], executables: {})
2526
@schema = schema
2627
@resolvers = resolvers
2728
@resolvers_by_version = nil
@@ -49,7 +50,12 @@ def initialize(schema:, fields: {}, resolvers: {}, executables: {})
4950
end
5051
end.freeze
5152

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
5359
end
5460

5561
def to_definition

0 commit comments

Comments
 (0)