graphql 1.12.3 → 1.12.8

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +4 -1
  3. data/lib/generators/graphql/loader_generator.rb +1 -0
  4. data/lib/generators/graphql/mutation_generator.rb +1 -0
  5. data/lib/generators/graphql/relay.rb +55 -0
  6. data/lib/generators/graphql/relay_generator.rb +4 -46
  7. data/lib/generators/graphql/type_generator.rb +1 -0
  8. data/lib/graphql.rb +4 -2
  9. data/lib/graphql/backtrace/inspect_result.rb +0 -1
  10. data/lib/graphql/backtrace/table.rb +0 -1
  11. data/lib/graphql/backtrace/traced_error.rb +0 -1
  12. data/lib/graphql/backtrace/tracer.rb +4 -8
  13. data/lib/graphql/dataloader.rb +102 -92
  14. data/lib/graphql/dataloader/null_dataloader.rb +5 -5
  15. data/lib/graphql/dataloader/request.rb +1 -6
  16. data/lib/graphql/dataloader/request_all.rb +1 -4
  17. data/lib/graphql/dataloader/source.rb +20 -6
  18. data/lib/graphql/execution/errors.rb +109 -11
  19. data/lib/graphql/execution/interpreter.rb +2 -2
  20. data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
  21. data/lib/graphql/execution/interpreter/resolve.rb +33 -25
  22. data/lib/graphql/execution/interpreter/runtime.rb +41 -78
  23. data/lib/graphql/execution/multiplex.rb +21 -22
  24. data/lib/graphql/introspection.rb +1 -1
  25. data/lib/graphql/introspection/directive_type.rb +7 -3
  26. data/lib/graphql/language.rb +1 -0
  27. data/lib/graphql/language/cache.rb +37 -0
  28. data/lib/graphql/language/parser.rb +15 -5
  29. data/lib/graphql/language/parser.y +15 -5
  30. data/lib/graphql/object_type.rb +0 -2
  31. data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
  32. data/lib/graphql/pagination/connection.rb +15 -1
  33. data/lib/graphql/pagination/connections.rb +1 -0
  34. data/lib/graphql/pagination/relation_connection.rb +12 -1
  35. data/lib/graphql/parse_error.rb +0 -1
  36. data/lib/graphql/query.rb +9 -5
  37. data/lib/graphql/query/arguments_cache.rb +0 -1
  38. data/lib/graphql/query/context.rb +1 -3
  39. data/lib/graphql/query/executor.rb +0 -1
  40. data/lib/graphql/query/null_context.rb +3 -2
  41. data/lib/graphql/query/validation_pipeline.rb +1 -1
  42. data/lib/graphql/query/variable_validation_error.rb +1 -1
  43. data/lib/graphql/railtie.rb +9 -1
  44. data/lib/graphql/relay/range_add.rb +10 -5
  45. data/lib/graphql/schema.rb +14 -27
  46. data/lib/graphql/schema/argument.rb +61 -0
  47. data/lib/graphql/schema/field.rb +10 -5
  48. data/lib/graphql/schema/field/connection_extension.rb +1 -0
  49. data/lib/graphql/schema/find_inherited_value.rb +3 -1
  50. data/lib/graphql/schema/input_object.rb +6 -2
  51. data/lib/graphql/schema/member/has_arguments.rb +43 -56
  52. data/lib/graphql/schema/member/has_fields.rb +1 -4
  53. data/lib/graphql/schema/member/instrumentation.rb +0 -1
  54. data/lib/graphql/schema/resolver.rb +28 -1
  55. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
  56. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
  57. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  58. data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
  59. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  60. data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
  61. data/lib/graphql/subscriptions/event.rb +0 -1
  62. data/lib/graphql/subscriptions/instrumentation.rb +0 -1
  63. data/lib/graphql/subscriptions/serialize.rb +3 -1
  64. data/lib/graphql/tracing/active_support_notifications_tracing.rb +2 -1
  65. data/lib/graphql/types/relay/base_connection.rb +4 -0
  66. data/lib/graphql/types/relay/connection_behaviors.rb +38 -5
  67. data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
  68. data/lib/graphql/version.rb +1 -1
  69. data/readme.md +1 -1
  70. metadata +8 -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
@@ -18,21 +18,83 @@ module GraphQL
18
18
  #
19
19
  class Errors
20
20
  def self.use(schema)
21
- if schema.plugins.any? { |(plugin, kwargs)| plugin == self }
22
- definition_line = caller(2, 1).first
23
- GraphQL::Deprecation.warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
24
- end
25
- schema.error_handler = self.new(schema)
21
+ definition_line = caller(2, 1).first
22
+ GraphQL::Deprecation.warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
26
23
  end
27
24
 
25
+ NEW_HANDLER_HASH = ->(h, k) {
26
+ h[k] = {
27
+ class: k,
28
+ handler: nil,
29
+ subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
30
+ }
31
+ }
32
+
28
33
  def initialize(schema)
29
34
  @schema = schema
35
+ @handlers = {
36
+ class: nil,
37
+ handler: nil,
38
+ subclass_handlers: Hash.new(&NEW_HANDLER_HASH),
39
+ }
40
+ end
41
+
42
+ # @api private
43
+ def each_rescue
44
+ handlers = @handlers.values
45
+ while (handler = handlers.shift) do
46
+ yield(handler[:class], handler[:handler])
47
+ handlers.concat(handler[:subclass_handlers].values)
48
+ end
30
49
  end
