graphql 2.0.13 → 2.0.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8f389d3b7c8052a74ccd4bd9e8412a1fa9d93415e6caeec883b8f179a167395
4
- data.tar.gz: f2e6ab7ba5c1e76b0c44557855ab3af55fb2b718b8de1ff6483917de603734b3
3
+ metadata.gz: a3bc6610f88b6689ee6991a6ad3543580f9bdf1b3cb9f2f7e3f78862d76072f7
4
+ data.tar.gz: ce52505d6c43e330aa8e5def38719f62bdfe067274a86530c889350c64994bd0
5
5
  SHA512:
6
- metadata.gz: 10a24271a65c65a402d3243d2e43b3b257f74eb7bbc0ec13c254304bdce3122c444a41fbccf943bd3d626d301aec4b973485f742b831e8658ea252a0c95cfcfa
7
- data.tar.gz: 9762008c699f2a53b14913e057dc31ec6c97b4203b1dd28a7a2a4fc18153d5bc200d0a95e3d2a04da80d0486131f5b1ce8d597e1ad6a7085b0766e35e0982959
6
+ metadata.gz: 8f3aac93aa1013c257c84521b54285719d6edb8c212a83140123902a06f3cffabc1005d2a176b723cff5677614eab7ae1617b71ce7eb403e721fcbafbe07c1e1
7
+ data.tar.gz: a84bf37728b5c0ff8795070fd682ac7af83e8888d47cadff5103a76ee333788c30bf78f505528fee4f868288b99dfc11d82f7c604710e13b4a7d9690eb2efe36
@@ -23,5 +23,8 @@ class <%= schema_name %> < GraphQL::Schema
23
23
  # to return the correct GraphQL object type for `obj`
24
24
  raise(GraphQL::RequiredImplementationMissingError)
25
25
  end
26
+
27
+ # Stop validating when it encounters this many errors:
28
+ validate_max_errors(100)
26
29
  end
27
30
  <% end -%>
@@ -86,6 +86,15 @@ module GraphQL
86
86
  !@pending_keys.empty?
87
87
  end
88
88
 
89
+ # Add these key-value pairs to this source's cache
90
+ # (future loads will use these merged values).
91
+ # @param results [Hash<Object => Object>] key-value pairs to cache in this source
92
+ # @return [void]
93
+ def merge(results)
94
+ @results.merge!(results)
95
+ nil
96
+ end
97
+
89
98
  # Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
90
99
  # @api private
91
100
  # @return [void]
@@ -11,76 +11,202 @@ require "graphql/execution/interpreter/handles_raw_value"
11
11
  module GraphQL
12
12
  module Execution
13
13
  class Interpreter
14
- def self.begin_multiplex(multiplex)
15
- # Since this is basically the batching context,
16
- # share it for a whole multiplex
17
- multiplex.context[:interpreter_instance] ||= self.new
18
- end
14
+ class << self
15
+ # Used internally to signal that the query shouldn't be executed
16
+ # @api private
17
+ NO_OPERATION = {}.freeze
19
18
 
20
- def self.begin_query(query, multiplex)
21
- # The batching context is shared by the multiplex,
22
- # so fetch it out and use that instance.
23
- interpreter =
24
- query.context.namespace(:interpreter)[:interpreter_instance] =
25
- multiplex.context[:interpreter_instance]
26
- interpreter.evaluate(query)
27
- query
28
- end
19
+ # @param schema [GraphQL::Schema]
20
+ # @param queries [Array<GraphQL::Query, Hash>]
21
+ # @param context [Hash]
22
+ # @param max_complexity [Integer, nil]
23
+ # @return [Array<Hash>] One result per query
24
+ def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
25
+ queries = query_options.map do |opts|
26
+ case opts
27
+ when Hash
28
+ GraphQL::Query.new(schema, nil, **opts)
29
+ when GraphQL::Query
30
+ opts
31
+ else
32
+ raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
33
+ end
34
+ end
29
35
 
30
- def self.finish_multiplex(_results, multiplex)
31
- interpreter = multiplex.context[:interpreter_instance]
32
- interpreter.sync_lazies(multiplex: multiplex)
33
- end
36
+ multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
37
+ multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
38
+ schema = multiplex.schema
39
+ queries = multiplex.queries
40
+ query_instrumenters = schema.instrumenters[:query]
41
+ multiplex_instrumenters = schema.instrumenters[:multiplex]
34
42
 
35
- def self.finish_query(query, _multiplex)
36
- {
37
- "data" => query.context.namespace(:interpreter)[:runtime].final_result
38
- }
39
- end
43
+ # First, run multiplex instrumentation, then query instrumentation for each query
44
+ call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do
45
+ each_query_call_hooks(query_instrumenters, queries) do
46
+ schema = multiplex.schema
47
+ multiplex_analyzers = schema.multiplex_analyzers
48
+ queries = multiplex.queries
49
+ if multiplex.max_complexity
50
+ multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
51
+ end
40
52
 
