graphql-stitching 1.3.0 → 1.4.1

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.
@@ -0,0 +1,165 @@
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
+ introspection_types = schema.introspection_system.types.keys
21
+
22
+ schema.types.each do |type_name, type|
23
+ next if introspection_types.include?(type_name)
24
+
25
+ # Collect/build key definitions for each type
26
+ locations_by_key = type.directives.each_with_object({}) do |directive, memo|
27
+ next unless directive.graphql_name == KeyDirective.graphql_name
28
+
29
+ kwargs = directive.arguments.keyword_arguments
30
+ memo[kwargs[:key]] ||= []
31
+ memo[kwargs[:key]] << kwargs[:location]
32
+ end
33
+
34
+ key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
35
+ memo[key] = Resolver.parse_key(key, locations)
36
+ end
37
+
38
+ # Collect/build resolver definitions for each type
39
+ type.directives.each do |directive|
40
+ next unless directive.graphql_name == ResolverDirective.graphql_name
41
+
42
+ kwargs = directive.arguments.keyword_arguments
43
+ resolver_map[type_name] ||= []
44
+ resolver_map[type_name] << Resolver.new(
45
+ location: kwargs[:location],
46
+ type_name: kwargs.fetch(:type_name, type_name),
47
+ field: kwargs[:field],
48
+ list: kwargs[:list] || false,
49
+ key: key_definitions[kwargs[:key]],
50
+ arguments: Resolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
51
+ )
52
+ end
53
+
54
+ next unless type.kind.fields?
55
+
56
+ type.fields.each do |field_name, field|
57
+ # Collection locations for each field definition
58
+ field.directives.each do |d|
59
+ next unless d.graphql_name == SourceDirective.graphql_name
60
+
61
+ location = d.arguments.keyword_arguments[:location]
62
+ field_map[type_name] ||= {}
63
+ field_map[type_name][field_name] ||= []
64
+ field_map[type_name][field_name] << location
65
+ possible_locations[location] = true
66
+ end
67
+ end
68
+ end
69
+
70
+ executables = possible_locations.keys.each_with_object({}) do |location, memo|
71
+ executable = executables[location] || executables[location.to_sym]
72
+ if validate_executable!(location, executable)
73
+ memo[location] = executable
74
+ end
75
+ end
76
+
77
+ new(
78
+ schema: schema,
79
+ fields: field_map,
80
+ resolvers: resolver_map,
81
+ executables: executables,
82
+ )
83
+ end
84
+ end
85
+
86
+ def to_definition
87
+ if @schema.directives[KeyDirective.graphql_name].nil?
88
+ @schema.directive(KeyDirective)
89
+ end
90
+ if @schema.directives[ResolverDirective.graphql_name].nil?
91
+ @schema.directive(ResolverDirective)
92
+ end
93
+ if @schema.directives[SourceDirective.graphql_name].nil?
94
+ @schema.directive(SourceDirective)
95
+ end
96
+
97
+ @schema.types.each do |type_name, type|
98
+ if resolvers_for_type = @resolvers.dig(type_name)
99
+ # Apply key directives for each unique type/key/location
100
+ # (this allows keys to be composite selections and/or omitted from the supergraph schema)
101
+ keys_for_type = resolvers_for_type.each_with_object({}) do |resolver, memo|
102
+ memo[resolver.key.to_definition] ||= Set.new
103
+ memo[resolver.key.to_definition].merge(resolver.key.locations)
104
+ end
105
+
106
+ keys_for_type.each do |key, locations|
107
+ locations.each do |location|
108
+ params = { key: key, location: location }
109
+
110
+ unless has_directive?(type, KeyDirective.graphql_name, params)
111
+ type.directive(KeyDirective, **params)
112
+ end
113
+ end
114
+ end
115
+
116
+ # Apply resolver directives for each unique query resolver
117
+ resolvers_for_type.each do |resolver|
118
+ params = {
119
+ location: resolver.location,
120
+ field: resolver.field,
121
+ list: resolver.list? || nil,
122
+ key: resolver.key.to_definition,
123
+ arguments: resolver.arguments.map(&:to_definition).join(", "),
124
+ argument_types: resolver.arguments.map(&:to_type_definition).join(", "),
125
+ type_name: (resolver.type_name if resolver.type_name != type_name),
126
+ }
127
+
128
+ unless has_directive?(type, ResolverDirective.graphql_name, params)
129
+ type.directive(ResolverDirective, **params.tap(&:compact!))
130
+ end
131
+ end
132
+ end
133
+
134
+ next unless type.kind.fields?
135
+
136
+ type.fields.each do |field_name, field|
137
+ locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
138
+ next if locations_for_field.nil?
139
+
140
+ # Apply source directives to annotate the possible locations of each field
141
+ locations_for_field.each do |location|
142
+ params = { location: location }
143
+
144
+ unless has_directive?(field, SourceDirective.graphql_name, params)
145
+ field.directive(SourceDirective, **params)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ @schema.to_definition
152
+ end
153
+
154
+ private
155
+
156
+ def has_directive?(element, directive_name, params)
157
+ existing = element.directives.find do |d|
158
+ kwargs = d.arguments.keyword_arguments
159
+ d.graphql_name == directive_name && params.all? { |k, v| kwargs[k] == v }
160
+ end
161
+
162
+ !existing.nil?
163
+ end
164
+ end
165
+ end
@@ -1,78 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./supergraph/resolver_directive"
4
- require_relative "./supergraph/source_directive"
3
+ require_relative "./supergraph/to_definition"
5
4
 
