graphql-stitching 1.5.2 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf60ae0ef85426a3011223bcc146a74a1664762e5488d8266d0afbf1c2143457
4
- data.tar.gz: d20da5d4817193de7e156441cab88b0855f3fa93aeabe5e760f8fd66ac5b3547
3
+ metadata.gz: da2c14565c38d14e4e49f51ff2027f89a2ee4c0313102083c9ea3baadbcd02d4
4
+ data.tar.gz: eec311845ed143af3f8d598c22f2b6026a46d34868fd7546abfb585a8fed6ebc
5
5
  SHA512:
6
- metadata.gz: b89843bfdd353a6b4aec47fce17a6bd6524b29f7d5fa5dc0452570dc1b4bec63691171f7cd3f841c393015df763e20e1be7cf907cc660fcc576cc93e08c3d4dd
7
- data.tar.gz: a20f68f8bac0a52271fdc76ec4f173347669acda7f49b18e177746c83df1ec42dc878e8d3af37545a99f5456acc6d9f7577600d79c4568925cd23837fe5c2d0a
6
+ metadata.gz: 87608b9f5391ec190e0d0f534e1a58447f056b37e37c3f569d908b2c7043d40ef38732b3bff6e4975d81d34b04bb84cd88f809b364d73aeade68675f8c35f35f
7
+ data.tar.gz: eb1e085ad36c36b52b0c871d9d8f3ff0ca5b6d29ff51c1ea6cd9564c0cdba8e554381615099a331426f4857b82049290287c79cc1338388a36027cb451aa3bb9
@@ -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
 
data/docs/request.md CHANGED
@@ -15,7 +15,6 @@ request = GraphQL::Stitching::Request.new(
15
15
 
16
16
  A `Request` provides the following information:
17
17
 
18
- - `req.document`: parsed AST of the GraphQL source.
19
18
  - `req.variables`: a hash of user-submitted variables.
20
19
  - `req.string`: the original GraphQL source string, or printed document.
21
20
  - `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
@@ -31,6 +30,5 @@ A request manages the flow of stitching behaviors. These are sequenced by the `C
31
30
  component, or you may invoke them manually:
32
31
 
33
32
  1. `request.validate`: runs static validations on the request using the combined schema.
34
- 2. `request.prepare!`: inserts variable defaults and pre-renders skip/include conditional shaping.
35
- 3. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
36
- 4. `request.execute`: executes the request, and returns the resulting data.
33
+ 2. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
34
+ 3. `request.execute`: executes the request, and returns the resulting data.
@@ -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: "../"
@@ -45,7 +45,6 @@ module GraphQL
45
45
  return error_result(request, validation_errors) if validation_errors.any?
46
46
  end
47
47
 
48
- request.prepare!
49
48
  load_plan(request)
50
49
  request.execute
51
50
  rescue GraphQL::ParseError, GraphQL::ExecutionError => e
@@ -13,13 +13,11 @@ module GraphQL
13
13
  class Composer
14
14
  # @api private
15
15
  NO_DEFAULT_VALUE = begin
16
- class T < GraphQL::Schema::Object
17
- field(:f, String) do
18
- argument(:a, String)
19
- end
16
+ t = Class.new(GraphQL::Schema::Object) do
17
+ field(:f, String) { _1.argument(:a, String) }
20
18
  end
21
19
 
22
- T.get_field("f").get_argument("a").default_value
20
+ t.get_field("f").get_argument("a").default_value
23
21
  end
24
22
 
25
23
  # @api private
@@ -103,9 +101,8 @@ module GraphQL
103
101
  @subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
104
102
  raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
105
103
 
106
- introspection_types = schema.introspection_system.types.keys
107
104
  schema.types.each do |type_name, subgraph_type|
108
- next if introspection_types.include?(type_name)
105
+ next if subgraph_type.introspection?
109
106
 
110
107
  if type_name == @query_name && subgraph_type != schema.query
111
108
  raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
@@ -373,6 +370,7 @@ module GraphQL
373
370
  deprecation_reason: merge_deprecations(type_name, fields_by_location, field_name: field_name),
374
371
  type: Util.unwrap_non_null(type),
375
372
  null: !type.non_null?,
373
+ connection: false,
376
374
  camelize: false,
377
375
  )
378
376
 
@@ -403,14 +401,6 @@ module GraphQL
403
401
  next
404
402
  end
405
403
 
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
404
  kwargs = {}
415
405
  default_values_by_location = arguments_by_location.each_with_object({}) do |(location, argument), memo|
416
406
  next if argument.default_value == NO_DEFAULT_VALUE
@@ -648,9 +638,8 @@ module GraphQL
648
638
  writes = []
649
639
 
650
640
  schemas.each do |schema|
651
- introspection_types = schema.introspection_system.types.keys
652
641
  schema.types.each_value do |type|
653
- next if introspection_types.include?(type.graphql_name)
642
+ next if type.introspection?
654
643
 
655
644
  if type.kind.object? || type.kind.interface?
656
645
  type.fields.each_value do |field|