41
- # Run the eager part of `query`
42
- # @return {Interpreter::Runtime}
43
- def evaluate(query)
44
- # Although queries in a multiplex _share_ an Interpreter instance,
45
- # they also have another item of state, which is private to that query
46
- # in particular, assign it here:
47
- runtime = Runtime.new(query: query)
48
- query.context.namespace(:interpreter)[:runtime] = runtime
49
-
50
- query.trace("execute_query", {query: query}) do
51
- runtime.run_eager
52
- end
53
+ schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
54
+ begin
55
+ # Since this is basically the batching context,
56
+ # share it for a whole multiplex
57
+ multiplex.context[:interpreter_instance] ||= multiplex.schema.query_execution_strategy.new
58
+ # Do as much eager evaluation of the query as possible
59
+ results = []
60
+ queries.each_with_index do |query, idx|
61
+ multiplex.dataloader.append_job {
62
+ operation = query.selected_operation
63
+ result = if operation.nil? || !query.valid? || query.context.errors.any?
64
+ NO_OPERATION
65
+ else
66
+ begin
67
+ # Although queries in a multiplex _share_ an Interpreter instance,
68
+ # they also have another item of state, which is private to that query
69
+ # in particular, assign it here:
70
+ runtime = Runtime.new(query: query)
71
+ query.context.namespace(:interpreter)[:runtime] = runtime
53
72
 
54
- runtime
55
- end
73
+ query.trace("execute_query", {query: query}) do
74
+ runtime.run_eager
75
+ end
76
+ rescue GraphQL::ExecutionError => err
77
+ query.context.errors << err
78
+ NO_OPERATION
79
+ end
80
+ end
81
+ results[idx] = result
82
+ }
83
+ end
84
+
85
+ multiplex.dataloader.run
86
+
87
+ # Then, work through lazy results in a breadth-first way
88
+ multiplex.dataloader.append_job {
89
+ tracer = multiplex
90
+ query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
91
+ queries = multiplex ? multiplex.queries : [query]
92
+ final_values = queries.map do |query|
93
+ runtime = query.context.namespace(:interpreter)[:runtime]
94
+ # it might not be present if the query has an error
95
+ runtime ? runtime.final_result : nil
96
+ end
97
+ final_values.compact!
98
+ tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
99
+ Interpreter::Resolve.resolve_all(final_values, multiplex.dataloader)
100
+ end
101
+ queries.each do |query|
102
+ runtime = query.context.namespace(:interpreter)[:runtime]
103
+ if runtime
104
+ runtime.delete_interpreter_context(:current_path)
105
+ runtime.delete_interpreter_context(:current_field)
106
+ runtime.delete_interpreter_context(:current_object)
107
+ runtime.delete_interpreter_context(:current_arguments)
108
+ end
109
+ end
110
+ }
111
+ multiplex.dataloader.run
112
+
113
+ # Then, find all errors and assign the result to the query object
114
+ results.each_with_index do |data_result, idx|
115
+ query = queries[idx]
116
+ # Assign the result so that it can be accessed in instrumentation
117
+ query.result_values = if data_result.equal?(NO_OPERATION)
118
+ if !query.valid? || query.context.errors.any?
119
+ # A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
120
+ { "errors" => query.static_errors.map(&:to_h) }
121
+ else
122
+ data_result
123
+ end
124
+ else
125
+ result = {
126
+ "data" => query.context.namespace(:interpreter)[:runtime].final_result
127
+ }
128
+
129
+ if query.context.errors.any?
130
+ error_result = query.context.errors.map(&:to_h)
131
+ result["errors"] = error_result
132
+ end
56
133
 
57
- # Run the lazy part of `query` or `multiplex`.
58
- # @return [void]
59
- def sync_lazies(query: nil, multiplex: nil)
60
- tracer = query || multiplex
61
- if query.nil? && multiplex.queries.length == 1
62
- query = multiplex.queries[0]
134
+ result
135
+ end
136
+ if query.context.namespace?(:__query_result_extensions__)
137
+ query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
138
+ end
139
+ # Get the Query::Result, not the Hash
140
+ results[idx] = query.result
141
+ end
142
+
143
+ results
144
+ rescue Exception
145
+ # TODO rescue at a higher level so it will catch errors in analysis, too
146
+ # Assign values here so that the query's `@executed` becomes true
147
+ queries.map { |q| q.result_values ||= {} }
148
+ raise
149
+ end
150
+ end
151
+ end
152
+ end
63
153
  end
