Skip to content

Commit 9186c2f

Browse files
authored
Restore AlwaysVisible (#171)
1 parent 89f1821 commit 9186c2f

20 files changed

+387
-258
lines changed

lib/graphql/stitching.rb

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ def digest(&block)
4949
def stitch_directive
5050
@stitch_directive ||= "stitch"
5151
end
52-
53-
# Names of stitching directives to omit from the composed supergraph.
54-
# @returns [Array<String>] list of stitching directive names.
55-
def stitching_directive_names
56-
[stitch_directive]
57-
end
5852
end
5953
end
6054
end

lib/graphql/stitching/composer.rb

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

33
require_relative "composer/base_validator"
4+
require_relative "composer/supergraph_directives"
45
require_relative "composer/validate_interfaces"
56
require_relative "composer/validate_type_resolvers"
67
require_relative "composer/type_resolver_config"
@@ -82,9 +83,16 @@ def perform(locations_input)
8283

8384
schemas, executables = prepare_locations_input(locations_input)
8485

86+
directives_to_omit = [
87+
GraphQL::Stitching.stitch_directive,
88+
KeyDirective.graphql_name,
89+
ResolverDirective.graphql_name,
90+
SourceDirective.graphql_name,
91+
]
92+
8593
# "directive_name" => "location" => subgraph_directive
8694
@subgraph_directives_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
87-
(schema.directives.keys - schema.default_directives.keys - GraphQL::Stitching.stitching_directive_names).each do |directive_name|
95+
(schema.directives.keys - schema.default_directives.keys - directives_to_omit).each do |directive_name|
8896
memo[directive_name] ||= {}
8997
memo[directive_name][location] = schema.directives[directive_name]
9098
end
@@ -154,25 +162,26 @@ def perform(locations_input)
154162

155163
builder = self
156164
schema = Class.new(GraphQL::Schema) do
165+
object_types = schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? }
157166
add_type_and_traverse(schema_types.values, root: false)
158-
orphan_types(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
167+
orphan_types(object_types)
159168
query schema_types[builder.query_name]
160169
mutation schema_types[builder.mutation_name]
161170
subscription schema_types[builder.subscription_name]
162171
directives builder.schema_directives.values
163172

173+
object_types.each do |t|
174+
t.interfaces.each { _1.orphan_types(t) }
175+
end
176+
164177
own_orphan_types.clear
165178
end
166179

167180
select_root_field_locations(schema)
168181
expand_abstract_resolvers(schema, schemas)
182+
apply_supergraph_directives(schema, @resolver_map, @field_map)
169183

170-
supergraph = Supergraph.new(
171-
schema: schema,
172-
fields: @field_map,
173-
resolvers: @resolver_map,
174-
executables: executables,
175-
)
184+
supergraph = Supergraph.from_definition(schema, executables: executables)
176185

177186
COMPOSITION_VALIDATORS.each do |validator_class|
178187
validator_class.new.perform(supergraph, self)
@@ -670,6 +679,72 @@ def build_enum_usage_map(schemas)
670679
memo[enum_name] << :write
671680
end
672681
end
682+
683+
def apply_supergraph_directives(schema, resolvers_by_type_name, locations_by_type_and_field)
684+
schema_directives = {}
685+
schema.types.each do |type_name, type|
686+
if resolvers_for_type = resolvers_by_type_name.dig(type_name)
687+
# Apply key directives for each unique type/key/location
688+
# (this allows keys to be composite selections and/or omitted from the supergraph schema)
689+
keys_for_type = resolvers_for_type.each_with_object({}) do |resolver, memo|
690+
memo[resolver.key.to_definition] ||= Set.new
691+
memo[resolver.key.to_definition].merge(resolver.key.locations)
692+
end
693+
694+
keys_for_type.each do |key, locations|
695+
locations.each do |location|
696+
schema_directives[KeyDirective.graphql_name] ||= KeyDirective
697+
type.directive(KeyDirective, key: key, location: location)
698+
end
699+
end
700+
701+
# Apply resolver directives for each unique query resolver
702+
resolvers_for_type.each do |resolver|
703+
params = {
704+
location: resolver.location,
705+
field: resolver.field,
706+
list: resolver.list? || nil,
707+
key: resolver.key.to_definition,
708+
arguments: resolver.arguments.map(&:to_definition).join(", "),
709+
argument_types: resolver.arguments.map(&:to_type_definition).join(", "),
710+
type_name: (resolver.type_name if resolver.type_name != type_name),
711+
}
712+
713+
schema_directives[ResolverDirective.graphql_name] ||= ResolverDirective
714+
type.directive(ResolverDirective, **params.tap(&:compact!))
715+
end
716+
end
717+
718+
next unless type.kind.fields? && !type.introspection?
719+
720+
type.fields.each do |field_name, field|
721+
if field.owner != type
722+
# make a local copy of fields inherited from an interface
723+
# to assure that source attributions reflect the object, not the interface.
724+
field = type.field(
725+
field.graphql_name,
726+
description: field.description,
727+
deprecation_reason: field.deprecation_reason,
728+
type: Util.unwrap_non_null(field.type),
729+
null: !field.type.non_null?,
730+
connection: false,
731+
camelize: false,
732+
)
733+
end
734+
735+
locations_for_field = locations_by_type_and_field.dig(type_name, field_name)
736+
next if locations_for_field.nil?
737+
738+
# Apply source directives to annotate the possible locations of each field
739+
locations_for_field.each do |location|
740+
schema_directives[SourceDirective.graphql_name] ||= SourceDirective
741+
field.directive(SourceDirective, location: location)
742+
end
743+
end
744+
end
745+
746+
schema_directives.each_value { |directive_class| schema.directive(directive_class) }
747+
end
673748
end
674749
end
675750
end

lib/graphql/stitching/supergraph/resolver_directive.rb renamed to lib/graphql/stitching/composer/supergraph_directives.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
# frozen_string_literal: true
22

33
module GraphQL::Stitching
4-
class Supergraph
4+
class Composer
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+
513
class ResolverDirective < GraphQL::Schema::Directive
614
graphql_name "resolver"
715
locations OBJECT, INTERFACE, UNION
@@ -14,5 +22,12 @@ class ResolverDirective < GraphQL::Schema::Directive
1422
argument :type_name, String, required: false
1523
repeatable true
1624
end
25+
26+
class SourceDirective < GraphQL::Schema::Directive
27+
graphql_name "source"
28+
locations FIELD_DEFINITION
29+
argument :location, String, required: true
30+
repeatable true
31+
end
1732
end
1833
end

lib/graphql/stitching/supergraph.rb

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

3-
require_relative "supergraph/to_definition"
3+
require_relative "supergraph/from_definition"
44

55
module GraphQL
66
module Stitching
@@ -16,23 +16,25 @@ class Supergraph
1616
# @return [Hash<String, Executable>] a map of executable resources by location.
1717
attr_reader :executables
1818

19-
attr_reader :resolvers, :locations_by_type_and_field
19+
attr_reader :resolvers
20+
attr_reader :memoized_schema_types
21+
attr_reader :memoized_introspection_types
22+
attr_reader :locations_by_type_and_field
2023

2124
def initialize(schema:, fields: {}, resolvers: {}, executables: {})
2225
@schema = schema
2326
@resolvers = resolvers
2427
@resolvers_by_version = nil
2528
@fields_by_type_and_location = nil
2629
@locations_by_type = nil
27-
@memoized_introspection_types = nil
30+
@memoized_introspection_types = @schema.introspection_system.types
31+
@memoized_schema_types = @schema.types
2832
@memoized_schema_fields = {}
29-
@memoized_schema_types = nil
3033
@possible_keys_by_type = {}
3134
@possible_keys_by_type_and_location = {}
32-
@static_validator = nil
3335

3436
# 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|
37+
@locations_by_type_and_field = @memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
3638
next unless type.kind.fields?
3739

3840
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
@@ -46,6 +48,12 @@ def initialize(schema:, fields: {}, resolvers: {}, executables: {})
4648
memo[location.to_s] = executable
4749
end
4850
end.freeze
51+
52+
@schema.use(GraphQL::Schema::AlwaysVisible)
53+
end
54+
55+
def to_definition
56+
@schema.to_definition
4957
end
5058

5159
def resolvers_by_version
@@ -62,17 +70,9 @@ def locations
6270
@executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
6371
end
6472

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-
7373
def memoized_schema_fields(type_name)
7474
@memoized_schema_fields[type_name] ||= begin
75-
fields = memoized_schema_types[type_name].fields
75+
fields = @memoized_schema_types[type_name].fields
7676
@schema.introspection_system.dynamic_fields.each do |field|
7777
fields[field.name] ||= field # adds __typename
7878
end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
14+
field_map = {}
15+
resolver_map = {}
16+
possible_locations = {}
17+
18+
schema.types.each do |type_name, type|
19+
next if type.introspection?
20+
21+
# Collect/build key definitions for each type
22+
locations_by_key = type.directives.each_with_object({}) do |directive, memo|
23+
next unless directive.graphql_name == Composer::KeyDirective.graphql_name
24+
25+
kwargs = directive.arguments.keyword_arguments
26+
memo[kwargs[:key]] ||= []
27+
memo[kwargs[:key]] << kwargs[:location]
28+
end
29+
30+
key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
31+
memo[key] = TypeResolver.parse_key(key, locations)
32+
end
33+
34+
# 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+
)
48+
end
49+
50+
next unless type.kind.fields?
51+
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
62+
end
63+
end
64+
end
65+
66+
executables = possible_locations.keys.each_with_object({}) do |location, memo|
67+
executable = executables[location] || executables[location.to_sym]
68+
if validate_executable!(location, executable)
69+
memo[location] = executable
70+
end
71+
end
72+
73+
new(
74+
schema: schema,
75+
fields: field_map,
76+
resolvers: resolver_map,
77+
executables: executables,
78+
)
79+
end
80+
end
81+
end
82+
end

lib/graphql/stitching/supergraph/key_directive.rb

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

lib/graphql/stitching/supergraph/source_directive.rb

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

0 commit comments

Comments
 (0)