6
5
  module GraphQL
7
6
  module Stitching
8
7
  class Supergraph
9
8
  SUPERGRAPH_LOCATION = "__super"
10
9
 
11
- class << self
12
- def validate_executable!(location, executable)
13
- return true if executable.is_a?(Class) && executable <= GraphQL::Schema
14
- return true if executable && executable.respond_to?(:call)
15
- raise StitchingError, "Invalid executable provided for location `#{location}`."
16
- end
17
-
18
- def from_definition(schema, executables:)
19
- schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
20
- field_map = {}
21
- resolver_map = {}
22
- possible_locations = {}
23
- introspection_types = schema.introspection_system.types.keys
24
-
25
- schema.types.each do |type_name, type|
26
- next if introspection_types.include?(type_name)
27
-
28
- type.directives.each do |directive|
29
- next unless directive.graphql_name == ResolverDirective.graphql_name
30
-
31
- kwargs = directive.arguments.keyword_arguments
32
- resolver_map[type_name] ||= []
33
- resolver_map[type_name] << Resolver.new(
34
- type_name: kwargs.fetch(:type_name, type_name),
35
- location: kwargs[:location],
36
- key: kwargs[:key],
37
- field: kwargs[:field],
38
- list: kwargs[:list] || false,
39
- arg: kwargs[:arg],
40
- arg_type_name: kwargs[:arg_type_name],
41
- representations: kwargs[:representations] || false,
42
- )
43
- end
44
-
45
- next unless type.kind.fields?
46
-
47
- type.fields.each do |field_name, field|
48
- field.directives.each do |d|
49
- next unless d.graphql_name == SourceDirective.graphql_name
50
-
51
- location = d.arguments.keyword_arguments[:location]
52
- field_map[type_name] ||= {}
53
- field_map[type_name][field_name] ||= []
54
- field_map[type_name][field_name] << location
55
- possible_locations[location] = true
56
- end
57
- end
58
- end
59
-
60
- executables = possible_locations.keys.each_with_object({}) do |location, memo|
61
- executable = executables[location] || executables[location.to_sym]
62
- if validate_executable!(location, executable)
63
- memo[location] = executable
64
- end
65
- end
66
-
67
- new(
68
- schema: schema,
69
- fields: field_map,
70
- resolvers: resolver_map,
71
- executables: executables,
72
- )
73
- end
74
- end
75
-
76
10
  # @return [GraphQL::Schema] the composed schema for the supergraph.
77
11
  attr_reader :schema
78
12
 
@@ -86,6 +20,7 @@ module GraphQL
86
20
  @schema.use(GraphQL::Schema::AlwaysVisible)
87
21
 
88
22
  @resolvers = resolvers
23
+ @resolvers_by_version = nil
89
24
  @fields_by_type_and_location = nil
90
25
  @locations_by_type = nil
91
26
  @memoized_introspection_types = nil
@@ -112,66 +47,17 @@ module GraphQL
112
47
  end.freeze
113
48
  end
114
49
 
