graphql 2.0.11 → 2.0.14

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/templates/schema.erb +3 -0
  3. data/lib/graphql/dataloader/source.rb +9 -0
  4. data/lib/graphql/execution/interpreter/runtime.rb +10 -8
  5. data/lib/graphql/execution/interpreter.rb +185 -59
  6. data/lib/graphql/execution/lookahead.rb +26 -26
  7. data/lib/graphql/execution/multiplex.rb +1 -116
  8. data/lib/graphql/execution.rb +0 -1
  9. data/lib/graphql/introspection/type_type.rb +7 -0
  10. data/lib/graphql/introspection.rb +2 -1
  11. data/lib/graphql/language/printer.rb +8 -5
  12. data/lib/graphql/query/validation_pipeline.rb +4 -0
  13. data/lib/graphql/query/variable_validation_error.rb +2 -2
  14. data/lib/graphql/query/variables.rb +13 -3
  15. data/lib/graphql/query.rb +11 -2
  16. data/lib/graphql/rake_task/validate.rb +1 -1
  17. data/lib/graphql/schema/argument.rb +25 -13
  18. data/lib/graphql/schema/build_from_definition.rb +1 -2
  19. data/lib/graphql/schema/directive/one_of.rb +12 -0
  20. data/lib/graphql/schema/enum.rb +1 -1
  21. data/lib/graphql/schema/field.rb +12 -11
  22. data/lib/graphql/schema/input_object.rb +36 -1
  23. data/lib/graphql/schema/late_bound_type.rb +4 -0
  24. data/lib/graphql/schema/list.rb +19 -5
  25. data/lib/graphql/schema/member/has_arguments.rb +8 -2
  26. data/lib/graphql/schema/member/validates_input.rb +2 -2
  27. data/lib/graphql/schema/non_null.rb +2 -2
  28. data/lib/graphql/schema/scalar.rb +1 -1
  29. data/lib/graphql/schema.rb +13 -3
  30. data/lib/graphql/static_validation/all_rules.rb +1 -0
  31. data/lib/graphql/static_validation/error.rb +2 -2
  32. data/lib/graphql/static_validation/literal_validator.rb +4 -0
  33. data/lib/graphql/static_validation/rules/directives_are_defined.rb +11 -5
  34. data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb +66 -0
  35. data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb +29 -0
  36. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +12 -6
  37. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
  38. data/lib/graphql/subscriptions.rb +2 -5
  39. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  40. data/lib/graphql/version.rb +1 -1
  41. metadata +6 -4
  42. data/lib/graphql/execution/instrumentation.rb +0 -92
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9467c38d77e2174c83c1f74c8d4450cea047a5b4f8421b7c6c78323142c86c1
4
- data.tar.gz: 9b93dfe4b8e72ff80f25d6cd20d897d73a32ae423439d819f810c8db6d265165
3
+ metadata.gz: a3bc6610f88b6689ee6991a6ad3543580f9bdf1b3cb9f2f7e3f78862d76072f7
4
+ data.tar.gz: ce52505d6c43e330aa8e5def38719f62bdfe067274a86530c889350c64994bd0
5
5
  SHA512:
6
- metadata.gz: 9cfce211f91fe24a0023add3f1e9e30e8ad66081c65d897053be6af6832bd8c413433a3a1721b6ccb5e670c91365d73d49b5d0c9912875b828860783536004a0
7
- data.tar.gz: 3ce2af311a09a7a14ba52386049640f8b34636b8380dcfba9874745e348bc30ef2bcec58ed783897c39447379b774b3e8e5b690c477223d9532092b7c0b13112
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]
@@ -401,8 +401,7 @@ module GraphQL
401
401
  end
402
402
  return_type = field_defn.type
403
403
 
404
- next_path = path.dup
405
- next_path << result_name
404
+ next_path = path + [result_name]
406
405
  next_path.freeze
407
406
 
408
407
  # This seems janky, but we need to know
@@ -685,12 +684,16 @@ module GraphQL
685
684
  set_result(selection_result, result_name, r)
686
685
  r
687
686
  when "UNION", "INTERFACE"
688
- resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
689
- resolved_value ||= value
687
+ resolved_type_or_lazy = resolve_type(current_type, value, path)
688
+ after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type_result|
689
+ if resolved_type_result.is_a?(Array) && resolved_type_result.length == 2
690
+ resolved_type, resolved_value = resolved_type_result
691
+ else
692
+ resolved_type = resolved_type_result
693
+ resolved_value = value
694
+ end
690
695
 
691
- after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |resolved_type|
692
696
  possible_types = query.possible_types(current_type)
693
-
694
697
  if !possible_types.include?(resolved_type)
695
698
  parent_type = field.owner_type
696
699
  err_class = current_type::UnresolvedTypeError
@@ -764,8 +767,7 @@ module GraphQL
764
767
  set_result(selection_result, result_name, response_list)
765
768
  result_was_set = true
766
769
  end
767
- next_path = path.dup
768
- next_path << idx
770
+ next_path = path + [idx]
769
771
  this_idx = idx
770
772
  next_path.freeze
771
773
  idx += 1
@@ -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
@@ -236,12 +236,15 @@ module GraphQL
236
236
  out = print_description(input_object_type)
237
237
  out << "input #{input_object_type.name}"
238
238
  out << print_directives(input_object_type.directives)
239
- out << " {\n"
240
- input_object_type.fields.each.with_index do |field, i|
241
- out << print_description(field, indent: ' ', first_in_block: i == 0)
242
- out << " #{print_input_value_definition(field)}\n"
239
+ if !input_object_type.fields.empty?
240
+ out << " {\n"
241
+ input_object_type.fields.each.with_index do |field, i|
242
+ out << print_description(field, indent: ' ', first_in_block: i == 0)
243
+ out << " #{print_input_value_definition(field)}\n"
244
+ end
245
+ out << "}"
243
246
  end
244
- out << "}"
247
+ out
245
248
  end
246
249
 
247
250
  def print_directive_definition(directive)
@@ -45,6 +45,10 @@ module GraphQL
45
45
  @query_analyzers
46
46
  end
47
47
 
48
+ def has_validated?
49
+ @has_validated == true
50
+ end
51
+
48
52
  private
49
53
 
50
54
  # If the pipeline wasn't run yet, run it.
@@ -4,11 +4,11 @@ module GraphQL
4
4
  class VariableValidationError < GraphQL::ExecutionError
5
5
  attr_accessor :value, :validation_result
6
6
 
7
- def initialize(variable_ast, type, value, validation_result)
7
+ def initialize(variable_ast, type, value, validation_result, msg: nil)
8
8
  @value = value
9
9
  @validation_result = validation_result
10
10
 
11
- msg = "Variable $#{variable_ast.name} of type #{type.to_type_signature} was provided invalid value"
11
+ msg ||= "Variable $#{variable_ast.name} of type #{type.to_type_signature} was provided invalid value"
12
12
 
13
13
  if problem_fields.any?
14
14
  msg += " for #{problem_fields.join(", ")}"