graphql-stitching 1.6.0 → 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 +4 -4
- data/.github/workflows/ci.yml +2 -0
- data/README.md +2 -2
- data/gemfiles/graphql_2.4.0.gemfile +9 -0
- data/lib/graphql/stitching/{supergraph/resolver_directive.rb → composer/supergraph_directives.rb} +16 -1
- data/lib/graphql/stitching/composer.rb +89 -25
- data/lib/graphql/stitching/executor/shaper.rb +5 -17
- data/lib/graphql/stitching/supergraph/from_definition.rb +82 -0
- data/lib/graphql/stitching/supergraph.rb +15 -17
- data/lib/graphql/stitching/type_resolver.rb +4 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +0 -6
- metadata +5 -6
- data/lib/graphql/stitching/supergraph/key_directive.rb +0 -13
- data/lib/graphql/stitching/supergraph/source_directive.rb +0 -12
- data/lib/graphql/stitching/supergraph/to_definition.rb +0 -165
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6b2cc734796d7455701bcc5b376a8efb49534c43b7a75de53bc1f2e686bf54c
|
4
|
+
data.tar.gz: 449c09b94257de6ae720b4b7938c13b8881da9f1f8dc7684aabdc4c493b2c6d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94345a14cc9bcee462854188b543a170b3dbd65cc948e55773e1a07357f47026f41018ea35eedbacc8647d8a92634be1f143d86543f0ea092f48ddb1281a86f5
|
7
|
+
data.tar.gz: ae7cb67b6f36ca209f327d43bd293fa6afc4e6a93e8e91e83f4acf2893b744831836959402cd236e789ade5602857bb39d484025ea58c7e16d19b9ec5eb69d67
|
data/.github/workflows/ci.yml
CHANGED
data/README.md
CHANGED
@@ -74,7 +74,7 @@ result = client.execute(
|
|
74
74
|
|
75
75
|
Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
|
76
76
|
|
77
|
-
|
77
|
+
A Client bundles up the component parts of stitching, which are worth familiarizing with:
|
78
78
|
|
79
79
|
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
|
80
80
|
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
|
@@ -87,7 +87,7 @@ While `Client` is sufficient for most usecases, the library offers several discr
|
|
87
87
|
|
88
88
|

|
89
89
|
|
90
|
-
To facilitate this, schemas should be designed around
|
90
|
+
To facilitate this, schemas should be designed around **merged type keys** that stitching can cross-reference and fetch across locations using **type resolver queries** (discussed below). For those in an Apollo ecosystem, there's also _limited_ support for merging types though [federation `_entities`](./docs/federation_entities.md).
|
91
91
|
|
92
92
|
### Merged type keys
|
93
93
|
|
data/lib/graphql/stitching/{supergraph/resolver_directive.rb → composer/supergraph_directives.rb}
RENAMED
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module GraphQL::Stitching
|
4
|
-
class
|
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"
|
@@ -13,13 +14,11 @@ module GraphQL
|
|
13
14
|
class Composer
|
14
15
|
# @api private
|
15
16
|
NO_DEFAULT_VALUE = begin
|
16
|
-
|
17
|
-
field(:f, String)
|
18
|
-
argument(:a, String)
|
19
|
-
end
|
17
|
+
t = Class.new(GraphQL::Schema::Object) do
|
18
|
+
field(:f, String) { _1.argument(:a, String) }
|
20
19
|
end
|
21
20
|
|
22
|
-
|
21
|
+
t.get_field("f").get_argument("a").default_value
|
23
22
|
end
|
24
23
|
|
25
24
|
# @api private
|
@@ -84,9 +83,16 @@ module GraphQL
|
|
84
83
|
|
85
84
|
schemas, executables = prepare_locations_input(locations_input)
|
86
85
|
|
86
|
+
directives_to_omit = [
|
87
|
+
GraphQL::Stitching.stitch_directive,
|
88
|
+
KeyDirective.graphql_name,
|
89
|
+
ResolverDirective.graphql_name,
|
90
|
+
SourceDirective.graphql_name,
|
91
|
+
]
|
92
|
+
|
87
93
|
# "directive_name" => "location" => subgraph_directive
|
88
94
|
@subgraph_directives_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
|
89
|
-
(schema.directives.keys - schema.default_directives.keys -
|
95
|
+
(schema.directives.keys - schema.default_directives.keys - directives_to_omit).each do |directive_name|
|
90
96
|
memo[directive_name] ||= {}
|
91
97
|
memo[directive_name][location] = schema.directives[directive_name]
|
92
98
|
end
|
@@ -103,9 +109,8 @@ module GraphQL
|
|
103
109
|
@subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
|
104
110
|
raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
|
105
111
|
|
106
|
-
introspection_types = schema.introspection_system.types.keys
|
107
112
|
schema.types.each do |type_name, subgraph_type|
|
108
|
-
next if
|
113
|
+
next if subgraph_type.introspection?
|
109
114
|
|
110
115
|
if type_name == @query_name && subgraph_type != schema.query
|
111
116
|
raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
|
@@ -157,25 +162,26 @@ module GraphQL
|
|
157
162
|
|
158
163
|
builder = self
|
159
164
|
schema = Class.new(GraphQL::Schema) do
|
165
|
+
object_types = schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? }
|
160
166
|
add_type_and_traverse(schema_types.values, root: false)
|
161
|
-
orphan_types(
|
167
|
+
orphan_types(object_types)
|
162
168
|
query schema_types[builder.query_name]
|
163
169
|
mutation schema_types[builder.mutation_name]
|
164
170
|
subscription schema_types[builder.subscription_name]
|
165
171
|
directives builder.schema_directives.values
|
166
172
|
|
173
|
+
object_types.each do |t|
|
174
|
+
t.interfaces.each { _1.orphan_types(t) }
|
175
|
+
end
|
176
|
+
|
167
177
|
own_orphan_types.clear
|
168
178
|
end
|
169
179
|
|
170
180
|
select_root_field_locations(schema)
|
171
181
|
expand_abstract_resolvers(schema, schemas)
|
182
|
+
apply_supergraph_directives(schema, @resolver_map, @field_map)
|
172
183
|
|
173
|
-
supergraph = Supergraph.
|
174
|
-
schema: schema,
|
175
|
-
fields: @field_map,
|
176
|
-
resolvers: @resolver_map,
|
177
|
-
executables: executables,
|
178
|
-
)
|
184
|
+
supergraph = Supergraph.from_definition(schema, executables: executables)
|
179
185
|
|
180
186
|
COMPOSITION_VALIDATORS.each do |validator_class|
|
181
187
|
validator_class.new.perform(supergraph, self)
|
@@ -373,6 +379,7 @@ module GraphQL
|
|
373
379
|
deprecation_reason: merge_deprecations(type_name, fields_by_location, field_name: field_name),
|
374
380
|
type: Util.unwrap_non_null(type),
|
375
381
|
null: !type.non_null?,
|
382
|
+
connection: false,
|
376
383
|
camelize: false,
|
377
384
|
)
|
378
385
|
|
@@ -403,14 +410,6 @@ module GraphQL
|
|
403
410
|
next
|
404
411
|
end
|
405
412
|
|
406
|
-
# Getting double args sometimes... why?
|
407
|
-
begin
|
408
|
-
next if owner.arguments(GraphQL::Query::NullContext.instance, false).key?(argument_name)
|
409
|
-
rescue ArgumentError
|
410
|
-
# pre- graphql v2.4.5
|
411
|
-
next if owner.arguments.key?(argument_name)
|
412
|
-
end
|
413
|
-
|
414
413
|
kwargs = {}
|
415
414
|
default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
|
416
415
|
next if argument.default_value == NO_DEFAULT_VALUE
|
@@ -648,9 +647,8 @@ module GraphQL
|
|
648
647
|
writes = []
|
649
648
|
|
650
649
|
schemas.each do |schema|
|
651
|
-
introspection_types = schema.introspection_system.types.keys
|
652
650
|
schema.types.each_value do |type|
|
653
|
-
next if
|
651
|
+
next if type.introspection?
|
654
652
|
|
655
653
|
if type.kind.object? || type.kind.interface?
|
656
654
|
type.fields.each_value do |field|
|
@@ -681,6 +679,72 @@ module GraphQL
|
|
681
679
|
memo[enum_name] << :write
|
682
680
|
end
|
683
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
|
684
748
|
end
|
685
749
|
end
|
686
750
|
end
|
@@ -31,8 +31,11 @@ module GraphQL::Stitching
|
|
31
31
|
when GraphQL::Language::Nodes::Field
|
32
32
|
field_name = node.alias || node.name
|
33
33
|
|
34
|
-
|
35
|
-
|
34
|
+
if @request.query.get_field(parent_type, node.name).introspection?
|
35
|
+
if node.name == TYPENAME && parent_type == @root_type
|
36
|
+
raw_object[field_name] = @root_type.graphql_name
|
37
|
+
end
|
38
|
+
next
|
36
39
|
end
|
37
40
|
|
38
41
|
node_type = @supergraph.memoized_schema_fields(parent_type.graphql_name)[node.name].type
|
@@ -100,21 +103,6 @@ module GraphQL::Stitching
|
|
100
103
|
resolved_list
|
101
104
|
end
|
102
105
|
|
103
|
-
def introspection_field?(parent_type, node)
|
104
|
-
return false unless node.name.start_with?("__")
|
105
|
-
is_root = parent_type == @root_type
|
106
|
-
|
107
|
-
case node.name
|
108
|
-
when TYPENAME
|
109
|
-
yield(is_root)
|
110
|
-
true
|
111
|
-
when "__schema", "__type"
|
112
|
-
is_root && @request.operation.operation_type == "query"
|
113
|
-
else
|
114
|
-
false
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
106
|
def typename_in_type?(typename, type)
|
119
107
|
return true if type.graphql_name == typename
|
120
108
|
|
@@ -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/
|
3
|
+
require_relative "supergraph/from_definition"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
@@ -16,25 +16,25 @@ 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
|
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
|
23
|
-
@schema.use(GraphQL::Schema::AlwaysVisible)
|
24
|
-
|
25
26
|
@resolvers = resolvers
|
26
27
|
@resolvers_by_version = nil
|
27
28
|
@fields_by_type_and_location = nil
|
28
29
|
@locations_by_type = nil
|
29
|
-
@memoized_introspection_types =
|
30
|
+
@memoized_introspection_types = @schema.introspection_system.types
|
31
|
+
@memoized_schema_types = @schema.types
|
30
32
|
@memoized_schema_fields = {}
|
31
|
-
@memoized_schema_types = nil
|
32
33
|
@possible_keys_by_type = {}
|
33
34
|
@possible_keys_by_type_and_location = {}
|
34
|
-
@static_validator = nil
|
35
35
|
|
36
36
|
# add introspection types into the fields mapping
|
37
|
-
@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|
|
38
38
|
next unless type.kind.fields?
|
39
39
|
|
40
40
|
memo[type_name] = type.fields.keys.each_with_object({}) do |field_name, m|
|
@@ -48,6 +48,12 @@ module GraphQL
|
|
48
48
|
memo[location.to_s] = executable
|
49
49
|
end
|
50
50
|
end.freeze
|
51
|
+
|
52
|
+
@schema.use(GraphQL::Schema::AlwaysVisible)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_definition
|
56
|
+
@schema.to_definition
|
51
57
|
end
|
52
58
|
|
53
59
|
def resolvers_by_version
|
@@ -64,17 +70,9 @@ module GraphQL
|
|
64
70
|
@executables.keys.reject { _1 == SUPERGRAPH_LOCATION }
|
65
71
|
end
|
66
72
|
|
67
|
-
def memoized_introspection_types
|
68
|
-
@memoized_introspection_types ||= schema.introspection_system.types
|
69
|
-
end
|
70
|
-
|
71
|
-
def memoized_schema_types
|
72
|
-
@memoized_schema_types ||= @schema.types
|
73
|
-
end
|
74
|
-
|
75
73
|
def memoized_schema_fields(type_name)
|
76
74
|
@memoized_schema_fields[type_name] ||= begin
|
77
|
-
fields = memoized_schema_types[type_name].fields
|
75
|
+
fields = @memoized_schema_types[type_name].fields
|
78
76
|
@schema.introspection_system.dynamic_fields.each do |field|
|
79
77
|
fields[field.name] ||= field # adds __typename
|
80
78
|
end
|
data/lib/graphql/stitching.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2025-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -155,11 +155,13 @@ files:
|
|
155
155
|
- gemfiles/graphql_2.1.0.gemfile
|
156
156
|
- gemfiles/graphql_2.2.0.gemfile
|
157
157
|
- gemfiles/graphql_2.3.0.gemfile
|
158
|
+
- gemfiles/graphql_2.4.0.gemfile
|
158
159
|
- graphql-stitching.gemspec
|
159
160
|
- lib/graphql/stitching.rb
|
160
161
|
- lib/graphql/stitching/client.rb
|
161
162
|
- lib/graphql/stitching/composer.rb
|
162
163
|
- lib/graphql/stitching/composer/base_validator.rb
|
164
|
+
- lib/graphql/stitching/composer/supergraph_directives.rb
|
163
165
|
- lib/graphql/stitching/composer/type_resolver_config.rb
|
164
166
|
- lib/graphql/stitching/composer/validate_interfaces.rb
|
165
167
|
- lib/graphql/stitching/composer/validate_type_resolvers.rb
|
@@ -174,10 +176,7 @@ files:
|
|
174
176
|
- lib/graphql/stitching/request.rb
|
175
177
|
- lib/graphql/stitching/request/skip_include.rb
|
176
178
|
- lib/graphql/stitching/supergraph.rb
|
177
|
-
- lib/graphql/stitching/supergraph/
|
178
|
-
- lib/graphql/stitching/supergraph/resolver_directive.rb
|
179
|
-
- lib/graphql/stitching/supergraph/source_directive.rb
|
180
|
-
- lib/graphql/stitching/supergraph/to_definition.rb
|
179
|
+
- lib/graphql/stitching/supergraph/from_definition.rb
|
181
180
|
- lib/graphql/stitching/type_resolver.rb
|
182
181
|
- lib/graphql/stitching/type_resolver/arguments.rb
|
183
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,165 +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
|
-
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] = TypeResolver.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] << TypeResolver.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: TypeResolver.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
|