graphql-stitching 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0"
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.0
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-02 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