64
- queries = multiplex ? multiplex.queries : [query]
65
- final_values = queries.map do |query|
66
- runtime = query.context.namespace(:interpreter)[:runtime]
67
- # it might not be present if the query has an error
68
- runtime ? runtime.final_result : nil
154
+
155
+ private
156
+
157
+ # Call the before_ hooks of each query,
158
+ # Then yield if no errors.
159
+ # `call_hooks` takes care of appropriate cleanup.
160
+ def each_query_call_hooks(instrumenters, queries, i = 0)
161
+ if i >= queries.length
162
+ yield
163
+ else
164
+ query = queries[i]
165
+ call_hooks(instrumenters, query, :before_query, :after_query) {
166
+ each_query_call_hooks(instrumenters, queries, i + 1) {
167
+ yield
168
+ }
169
+ }
170
+ end
69
171
  end
70
- final_values.compact!
71
- tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
72
- Interpreter::Resolve.resolve_all(final_values, multiplex.dataloader)
172
+
173
+ # Call each before hook, and if they all succeed, yield.
174
+ # If they don't all succeed, call after_ for each one that succeeded.
175
+ def call_hooks(instrumenters, object, before_hook_name, after_hook_name)
176
+ begin
177
+ successful = []
178
+ instrumenters.each do |instrumenter|
179
+ instrumenter.public_send(before_hook_name, object)
180
+ successful << instrumenter
181
+ end
182
+
183
+ # if any before hooks raise an exception, quit calling before hooks,
184
+ # but call the after hooks on anything that succeeded but also
185
+ # raise the exception that came from the before hook.
186
+ rescue GraphQL::ExecutionError => err
187
+ object.context.errors << err
188
+ rescue => e
189
+ raise call_after_hooks(successful, object, after_hook_name, e)
190
+ end
191
+
192
+ begin
193
+ yield # Call the user code
194
+ ensure
195
+ ex = call_after_hooks(successful, object, after_hook_name, nil)
196
+ raise ex if ex
197
+ end
73
198
  end
74
- queries.each do |query|
75
- runtime = query.context.namespace(:interpreter)[:runtime]
76
- if runtime
77
- runtime.delete_interpreter_context(:current_path)
78
- runtime.delete_interpreter_context(:current_field)
79
- runtime.delete_interpreter_context(:current_object)
80
- runtime.delete_interpreter_context(:current_arguments)
199
+
200
+ def call_after_hooks(instrumenters, object, after_hook_name, ex)
201
+ instrumenters.reverse_each do |instrumenter|
202
+ begin
203
+ instrumenter.public_send(after_hook_name, object)
204
+ rescue => e
205
+ ex = e
206
+ end
81
207
  end
208
+ ex
82
209
  end
83
- nil
84
210
  end
85
211
 
86
212
  class ListResultFailedError < GraphQL::Error
@@ -87,16 +87,28 @@ module GraphQL
87
87
 
88
88
  # Like {#selects?}, but can be used for chaining.
89
89
  # It returns a null object (check with {#selected?})
90
+ # @param field_name [String, Symbol]
90
91
  # @return [GraphQL::Execution::Lookahead]
91
92
  def selection(field_name, selected_type: @selected_type, arguments: nil)
92
- next_field_name = normalize_name(field_name)
93
+ next_field_defn = case field_name
94
+ when String
95
+ @query.get_field(selected_type, field_name)
96
+ when Symbol
97
+ # Try to avoid the `.to_s` below, if possible
98
+ all_fields = @query.warden.fields(selected_type)
99
+ if (match_by_orig_name = all_fields.find { |f| f.original_name == field_name })
100
+ match_by_orig_name
101
+ else
102
+ guessed_name = Schema::Member::BuildType.camelize(field_name.to_s)
103
+ @query.get_field(selected_type, guessed_name)
104
+ end
105
+ end
93
106
 
94
- next_field_defn = @query.get_field(selected_type, next_field_name)
95
107
  if next_field_defn
96
108
  next_nodes = []
97
109
  @ast_nodes.each do |ast_node|
98
110
  ast_node.selections.each do |selection|
99
- find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
111
+ find_selected_nodes(selection, next_field_defn, arguments: arguments, matches: next_nodes)
100
112
  end
101
113
  end
102
114
 
@@ -196,23 +208,6 @@ module GraphQL
196
208
 
197
209
  private
198
210
 
199
- # If it's a symbol, stringify and camelize it
200
- def normalize_name(name)
201
- if name.is_a?(Symbol)
202
- Schema::Member::BuildType.camelize(name.to_s)
203
- else
204
- name
205
- end
206
- end
207
-
208
- def normalize_keyword(keyword)
209
- if keyword.is_a?(String)
210
- Schema::Member::BuildType.underscore(keyword).to_sym
211
- else
212
- keyword
213
- end
214
- end
215
-
216
211
  def skipped_by_directive?(ast_selection)
