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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 227a7813eed1050c451d9bbee900305cfbac67aff35baa88f6ba9551535d1e97
4
- data.tar.gz: 1533e6109c6a72af3f7d07891714c09a73476ca0a5a7ea53d83cbe829564ace3
3
+ metadata.gz: b6b2cc734796d7455701bcc5b376a8efb49534c43b7a75de53bc1f2e686bf54c
4
+ data.tar.gz: 449c09b94257de6ae720b4b7938c13b8881da9f1f8dc7684aabdc4c493b2c6d2
5
5
  SHA512:
6
- metadata.gz: 8342158a10bc25fb68c833a3366c2fd6e831cf86cccf15a2491067680bb5299700783af57fd5042b394131b48759f077d32f4b210149a2b3cb34e43e3cca77ff
7
- data.tar.gz: 7313c93bb8cedf7ac06708a1f7fba6a2c46196982380a28a6d9244cf7320ad0149cb17df8ecb1abe438e14eac08b2f71e154dd9f2d6c3095be8e849c46061e16
6
+ metadata.gz: 94345a14cc9bcee462854188b543a170b3dbd65cc948e55773e1a07357f47026f41018ea35eedbacc8647d8a92634be1f143d86543f0ea092f48ddb1281a86f5
7
+ data.tar.gz: ae7cb67b6f36ca209f327d43bd293fa6afc4e6a93e8e91e83f4acf2893b744831836959402cd236e789ade5602857bb39d484025ea58c7e16d19b9ec5eb69d67
@@ -14,6 +14,8 @@ jobs:
14
14
  include:
15
15
  - gemfile: Gemfile
16
16
  ruby: 3.3
17
+ - gemfile: gemfiles/graphql_2.4.0.gemfile
18
+ ruby: 3.2
17
19
  - gemfile: gemfiles/graphql_2.3.0.gemfile
18
20
  ruby: 3.2
19
21
  - gemfile: gemfiles/graphql_2.2.0.gemfile
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
- While `Client` is sufficient for most usecases, the library offers several discrete components that can be assembled into tailored workflows:
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
  ![Merging types](./docs/images/merging.png)
89
89
 
90
- To facilitate this, schemas should be designed around [merged type keys](#merged-type-keys) that stitching can cross-reference and fetch across locations using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
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
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'graphql', '~> 2.4.0'
6
+ gem 'warning'
7
+ gem 'minitest-stub-const'
8
+
9
+ gemspec path: "../"
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphQL::Stitching
4
- class Supergraph
4
+ class Composer
5
+ class KeyDirective < GraphQL::Schema::Directive
6
+ graphql_name "key"
7
+ locations OBJECT, INTERFACE, UNION
8
+ argument :key, String, required: true
9
+ argument :location, String, required: true
10
+ repeatable true
11
+ end
12
+
5
13
  class ResolverDirective < GraphQL::Schema::Directive
6
14
  graphql_name "resolver"
7
15
  locations OBJECT, INTERFACE, UNION
@@ -14,5 +22,12 @@ module GraphQL::Stitching
14
22
  argument :type_name, String, required: false
15
23
  repeatable true
16
24
  end
25
+
26
+ class SourceDirective < GraphQL::Schema::Directive
27
+ graphql_name "source"
28
+ locations FIELD_DEFINITION
29
+ argument :location, String, required: true
30
+ repeatable true
31
+ end
17
32
  end
18
33
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "composer/base_validator"
4
+ require_relative "composer/supergraph_directives"
4
5
  require_relative "composer/validate_interfaces"
5
6
  require_relative "composer/validate_type_resolvers"
6
7
  require_relative "composer/type_resolver_config"
@@ -13,13 +14,11 @@ module GraphQL
13
14
  class Composer
14
15
  # @api private
15
16
  NO_DEFAULT_VALUE = begin
16
- class T < GraphQL::Schema::Object
17
- field(:f, String) do
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
- T.get_field("f").get_argument("a").default_value
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 - GraphQL::Stitching.stitching_directive_names).each do |directive_name|
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 introspection_types.include?(type_name)
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(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
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.new(
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 introspection_types.include?(type.graphql_name)
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
- next if introspection_field?(parent_type, node) do |is_root_typename|
35
- raw_object[field_name] = @root_type.graphql_name if is_root_typename
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/to_definition"
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, :locations_by_type_and_field
19
+ attr_reader :resolvers
20
+ attr_reader :memoized_schema_types
21
+ attr_reader :memoized_introspection_types
22
+ attr_reader :locations_by_type_and_field
20
23
 
21
24
  def initialize(schema:, fields: {}, resolvers: {}, executables: {})
22
25
  @schema = schema
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 = nil
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
@@ -65,6 +65,10 @@ module GraphQL
65
65
  argument_types: arguments.map(&:to_type_definition).join(", "),
66
66
  }.tap(&:compact!)
67
67
  end
68
+
69
+ def inspect
70
+ as_json.to_json
71
+ end
68
72
  end
69
73
  end
70
74
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.6.0"
5
+ VERSION = "1.6.2"
6
6
  end
7
7
  end
@@ -49,12 +49,6 @@ module GraphQL
49
49
  def stitch_directive
50
50
  @stitch_directive ||= "stitch"
51
51
  end
52
-
53
- # Names of stitching directives to omit from the composed supergraph.
54
- # @returns [Array<String>] list of stitching directive names.
55
- def stitching_directive_names
56
- [stitch_directive]
57
- end
58
52
  end
59
53
  end
60
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-stitching
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
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-03-17 00:00:00.000000000 Z
11
+ date: 2025-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -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/key_directive.rb
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