115
- def to_definition
116
- if @schema.directives[ResolverDirective.graphql_name].nil?
117
- @schema.directive(ResolverDirective)
118
- end
119
- if @schema.directives[SourceDirective.graphql_name].nil?
120
- @schema.directive(SourceDirective)
121
- end
122
-
123
- @schema.types.each do |type_name, type|
124
- if resolvers_for_type = @resolvers.dig(type_name)
125
- resolvers_for_type.each do |resolver|
126
- existing = type.directives.find do |d|
127
- kwargs = d.arguments.keyword_arguments
128
- d.graphql_name == ResolverDirective.graphql_name &&
129
- kwargs[:location] == resolver.location &&
130
- kwargs[:key] == resolver.key &&
131
- kwargs[:field] == resolver.field &&
132
- kwargs[:arg] == resolver.arg &&
133
- kwargs.fetch(:list, false) == resolver.list &&
134
- kwargs.fetch(:representations, false) == resolver.representations
135
- end
136
-
137
- type.directive(ResolverDirective, **{
138
- type_name: (resolver.type_name if resolver.type_name != type_name),
139
- location: resolver.location,
140
- key: resolver.key,
141
- field: resolver.field,
142
- list: resolver.list || nil,
143
- arg: resolver.arg,
144
- arg_type_name: resolver.arg_type_name,
145
- representations: resolver.representations || nil,
146
- }.tap(&:compact!)) if existing.nil?
147
- end
148
- end
149
-
150
- next unless type.kind.fields?
151
-
152
- type.fields.each do |field_name, field|
153
- locations_for_field = @locations_by_type_and_field.dig(type_name, field_name)
154
- next if locations_for_field.nil?
155
-
156
- locations_for_field.each do |location|
157
- existing = field.directives.find do |d|
158
- d.graphql_name == SourceDirective.graphql_name &&
159
- d.arguments.keyword_arguments[:location] == location
160
- end
161
-
162
- field.directive(SourceDirective, location: location) if existing.nil?
163
- end
164
- end
165
- end
166
-
167
- @schema.to_definition
168
- end
169
-
170
50
  # @return [GraphQL::StaticValidation::Validator] static validator for the supergraph schema.
171
51
  def static_validator
172
52
  @static_validator ||= @schema.static_validator
173
53
  end
174
54
 
55
+ def resolvers_by_version
56
+ @resolvers_by_version ||= resolvers.values.tap(&:flatten!).each_with_object({}) do |resolver, memo|
57
+ memo[resolver.version] = resolver
58
+ end
59
+ end
60
+
175
61
  def fields
176
62
  @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
177
63
  end
@@ -245,24 +131,23 @@ module GraphQL
245
131
  end
246
132
 
247
133
  # collects all possible resolver keys for a given type
248
- # ("Type") => ["id", ...]
134
+ # ("Type") => [Key("id"), ...]
249
135
  def possible_keys_for_type(type_name)
250
136
  @possible_keys_by_type[type_name] ||= begin
251
137
  if type_name == @schema.query.graphql_name
252
138
  GraphQL::Stitching::EMPTY_ARRAY
253
139
  else
254
- @resolvers[type_name].map(&:key).tap(&:uniq!)
140
+ @resolvers[type_name].map(&:key).uniq(&:to_definition)
255
141
  end
256
142
  end
257
143
  end
258
144
 
259
145
  # collects possible resolver keys for a given type and location
260
- # ("Type", "location") => ["id", ...]
146
+ # ("Type", "location") => [Key("id"), ...]
261
147
  def possible_keys_for_type_and_location(type_name, location)
262
148
  possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
263
- possible_keys_by_type[location] ||= begin
264
- location_fields = fields_by_type_and_location[type_name][location] || []
265
- location_fields & possible_keys_for_type(type_name)
149
+ possible_keys_by_type[location] ||= possible_keys_for_type(type_name).select do |key|
150
+ key.locations.include?(location)
266
151
  end
267
152
  end
268
153
 
@@ -47,6 +47,34 @@ module GraphQL
47
47
  structure
48
48
  end
49
49
 
50
+ # builds a single-dimensional representation of a wrapped type structure from AST
51
+ def flatten_ast_type_structure(ast, structure: [])
52
+ null = true
53
+
54
+ while ast.is_a?(GraphQL::Language::Nodes::NonNullType)
55
+ ast = ast.of_type
56
+ null = false
57
+ end
58
+
59
+ if ast.is_a?(GraphQL::Language::Nodes::ListType)
60
+ structure << TypeStructure.new(
61
+ list: true,
62
+ null: null,
63
+ name: nil,
64
+ )
65
+
66
+ flatten_ast_type_structure(ast.of_type, structure: structure)
67
+ else
68
+ structure << TypeStructure.new(
69
+ list: false,
70
+ null: null,
71
+ name: ast.name,
72
+ )
73
+ end
74
+
75
+ structure
76
+ end
77
+
50
78
  # expands interfaces and unions to an array of their memberships
