graphql-stitching 1.6.1 → 1.6.2

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: da2c14565c38d14e4e49f51ff2027f89a2ee4c0313102083c9ea3baadbcd02d4
4
- data.tar.gz: eec311845ed143af3f8d598c22f2b6026a46d34868fd7546abfb585a8fed6ebc
3
+ metadata.gz: b6b2cc734796d7455701bcc5b376a8efb49534c43b7a75de53bc1f2e686bf54c
4
+ data.tar.gz: 449c09b94257de6ae720b4b7938c13b8881da9f1f8dc7684aabdc4c493b2c6d2
5
5
  SHA512:
6
- metadata.gz: 87608b9f5391ec190e0d0f534e1a58447f056b37e37c3f569d908b2c7043d40ef38732b3bff6e4975d81d34b04bb84cd88f809b364d73aeade68675f8c35f35f
7
- data.tar.gz: eb1e085ad36c36b52b0c871d9d8f3ff0ca5b6d29ff51c1ea6cd9564c0cdba8e554381615099a331426f4857b82049290287c79cc1338388a36027cb451aa3bb9
6
+ metadata.gz: 94345a14cc9bcee462854188b543a170b3dbd65cc948e55773e1a07357f47026f41018ea35eedbacc8647d8a92634be1f143d86543f0ea092f48ddb1281a86f5
7
+ data.tar.gz: ae7cb67b6f36ca209f327d43bd293fa6afc4e6a93e8e91e83f4acf2893b744831836959402cd236e789ade5602857bb39d484025ea58c7e16d19b9ec5eb69d67
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  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
+
5
13
  class ResolverDirective < GraphQL::Schema::Directive
6
14
  graphql_name "resolver"
7
15
  locations OBJECT, INTERFACE, UNION
@@ -14,5 +22,12 @@ module GraphQL::Stitching
14
22
  argument :type_name, String, required: false
15
23
  repeatable true
16
24
  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
17
32
  end
18
33
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "composer/base_validator"
4
+ require_relative "composer/supergraph_directives"
4
5
  require_relative "composer/validate_interfaces"
5
6
  require_relative "composer/validate_type_resolvers"
6
7
  require_relative "composer/type_resolver_config"
@@ -82,9 +83,16 @@ module GraphQL
82
83
 
83
84
  schemas, executables = prepare_locations_input(locations_input)
84
85
 
86
+ directives_to_omit = [
87
+ GraphQL::Stitching.stitch_directive,
88
+ KeyDirective.graphql_name,
89
+ ResolverDirective.graphql_name,
90
+ SourceDirective.graphql_name,
91
+ ]
92
+
85
93
  # "directive_name" => "location" => subgraph_directive
86
94
  @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|
88
96
  memo[directive_name] ||= {}
89
97
  memo[directive_name][location] = schema.directives[directive_name]
90
98
  end
@@ -154,25 +162,26 @@ module GraphQL
154
162
 
155
163
  builder = self
156
164
  schema = Class.new(GraphQL::Schema) do
165
+ object_types = schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? }
157
166
  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)
159
168
  query schema_types[builder.query_name]
160
169
  mutation schema_types[builder.mutation_name]
161
170
  subscription schema_types[builder.subscription_name]
162
171
  directives builder.schema_directives.values
163
172
 
173
+ object_types.each do |t|
174
+ t.interfaces.each { _1.orphan_types(t) }
175
+ end
176
+
164
177
  own_orphan_types.clear
165
178
  end
166
179
 
167
180
  select_root_field_locations(schema)
168
181
  expand_abstract_resolvers(schema, schemas)
182
+ apply_supergraph_directives(schema, @resolver_map, @field_map)
169
183
 
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)
176
185
 
177
186
  COMPOSITION_VALIDATORS.each do |validator_class|
178
187
  validator_class.new.perform(supergraph, self)
@@ -670,6 +679,72 @@ module GraphQL
670
679
  memo[enum_name] << :write
671
680
  end
