graphql-stitching 1.4.3 → 1.5.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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/README.md +10 -7
- data/docs/README.md +1 -0
- data/docs/client.md +6 -0
- data/docs/composer.md +1 -1
- data/docs/subscriptions.md +208 -0
- data/docs/{resolver.md → type_resolver.md} +3 -3
- data/examples/subscriptions/.gitattributes +9 -0
- data/examples/subscriptions/.gitignore +35 -0
- data/examples/subscriptions/Gemfile +65 -0
- data/examples/subscriptions/README.md +38 -0
- data/examples/subscriptions/Rakefile +6 -0
- data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
- data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
- data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
- data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
- data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
- data/examples/subscriptions/app/models/repository.rb +39 -0
- data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
- data/examples/subscriptions/bin/bundle +109 -0
- data/examples/subscriptions/bin/docker-entrypoint +8 -0
- data/examples/subscriptions/bin/importmap +4 -0
- data/examples/subscriptions/bin/rails +4 -0
- data/examples/subscriptions/bin/rake +4 -0
- data/examples/subscriptions/bin/setup +33 -0
- data/examples/subscriptions/config/application.rb +14 -0
- data/examples/subscriptions/config/boot.rb +4 -0
- data/examples/subscriptions/config/cable.yml +10 -0
- data/examples/subscriptions/config/credentials.yml.enc +1 -0
- data/examples/subscriptions/config/database.yml +25 -0
- data/examples/subscriptions/config/environment.rb +5 -0
- data/examples/subscriptions/config/environments/development.rb +74 -0
- data/examples/subscriptions/config/environments/production.rb +91 -0
- data/examples/subscriptions/config/environments/test.rb +64 -0
- data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
- data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/subscriptions/config/initializers/inflections.rb +16 -0
- data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
- data/examples/subscriptions/config/locales/en.yml +31 -0
- data/examples/subscriptions/config/master.key +1 -0
- data/examples/subscriptions/config/puma.rb +35 -0
- data/examples/subscriptions/config/routes.rb +8 -0
- data/examples/subscriptions/config/storage.yml +34 -0
- data/examples/subscriptions/config.ru +6 -0
- data/examples/subscriptions/db/seeds.rb +9 -0
- data/examples/subscriptions/public/404.html +17 -0
- data/examples/subscriptions/public/422.html +17 -0
- data/examples/subscriptions/public/500.html +16 -0
- data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/subscriptions/public/apple-touch-icon.png +0 -0
- data/examples/subscriptions/public/favicon.ico +0 -0
- data/examples/subscriptions/public/robots.txt +1 -0
- data/lib/graphql/stitching/client.rb +18 -11
- data/lib/graphql/stitching/composer/{resolver_config.rb → type_resolver_config.rb} +3 -3
- data/lib/graphql/stitching/composer/{validate_resolvers.rb → validate_type_resolvers.rb} +8 -2
- data/lib/graphql/stitching/composer.rb +48 -42
- data/lib/graphql/stitching/executor/shaper.rb +3 -3
- data/lib/graphql/stitching/executor/{resolver_source.rb → type_resolver_source.rb} +2 -2
- data/lib/graphql/stitching/executor.rb +19 -11
- data/lib/graphql/stitching/http_executable.rb +3 -0
- data/lib/graphql/stitching/plan.rb +1 -1
- data/lib/graphql/stitching/planner/step.rb +1 -1
- data/lib/graphql/stitching/planner.rb +29 -15
- data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +3 -3
- data/lib/graphql/stitching/request.rb +44 -6
- data/lib/graphql/stitching/supergraph/to_definition.rb +3 -3
- data/lib/graphql/stitching/supergraph.rb +6 -3
- data/lib/graphql/stitching/{resolver → type_resolver}/arguments.rb +7 -7
- data/lib/graphql/stitching/{resolver → type_resolver}/keys.rb +3 -4
- data/lib/graphql/stitching/{resolver.rb → type_resolver.rb} +5 -5
- data/lib/graphql/stitching/util.rb +1 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +32 -4
- metadata +56 -10
@@ -1,12 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "
|
5
|
-
require_relative "
|
6
|
-
require_relative "
|
3
|
+
require_relative "composer/base_validator"
|
4
|
+
require_relative "composer/validate_interfaces"
|
5
|
+
require_relative "composer/validate_type_resolvers"
|
6
|
+
require_relative "composer/type_resolver_config"
|
7
7
|
|
8
8
|
module GraphQL
|
9
9
|
module Stitching
|
10
|
+
# Composer receives many individual `GraphQL::Schema` instances
|
11
|
+
# representing various graph locations and merges them into one
|
12
|
+
# combined Supergraph that is validated for integrity.
|
10
13
|
class Composer
|
11
14
|
# @api private
|
12
15
|
NO_DEFAULT_VALUE = begin
|
@@ -26,9 +29,9 @@ module GraphQL
|
|
26
29
|
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
|
27
30
|
|
28
31
|
# @api private
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
COMPOSITION_VALIDATORS = [
|
33
|
+
ValidateInterfaces,
|
34
|
+
ValidateTypeResolvers,
|
32
35
|
].freeze
|
33
36
|
|
34
37
|
# @return [String] name of the Query type in the composed schema.
|
@@ -37,6 +40,9 @@ module GraphQL
|
|
37
40
|
# @return [String] name of the Mutation type in the composed schema.
|
38
41
|
attr_reader :mutation_name
|
39
42
|
|
43
|
+
# @return [String] name of the Subscription type in the composed schema.
|
44
|
+
attr_reader :subscription_name
|
45
|
+
|
40
46
|
# @api private
|
41
47
|
attr_reader :subgraph_types_by_name_and_location
|
42
48
|
|
@@ -46,6 +52,7 @@ module GraphQL
|
|
46
52
|
def initialize(
|
47
53
|
query_name: "Query",
|
48
54
|
mutation_name: "Mutation",
|
55
|
+
subscription_name: "Subscription",
|
49
56
|
description_merger: nil,
|
50
57
|
deprecation_merger: nil,
|
51
58
|
default_value_merger: nil,
|
@@ -54,23 +61,27 @@ module GraphQL
|
|
54
61
|
)
|
55
62
|
@query_name = query_name
|
56
63
|
@mutation_name = mutation_name
|
64
|
+
@subscription_name = subscription_name
|
57
65
|
@description_merger = description_merger || BASIC_VALUE_MERGER
|
58
66
|
@deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
|
59
67
|
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
|
60
68
|
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
|
61
69
|
@root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
|
70
|
+
|
71
|
+
@field_map = {}
|
72
|
+
@resolver_map = {}
|
62
73
|
@resolver_configs = {}
|
63
|
-
|
64
|
-
@field_map = nil
|
65
|
-
@resolver_map = nil
|
66
|
-
@mapped_type_names = nil
|
74
|
+
@mapped_type_names = {}
|
67
75
|
@subgraph_directives_by_name_and_location = nil
|
68
76
|
@subgraph_types_by_name_and_location = nil
|
69
77
|
@schema_directives = nil
|
70
78
|
end
|
71
79
|
|
72
80
|
def perform(locations_input)
|
73
|
-
|
81
|
+
if @subgraph_types_by_name_and_location
|
82
|
+
raise CompositionError, "Composer may only perform once per instance."
|
83
|
+
end
|
84
|
+
|
74
85
|
schemas, executables = prepare_locations_input(locations_input)
|
75
86
|
|
76
87
|
# "directive_name" => "location" => subgraph_directive
|
@@ -91,7 +102,6 @@ module GraphQL
|
|
91
102
|
# "Typename" => "location" => subgraph_type
|
92
103
|
@subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
|
93
104
|
raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
|
94
|
-
raise CompositionError, "The subscription operation is not supported." if schema.subscription
|
95
105
|
|
96
106
|
introspection_types = schema.introspection_system.types.keys
|
97
107
|
schema.types.each do |type_name, subgraph_type|
|
@@ -101,10 +111,13 @@ module GraphQL
|
|
101
111
|
raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
|
102
112
|
elsif type_name == @mutation_name && subgraph_type != schema.mutation
|
103
113
|
raise CompositionError, "Mutation name \"#{@mutation_name}\" is used by non-mutation type in #{location} schema."
|
114
|
+
elsif type_name == @subscription_name && subgraph_type != schema.subscription
|
115
|
+
raise CompositionError, "Subscription name \"#{@subscription_name}\" is used by non-subscription type in #{location} schema."
|
104
116
|
end
|
105
117
|
|
106
118
|
type_name = @query_name if subgraph_type == schema.query
|
107
119
|
type_name = @mutation_name if subgraph_type == schema.mutation
|
120
|
+
type_name = @subscription_name if subgraph_type == schema.subscription
|
108
121
|
@mapped_type_names[subgraph_type.graphql_name] = type_name if subgraph_type.graphql_name != type_name
|
109
122
|
|
110
123
|
memo[type_name] ||= {}
|
@@ -148,13 +161,14 @@ module GraphQL
|
|
148
161
|
orphan_types(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
|
149
162
|
query schema_types[builder.query_name]
|
150
163
|
mutation schema_types[builder.mutation_name]
|
164
|
+
subscription schema_types[builder.subscription_name]
|
151
165
|
directives builder.schema_directives.values
|
152
166
|
|
153
167
|
own_orphan_types.clear
|
154
168
|
end
|
155
169
|
|
156
170
|
select_root_field_locations(schema)
|
157
|
-
expand_abstract_resolvers(schema)
|
171
|
+
expand_abstract_resolvers(schema, schemas)
|
158
172
|
|
159
173
|
supergraph = Supergraph.new(
|
160
174
|
schema: schema,
|
@@ -163,9 +177,8 @@ module GraphQL
|
|
163
177
|
executables: executables,
|
164
178
|
)
|
165
179
|
|
166
|
-
|
167
|
-
|
168
|
-
klass.new.perform(supergraph, self)
|
180
|
+
COMPOSITION_VALIDATORS.each do |validator_class|
|
181
|
+
validator_class.new.perform(supergraph, self)
|
169
182
|
end
|
170
183
|
|
171
184
|
supergraph
|
@@ -186,8 +199,8 @@ module GraphQL
|
|
186
199
|
raise CompositionError, "The schema for `#{location}` location must be a GraphQL::Schema class."
|
187
200
|
end
|
188
201
|
|
189
|
-
@resolver_configs.merge!(
|
190
|
-
@resolver_configs.merge!(
|
202
|
+
@resolver_configs.merge!(TypeResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
|
203
|
+
@resolver_configs.merge!(TypeResolverConfig.extract_federation_entities(schema, location))
|
191
204
|
|
192
205
|
schemas[location.to_s] = schema
|
193
206
|
executables[location.to_s] = input[:executable] || schema
|
@@ -533,13 +546,13 @@ module GraphQL
|
|
533
546
|
|
534
547
|
subgraph_field.directives.each do |directive|
|
535
548
|
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
|
536
|
-
resolver_configs <<
|
549
|
+
resolver_configs << TypeResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
|
537
550
|
end
|
538
551
|
|
539
552
|
resolver_configs.each do |config|
|
540
553
|
resolver_type_name = if config.type_name
|
541
554
|
if !resolver_type.kind.abstract?
|
542
|
-
raise CompositionError, "
|
555
|
+
raise CompositionError, "Type resolver config may only specify a type name for abstract resolvers."
|
543
556
|
elsif !resolver_type.possible_types.find { _1.graphql_name == config.type_name }
|
544
557
|
raise CompositionError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
|
545
558
|
end
|
@@ -548,7 +561,7 @@ module GraphQL
|
|
548
561
|
resolver_type.graphql_name
|
549
562
|
end
|
550
563
|
|
551
|
-
key =
|
564
|
+
key = TypeResolver.parse_key_with_types(
|
552
565
|
config.key,
|
553
566
|
@subgraph_types_by_name_and_location[resolver_type_name],
|
554
567
|
)
|
@@ -568,11 +581,11 @@ module GraphQL
|
|
568
581
|
"#{argument.graphql_name}: $.#{key.primitive_name}"
|
569
582
|
end
|
570
583
|
|
571
|
-
arguments =
|
584
|
+
arguments = TypeResolver.parse_arguments_with_field(arguments_format, subgraph_field)
|
572
585
|
arguments.each { _1.verify_key(key) }
|
573
586
|
|
574
587
|
@resolver_map[resolver_type_name] ||= []
|
575
|
-
@resolver_map[resolver_type_name] <<
|
588
|
+
@resolver_map[resolver_type_name] << TypeResolver.new(
|
576
589
|
location: location,
|
577
590
|
type_name: resolver_type_name,
|
578
591
|
field: subgraph_field.name,
|
@@ -588,7 +601,7 @@ module GraphQL
|
|
588
601
|
# @!scope class
|
589
602
|
# @!visibility private
|
590
603
|
def select_root_field_locations(schema)
|
591
|
-
[schema.query, schema.mutation].tap(&:compact!).each do |root_type|
|
604
|
+
[schema.query, schema.mutation, schema.subscription].tap(&:compact!).each do |root_type|
|
592
605
|
root_type.fields.each do |root_field_name, root_field|
|
593
606
|
root_field_locations = @field_map[root_type.graphql_name][root_field_name]
|
594
607
|
next unless root_field_locations.length > 1
|
@@ -607,15 +620,18 @@ module GraphQL
|
|
607
620
|
|
608
621
|
# @!scope class
|
609
622
|
# @!visibility private
|
610
|
-
def expand_abstract_resolvers(
|
623
|
+
def expand_abstract_resolvers(composed_schema, schemas_by_location)
|
611
624
|
@resolver_map.keys.each do |type_name|
|
612
|
-
|
613
|
-
next unless resolver_type.kind.abstract?
|
625
|
+
next unless composed_schema.get_type(type_name).kind.abstract?
|
614
626
|
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
627
|
+
@resolver_map[type_name].each do |resolver|
|
628
|
+
abstract_type = @subgraph_types_by_name_and_location[type_name][resolver.location]
|
629
|
+
expanded_types = Util.expand_abstract_type(schemas_by_location[resolver.location], abstract_type)
|
630
|
+
|
631
|
+
expanded_types.select { @subgraph_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |impl_type|
|
632
|
+
@resolver_map[impl_type.graphql_name] ||= []
|
633
|
+
@resolver_map[impl_type.graphql_name].push(resolver)
|
634
|
+
end
|
619
635
|
end
|
620
636
|
end
|
621
637
|
end
|
@@ -660,16 +676,6 @@ module GraphQL
|
|
660
676
|
memo[enum_name] << :write
|
661
677
|
end
|
662
678
|
end
|
663
|
-
|
664
|
-
private
|
665
|
-
|
666
|
-
def reset!
|
667
|
-
@field_map = {}
|
668
|
-
@resolver_map = {}
|
669
|
-
@mapped_type_names = {}
|
670
|
-
@subgraph_directives_by_name_and_location = nil
|
671
|
-
@schema_directives = nil
|
672
|
-
end
|
673
679
|
end
|
674
680
|
end
|
675
681
|
end
|
@@ -23,8 +23,8 @@ module GraphQL::Stitching
|
|
23
23
|
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
|
24
24
|
return nil if raw_object.nil?
|
25
25
|
|
26
|
-
typename ||= raw_object[
|
27
|
-
raw_object.reject! { |key, _v|
|
26
|
+
typename ||= raw_object[TypeResolver::TYPENAME_EXPORT_NODE.alias]
|
27
|
+
raw_object.reject! { |key, _v| TypeResolver.export_key?(key) }
|
28
28
|
|
29
29
|
selections.each do |node|
|
30
30
|
case node
|
@@ -105,7 +105,7 @@ module GraphQL::Stitching
|
|
105
105
|
is_root = parent_type == @root_type
|
106
106
|
|
107
107
|
case node.name
|
108
|
-
when
|
108
|
+
when TYPENAME
|
109
109
|
yield(is_root)
|
110
110
|
true
|
111
111
|
when "__schema", "__type"
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module GraphQL::Stitching
|
4
4
|
class Executor
|
5
|
-
class
|
5
|
+
class TypeResolverSource < GraphQL::Dataloader::Source
|
6
6
|
def initialize(executor, location)
|
7
7
|
@executor = executor
|
8
8
|
@location = location
|
@@ -17,7 +17,7 @@ module GraphQL::Stitching
|
|
17
17
|
|
18
18
|
if op.if_type
|
19
19
|
# operations planned around unused fragment conditions should not trigger requests
|
20
|
-
origin_set.select! { _1[
|
20
|
+
origin_set.select! { _1[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
|
21
21
|
end
|
22
22
|
|
23
23
|
memo[op] = origin_set if origin_set.any?
|
@@ -1,12 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
-
require_relative "
|
5
|
-
require_relative "
|
6
|
-
require_relative "
|
4
|
+
require_relative "executor/root_source"
|
5
|
+
require_relative "executor/type_resolver_source"
|
6
|
+
require_relative "executor/shaper"
|
7
7
|
|
8
8
|
module GraphQL
|
9
9
|
module Stitching
|
10
|
+
# Executor handles executing upon a planned request.
|
11
|
+
# All planned steps are initiated, their results merged,
|
12
|
+
# and loaded keys are collected for batching subsequent steps.
|
13
|
+
# Final execution results are then shaped to match the request selection.
|
10
14
|
class Executor
|
11
15
|
# @return [Request] the stitching request to execute.
|
12
16
|
attr_reader :request
|
@@ -20,10 +24,14 @@ module GraphQL
|
|
20
24
|
# @return [Integer] tally of queries performed while executing.
|
21
25
|
attr_accessor :query_count
|
22
26
|
|
23
|
-
|
27
|
+
# Builds a new executor.
|
28
|
+
# @param request [Request] the stitching request to execute.
|
29
|
+
# @param nonblocking [Boolean] specifies if the dataloader should use async concurrency.
|
30
|
+
def initialize(request, data: {}, errors: [], after: 0, nonblocking: false)
|
24
31
|
@request = request
|
25
|
-
@data =
|
26
|
-
@errors =
|
32
|
+
@data = data
|
33
|
+
@errors = errors
|
34
|
+
@after = after
|
27
35
|
@query_count = 0
|
28
36
|
@exec_cycles = 0
|
29
37
|
@dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
|
@@ -40,13 +48,13 @@ module GraphQL
|
|
40
48
|
if @errors.length > 0
|
41
49
|
result["errors"] = @errors
|
42
50
|
end
|
43
|
-
|
44
|
-
result
|
51
|
+
|
52
|
+
GraphQL::Query::Result.new(query: @request, values: result)
|
45
53
|
end
|
46
54
|
|
47
55
|
private
|
48
56
|
|
49
|
-
def exec!(next_steps = [
|
57
|
+
def exec!(next_steps = [@after])
|
50
58
|
if @exec_cycles > @request.plan.ops.length
|
51
59
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
52
60
|
raise StitchingError, "Too many execution requests attempted."
|
@@ -58,8 +66,8 @@ module GraphQL
|
|
58
66
|
.select { next_steps.include?(_1.after) }
|
59
67
|
.group_by { [_1.location, _1.resolver.nil?] }
|
60
68
|
.map do |(location, root_source), ops|
|
61
|
-
|
62
|
-
@dataloader.with(
|
69
|
+
source_class = root_source ? RootSource : TypeResolverSource
|
70
|
+
@dataloader.with(source_class, self, location).request_all(ops)
|
63
71
|
end
|
64
72
|
|
65
73
|
tasks.each(&method(:exec_task))
|
@@ -6,6 +6,9 @@ require "json"
|
|
6
6
|
|
7
7
|
module GraphQL
|
8
8
|
module Stitching
|
9
|
+
# HttpExecutable provides an out-of-the-box convenience for sending
|
10
|
+
# HTTP post requests to a remote location, or a base class
|
11
|
+
# for other implementations with GraphQL multipart uploads.
|
9
12
|
class HttpExecutable
|
10
13
|
# Builds a new executable for proxying subgraph requests via HTTP.
|
11
14
|
# @param url [String] the url of the remote location to proxy.
|
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "planner/step"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
|
+
# Planner partitions request selections by best-fit graph locations,
|
8
|
+
# and provides a query plan with sequential execution steps.
|
7
9
|
class Planner
|
8
10
|
SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
|
9
|
-
TYPENAME = "__typename"
|
10
|
-
QUERY_OP = "query"
|
11
|
-
MUTATION_OP = "mutation"
|
12
11
|
ROOT_INDEX = 0
|
13
12
|
|
14
13
|
def initialize(request)
|
@@ -36,6 +35,7 @@ module GraphQL
|
|
36
35
|
# A) Group all root selections by their preferred entrypoint locations.
|
37
36
|
# A.1) Group query fields by location for parallel execution.
|
38
37
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
38
|
+
# A.3) Permit exactly one subscription field.
|
39
39
|
#
|
40
40
|
# B) Extract contiguous selections for each entrypoint location.
|
41
41
|
# B.1) Selections on interface types that do not belong to the interface at the
|
@@ -76,7 +76,7 @@ module GraphQL
|
|
76
76
|
resolver: nil
|
77
77
|
)
|
78
78
|
# coalesce repeat parameters into a single entrypoint
|
79
|
-
entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key&.to_definition}")
|
79
|
+
entrypoint = String.new("#{parent_index}/#{location}/#{parent_type.graphql_name}/#{resolver&.key&.to_definition}/#")
|
80
80
|
path.each { entrypoint << "/#{_1}" }
|
81
81
|
|
82
82
|
step = @steps_by_entrypoint[entrypoint]
|
@@ -108,11 +108,11 @@ module GraphQL
|
|
108
108
|
|
109
109
|
# A) Group all root selections by their preferred entrypoint locations.
|
110
110
|
def build_root_entrypoints
|
111
|
+
parent_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
|
112
|
+
|
111
113
|
case @request.operation.operation_type
|
112
114
|
when QUERY_OP
|
113
115
|
# A.1) Group query fields by location for parallel execution.
|
114
|
-
parent_type = @supergraph.schema.query
|
115
|
-
|
116
116
|
selections_by_location = {}
|
117
117
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
118
118
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
@@ -126,13 +126,12 @@ module GraphQL
|
|
126
126
|
parent_index: ROOT_INDEX,
|
127
127
|
parent_type: parent_type,
|
128
128
|
selections: selections,
|
129
|
+
operation_type: QUERY_OP,
|
129
130
|
)
|
130
131
|
end
|
131
132
|
|
132
133
|
when MUTATION_OP
|
133
134
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
134
|
-
parent_type = @supergraph.schema.mutation
|
135
|
-
|
136
135
|
partitions = []
|
137
136
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
138
137
|
next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].first
|
@@ -154,6 +153,21 @@ module GraphQL
|
|
154
153
|
).index
|
155
154
|
end
|
156
155
|
|
156
|
+
when SUBSCRIPTION_OP
|
157
|
+
# A.3) Permit exactly one subscription field.
|
158
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
159
|
+
raise StitchingError, "Too many root fields for subscription." unless @steps_by_entrypoint.empty?
|
160
|
+
|
161
|
+
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
162
|
+
add_step(
|
163
|
+
location: locations.first,
|
164
|
+
parent_index: ROOT_INDEX,
|
165
|
+
parent_type: parent_type,
|
166
|
+
selections: [node],
|
167
|
+
operation_type: SUBSCRIPTION_OP,
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
157
171
|
else
|
158
172
|
raise StitchingError, "Invalid operation type."
|
159
173
|
end
|
@@ -201,8 +215,8 @@ module GraphQL
|
|
201
215
|
input_selections.each do |node|
|
202
216
|
case node
|
203
217
|
when GraphQL::Language::Nodes::Field
|
204
|
-
if node.alias&.start_with?(
|
205
|
-
raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{
|
218
|
+
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX)
|
219
|
+
raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
|
206
220
|
elsif node.name == TYPENAME
|
207
221
|
locale_selections << node
|
208
222
|
next
|
@@ -263,8 +277,8 @@ module GraphQL
|
|
263
277
|
|
264
278
|
# B.4) Add a `__typename` export to abstracts and types that implement
|
265
279
|
# fragments so that resolved type information is available during execution.
|
266
|
-
if requires_typename && !locale_selections.include?(
|
267
|
-
locale_selections <<
|
280
|
+
if requires_typename && !locale_selections.include?(TypeResolver::TYPENAME_EXPORT_NODE)
|
281
|
+
locale_selections << TypeResolver::TYPENAME_EXPORT_NODE
|
268
282
|
end
|
269
283
|
|
270
284
|
if remote_selections
|
@@ -280,8 +294,8 @@ module GraphQL
|
|
280
294
|
# E.1) Add the key of each resolver query into the prior location's selection set.
|
281
295
|
parent_selections.push(*resolver.key.export_nodes) if resolver.key
|
282
296
|
parent_selections.uniq! do |node|
|
283
|
-
export_node = node.is_a?(GraphQL::Language::Nodes::Field) &&
|
284
|
-
export_node ? node.alias : node
|
297
|
+
export_node = node.is_a?(GraphQL::Language::Nodes::Field) && TypeResolver.export_key?(node.alias)
|
298
|
+
export_node ? node.alias : node.object_id
|
285
299
|
end
|
286
300
|
|
287
301
|
# E.2) Add a planner step for each new entrypoint location.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Request
|
5
5
|
# Faster implementation of an AST visitor for prerendering
|
6
6
|
# @skip and @include conditional directives into a document.
|
7
7
|
# This avoids unnecessary planning steps, and prepares result shaping.
|
@@ -40,7 +40,7 @@ module GraphQL
|
|
40
40
|
end
|
41
41
|
|
42
42
|
if filtered_selections.none?
|
43
|
-
filtered_selections <<
|
43
|
+
filtered_selections << TypeResolver::TYPENAME_EXPORT_NODE
|
44
44
|
end
|
45
45
|
|
46
46
|
if changed
|
@@ -1,9 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "request/skip_include"
|
4
|
+
|
3
5
|
module GraphQL
|
4
6
|
module Stitching
|
7
|
+
# Request combines a supergraph, GraphQL document, variables,
|
8
|
+
# variable/fragment definitions, and the selected operation.
|
9
|
+
# It provides the lifecycle of validating, preparing,
|
10
|
+
# planning, and executing upon these inputs.
|
5
11
|
class Request
|
6
|
-
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
|
7
12
|
SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
|
8
13
|
|
9
14
|
# @return [Supergraph] supergraph instance that resolves the request.
|
@@ -70,12 +75,12 @@ module GraphQL
|
|
70
75
|
|
71
76
|
# @return [String] a digest of the original document string. Generally faster but less consistent.
|
72
77
|
def digest
|
73
|
-
@digest ||=
|
78
|
+
@digest ||= Stitching.digest.call("#{Stitching::VERSION}/#{string}")
|
74
79
|
end
|
75
80
|
|
76
81
|
# @return [String] a digest of the normalized document string. Slower but more consistent.
|
77
82
|
def normalized_digest
|
78
|
-
@normalized_digest ||=
|
83
|
+
@normalized_digest ||= Stitching.digest.call("#{Stitching::VERSION}/#{normalized_string}")
|
79
84
|
end
|
80
85
|
|
81
86
|
# @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
|
@@ -83,7 +88,6 @@ module GraphQL
|
|
83
88
|
@operation ||= begin
|
84
89
|
operation_defs = @document.definitions.select do |d|
|
85
90
|
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
86
|
-
next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
|
87
91
|
@operation_name ? d.name == @operation_name : true
|
88
92
|
end
|
89
93
|
|
@@ -97,6 +101,21 @@ module GraphQL
|
|
97
101
|
end
|
98
102
|
end
|
99
103
|
|
104
|
+
# @return [Boolean] true if operation type is a query
|
105
|
+
def query?
|
106
|
+
operation.operation_type == QUERY_OP
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Boolean] true if operation type is a mutation
|
110
|
+
def mutation?
|
111
|
+
operation.operation_type == MUTATION_OP
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] true if operation type is a subscription
|
115
|
+
def subscription?
|
116
|
+
operation.operation_type == SUBSCRIPTION_OP
|
117
|
+
end
|
118
|
+
|
100
119
|
# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
|
101
120
|
def operation_directives
|
102
121
|
@operation_directives ||= if operation.directives.any?
|
@@ -162,7 +181,7 @@ module GraphQL
|
|
162
181
|
raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
|
163
182
|
@plan = new_plan
|
164
183
|
else
|
165
|
-
@plan ||=
|
184
|
+
@plan ||= Planner.new(self).perform
|
166
185
|
end
|
167
186
|
end
|
168
187
|
|
@@ -170,7 +189,26 @@ module GraphQL
|
|
170
189
|
# @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
|
171
190
|
# @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
|
172
191
|
def execute(raw: false)
|
173
|
-
|
192
|
+
add_subscription_update_handler if subscription?
|
193
|
+
Executor.new(self).perform(raw: raw)
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
# Adds a handler into context for enriching subscription updates with stitched data
|
199
|
+
def add_subscription_update_handler
|
200
|
+
request = self
|
201
|
+
@context[:stitch_subscription_update] = -> (result) {
|
202
|
+
stitched_result = Executor.new(
|
203
|
+
request,
|
204
|
+
data: result.to_h["data"] || {},
|
205
|
+
errors: result.to_h["errors"] || [],
|
206
|
+
after: request.plan.ops.first.step,
|
207
|
+
).perform
|
208
|
+
|
209
|
+
result.to_h.merge!(stitched_result.to_h)
|
210
|
+
result
|
211
|
+
}
|
174
212
|
end
|
175
213
|
end
|
176
214
|
end
|
@@ -32,7 +32,7 @@ module GraphQL::Stitching
|
|
32
32
|
end
|
33
33
|
|
34
34
|
key_definitions = locations_by_key.each_with_object({}) do |(key, locations), memo|
|
35
|
-
memo[key] =
|
35
|
+
memo[key] = TypeResolver.parse_key(key, locations)
|
36
36
|
end
|
37
37
|
|
38
38
|
# Collect/build resolver definitions for each type
|
@@ -41,13 +41,13 @@ module GraphQL::Stitching
|
|
41
41
|
|
42
42
|
kwargs = directive.arguments.keyword_arguments
|
43
43
|
resolver_map[type_name] ||= []
|
44
|
-
resolver_map[type_name] <<
|
44
|
+
resolver_map[type_name] << TypeResolver.new(
|
45
45
|
location: kwargs[:location],
|
46
46
|
type_name: kwargs.fetch(:type_name, type_name),
|
47
47
|
field: kwargs[:field],
|
48
48
|
list: kwargs[:list] || false,
|
49
49
|
key: key_definitions[kwargs[:key]],
|
50
|
-
arguments:
|
50
|
+
arguments: TypeResolver.parse_arguments_with_type_defs(kwargs[:arguments], kwargs[:argument_types]),
|
51
51
|
)
|
52
52
|
end
|
53
53
|
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "supergraph/to_definition"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
|
+
# Supergraph is the singuar representation of a stitched graph.
|
8
|
+
# It provides the combined GraphQL schema and delegation maps
|
9
|
+
# used to route selections across subgraph locations.
|
7
10
|
class Supergraph
|
8
11
|
SUPERGRAPH_LOCATION = "__super"
|
9
12
|
|
@@ -100,7 +103,7 @@ module GraphQL
|
|
100
103
|
executable.execute(
|
101
104
|
query: source,
|
102
105
|
variables: variables,
|
103
|
-
context: request.context.
|
106
|
+
context: request.context.to_h,
|
104
107
|
validate: false,
|
105
108
|
)
|
106
109
|
elsif executable.respond_to?(:call)
|
@@ -163,7 +166,7 @@ module GraphQL
|
|
163
166
|
if key_count.zero?
|
164
167
|
# nested root scopes have no resolver keys and just return a location
|
165
168
|
goal_locations.each_with_object({}) do |goal_location, memo|
|
166
|
-
memo[goal_location] = [
|
169
|
+
memo[goal_location] = [TypeResolver.new(location: goal_location)]
|
167
170
|
end
|
168
171
|
|
169
172
|
elsif key_count > 1
|