graphql 1.12.3 → 1.12.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +3 -1
  3. data/lib/generators/graphql/relay.rb +55 -0
  4. data/lib/generators/graphql/relay_generator.rb +3 -46
  5. data/lib/graphql.rb +1 -1
  6. data/lib/graphql/backtrace/inspect_result.rb +0 -1
  7. data/lib/graphql/backtrace/table.rb +0 -1
  8. data/lib/graphql/backtrace/traced_error.rb +0 -1
  9. data/lib/graphql/backtrace/tracer.rb +2 -6
  10. data/lib/graphql/dataloader.rb +102 -92
  11. data/lib/graphql/dataloader/null_dataloader.rb +5 -5
  12. data/lib/graphql/dataloader/request.rb +1 -6
  13. data/lib/graphql/dataloader/request_all.rb +1 -4
  14. data/lib/graphql/dataloader/source.rb +20 -6
  15. data/lib/graphql/execution/interpreter.rb +1 -1
  16. data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
  17. data/lib/graphql/execution/interpreter/resolve.rb +33 -25
  18. data/lib/graphql/execution/interpreter/runtime.rb +36 -74
  19. data/lib/graphql/execution/multiplex.rb +21 -22
  20. data/lib/graphql/object_type.rb +0 -2
  21. data/lib/graphql/parse_error.rb +0 -1
  22. data/lib/graphql/query.rb +8 -2
  23. data/lib/graphql/query/arguments_cache.rb +0 -1
  24. data/lib/graphql/query/context.rb +1 -3
  25. data/lib/graphql/query/executor.rb +0 -1
  26. data/lib/graphql/query/null_context.rb +3 -2
  27. data/lib/graphql/query/variable_validation_error.rb +1 -1
  28. data/lib/graphql/schema/argument.rb +61 -0
  29. data/lib/graphql/schema/field.rb +10 -5
  30. data/lib/graphql/schema/find_inherited_value.rb +3 -1
  31. data/lib/graphql/schema/input_object.rb +6 -2
  32. data/lib/graphql/schema/member/has_arguments.rb +43 -56
  33. data/lib/graphql/schema/member/has_fields.rb +1 -4
  34. data/lib/graphql/schema/member/instrumentation.rb +0 -1
  35. data/lib/graphql/subscriptions/event.rb +0 -1
  36. data/lib/graphql/subscriptions/instrumentation.rb +0 -1
  37. data/lib/graphql/subscriptions/serialize.rb +0 -1
  38. data/lib/graphql/version.rb +1 -1
  39. metadata +7 -90
@@ -7,15 +7,15 @@ module GraphQL
7
7
  # The Dataloader interface isn't public, but it enables
8
8
  # simple internal code while adding the option to add Dataloader.
9
9
  class NullDataloader < Dataloader
10
- def enqueue
11
- yield
12
- end
13
-
14
10
  # These are all no-ops because code was
15
11
  # executed sychronously.
16
12
  def run; end
17
13
  def yield; end
18
- def yielded?(_path); false; end
14
+
15
+ def append_job
16
+ yield
17
+ nil
18
+ end
19
19
  end
20
20
  end
21
21
  end
@@ -12,12 +12,7 @@ module GraphQL
12
12
  #
13
13
  # @return [Object] the object loaded for `key`
14
14
  def load
15
- if @source.results.key?(@key)
16
- @source.results[@key]
17
- else
18
- @source.sync
19
- @source.results[@key]
20
- end
15
+ @source.load(@key)
21
16
  end
22
17
  end
23
18
  end
@@ -12,10 +12,7 @@ module GraphQL
12
12
  #
13
13
  # @return [Array<Object>] One object for each of `keys`
14
14
  def load
15
- if @keys.any? { |k| !@source.results.key?(k) }
16
- @source.sync
17
- end
18
- @keys.map { |k| @source.results[k] }
15
+ @source.load_all(@keys)
19
16
  end
20
17
  end
21
18
  end
@@ -3,9 +3,6 @@
3
3
  module GraphQL
4
4
  class Dataloader
5
5
  class Source
6
- # @api private
7
- attr_reader :results
8
-
9
6
  # Called by {Dataloader} to prepare the {Source}'s internal state
10
7
  # @api private
11
8
  def setup(dataloader)
@@ -35,11 +32,11 @@ module GraphQL
35
32
  # @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded.
36
33
  def load(key)
37
34
  if @results.key?(key)
38
- @results[key]
35
+ result_for(key)
39
36
  else
40
37
  @pending_keys << key
41
38
  sync
42
- @results[key]
39
+ result_for(key)
43
40
  end
44
41
  end
45
42
 
@@ -52,7 +49,7 @@ module GraphQL
52
49
  sync
53
50
  end