51
79
  # like `schema.possible_types`, but includes child interfaces
52
80
  def expand_abstract_type(schema, parent_type)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.3.0"
5
+ VERSION = "1.4.1"
6
6
  end
7
7
  end
@@ -8,6 +8,8 @@ module GraphQL
8
8
  EMPTY_ARRAY = [].freeze
9
9
 
10
10
  class StitchingError < StandardError; end
11
+ class CompositionError < StitchingError; end
12
+ class ValidationError < CompositionError; end
11
13
 
12
14
  class << self
13
15
  def stitch_directive
@@ -28,7 +30,6 @@ require_relative "stitching/resolver"
28
30
  require_relative "stitching/client"
29
31
  require_relative "stitching/composer"
30
32
  require_relative "stitching/executor"
31
- require_relative "stitching/export_selection"
32
33
  require_relative "stitching/http_executable"
33
34
  require_relative "stitching/plan"
34
35
  require_relative "stitching/planner_step"
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.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-04 00:00:00.000000000 Z
11
+ date: 2024-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -89,6 +89,7 @@ files:
89
89
  - docs/images/stitching.png
90
90
  - docs/mechanics.md
91
91
  - docs/request.md
92
+ - docs/resolver.md
92
93
  - docs/supergraph.md
93
94
  - examples/file_uploads/Gemfile
94
95
  - examples/file_uploads/Procfile
@@ -119,18 +120,21 @@ files:
119
120
  - lib/graphql/stitching/executor.rb
120
121
  - lib/graphql/stitching/executor/resolver_source.rb
121
122
  - lib/graphql/stitching/executor/root_source.rb
122
- - lib/graphql/stitching/export_selection.rb
123
123
  - lib/graphql/stitching/http_executable.rb
124
124
  - lib/graphql/stitching/plan.rb
125
125
  - lib/graphql/stitching/planner.rb
126
126
  - lib/graphql/stitching/planner_step.rb
127
127
  - lib/graphql/stitching/request.rb
128
128
  - lib/graphql/stitching/resolver.rb
129
+ - lib/graphql/stitching/resolver/arguments.rb
130
+ - lib/graphql/stitching/resolver/keys.rb
129
131
  - lib/graphql/stitching/shaper.rb
130
132
  - lib/graphql/stitching/skip_include.rb
131
133
  - lib/graphql/stitching/supergraph.rb
134
+ - lib/graphql/stitching/supergraph/key_directive.rb
132
135
  - lib/graphql/stitching/supergraph/resolver_directive.rb
133
136
  - lib/graphql/stitching/supergraph/source_directive.rb
137
+ - lib/graphql/stitching/supergraph/to_definition.rb
134
138
  - lib/graphql/stitching/util.rb
135
139
  - lib/graphql/stitching/version.rb
136
140
  homepage: https://github.com/gmac/graphql-stitching-ruby
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Stitching
5
- # Builds hidden selection fields added by stitiching code,
6
- # used to request operational data about resolved objects.
7
- class ExportSelection
8
- EXPORT_PREFIX = "_export_"
9
-
10
- class << self
11
- @typename_node = nil
12
-
13
- def key?(name)
14
- return false unless name
15
-
16
- name.start_with?(EXPORT_PREFIX)
17
- end
18
-
19
- def key(name)
20
- "#{EXPORT_PREFIX}#{name}"
21
- end
22
-
23
- # The argument assigning Field.alias changed from
24
- # a generic `alias` hash key to a structured `field_alias` kwarg.
25
- # See https://github.com/rmosolgo/graphql-ruby/pull/4718
26
- FIELD_ALIAS_KWARG = !GraphQL::Language::Nodes::Field.new(field_alias: "a").alias.nil?
27
-
28
- def key_node(field_name)
29
- if FIELD_ALIAS_KWARG
30
- GraphQL::Language::Nodes::Field.new(field_alias: key(field_name), name: field_name)
31
- else
32
- GraphQL::Language::Nodes::Field.new(alias: key(field_name), name: field_name)
33
- end
34
- end
35
-
36
- def typename_node
37
- @typename_node ||= key_node("__typename")
38
- end
39
- end
40
- end
41
- end
42
- end