@@ -14,7 +14,7 @@ module GraphQL::Stitching
14
14
  end
15
15
 
16
16
  def perform!(raw)
17
- @root_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
17
+ @root_type = @request.query.root_type_for_operation(@request.operation.operation_type)
18
18
  resolve_object_scope(raw, @root_type, @request.operation.selections, @root_type.graphql_name)
19
19
  end
20
20
 
@@ -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,25 +103,10 @@ 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
 
121
- type.kind.abstract? && @supergraph.schema.possible_types(type).any? do |t|
109
+ type.kind.abstract? && @request.query.possible_types(type).any? do |t|
122
110
  t.graphql_name == typename
123
111
  end
124
112
  end
@@ -132,7 +132,7 @@ module GraphQL
132
132
  elsif @upload_types.include?(ast_node.name)
133
133
  files_by_path[path.dup] = value
134
134
  else
135
- type_def = request.supergraph.schema.get_type(ast_node.name)
135
+ type_def = request.query.get_type(ast_node.name)
136
136
  extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
137
137
  end
138
138
  end
@@ -106,7 +106,7 @@ module GraphQL
106
106
 
107
107
  # A) Group all root selections by their preferred entrypoint locations.
108
108
  def build_root_entrypoints
109
- parent_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
109
+ parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
110
110
 
111
111
  case @request.operation.operation_type
112
112
  when QUERY_OP
@@ -331,7 +331,7 @@ module GraphQL
331
331
  end
332
332
 
333
333
  if expanded_selections
334
- @supergraph.schema.possible_types(parent_type).each do |possible_type|
334
+ @request.query.possible_types(parent_type).each do |possible_type|
335
335
  next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
336
336
 
337
337
  type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
@@ -14,26 +14,21 @@ module GraphQL
14
14
  # @return [Supergraph] supergraph instance that resolves the request.
15
15
  attr_reader :supergraph
16
16
 
17
- # @return [GraphQL::Language::Nodes::Document] the parsed GraphQL AST document.
18
- attr_reader :document
19
-
20
- # @return [Hash] input variables for the request.
21
- attr_reader :variables
22
-
23
- # @return [String] operation name selected for the request.
24
- attr_reader :operation_name
17
+ # @return [GraphQL::Query] query object defining the request.
18
+ attr_reader :query
25
19
 
26
20
  # @return [Hash] contextual object passed through resolver flows.
27
21
  attr_reader :context
28
22
 
29
23
  # Creates a new supergraph request.
30
24
  # @param supergraph [Supergraph] supergraph instance that resolves the request.
31
- # @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
25
+ # @param source [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
32
26
  # @param operation_name [String, nil] operation name selected for the request.
33
27
  # @param variables [Hash, nil] input variables for the request.
34
28
  # @param context [Hash, nil] a contextual object passed through resolver flows.
35
- def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
29
+ def initialize(supergraph, source, operation_name: nil, variables: nil, context: nil)
36
30
  @supergraph = supergraph
31
+ @prepared_document = nil
37
32
  @string = nil
38
33
  @digest = nil
39
34
  @normalized_string = nil
@@ -44,29 +39,32 @@ module GraphQL
44
39
  @fragment_definitions = nil
45
40
  @plan = nil
46
41
 
47
- @document = if document.is_a?(String)
48
- @string = document
49
- GraphQL.parse(document)
42
+ params = {
43
+ operation_name: operation_name,
44
+ variables: variables,
45
+ context: context,
46
+ }
47
+
48
+ if source.is_a?(String)
49
+ @string = source
50
+ params[:query] = source
50
51
  else
51
- document
52
+ params[:document] = source
52
53
  end
53
54
 
54
- @operation_name = operation_name
55
- @variables = variables || {}
56
-
57
- @query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
55
+ @query = GraphQL::Query.new(@supergraph.schema, **params)
58
56
  @context = @query.context
59
57
  @context[:request] = self
60
58
  end
61
59
 
62
60
  # @return [String] the original document string, or a print of the parsed AST document.
63
61
  def string
64
- @string || normalized_string
62
+ with_prepared_document { @string || normalized_string }
65
63
  end
66
64
 
67
65
  # @return [String] a print of the parsed AST document with consistent whitespace.
68
66
  def normalized_string
69
- @normalized_string ||= @document.to_query_string
67
+ @normalized_string ||= prepared_document.to_query_string
70
68
  end
71
69
 
72
70
  # @return [String] a digest of the original document string. Generally faster but less consistent.
@@ -81,43 +79,48 @@ module GraphQL
81
79
 
82
80
  # @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
83
81
  def operation
84
- @operation ||= begin
85
- operation_defs = @document.definitions.select do |d|
82
+ @operation ||= with_prepared_document do
83
+ selected_op = @query.selected_operation
84
+ raise GraphQL::ExecutionError, "No operation selected" unless selected_op
85
+
86
+ @prepared_document.definitions.find do |d|
86
87
  next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
87
- @operation_name ? d.name == @operation_name : true
88
- end
89
88
 