217
212
  ast_selection.directives.each do |directive|
218
213
  dir_defn = @query.schema.directives.fetch(directive.name)
@@ -265,11 +260,11 @@ module GraphQL
265
260
 
266
261
  # If a selection on `node` matches `field_name` (which is backed by `field_defn`)
267
262
  # and matches the `arguments:` constraints, then add that node to `matches`
268
- def find_selected_nodes(node, field_name, field_defn, arguments:, matches:)
263
+ def find_selected_nodes(node, field_defn, arguments:, matches:)
269
264
  return if skipped_by_directive?(node)
270
265
  case node
271
266
  when GraphQL::Language::Nodes::Field
272
- if node.name == field_name
267
+ if node.name == field_defn.graphql_name
273
268
  if arguments.nil? || arguments.empty?
274
269
  # No constraint applied
275
270
  matches << node
@@ -278,10 +273,10 @@ module GraphQL
278
273
  end
279
274
  end
280
275
  when GraphQL::Language::Nodes::InlineFragment
281
- node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) }
276
+ node.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
282
277
  when GraphQL::Language::Nodes::FragmentSpread
283
278
  frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})")
284
- frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) }
279
+ frag_defn.selections.each { |s| find_selected_nodes(s, field_defn, arguments: arguments, matches: matches) }
285
280
  else
286
281
  raise "Unexpected selection comparison on #{node.class.name} (#{node})"
287
282
  end
@@ -290,9 +285,14 @@ module GraphQL
290
285
  def arguments_match?(arguments, field_defn, field_node)
291
286
  query_kwargs = @query.arguments_for(field_node, field_defn)
292
287
  arguments.all? do |arg_name, arg_value|
293
- arg_name = normalize_keyword(arg_name)
288
+ arg_name_sym = if arg_name.is_a?(String)
289
+ Schema::Member::BuildType.underscore(arg_name).to_sym
290
+ else
291
+ arg_name
292
+ end
293
+
294
294
  # Make sure the constraint is present with a matching value
295
- query_kwargs.key?(arg_name) && query_kwargs[arg_name] == arg_value
295
+ query_kwargs.key?(arg_name_sym) && query_kwargs[arg_name_sym] == arg_value
296
296
  end
297
297
  end
298
298
  end
@@ -23,13 +23,10 @@ module GraphQL
23
23
  # @see {Schema#multiplex} for public API
24
24
  # @api private
25
25
  class Multiplex
26
- # Used internally to signal that the query shouldn't be executed
27
- # @api private
28
- NO_OPERATION = {}.freeze
29
-
30
26
  include Tracing::Traceable
31
27
 
32
28
  attr_reader :context, :queries, :schema, :max_complexity, :dataloader
29
+
33
30
  def initialize(schema:, queries:, context:, max_complexity:)
34
31
  @schema = schema
35
32
  @queries = queries
@@ -43,118 +40,6 @@ module GraphQL
43
40
  end
44
41
  @max_complexity = max_complexity
45
42
  end