672
681
  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
673
748
  end
674
749
  end
675
750
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "supergraph/to_definition"
3
+ require_relative "supergraph/from_definition"
4
4
 
5
5
  module GraphQL
6
6
  module Stitching
@@ -16,7 +16,10 @@ module GraphQL
16
16
  # @return [Hash<String, Executable>] a map of executable resources by location.
17
17
  attr_reader :executables
18
18
 
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
20
23
 
21
24
  def initialize(schema:, fields: {}, resolvers: {}, executables: {})
22
25
  @schema = schema
@@ -24,15 +27,14 @@ module GraphQL
24
27
  @resolvers_by_version = nil
25
28
  @fields_by_type_and_location = nil
26
29
  @locations_by_type = nil
27
- @memoized_introspection_types = nil
30
+ @memoized_introspection_types = @schema.introspection_system.types
31
+ @memoized_schema_types = @schema.types
28
32
  @memoized_schema_fields = {}
29
- @memoized_schema_types = nil
30
33
  @possible_keys_by_type = {}
31
34
  @possible_keys_by_type_and_location = {}
32
- @static_validator = nil
33
35
 
34
36
  # 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|
36
38
  next unless type.kind.fields?
37
39
 
38
40
  memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
@@ -46,6 +48,12 @@ module GraphQL
46
48
  memo[location.to_s] = executable
47
49
  end
48
50
  end.freeze
51
+
52
+ @schema.use(GraphQL::Schema::AlwaysVisible)
53
+ end
54
+
55
+ def to_definition
56
+ @schema.to_definition
49
57
  end
50
58
 
51
59
  def resolvers_by_version
@@ -62,17 +70,9 @@ module GraphQL
62
70
  @executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
63
71
  end
64
72
 
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
73
  def memoized_schema_fields(type_name)
74
74
  @memoized_schema_fields[type_name] ||= begin
75
- fields = memoized_schema_types[type_name].fields
75
+ fields = @memoized_schema_types[type_name].fields
76
76
  @schema.introspection_system.dynamic_fields.each do |field|
77
77
  fields[field.name] ||= field # adds __typename
78
78
  end
@@ -65,6 +65,10 @@ module GraphQL
65
65
  argument_types: arguments.map(&:to_type_definition).join(", "),
66
66
  }.tap(&:compact!)
67
67
  end
68
+
69
+ def inspect
70
+ as_json.to_json
71
+ end
68
72
  end
69
73
  end
70
74
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.6.1"
5
+ VERSION = "1.6.2"
6
6
  end
7
7
  end
@@ -49,12 +49,6 @@ module GraphQL
49
49
  def stitch_directive
50
50
  @stitch_directive ||= "stitch"
51
51
  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
58
52
  end
59
53
  end
60
54
  end
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.1
4
+ version: 1.6.2
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-22 00:00:00.000000000 Z
11
+ date: 2025-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -161,6 +161,7 @@ files:
161
161
  - lib/graphql/stitching/client.rb
162
162
  - lib/graphql/stitching/composer.rb
163
163
  - lib/graphql/stitching/composer/base_validator.rb
164
+ - lib/graphql/stitching/composer/supergraph_directives.rb
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
@@ -175,10 +176,7 @@ files:
175
176
  - lib/graphql/stitching/request.rb
176
177
  - lib/graphql/stitching/request/skip_include.rb
177
178
  - lib/graphql/stitching/supergraph.rb
178
- - lib/graphql/stitching/supergraph/key_directive.rb
179
- - lib/graphql/stitching/supergraph/resolver_directive.rb
180
- - lib/graphql/stitching/supergraph/source_directive.rb
181
- - lib/graphql/stitching/supergraph/to_definition.rb
179
+ - lib/graphql/stitching/supergraph/from_definition.rb
182
180
  - lib/graphql/stitching/type_resolver.rb
183
181
  - lib/graphql/stitching/type_resolver/arguments.rb
184
182
  - 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,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