54
51
 
55
- keys.map { |k| @results[k] }
52
+ keys.map { |k| result_for(k) }
56
53
  end
57
54
 
58
55
  # Subclasses must implement this method to return a value for each of `keys`
@@ -86,8 +83,25 @@ module GraphQL
86
83
  fetch_keys.each_with_index do |key, idx|
87
84
  @results[key] = results[idx]
88
85
  end
86
+ rescue StandardError => error
87
+ fetch_keys.each { |key| @results[key] = error }
88
+ ensure
89
89
  nil
90
90
  end
91
+
92
+ private
93
+
94
+ # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
95
+ # @param key [Object] key passed to {#load} or {#load_all}
96
+ # @return [Object] The result from {#fetch} for `key`.
97
+ # @api private
98
+ def result_for(key)
99
+ result = @results[key]
100
+
101
+ raise result if result.class <= StandardError
102
+
103
+ result
104
+ end
91
105
  end
92
106
  end
93
107
  end
@@ -95,7 +95,7 @@ module GraphQL
95
95
  end
96
96
  final_values.compact!
97
97
  tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
98
- Interpreter::Resolve.resolve_all(final_values)
98
+ Interpreter::Resolve.resolve_all(final_values, multiplex.dataloader)
99
99
  end
100
100
  queries.each do |query|
101
101
  runtime = query.context.namespace(:interpreter)[:runtime]
@@ -6,17 +6,21 @@ module GraphQL
6
6
  class ArgumentsCache
7
7
  def initialize(query)
8
8
  @query = query
9
+ @dataloader = query.context.dataloader
9
10
  @storage = Hash.new do |h, ast_node|
10
11
  h[ast_node] = Hash.new do |h2, arg_owner|
11
12
  h2[arg_owner] = Hash.new do |h3, parent_object|
12
- # First, normalize all AST or Ruby values to a plain Ruby hash
13
- args_hash = prepare_args_hash(ast_node)
14
- # Then call into the schema to coerce those incoming values
15
- args = arg_owner.coerce_arguments(parent_object, args_hash, query.context)
13
+ dataload_for(ast_node, arg_owner, parent_object) do |kwarg_arguments|
14
+ h3[parent_object] = @query.schema.after_lazy(kwarg_arguments) do |resolved_args|
15
+ h3[parent_object] = resolved_args
16
+ end
17
+ end
16
18
 
17
- h3[parent_object] = @query.schema.after_lazy(args) do |resolved_args|
18
- # when this promise is resolved, update the cache with the resolved value
19
- h3[parent_object] = resolved_args
19
+ if !h3.key?(parent_object)
20
+ # TODO should i bother putting anything here?
21
+ h3[parent_object] = NO_ARGUMENTS
22
+ else
23
+ h3[parent_object]
20
24
  end
21
25
  end
22
26
  end
@@ -25,6 +29,25 @@ module GraphQL
25
29
 
26
30
  def fetch(ast_node, argument_owner, parent_object)
27
31
  @storage[ast_node][argument_owner][parent_object]
32
+ # If any jobs were enqueued, run them now,
33
+ # since this might have been called outside of execution.
34
+ # (The jobs are responsible for updating `result` in-place.)
35
+ @dataloader.run
36
+ # Ack, the _hash_ is updated, but the key is eventually
37
+ # overridden with an immutable arguments instance.
38
+ # The first call queues up the job,
39
+ # then this call fetches the result.
40
+ # TODO this should be better, find a solution
41
+ # that works with merging the runtime.rb code
42
+ @storage[ast_node][argument_owner][parent_object]
43
+ end
44
+
45
+ # @yield [Interpreter::Arguments, Lazy<Interpreter::Arguments>] The finally-loaded arguments
46
+ def dataload_for(ast_node, argument_owner, parent_object, &block)
47
+ # First, normalize all AST or Ruby values to a plain Ruby hash
48
+ args_hash = self.class.prepare_args_hash(@query, ast_node)
49
+ argument_owner.coerce_arguments(parent_object, args_hash, @query.context, &block)
50
+ nil
28
51
  end
29
52
 
30
53
  private
@@ -33,7 +56,7 @@ module GraphQL
33
56
 
34
57
  NO_VALUE_GIVEN = Object.new
35
58
 
36
- def prepare_args_hash(ast_arg_or_hash_or_value)
59
+ def self.prepare_args_hash(query, ast_arg_or_hash_or_value)
37
60
  case ast_arg_or_hash_or_value
38
61
  when Hash
39
62
  if ast_arg_or_hash_or_value.empty?
@@ -41,27 +64,27 @@ module GraphQL
41
64
  end
42
65
  args_hash = {}
43
66
  ast_arg_or_hash_or_value.each do |k, v|