46
-
47
- class << self
48
- # @param schema [GraphQL::Schema]
49
- # @param queries [Array<GraphQL::Query, Hash>]
50
- # @param context [Hash]
51
- # @param max_complexity [Integer, nil]
52
- # @return [Array<Hash>] One result per query
53
- def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
54
- queries = query_options.map do |opts|
55
- case opts
56
- when Hash
57
- GraphQL::Query.new(schema, nil, **opts)
58
- when GraphQL::Query
59
- opts
60
- else
61
- raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
62
- end
63
- end
64
-
65
- multiplex = self.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
66
- multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
67
- GraphQL::Execution::Instrumentation.apply_instrumenters(multiplex) do
68
- schema = multiplex.schema
69
- multiplex_analyzers = schema.multiplex_analyzers
70
- if multiplex.max_complexity
71
- multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
72
- end
73
-
74
- schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
75
-
76
- begin
77
- multiplex.schema.query_execution_strategy.begin_multiplex(multiplex)
78
- # Do as much eager evaluation of the query as possible
79
- results = []
80
- queries.each_with_index do |query, idx|
81
- multiplex.dataloader.append_job { begin_query(results, idx, query, multiplex) }
82
- end
83
-
84
- multiplex.dataloader.run
85
-
86
- # Then, work through lazy results in a breadth-first way
87
- multiplex.dataloader.append_job {
88
- multiplex.schema.query_execution_strategy.finish_multiplex(results, multiplex)
89
- }
90
- multiplex.dataloader.run
91
-
92
- # Then, find all errors and assign the result to the query object
93
- results.each_with_index do |data_result, idx|
94
- query = queries[idx]
95
- finish_query(data_result, query, multiplex)
96
- # Get the Query::Result, not the Hash
97
- results[idx] = query.result
98
- end
99
-
100
- results
101
- rescue Exception
102
- # TODO rescue at a higher level so it will catch errors in analysis, too
103
- # Assign values here so that the query's `@executed` becomes true
104
- queries.map { |q| q.result_values ||= {} }
105
- raise
106
- end
107
- end
108
- end
109
- end
110
-
111
- # @param query [GraphQL::Query]
112
- def begin_query(results, idx, query, multiplex)
113
- operation = query.selected_operation
114
- result = if operation.nil? || !query.valid? || query.context.errors.any?
115
- NO_OPERATION
116
- else
117
- begin
118
- query.schema.query_execution_strategy.begin_query(query, multiplex)
119
- rescue GraphQL::ExecutionError => err
120
- query.context.errors << err
121
- NO_OPERATION
122
- end
123
- end
124
- results[idx] = result
125
- nil
126
- end
127
-
128
- private
129
-
130
- # @param data_result [Hash] The result for the "data" key, if any
131
- # @param query [GraphQL::Query] The query which was run
132
- # @return [Hash] final result of this query, including all values and errors
133
- def finish_query(data_result, query, multiplex)
134
- # Assign the result so that it can be accessed in instrumentation
135
- query.result_values = if data_result.equal?(NO_OPERATION)
136
- if !query.valid? || query.context.errors.any?
137
- # A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
138
- { "errors" => query.static_errors.map(&:to_h) }
139
- else
140
- data_result
141
- end
142
- else
143
- # Use `context.value` which was assigned during execution
144
- result = query.schema.query_execution_strategy.finish_query(query, multiplex)
145
-
146
- if query.context.errors.any?
147
- error_result = query.context.errors.map(&:to_h)
148
- result["errors"] = error_result
149
- end
150
-
151
- result
152
- end
153
- if query.context.namespace?(:__query_result_extensions__)
154
- query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
155
- end
156
- end
157
- end
158
43
  end
159
44
  end
160
45
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/execution/directive_checks"
3
- require "graphql/execution/instrumentation"
4
3
  require "graphql/execution/interpreter"
5
4
  require "graphql/execution/lazy"
6
5
  require "graphql/execution/lookahead"
@@ -29,6 +29,13 @@ module GraphQL
29
29
 
30
30
  field :specifiedByURL, String, resolver_method: :specified_by_url
31
31
 
32
+ field :is_one_of, Boolean, null: false
33
+
34
+ def is_one_of
35
+ object.kind.input_object? &&
36
+ object.directives.any? { |d| d.graphql_name == "oneOf" }
37
+ end
38
+
32
39
  def specified_by_url
33
40
  if object.kind.scalar?
34
41
  object.specified_by_url
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  module Introspection
4
- def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false)
4
+ def self.query(include_deprecated_args: false, include_schema_description: false, include_is_repeatable: false, include_specified_by_url: false, include_is_one_of: false)
5
5
  # The introspection query to end all introspection queries, copied from
6
6
  # https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js
7
7
  <<-QUERY
@@ -30,6 +30,7 @@ fragment FullType on __Type {
30
30
  name
31
31
  description
32
32
  #{include_specified_by_url ? "specifiedByURL" : ""}
33
+ #{include_is_one_of ? "isOneOf" : ""}
33
34
  fields(includeDeprecated: true) {
34
35
  name
35
36
  description
data/lib/graphql/query.rb CHANGED
@@ -196,7 +196,7 @@ module GraphQL
196
196
  # @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
197
197
  def result
198
198
  if !@executed
199
- Execution::Multiplex.run_all(@schema, [self], context: @context)
199
+ Execution::Interpreter.run_all(@schema, [self], context: @context)
200
200
  end
201
201
  @result ||= Query::Result.new(query: self, values: @result_values)
202
202
  end
@@ -55,14 +55,13 @@ module GraphQL
55
55
  end
56
56
  })
57
57
 
58
+ directives.merge!(GraphQL::Schema.default_directives)
58
59
  document.definitions.each do |definition|
59
60
  if definition.is_a?(GraphQL::Language::Nodes::DirectiveDefinition)
60
61
  directives[definition.name] = build_directive(definition, directive_type_resolver)
61
62
  end
62
63
  end
63
64
 
64
- directives = GraphQL::Schema.default_directives.merge(directives)
65
-
66
65
  # In case any directives referenced built-in types for their arguments:
67
66
  replace_late_bound_types_with_built_in(types)
