Skip to content

Commit 1146daa

Browse files
committed
visibility, take 3.
1 parent 5fefd60 commit 1146daa

File tree

7 files changed

+307
-34
lines changed

7 files changed

+307
-34
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/composer.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class Composer
2323

2424
# @api private
2525
BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
26+
27+
# @api private
28+
VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
2629

2730
# @api private
2831
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
@@ -237,7 +240,7 @@ def build_scalar_type(type_name, types_by_location)
237240

238241
builder = self
239242

240-
Class.new(GraphQL::Schema::Scalar) do
243+
Class.new(GraphQL::Stitching::Supergraph::BaseScalar) do
241244
graphql_name(type_name)
242245
description(builder.merge_descriptions(type_name, types_by_location))
243246
builder.build_merged_directives(type_name, types_by_location, self)
@@ -264,7 +267,7 @@ def build_enum_type(type_name, types_by_location, enum_usage)
264267
end
265268
end
266269

267-
Class.new(GraphQL::Schema::Enum) do
270+
Class.new(GraphQL::Stitching::Supergraph::BaseEnum) do
268271
graphql_name(type_name)
269272
description(builder.merge_descriptions(type_name, types_by_location))
270273
builder.build_merged_directives(type_name, types_by_location, self)
@@ -286,7 +289,7 @@ def build_enum_type(type_name, types_by_location, enum_usage)
286289
def build_object_type(type_name, types_by_location)
287290
builder = self
288291

289-
Class.new(GraphQL::Schema::Object) do
292+
Class.new(GraphQL::Stitching::Supergraph::BaseObject) do
290293
graphql_name(type_name)
291294
description(builder.merge_descriptions(type_name, types_by_location))
292295

@@ -306,7 +309,7 @@ def build_interface_type(type_name, types_by_location)
306309
builder = self
307310

308311
Module.new do
309-
include GraphQL::Schema::Interface
312+
include GraphQL::Stitching::Supergraph::BaseInterface
310313
graphql_name(type_name)
311314
description(builder.merge_descriptions(type_name, types_by_location))
312315

@@ -325,7 +328,7 @@ def build_interface_type(type_name, types_by_location)
325328
def build_union_type(type_name, types_by_location)
326329
builder = self
327330

328-
Class.new(GraphQL::Schema::Union) do
331+
Class.new(GraphQL::Stitching::Supergraph::BaseUnion) do
329332
graphql_name(type_name)
330333
description(builder.merge_descriptions(type_name, types_by_location))
331334

@@ -340,7 +343,7 @@ def build_union_type(type_name, types_by_location)
340343
def build_input_object_type(type_name, types_by_location)
341344
builder = self
342345

343-
Class.new(GraphQL::Schema::InputObject) do
346+
Class.new(GraphQL::Stitching::Supergraph::BaseInputObject) do
344347
graphql_name(type_name)
345348
description(builder.merge_descriptions(type_name, types_by_location))
346349
builder.build_merged_arguments(type_name, types_by_location, self)
@@ -451,12 +454,21 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
451454
end
452455

453456
directives_by_name_location.each do |directive_name, directives_by_location|
457+
kwarg_merger = @directive_kwarg_merger
454458
directive_class = @schema_directives[directive_name]
455459
next unless directive_class
456460

457461
# handled by deprecation_reason merger...
458462
next if directive_class.graphql_name == "deprecated"
459463