31
50
 
32
- class NullErrorHandler
33
- def self.with_error_handling(_ctx)
34
- yield
51
+ # Register this handler, updating the
52
+ # internal handler index to maintain least-to-most specific.
53
+ #
54
+ # @param error_class [Class<Exception>]
55
+ # @param error_handler [Proc]
56
+ # @return [void]
57
+ def rescue_from(error_class, error_handler)
58
+ subclasses_handlers = {}
59
+ this_level_subclasses = []
60
+ # During this traversal, do two things:
61
+ # - Identify any already-registered subclasses of this error class
62
+ # and gather them up to be inserted _under_ this class
63
+ # - Find the point in the index where this handler should be inserted
64
+ # (That is, _under_ any superclasses, or at top-level, if there are no superclasses registered)
65
+ handlers = @handlers[:subclass_handlers]
66
+ while (handlers) do
67
+ this_level_subclasses.clear
68
+ # First, identify already-loaded handlers that belong
69
+ # _under_ this one. (That is, they're handlers
70
+ # for subclasses of `error_class`.)
71
+ handlers.each do |err_class, handler|
72
+ if err_class < error_class
73
+ subclasses_handlers[err_class] = handler
74
+ this_level_subclasses << err_class
75
+ end
76
+ end
77
+ # Any handlers that we'll be moving, delete them from this point in the index
78
+ this_level_subclasses.each do |err_class|
79
+ handlers.delete(err_class)
80
+ end
81
+
82
+ # See if any keys in this hash are superclasses of this new class:
83
+ next_index_point = handlers.find { |err_class, handler| error_class < err_class }
84
+ if next_index_point
85
+ handlers = next_index_point[1][:subclass_handlers]
86
+ else
87
+ # this new handler doesn't belong to any sub-handlers,
88
+ # so insert it in the current set of `handlers`
89
+ break
90
+ end
35
91
  end
92
+ # Having found the point at which to insert this handler,
93
+ # register it and merge any subclass handlers back in at this point.
94
+ this_class_handlers = handlers[error_class]
95
+ this_class_handlers[:handler] = error_handler
96
+ this_class_handlers[:subclass_handlers].merge!(subclasses_handlers)
97
+ nil
36
98
  end
37
99
 
38
100
  # Call the given block with the schema's configured error handlers.
@@ -44,8 +106,7 @@ module GraphQL
44
106
  def with_error_handling(ctx)
45
107
  yield
46
108
  rescue StandardError => err
47
- rescues = ctx.schema.rescues
48
- _err_class, handler = rescues.find { |err_class, handler| err.is_a?(err_class) }
109
+ handler = find_handler_for(err.class)
49
110
  if handler
50
111
  runtime_info = ctx.namespace(:interpreter) || {}
51
112
  obj = runtime_info[:current_object]
@@ -54,11 +115,48 @@ module GraphQL
54
115
  if obj.is_a?(GraphQL::Schema::Object)
55
116
  obj = obj.object
56
117
  end
57
- handler.call(err, obj, args, ctx, field)
118
+ handler[:handler].call(err, obj, args, ctx, field)
58
119
  else
59
120
  raise err
60
121
  end
61
122
  end
123
+
124
+ # @return [Proc, nil] The handler for `error_class`, if one was registered on this schema or inherited
125
+ def find_handler_for(error_class)
126
+ handlers = @handlers[:subclass_handlers]
127
+ handler = nil
128
+ while (handlers) do
129
+ _err_class, next_handler = handlers.find { |err_class, handler| error_class <= err_class }
130
+ if next_handler
131
+ handlers = next_handler[:subclass_handlers]
132
+ handler = next_handler
133
+ else
134
+ # Don't reassign `handler` --
135
+ # let the previous assignment carry over outside this block.
136
+ break
137
+ end
138
+ end
139
+
140
+ # check for a handler from a parent class:
141
+ if @schema.superclass.respond_to?(:error_handler) && (parent_errors = @schema.superclass.error_handler)
142
+ parent_handler = parent_errors.find_handler_for(error_class)
143
+ end
144
+
145
+ # If the inherited handler is more specific than the one defined here,
146
+ # use it.
147
+ # If it's a tie (or there is no parent handler), use the one defined here.
148
+ # If there's an inherited one, but not one defined here, use the inherited one.
149
+ # Otherwise, there's no handler for this error, return `nil`.
150
+ if parent_handler && handler && parent_handler[:class] < handler[:class]
151
+ parent_handler
152
+ elsif handler
153
+ handler
154
+ elsif parent_handler
155
+ parent_handler
156
+ else
157
+ nil
158
+ end
159
+ end
62
160
  end
63
161
  end
64
162
  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]
@@ -113,7 +113,7 @@ module GraphQL
113
113
  def initialize(value:, path:, field:)
114
114
  message = "Failed to build a GraphQL list result for field `#{field.path}` at path `#{path.join(".")}`.\n".dup
115
115
 
116
- message << "Expected `#{value.inspect}` to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n"
116
+ message << "Expected `#{value.inspect}` (#{value.class}) to implement `.each` to satisfy the GraphQL return type `#{field.type.to_type_signature}`.\n"
117
117
 
118
118
  if field.connection?
119
119
  message << "\nThis field was treated as a Relay-style connection; add `connection: false` to the `field(...)` to disable this behavior."
@@ -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