68
67
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Directive < GraphQL::Schema::Member
5
+ class OneOf < GraphQL::Schema::Directive
6
+ description "Requires that exactly one field must be supplied and that field must not be `null`."
7
+ locations(GraphQL::Schema::Directive::INPUT_OBJECT)
8
+ default_directive true
9
+ end
10
+ end
11
+ end
12
+ end
@@ -69,6 +69,19 @@ module GraphQL
69
69
  true
70
70
  end
71
71
 
72
+ def self.one_of
73
+ if !one_of?
74
+ if all_argument_definitions.any? { |arg| arg.type.non_null? }
75
+ raise ArgumentError, "`one_of` may not be used with required arguments -- add `required: false` to argument definitions to use `one_of`"
76
+ end
77
+ directive(GraphQL::Schema::Directive::OneOf)
78
+ end
79
+ end
80
+
81
+ def self.one_of?
82
+ directives.any? { |d| d.is_a?(GraphQL::Schema::Directive::OneOf) }
83
+ end
84
+
72
85
  def unwrap_value(value)
73
86
  case value
74
87
  when Array
@@ -109,6 +122,14 @@ module GraphQL
109
122
  class << self
110
123
  def argument(*args, **kwargs, &block)
111
124
  argument_defn = super(*args, **kwargs, &block)
125
+ if one_of?
126
+ if argument_defn.type.non_null?
127
+ raise ArgumentError, "Argument '#{argument_defn.path}' must be nullable because it is part of a OneOf type, add `required: false`."
128
+ end
129
+ if argument_defn.default_value?
130
+ raise ArgumentError, "Argument '#{argument_defn.path}' cannot have a default value because it is part of a OneOf type, remove `default_value: ...`."
131
+ end
132
+ end
112
133
  # Add a method access
113
134
  method_name = argument_defn.keyword
114
135
  class_eval <<-RUBY, __FILE__, __LINE__
@@ -166,6 +187,20 @@ module GraphQL
166
187
  end
167
188
  end
168
189
 
190
+ if one_of?
191
+ if input.size == 1
192
+ input.each do |name, value|
193
+ if value.nil?
194
+ result ||= Query::InputValidationResult.new
195
+ result.add_problem("'#{graphql_name}' requires exactly one argument, but '#{name}' was `null`.")
196
+ end
197
+ end
198
+ else
199
+ result ||= Query::InputValidationResult.new
200
+ result.add_problem("'#{graphql_name}' requires exactly one argument, but #{input.size} were provided.")
201
+ end
202
+ end
203
+
169
204
  result
170
205
  end
171
206
 
@@ -27,6 +27,10 @@ module GraphQL
27
27
  "#<LateBoundType @name=#{name}>"
28
28
  end
29
29
 
30
+ def non_null?
31
+ false
32
+ end
33
+
30
34
  alias :to_s :inspect
31
35
  end
32
36
  end
@@ -31,6 +31,7 @@ require "graphql/schema/union"
31
31
  require "graphql/schema/directive"
32
32
  require "graphql/schema/directive/deprecated"
33
33
  require "graphql/schema/directive/include"
34
+ require "graphql/schema/directive/one_of"
34
35
  require "graphql/schema/directive/skip"
35
36
  require "graphql/schema/directive/feature"
36
37
  require "graphql/schema/directive/flagged"
@@ -913,6 +914,7 @@ module GraphQL
913
914
  "include" => GraphQL::Schema::Directive::Include,
914
915
  "skip" => GraphQL::Schema::Directive::Skip,
915
916
  "deprecated" => GraphQL::Schema::Directive::Deprecated,
917
+ "oneOf" => GraphQL::Schema::Directive::OneOf,
916
918
  }.freeze
917
919
  end
918
920
 
@@ -990,7 +992,7 @@ module GraphQL
990
992
  # @param context [Hash] Multiplex-level context
991
993
  # @return [Array<Hash>] One result for each query in the input
992
994
  def multiplex(queries, **kwargs)
993
- GraphQL::Execution::Multiplex.run_all(self, queries, **kwargs)
995
+ GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
994
996
  end
995
997
 
996
998
  def instrumenters
@@ -36,6 +36,7 @@ module GraphQL
36
36
  GraphQL::StaticValidation::QueryRootExists,
37
37
  GraphQL::StaticValidation::SubscriptionRootExists,
38
38
  GraphQL::StaticValidation::InputObjectNamesAreUnique,
39
+ GraphQL::StaticValidation::OneOfInputObjectsAreValid,
39
40
  ]
40
41
  end
41
42
  end
@@ -108,6 +108,10 @@ module GraphQL
108
108
  arg_type = @warden.get_argument(type, name).type
109
109
  recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
110
110
  end