44
- args_hash[k] = prepare_args_hash(v)
67
+ args_hash[k] = prepare_args_hash(query, v)
45
68
  end
46
69
  args_hash
47
70
  when Array
48
- ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
71
+ ast_arg_or_hash_or_value.map { |v| prepare_args_hash(query, v) }
49
72
  when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
50
73
  if ast_arg_or_hash_or_value.arguments.empty?
51
74
  return NO_ARGUMENTS
52
75
  end
53
76
  args_hash = {}
54
77
  ast_arg_or_hash_or_value.arguments.each do |arg|
55
- v = prepare_args_hash(arg.value)
78
+ v = prepare_args_hash(query, arg.value)
56
79
  if v != NO_VALUE_GIVEN
57
80
  args_hash[arg.name] = v
58
81
  end
59
82
  end
60
83
  args_hash
61
84
  when GraphQL::Language::Nodes::VariableIdentifier
62
- if @query.variables.key?(ast_arg_or_hash_or_value.name)
63
- variable_value = @query.variables[ast_arg_or_hash_or_value.name]
64
- prepare_args_hash(variable_value)
85
+ if query.variables.key?(ast_arg_or_hash_or_value.name)
86
+ variable_value = query.variables[ast_arg_or_hash_or_value.name]
87
+ prepare_args_hash(query, variable_value)
65
88
  else
66
89
  NO_VALUE_GIVEN
67
90
  end
@@ -6,10 +6,9 @@ module GraphQL
6
6
  module Resolve
7
7
  # Continue field results in `results` until there's nothing else to continue.
8
8
  # @return [void]
9
- def self.resolve_all(results)
10
- while results.any?
11
- results = resolve(results)
12
- end
9
+ def self.resolve_all(results, dataloader)
10
+ dataloader.append_job { resolve(results, dataloader) }
11
+ nil
13
12
  end
14
13
 
15
14
  # After getting `results` back from an interpreter evaluation,
@@ -24,33 +23,42 @@ module GraphQL
24
23
  # return {Lazy} instances if there's more work to be done,
25
24
  # or return {Hash}/{Array} if the query should be continued.
26
25
  #
27
- # @param results [Array]
28
- # @return [Array] Same size, filled with finished values
29
- def self.resolve(results)
26
+ # @return [void]
27
+ def self.resolve(results, dataloader)
28
+ # There might be pending jobs here that _will_ write lazies
29
+ # into the result hash. We should run them out, so we
30
+ # can be sure that all lazies will be present in the result hashes.
31
+ # A better implementation would somehow interleave (or unify)
32
+ # these approaches.
33
+ dataloader.run
30
34
  next_results = []
31
-
32
- # Work through the queue until it's empty
33
- while results.size > 0
35
+ while results.any?
34
36
  result_value = results.shift
35
-
36
- if result_value.is_a?(Lazy)
37
- result_value = result_value.value
38
- end
39
-
40
- if result_value.is_a?(Lazy)
41
- # Since this field returned another lazy,
42
- # add it to the same queue
43
- results << result_value
44
- elsif result_value.is_a?(Hash)
45
- # This is part of the next level, add it
46
- next_results.concat(result_value.values)
37
+ if result_value.is_a?(Hash)
38
+ results.concat(result_value.values)
39
+ next
47
40
  elsif result_value.is_a?(Array)
48
- # This is part of the next level, add it
49
- next_results.concat(result_value)
41
+ results.concat(result_value)
42
+ next
43
+ elsif result_value.is_a?(Lazy)
44
+ loaded_value = result_value.value
45
+ if loaded_value.is_a?(Lazy)
46
+ # Since this field returned another lazy,
47
+ # add it to the same queue
48
+ results << loaded_value
49
+ elsif loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
50
+ # Add these values in wholesale --
51
+ # they might be modified by later work in the dataloader.
52
+ next_results << loaded_value
53
+ end
50
54
  end
51
55
  end
52
56
 
53
- next_results
57
+ if next_results.any?
58
+ dataloader.append_job { resolve(next_results, dataloader) }
59
+ end
60
+
61
+ nil
54
62
  end
55
63
  end
56
64
  end
@@ -56,17 +56,18 @@ module GraphQL
56
56
  # Root .authorized? returned false.
57
57
  write_in_response(path, nil)
58
58
  else
59
- # Prepare this runtime state to be encapsulated in a Fiber
60
- @progress_path = path
61
- @progress_scoped_context = context.scoped_context
62
- @progress_object = object_proxy
63
- @progress_object_type = root_type
64
- @progress_index = nil
65
- @progress_is_eager_selection = root_op_type == "mutation"
66
- @progress_selections = gather_selections(object_proxy, root_type, root_operation.selections)
67
-
59
+ gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
68
60
  # Make the first fiber which will begin execution