90
- if operation_defs.length < 1
91
- raise GraphQL::ExecutionError, "Invalid root operation for given name and operation type."
92
- elsif operation_defs.length > 1
93
- raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
89
+ selected_op.name.nil? || d.name == selected_op.name
94
90
  end
91
+ end
92
+ end
95
93
 
96
- operation_defs.first
94
+ def operation_name
95
+ operation.name
96
+ end
97
+
98
+ # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
99
+ def operation_directives
100
+ @operation_directives ||= if operation.directives.any?
101
+ printer = GraphQL::Language::Printer.new
102
+ operation.directives.map { printer.print(_1) }.join(" ")
97
103
  end
98
104
  end
99
105
 
100
106
  # @return [Boolean] true if operation type is a query
101
107
  def query?
102
- operation.operation_type == QUERY_OP
108
+ @query.query?
103
109
  end
104
110
 
105
111
  # @return [Boolean] true if operation type is a mutation
106
112
  def mutation?
107
- operation.operation_type == MUTATION_OP
113
+ @query.mutation?
108
114
  end
109
115
 
110
116
  # @return [Boolean] true if operation type is a subscription
111
117
  def subscription?
112
- operation.operation_type == SUBSCRIPTION_OP
118
+ @query.subscription?
113
119
  end
114
120
 
115
- # @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
116
- def operation_directives
117
- @operation_directives ||= if operation.directives.any?
118
- printer = GraphQL::Language::Printer.new
119
- operation.directives.map { printer.print(_1) }.join(" ")
120
- end
121
+ # @return [Hash<String, Any>] provided variables hash filled in with default values from definitions
122
+ def variables
123
+ @variables || with_prepared_document { @variables }
121
124
  end
122
125
 
123
126
  # @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
@@ -129,7 +132,7 @@ module GraphQL
129
132
 
130
133
  # @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
131
134
  def fragment_definitions
132
- @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
135
+ @fragment_definitions ||= prepared_document.definitions.each_with_object({}) do |d, memo|
133
136
  memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
134
137
  end
135
138
  end
@@ -137,26 +140,7 @@ module GraphQL
137
140
  # Validates the request using the combined supergraph schema.
138
141
  # @return [Array<GraphQL::ExecutionError>] an array of static validation errors
139
142
  def validate
140
- result = @supergraph.schema.static_validator.validate(@query)
141
- result[:errors]
142
- end
143
-
144
- # Prepares the request for stitching by inserting variable defaults and applying @skip/@include conditionals.
145
- def prepare!
146
- operation.variables.each do |v|
147
- @variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
148
- end
149
-
150
- if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
151
- SkipInclude.render(@document, @variables) do |modified_ast|
152
- @document = modified_ast
153
- @string = @normalized_string = nil
154
- @digest = @normalized_digest = nil
155
- @operation = @operation_directives = @variable_definitions = @plan = nil
156
- end
157
- end
158
-
159
- self
143
+ @query.static_errors
160
144
  end
161
145
 
162
146
  # Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
@@ -191,6 +175,27 @@ module GraphQL
191
175
 
192
176
  private
193
177
 
178
+ # Prepares the request for stitching by applying @skip/@include conditionals.
179
+ def prepared_document
180
+ @prepared_document || with_prepared_document { @prepared_document }
181
+ end
182
+
183
+ def with_prepared_document
184
+ unless @prepared_document
185
+ @variables = @query.variables.to_h
186
+
187
+ @prepared_document = if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
188
+ changed = false
189
+ doc = SkipInclude.render(@query.document, @variables) { changed = true }
190
+ @string = @normalized_string = doc.to_query_string if changed
191
+ doc
192
+ else
193
+ @query.document
194
+ end
195
+ end
196
+ yield
197
+ end
198
+
194
199
  # Adds a handler into context for enriching subscription updates with stitched data
195
200
  def add_subscription_update_handler
196
201
  request = self
@@ -17,10 +17,9 @@ module GraphQL::Stitching
17
17
  field_map = {}
18
18
  resolver_map = {}
19
19
  possible_locations = {}
20
- introspection_types = schema.introspection_system.types.keys
21
20
 
22
21
  schema.types.each do |type_name, type|
23
- next if introspection_types.include?(type_name)
22
+ next if type.introspection?
24
23
 
25
24
  # Collect/build key definitions for each type
26
25
  locations_by_key = type.directives.each_with_object({}) do |directive, memo|
@@ -20,8 +20,6 @@ module GraphQL
20
20
 
21
21
  def initialize(schema:, fields: {}, resolvers: {}, executables: {})
22
22
  @schema = schema
23
- @schema.use(GraphQL::Schema::AlwaysVisible)
24
-
25
23
  @resolvers = resolvers
26
24
  @resolvers_by_version = nil
27
25
  @fields_by_type_and_location = nil
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- VERSION = "1.5.2"
5
+ VERSION = "1.6.1"
6
6
  end
7
7
  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.5.2
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg MacWilliam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-09 00:00:00.000000000 Z
11
+ date: 2025-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -155,6 +155,7 @@ 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