111
+
112
+ if type.one_of? && ast_node.arguments.size != 1
113
+ results << Query::InputValidationResult.from_problem("`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{ast_node.arguments.size})")
114
+ end
111
115
  merge_results(results)
112
116
  end
113
117
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ module OneOfInputObjectsAreValid
5
+ def on_input_object(node, parent)
6
+ return super unless parent.is_a?(GraphQL::Language::Nodes::Argument)
7
+
8
+ parent_type = get_parent_type(context, parent)
9
+ return super unless parent_type && parent_type.kind.input_object? && parent_type.one_of?
10
+
11
+ validate_one_of_input_object(node, context, parent_type)
12
+ super
13
+ end
14
+
15
+ private
16
+
17
+ def validate_one_of_input_object(ast_node, context, parent_type)
18
+ present_fields = ast_node.arguments.map(&:name)
19
+ input_object_type = parent_type.to_type_signature
20
+
21
+ if present_fields.count != 1
22
+ add_error(
23
+ OneOfInputObjectsAreValidError.new(
24
+ "OneOf Input Object '#{input_object_type}' must specify exactly one key.",
25
+ path: context.path,
26
+ nodes: ast_node,
27
+ input_object_type: input_object_type
28
+ )
29
+ )
30
+ return
31
+ end
32
+
33
+ field = present_fields.first
34
+ value = ast_node.arguments.first.value
35
+
36
+ if value.is_a?(GraphQL::Language::Nodes::NullValue)
37
+ add_error(
38
+ OneOfInputObjectsAreValidError.new(
39
+ "Argument '#{input_object_type}.#{field}' must be non-null.",
40
+ path: [*context.path, field],
41
+ nodes: ast_node.arguments.first,
42
+ input_object_type: input_object_type
43
+ )
44
+ )
45
+ return
46
+ end
47
+
48
+ if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
49
+ variable_name = value.name
50
+ variable_type = @declared_variables[variable_name].type
51
+
52
+ unless variable_type.is_a?(GraphQL::Language::Nodes::NonNullType)
53
+ add_error(
54
+ OneOfInputObjectsAreValidError.new(
55
+ "Variable '#{variable_name}' must be non-nullable to be used for OneOf Input Object '#{input_object_type}'.",
56
+ path: [*context.path, field],
57
+ nodes: ast_node,
58
+ input_object_type: input_object_type
59
+ )
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class OneOfInputObjectsAreValidError < StaticValidation::Error
5
+ attr_reader :input_object_type
6
+
7
+ def initialize(message, path:, nodes:, input_object_type:)
8
+ super(message, path: path, nodes: nodes)
9
+ @input_object_type = input_object_type
10
+ end
11
+
12
+ # A hash representation of this Message
13
+ def to_h
14
+ extensions = {
15
+ "code" => code,
16
+ "inputObjectType" => input_object_type
17
+ }
18
+
19
+ super.merge({
20
+ "extensions" => extensions
21
+ })
22
+ end
23
+
24
+ def code
25
+ "invalidOneOfInputObject"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -23,7 +23,7 @@ module GraphQL
23
23
  problems = validation_result.problems
24
24
  first_problem = problems && problems.first
25
25
  if first_problem
26
- error_message = first_problem["message"]
26
+ error_message = first_problem["explanation"]
27
27
  end
28
28
 
29
29
  error_message ||= "Default value for $#{node.name} doesn't match type #{type.to_type_signature}"
@@ -75,10 +75,12 @@ module GraphQL
75
75
  end
76
76
 
77
77
  def analytics_enabled?
78
+ # [Deprecated] options[:analytics_enabled] will be removed in the future
78
79
  analytics_available? && Datadog::Contrib::Analytics.enabled?(options.fetch(:analytics_enabled, false))
79
80
  end
80
81
 
81
82
  def analytics_sample_rate
83
+ # [Deprecated] options[:analytics_sample_rate] will be removed in the future
82
84
  options.fetch(:analytics_sample_rate, 1.0)
83
85
  end
84
86
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.0.13"
3
+ VERSION = "2.0.14"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.13
4
+ version: 2.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-12 00:00:00.000000000 Z
11
+ date: 2022-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -303,7 +303,6 @@ files:
303
303
  - lib/graphql/execution.rb
304
304
  - lib/graphql/execution/directive_checks.rb
305
305
  - lib/graphql/execution/errors.rb
306
- - lib/graphql/execution/instrumentation.rb
307
306
  - lib/graphql/execution/interpreter.rb
308
307
  - lib/graphql/execution/interpreter/argument_value.rb
309
308
  - lib/graphql/execution/interpreter/arguments.rb
@@ -394,6 +393,7 @@ files:
394
393
  - lib/graphql/schema/directive/feature.rb
395
394
  - lib/graphql/schema/directive/flagged.rb
396
395
  - lib/graphql/schema/directive/include.rb
396
+ - lib/graphql/schema/directive/one_of.rb
397
397
  - lib/graphql/schema/directive/skip.rb
398
398
  - lib/graphql/schema/directive/transform.rb
399
399
  - lib/graphql/schema/enum.rb
@@ -497,6 +497,8 @@ files:
497
497
  - lib/graphql/static_validation/rules/mutation_root_exists_error.rb
498
498
  - lib/graphql/static_validation/rules/no_definitions_are_present.rb
499
499
  - lib/graphql/static_validation/rules/no_definitions_are_present_error.rb
500
+ - lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb
501
+ - lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb
500
502
  - lib/graphql/static_validation/rules/operation_names_are_valid.rb
501
503
  - lib/graphql/static_validation/rules/operation_names_are_valid_error.rb
502
504
  - lib/graphql/static_validation/rules/query_root_exists.rb
@@ -595,7 +597,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
595
597
  - !ruby/object:Gem::Version
596
598
  version: '0'
597
599
  requirements: []
598
- rubygems_version: 3.2.22
600
+ rubygems_version: 3.2.33
599
601
  signing_key:
600
602
  specification_version: 4
601
603
  summary: A GraphQL language and runtime for Ruby
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
- module GraphQL
3
- module Execution
4
- module Instrumentation
5
- # This function implements the instrumentation policy:
6
- #
7
- # - Instrumenters are a stack; the first `before_query` will have the last `after_query`
8
- # - If a `before_` hook returned without an error, its corresponding `after_` hook will run.
9
- # - If the `before_` hook did _not_ run, the `after_` hook will not be called.
10
- #
11
- # When errors are raised from `after_` hooks:
12
- # - Subsequent `after_` hooks _are_ called
13
- # - The first raised error is captured; later errors are ignored
14
- # - If an error was capture, it's re-raised after all hooks are finished
15
- #
16
- # Partial runs of instrumentation are possible:
17
- # - If a `before_multiplex` hook raises an error, no `before_query` hooks will run
18
- # - If a `before_query` hook raises an error, subsequent `before_query` hooks will not run (on any query)
19
- def self.apply_instrumenters(multiplex)
20
- schema = multiplex.schema
21
- queries = multiplex.queries
22
- query_instrumenters = schema.instrumenters[:query]
23
- multiplex_instrumenters = schema.instrumenters[:multiplex]
24
-
25
- # First, run multiplex instrumentation, then query instrumentation for each query
26
- call_hooks(multiplex_instrumenters, multiplex, :before_multiplex, :after_multiplex) do
27
- each_query_call_hooks(query_instrumenters, queries) do
28
- # Let them be executed
29
- yield
30
- end
31
- end
32
- end
33
-
34
- class << self
35
- private
36
- # Call the before_ hooks of each query,
37
- # Then yield if no errors.
38
- # `call_hooks` takes care of appropriate cleanup.
39
- def each_query_call_hooks(instrumenters, queries, i = 0)
40
- if i >= queries.length
41
- yield
42
- else
43
- query = queries[i]
44
- call_hooks(instrumenters, query, :before_query, :after_query) {
45
- each_query_call_hooks(instrumenters, queries, i + 1) {
46
- yield
47
- }
48
- }
49
- end
50
- end
51
-
52
- # Call each before hook, and if they all succeed, yield.
53
- # If they don't all succeed, call after_ for each one that succeeded.
54
- def call_hooks(instrumenters, object, before_hook_name, after_hook_name)
55
- begin
56
- successful = []
57
- instrumenters.each do |instrumenter|
58
- instrumenter.public_send(before_hook_name, object)
59
- successful << instrumenter
60
- end
61
-
62
- # if any before hooks raise an exception, quit calling before hooks,
63
- # but call the after hooks on anything that succeeded but also
64
- # raise the exception that came from the before hook.
65
- rescue GraphQL::ExecutionError => err
66
- object.context.errors << err
67
- rescue => e
68
- raise call_after_hooks(successful, object, after_hook_name, e)
69
- end
70
-
71
- begin
72
- yield # Call the user code
73
- ensure
74
- ex = call_after_hooks(successful, object, after_hook_name, nil)
75
- raise ex if ex
76
- end
77
- end
78
-
79
- def call_after_hooks(instrumenters, object, after_hook_name, ex)
80
- instrumenters.reverse_each do |instrumenter|
81
- begin
82
- instrumenter.public_send(after_hook_name, object)
83
- rescue => e
84
- ex = e
85
- end
86
- end
87
- ex
88
- end
89
- end
90
- end
91
- end
92
- end