464+
if directive_class.graphql_name == GraphQL::Stitching.visibility_directive
465+
unless GraphQL::Stitching.supports_visibility?
466+
raise CompositionError, "Using `@#{GraphQL::Stitching.visibility_directive}` directive " \
467+
"for schema visibility controls requires GraphQL Ruby v2.4.15 or later."
468+
end
469+
kwarg_merger = VISIBILITY_PROFILES_MERGER
470+
end
471+
460472
kwarg_values_by_name_location = directives_by_location.each_with_object({}) do |(location, directive), memo|
461473
directive.arguments.keyword_arguments.each do |key, value|
462474
key = key.to_s
@@ -468,7 +480,7 @@ def build_merged_directives(type_name, members_by_location, owner, field_name: n
468480
end
469481

470482
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, {
483+
memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
472484
type_name: type_name,
473485
field_name: field_name,
474486
argument_name: argument_name,

lib/graphql/stitching/supergraph.rb

Lines changed: 1 addition & 0 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
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 == Stitching.visibility_directive }
11+
return true if directive.nil?
12+
13+
directive.arguments.keyword_arguments[:profiles].include?(profile)
14+
end
15+
end
16+
17+
class BaseArgument < GraphQL::Schema::Argument
18+
include Visibility
19+
end
20+
21+
class BaseField < GraphQL::Schema::Field
22+
include Visibility
23+
argument_class(BaseArgument)
24+
end
25+
26+
class BaseInputObject < GraphQL::Schema::InputObject
27+
include Visibility
28+
argument_class(BaseArgument)
29+
end
30+
31+
module BaseInterface
32+
include GraphQL::Schema::Interface
33+
include Visibility
34+
field_class(BaseField)
35+
end
36+
37+
class BaseObject < GraphQL::Schema::Object
38+
include Visibility
39+
field_class(BaseField)
40+
end
41+
42+
class BaseEnumValue < GraphQL::Schema::EnumValue
43+
include Visibility
44+
end
45+
46+
class BaseEnum < GraphQL::Schema::Enum
47+
include Visibility
48+
enum_value_class(BaseEnumValue)
49+
end
50+
51+
class BaseScalar < GraphQL::Schema::Scalar
52+
include Visibility
53+
end
54+
55+
class BaseUnion < GraphQL::Schema::Union
56+
include Visibility
57+
end
58+
59+
BASE_TYPES = {
60+
enum: BaseEnum,
61+
input_object: BaseInputObject,
62+
interface: BaseInterface,
63+
object: BaseObject,
64+
scalar: BaseScalar,
65+
union: BaseUnion,
66+
}.freeze
67+
end
68+
end

lib/graphql/stitching/supergraph/from_definition.rb

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ def validate_executable!(location, executable)
1010
end
1111

1212
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+
1421
field_map = {}
1522
resolver_map = {}
1623
possible_locations = {}
@@ -32,34 +39,46 @@ def from_definition(schema, executables:)
3239
end
3340

3441
# Collect/build resolver definitions for each type
35-
type.directives.each do |directive|
36-
next unless directive.graphql_name == Composer::ResolverDirective.graphql_name
37-
38-
kwargs = directive.arguments.keyword_arguments
39-
resolver_map[type_name] ||= []
40-
resolver_map[type_name] << TypeResolver.new(
41-
location: kwargs[:location],
42-
type_name: kwargs.fetch(:type_name, type_name),
43-
field: kwargs[:field],
44-
list: kwargs[:list] || false,
45-
key: key_definitions[kwargs[:key]],
46-
arguments: TypeResolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
47-
)
42+
type.directives.each do |d|
43+
case d.graphql_name
44+
when Composer::ResolverDirective.graphql_name
45+
kwargs = d.arguments.keyword_arguments
46+
resolver_map[type_name] ||= []
47+
resolver_map[type_name] << TypeResolver.new(
48+
location: kwargs[:location],
49+
type_name: kwargs.fetch(:type_name, type_name),
50+
field: kwargs[:field],
51+
list: kwargs[:list] || false,
52+
key: key_definitions[kwargs[:key]],
53+
arguments: TypeResolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
54+
)
55+
when Stitching.visibility_directive
56+
# collect profiles...
57+
end
4858
end
4959

50-
next unless type.kind.fields?
60+
if type.kind.fields?
61+
type.fields.each do |field_name, field|
62+
# Collection locations for each field definition
63+
field.directives.each do |d|
64+
case d.graphql_name
65+
when Composer::SourceDirective.graphql_name
66+
location = d.arguments.keyword_arguments[:location]
67+
field_map[type_name] ||= {}
68+
field_map[type_name][field_name] ||= []
69+
field_map[type_name][field_name] << location
70+
possible_locations[location] = true
71+
when Stitching.visibility_directive
72+
# collect profiles...
73+
end
74+
end
5175

52-
type.fields.each do |field_name, field|
53-
# Collection locations for each field definition
54-
field.directives.each do |d|
55-
next unless d.graphql_name == Composer::SourceDirective.graphql_name
56-
57-
location = d.arguments.keyword_arguments[:location]
58-
field_map[type_name] ||= {}
59-
field_map[type_name][field_name] ||= []
60-
field_map[type_name][field_name] << location
61-
possible_locations[location] = true
76+
collect_visibility_profiles(field.arguments)
6277
end
78+
elsif type.kind.input_object?
79+
collect_visibility_profiles(type.arguments)
80+
elsif type.kind.enum?
81+
collect_visibility_profiles(type.values)
6382
end
6483
end
6584

@@ -77,6 +96,15 @@ def from_definition(schema, executables:)
7796
executables: executables,
7897
)
7998
end
99+
100+
def collect_visibility_profiles(elements)
101+
elements.each do |element_name, element|
102+
element.directives.each do |d|
103+
next unless d.graphql_name == Stitching.visibility_directive
104+
# collect profiles...
105+
end
106+
end
107+
end
80108
end
81109
end
82110
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module GraphQL::Stitching
4+
class Supergraph
5+
class VisibilityDirective < GraphQL::Schema::Directive
6+
graphql_name "visibility"
7+
locations(OBJECT, INTERFACE, UNION, INPUT_OBJECT, ENUM, SCALAR,
8+
FIELD_DEFINITION, ARGUMENT_DEFINITION, INPUT_FIELD_DEFINITION, ENUM_VALUE)
9+
argument :profiles, [String], required: true
10+
end
11+
end
12+
end

0 commit comments

Comments
 (0)