69
- enqueue_selections_fiber
61
+ @dataloader.append_job {
62
+ evaluate_selections(
63
+ path,
64
+ context.scoped_context,
65
+ object_proxy,
66
+ root_type,
67
+ root_op_type == "mutation",
68
+ gathered_selections,
69
+ )
70
+ }
70
71
  end
71
72
  delete_interpreter_context(:current_path)
72
73
  delete_interpreter_context(:current_field)
@@ -75,32 +76,6 @@ module GraphQL
75
76
  nil
76
77
  end
77
78
 
78
- # Use `@dataloader` to enqueue a fiber that will pick up from the current point.
79
- # @return [void]
80
- def enqueue_selections_fiber
81
- # Read these into local variables so that later assignments don't affect the block below.
82
- path = @progress_path
83
- scoped_context = @progress_scoped_context
84
- owner_object = @progress_object
85
- owner_type = @progress_object_type
86
- idx = @progress_index
87
- is_eager_selection = @progress_is_eager_selection
88
- gathered_selections = @progress_selections
89
-
90
- @dataloader.enqueue {
91
- evaluate_selections(
92
- path,
93
- scoped_context,
94
- owner_object,
95
- owner_type,
96
- is_eager_selection: is_eager_selection,
97
- after: idx,
98
- gathered_selections: gathered_selections,
99
- )
100
- }
101
- nil
102
- end
103
-
104
79
  def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
105
80
  selections.each do |node|
106
81
  # Skip gathering this if the directive says so
@@ -159,39 +134,17 @@ module GraphQL
159
134
  NO_ARGS = {}.freeze
160
135
 
161
136
  # @return [void]
162
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
137
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
163
138
  set_all_interpreter_context(owner_object, nil, nil, path)
164
139
 
165
- @progress_path = path
166
- @progress_scoped_context = scoped_context
167
- @progress_object = owner_object
168
- @progress_object_type = owner_type
169
- @progress_index = nil
170
- @progress_is_eager_selection = is_eager_selection
171
- @progress_selections = gathered_selections
172
-
173
- # Track `idx` manually to avoid an allocation on this hot path
174
- idx = 0
175
140
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
176
- prev_idx = idx
177
- idx += 1
178
- # TODO: this is how a `progress` resumes where this left off.
179
- # Is there a better way to seek in the hash?
180
- # I think we could also use the array of keys; it supports seeking just fine.
181
- if after && prev_idx <= after
182
- next
183
- end
184
- @progress_index = prev_idx
185
- # This is how the current runtime gives itself to `dataloader`
186
- # so that the dataloader can enqueue another fiber to resume if needed.
187
- @dataloader.current_runtime = self
188
- evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection)
189
- # The dataloader knows if ^^ that selection halted and later selections were executed in another fiber.
190
- # If that's the case, then don't continue execution here.
191
- if @dataloader.yielded?(path)
192
- break
193
- end
141
+ @dataloader.append_job {
142
+ evaluate_selection(
143
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
144
+ )
145
+ }
194
146
  end
147
+
195
148
  nil
196
149
  end
197
150
 
@@ -243,13 +196,21 @@ module GraphQL
243
196
  object = authorized_new(field_defn.owner, object, context, next_path)
244
197
  end
245
198
 
246
- begin
247
- kwarg_arguments = arguments(object, field_defn, ast_node)
248
- rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
249
- continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
250
- return
199
+ total_args_count = field_defn.arguments.size
200
+ if total_args_count == 0
201
+ kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
202
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
203
+ else
204
+ # TODO remove all arguments(...) usages?
205
+ @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
206
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
207
+ end
251
208
  end
209
+ end
252
210
 
211
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field) # rubocop:disable Metrics/ParameterLists
212
+ context.scoped_context = scoped_context
213
+ return_type = field_defn.type
253
214
  after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
254
215
  if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
255
216
  continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
@@ -329,10 +290,11 @@ module GraphQL
329
290
  # all of its child fields before moving on to the next root mutation field.
330
291
  # (Subselections of this mutation will still be resolved level-by-level.)
331
292
  if is_eager_field
332
- Interpreter::Resolve.resolve_all([field_result])
293
+ Interpreter::Resolve.resolve_all([field_result], @dataloader)
294
+ else
295
+ # Return this from `after_lazy` because it might be another lazy that needs to be resolved
296
+ field_result
333
297
  end
334
-
335
- nil
336
298
  end
337
299
  end
338
300
 
@@ -419,7 +381,7 @@ module GraphQL
419
381
  response_hash = {}
420
382
  write_in_response(path, response_hash)
421
383
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
422
- evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
384
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
423
385
  response_hash
424
386
  end
425
387
  end