graphql 1.12.16 → 1.12.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/relay.rb +19 -11
  3. data/lib/generators/graphql/templates/schema.erb +13 -1
  4. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  5. data/lib/graphql/dataloader/source.rb +50 -2
  6. data/lib/graphql/dataloader.rb +39 -16
  7. data/lib/graphql/define/instance_definable.rb +1 -1
  8. data/lib/graphql/deprecated_dsl.rb +11 -3
  9. data/lib/graphql/deprecation.rb +1 -5
  10. data/lib/graphql/execution/interpreter/runtime.rb +10 -6
  11. data/lib/graphql/integer_encoding_error.rb +18 -2
  12. data/lib/graphql/introspection/input_value_type.rb +6 -0
  13. data/lib/graphql/pagination/connections.rb +35 -16
  14. data/lib/graphql/query/validation_pipeline.rb +1 -1
  15. data/lib/graphql/query.rb +4 -0
  16. data/lib/graphql/schema/argument.rb +71 -28
  17. data/lib/graphql/schema/field.rb +14 -4
  18. data/lib/graphql/schema/input_object.rb +5 -9
  19. data/lib/graphql/schema/member/has_arguments.rb +90 -44
  20. data/lib/graphql/schema/resolver.rb +24 -59
  21. data/lib/graphql/schema/subscription.rb +6 -8
  22. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  23. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  24. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  25. data/lib/graphql/schema/validator/format_validator.rb +2 -1
  26. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  27. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  28. data/lib/graphql/schema/validator/numericality_validator.rb +12 -2
  29. data/lib/graphql/schema/validator.rb +36 -25
  30. data/lib/graphql/schema.rb +18 -5
  31. data/lib/graphql/static_validation/base_visitor.rb +3 -0
  32. data/lib/graphql/static_validation/error.rb +3 -1
  33. data/lib/graphql/static_validation/rules/fields_will_merge.rb +40 -21
  34. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  35. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  36. data/lib/graphql/static_validation/validation_context.rb +8 -2
  37. data/lib/graphql/static_validation/validator.rb +15 -12
  38. data/lib/graphql/string_encoding_error.rb +13 -3
  39. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +7 -1
  40. data/lib/graphql/subscriptions/event.rb +47 -2
  41. data/lib/graphql/subscriptions/serialize.rb +1 -1
  42. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  43. data/lib/graphql/types/int.rb +1 -1
  44. data/lib/graphql/types/string.rb +1 -1
  45. data/lib/graphql/unauthorized_error.rb +1 -1
  46. data/lib/graphql/version.rb +1 -1
  47. data/readme.md +1 -1
  48. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03b30407d3081dad5d25f3a3f9a9e2781cd34db838688816b01c231aaa5a6dc5
4
- data.tar.gz: a06092995f8e3ea0ed2c75485124b0123d41a23d77eedd773e15a1d00fad80ab
3
+ metadata.gz: b8312601f8e51973aaa9418c4c4691b6eab97140351c91d47f6c2c4f4deb88fb
4
+ data.tar.gz: 4cb698ef0a6739ceca4026b4dfaa594faf5658ac10a6005b051b70ec34af7f41
5
5
  SHA512:
6
- metadata.gz: 3af391b8f7394985a42595af2d7b3735a6666310a4583ffd2a9ddce46cf868a6a8d7f2a9cb28ba6a1b85fb70cce8d7425074df847f8b113420e59599bd0379b5
7
- data.tar.gz: ca7370709ff588ba437a2f5b277482f2a00543b057ee96e164198ea44f81ad1e470fafe4c7eb89a9e1e64c0aeeddcc128983a67458040c855115516c09adcfd0
6
+ metadata.gz: e9122ecfa0c4c28d219af2c828adb1b5e7e0ae29b10df55f543cbfa8973d8d9706ba5258c3de4f620b6cba5f16d6e6e031ebb6605193153ce5be821eaeb1adaa
7
+ data.tar.gz: 4db8f3435262d72f1d0d1995407f83c76a55dc0b5f28f7b5d8c6c4a378b2da00e4119e371585bdfac847ae5661c6e91e3bfdfcde82cd232198456905dee1f1a0
@@ -32,20 +32,28 @@ module Graphql
32
32
 
33
33
  # Return a string UUID for `object`
34
34
  def self.id_from_object(object, type_definition, query_ctx)
35
- # Here's a simple implementation which:
36
- # - joins the type name & object.id
37
- # - encodes it with base64:
38
- # GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
35
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
36
+ object_id = object.to_global_id.to_s
37
+ # Remove this redundant prefix to make IDs shorter:
38
+ object_id = object_id.sub("gid://\#{GlobalID.app}/", "")
39
+ encoded_id = Base64.urlsafe_encode64(object_id)
40
+ # Remove the "=" padding
41
+ encoded_id = encoded_id.sub(/=+/, "")
42
+ # Add a type hint
43
+ type_hint = type_definition.graphql_name.first
44
+ "\#{type_hint}_\#{encoded_id}"
39
45
  end
40
46
 
41
47
  # Given a string UUID, find the object
42
- def self.object_from_id(id, query_ctx)
43
- # For example, to decode the UUIDs generated above:
44
- # type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
45
- #
46
- # Then, based on `type_name` and `id`
47
- # find an object in your application
48
- # ...
48
+ def self.object_from_id(encoded_id_with_hint, query_ctx)
49
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
50
+ # Split off the type hint
51
+ _type_hint, encoded_id = encoded_id_with_hint.split("_", 2)
52
+ # Decode the ID
53
+ id = Base64.urlsafe_decode64(encoded_id)
54
+ # Rebuild it for Rails then find the object:
55
+ full_global_id = "gid://\#{GlobalID.app}/\#{id}"
56
+ GlobalID::Locator.locate(full_global_id)
49
57
  end
50
58
  RUBY
51
59
  inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false
@@ -4,11 +4,23 @@ class <%= schema_name %> < GraphQL::Schema
4
4
  <% if options[:batch] %>
5
5
  # GraphQL::Batch setup:
6
6
  use GraphQL::Batch
7
+ <% else %>
8
+ # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
9
+ use GraphQL::Dataloader
7
10
  <% end %>
11
+ # GraphQL-Ruby calls this when something goes wrong while running a query:
12
+ def self.type_error(err)
13
+ # if err.is_a?(GraphQL::InvalidNullError)
14
+ # # report to your bug tracker here
15
+ # return nil
16
+ # end
17
+ super
18
+ end
19
+
8
20
  # Union and Interface Resolution
9
21
  def self.resolve_type(abstract_type, obj, ctx)
10
22
  # TODO: Implement this function
11
- # to return the correct object type for `obj`
23
+ # to return the correct GraphQL object type for `obj`
12
24
  raise(GraphQL::RequiredImplementationMissingError)
13
25
  end
14
26
  end
@@ -37,7 +37,7 @@ module GraphQL
37
37
 
38
38
  if argument.definition.type.kind.input_object?
39
39
  extract_deprecated_arguments(argument.value.arguments.argument_values)
40
- elsif argument.definition.type.list?
40
+ elsif argument.definition.type.list? && !argument.value.nil?
41
41
  argument
42
42
  .value
43
43
  .select { |value| value.respond_to?(:arguments) }
@@ -6,7 +6,11 @@ module GraphQL
6
6
  # Called by {Dataloader} to prepare the {Source}'s internal state
7
7
  # @api private
8
8
  def setup(dataloader)
9
+ # These keys have been requested but haven't been fetched yet
9
10
  @pending_keys = []
11
+ # These keys have been passed to `fetch` but haven't been finished yet
12
+ @fetching_keys = []
13
+ # { key => result }
10
14
  @results = {}
11
15
  @dataloader = dataloader
12
16
  end
@@ -64,31 +68,68 @@ module GraphQL
64
68
  # Then run the batch and update the cache.
65
69
  # @return [void]
66
70
  def sync
71
+ pending_keys = @pending_keys.dup
67
72
  @dataloader.yield
73
+ iterations = 0
74
+ while pending_keys.any? { |k| !@results.key?(k) }
75
+ iterations += 1
76
+ if iterations > 1000
77
+ raise "#{self.class}#sync tried 1000 times to load pending keys (#{pending_keys}), but they still weren't loaded. There is likely a circular dependency."
78
+ end
79
+ @dataloader.yield
80
+ end
81
+ nil
68
82
  end
69
83
 
70
84
  # @return [Boolean] True if this source has any pending requests for data.
71
85
  def pending?
72
- @pending_keys.any?
86
+ !@pending_keys.empty?
73
87
  end
74
88
 
75
89
  # Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
76
90
  # @api private
77
91
  # @return [void]
78
92
  def run_pending_keys
93
+ if !@fetching_keys.empty?
94
+ @pending_keys -= @fetching_keys
95
+ end
79
96
  return if @pending_keys.empty?
80
97
  fetch_keys = @pending_keys.uniq
98
+ @fetching_keys.concat(fetch_keys)
81
99
  @pending_keys = []
82
100
  results = fetch(fetch_keys)
83
101
  fetch_keys.each_with_index do |key, idx|
84
102
  @results[key] = results[idx]
85
103
  end
104
+ nil
86
105
  rescue StandardError => error
87
106
  fetch_keys.each { |key| @results[key] = error }
88
107
  ensure
89
- nil
108
+ if fetch_keys
109
+ @fetching_keys -= fetch_keys
110
+ end
90
111
  end
91
112
 
113
+ # These arguments are given to `dataloader.with(source_class, ...)`. The object
114
+ # returned from this method is used to de-duplicate batch loads under the hood
115
+ # by using it as a Hash key.
116
+ #
117
+ # By default, the arguments are all put in an Array. To customize how this source's
118
+ # batches are merged, override this method to return something else.
119
+ #
120
+ # For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override
121
+ # this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply
122
+ # to equivalent relations.
123
+ #
124
+ # @param batch_args [Array<Object>]
125
+ # @param batch_kwargs [Hash]
126
+ # @return [Object]
127
+ def self.batch_key_for(*batch_args, **batch_kwargs)
128
+ [*batch_args, **batch_kwargs]
129
+ end
130
+
131
+ attr_reader :pending_keys
132
+
92
133
  private
93
134
 
94
135
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
@@ -96,6 +137,13 @@ module GraphQL
96
137
  # @return [Object] The result from {#fetch} for `key`.
97
138
  # @api private
98
139
  def result_for(key)
140
+ if !@results.key?(key)
141
+ raise <<-ERR
142
+ Invariant: fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
143
+
144
+ This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
145
+ ERR
146
+ end
99
147
  result = @results[key]
100
148
 
101
149
  raise result if result.class <= StandardError
@@ -27,18 +27,20 @@ module GraphQL
27
27
  schema.dataloader_class = self
28
28
  end
29
29
 
30
- def initialize
31
- @source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
32
- source = if RUBY_VERSION < "3"
33
- source_class.new(*batch_parameters)
34
- else
35
- batch_args, batch_kwargs = batch_parameters
36
- source_class.new(*batch_args, **batch_kwargs)
37
- end
38
- source.setup(self)
39
- h2[batch_parameters] = source
40
- }
30
+ # Call the block with a Dataloader instance,
31
+ # then run all enqueued jobs and return the result of the block.
32
+ def self.with_dataloading(&block)
33
+ dataloader = self.new
34
+ result = nil
35
+ dataloader.append_job {
36
+ result = block.call(dataloader)
41
37
  }
38
+ dataloader.run
39
+ result
40
+ end
41
+
42
+ def initialize
43
+ @source_cache = Hash.new { |h, k| h[k] = {} }
42
44
  @pending_jobs = []
43
45
  end
44
46
 
@@ -49,16 +51,24 @@ module GraphQL
49
51
  # @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
50
52
  # and cached for the lifetime of this {Multiplex}.
51
53
  if RUBY_VERSION < "3"
52
- def with(source_class, *batch_parameters)
53
- @source_cache[source_class][batch_parameters]
54
+ def with(source_class, *batch_args)
55
+ batch_key = source_class.batch_key_for(*batch_args)
56
+ @source_cache[source_class][batch_key] ||= begin
57
+ source = source_class.new(*batch_args)
58
+ source.setup(self)
59
+ source
60
+ end
54
61
  end
55
62
  else
56
63
  def with(source_class, *batch_args, **batch_kwargs)
57
- batch_parameters = [batch_args, batch_kwargs]
58
- @source_cache[source_class][batch_parameters]
64
+ batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs)
65
+ @source_cache[source_class][batch_key] ||= begin
66
+ source = source_class.new(*batch_args, **batch_kwargs)
67
+ source.setup(self)
68
+ source
69
+ end
59
70
  end
60
71
  end
61
-
62
72
  # Tell the dataloader that this fiber is waiting for data.
63
73
  #
64
74
  # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
@@ -80,6 +90,16 @@ module GraphQL
80
90
  # Use a self-contained queue for the work in the block.
81
91
  def run_isolated
82
92
  prev_queue = @pending_jobs
93
+ prev_pending_keys = {}
94
+ @source_cache.each do |source_class, batched_sources|
95
+ batched_sources.each do |batch_args, batched_source_instance|
96
+ if batched_source_instance.pending?
97
+ prev_pending_keys[batched_source_instance] = batched_source_instance.pending_keys.dup
98
+ batched_source_instance.pending_keys.clear
99
+ end
100
+ end
101
+ end
102
+
83
103
  @pending_jobs = []
84
104
  res = nil
85
105
  # Make sure the block is inside a Fiber, so it can `Fiber.yield`
@@ -90,6 +110,9 @@ module GraphQL
90
110
  res
91
111
  ensure
92
112
  @pending_jobs = prev_queue
113
+ prev_pending_keys.each do |source_instance, pending_keys|
114
+ source_instance.pending_keys.concat(pending_keys)
115
+ end
93
116
  end
94
117
 
95
118
  # @api private Move along, move along
@@ -76,7 +76,7 @@ ERR
76
76
  # Apply definition from `define(...)` kwargs
77
77
  defn.define_keywords.each do |keyword, value|
78
78
  # Don't splat string hashes, which blows up on Rubies before 2.7
79
- if value.is_a?(Hash) && value.each_key.all? { |k| k.is_a?(Symbol) }
79
+ if value.is_a?(Hash) && !value.empty? && value.each_key.all? { |k| k.is_a?(Symbol) }
80
80
  defn_proxy.public_send(keyword, **value)
81
81
  else
82
82
  defn_proxy.public_send(keyword, value)
@@ -38,9 +38,17 @@ module GraphQL
38
38
  end
39
39
  end
40
40
 
41
- TYPE_CLASSES.each do |type_class|
42
- refine type_class.singleton_class do
43
- include Methods
41
+ if defined?(::Refinement) && Refinement.private_method_defined?(:import_methods)
42
+ TYPE_CLASSES.each do |type_class|
43
+ refine type_class.singleton_class do
44
+ import_methods Methods
45
+ end
46
+ end
47
+ else
48
+ TYPE_CLASSES.each do |type_class|
49
+ refine type_class.singleton_class do
50
+ include Methods
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -3,11 +3,7 @@
3
3
  module GraphQL
4
4
  module Deprecation
5
5
  def self.warn(message)
6
- if defined?(ActiveSupport::Deprecation)
7
- ActiveSupport::Deprecation.warn(message)
8
- else
9
- Kernel.warn(message)
10
- end
6
+ Kernel.warn(message)
11
7
  end
12
8
  end
13
9
  end
@@ -236,6 +236,7 @@ module GraphQL
236
236
  selections,
237
237
  selection_response,
238
238
  final_response,
239
+ nil,
239
240
  )
240
241
  end
241
242
  }
@@ -347,7 +348,7 @@ module GraphQL
347
348
  NO_ARGS = {}.freeze
348
349
 
349
350
  # @return [void]
350
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
351
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result, parent_object) # rubocop:disable Metrics/ParameterLists
351
352
  set_all_interpreter_context(owner_object, nil, nil, path)
352
353
 
353
354
  finished_jobs = 0
@@ -355,7 +356,7 @@ module GraphQL
355
356
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
356
357
  @dataloader.append_job {
357
358
  evaluate_selection(
358
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
359
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result, parent_object
359
360
  )
360
361
  finished_jobs += 1
361
362
  if target_result && finished_jobs == enqueued_jobs
@@ -370,7 +371,7 @@ module GraphQL
370
371
  attr_reader :progress_path
371
372
 
372
373
  # @return [void]
373
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result) # rubocop:disable Metrics/ParameterLists
374
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result, parent_object) # rubocop:disable Metrics/ParameterLists
374
375
  return if dead_result?(selections_result)
375
376
  # As a performance optimization, the hash key will be a `Node` if
376
377
  # there's only one selection of the field. But if there are multiple
@@ -421,16 +422,16 @@ module GraphQL
421
422
  total_args_count = field_defn.arguments.size
422
423
  if total_args_count == 0
423
424
  kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
424
- evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
425
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
425
426
  else
426
427
  # TODO remove all arguments(...) usages?
427
428
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
428
- evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
429
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
429
430
  end
430
431
  end
431
432
  end
432
433
 
433
- def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
434
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result, parent_object) # rubocop:disable Metrics/ParameterLists
434
435
  context.scoped_context = scoped_context
435
436
  return_type = field_defn.type
436
437
  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, result_name: result_name, result: selection_result) do |resolved_arguments|
@@ -472,6 +473,8 @@ module GraphQL
472
473
  # This is used by `__typename` in order to support the legacy runtime,
473
474
  # but it has no use here (and it's always `nil`).
474
475
  # Stop adding it here to avoid the overhead of `.merge_extras` below.
476
+ when :parent
477
+ extra_args[:parent] = parent_object
475
478
  else
476
479
  extra_args[extra] = field_defn.fetch_extra(extra, context)
477
480
  end
@@ -733,6 +736,7 @@ module GraphQL
733
736
  selections,
734
737
  this_result,
735
738
  final_result,
739
+ owner_object.object,
736
740
  )
737
741
  this_result
738
742
  end
@@ -12,9 +12,25 @@ module GraphQL
12
12
  # The value which couldn't be encoded
13
13
  attr_reader :integer_value
14
14
 
15
- def initialize(value)
15
+ # @return [GraphQL::Schema::Field] The field that returned a too-big integer
16
+ attr_reader :field
17
+
18
+ # @return [Array<String, Integer>] Where the field appeared in the GraphQL response
19
+ attr_reader :path
20
+
21
+ def initialize(value, context:)
16
22
  @integer_value = value
17
- super("Integer out of bounds: #{value}. \nConsider using ID or GraphQL::Types::BigInt instead.")
23
+ @field = context[:current_field]
24
+ @path = context[:current_path]
25
+ message = "Integer out of bounds: #{value}".dup
26
+ if @path
27
+ message << " @ #{@path.join(".")}"
28
+ end
29
+ if @field
30
+ message << " (#{@field.path})"
31
+ end
32
+ message << ". Consider using ID or GraphQL::Types::BigInt instead."
33
+ super(message)
18
34
  end
19
35
  end
20
36
  end
@@ -23,6 +23,12 @@ module GraphQL
23
23
  if value.nil?
24
24
  'null'
25
25
  else
26
+ if (@object.type.kind.list? || (@object.type.kind.non_null? && @object.type.of_type.kind.list?)) && !value.respond_to?(:map)
27
+ # This is a bit odd -- we expect the default value to be an application-style value, so we use coerce result below.
28
+ # But coerce_result doesn't wrap single-item lists, which are valid inputs to list types.
29
+ # So, apply that wrapper here if needed.
30
+ value = [value]
31
+ end
26
32
  coerced_default_value = @object.type.coerce_result(value, @context)
27
33
  serialize_default_value(coerced_default_value, @object.type)
28
34
  end
@@ -70,23 +70,42 @@ module GraphQL
70
70
  wrappers = context ? context.namespace(:connections)[:all_wrappers] : all_wrappers
71
71
  impl = wrapper_for(items, wrappers: wrappers)
72
72
 
73
- if impl.nil?
74
- raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
73
+ if impl
74
+ impl.new(
75
+ items,
76
+ context: context,
77
+ parent: parent,
78
+ field: field,
79
+ max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
80
+ first: arguments[:first],
81
+ after: arguments[:after],
82
+ last: arguments[:last],
83
+ before: arguments[:before],
84
+ arguments: arguments,
85
+ edge_class: edge_class_for_field(field),
86
+ )
87
+ else
88
+ begin
89
+ connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(items)
90
+ if parent.is_a?(GraphQL::Schema::Object)
91
+ parent = parent.object
92
+ end
93
+ connection_class.new(
94
+ items,
95
+ arguments,
96
+ field: field,
97
+ max_page_size: field.max_page_size,
98
+ parent: parent,
99
+ context: context,
100
+ )
101
+ rescue RuntimeError => err
102
+ if err.message.include?("No connection implementation to wrap")
103
+ raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
104
+ else
105
+ raise err
106
+ end
107
+ end
75
108
  end
76
-
77
- impl.new(
78
- items,
79
- context: context,
80
- parent: parent,
81
- field: field,
82
- max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
83
- first: arguments[:first],
84
- after: arguments[:after],
85
- last: arguments[:last],
86
- before: arguments[:before],
87
- arguments: arguments,
88
- edge_class: edge_class_for_field(field),
89
- )
90
109
  end
91
110
 
92
111
  # use an override if there is one
@@ -72,7 +72,7 @@ module GraphQL
72
72
  elsif @operation_name_error
73
73
  @validation_errors << @operation_name_error
74
74
  else
75
- validation_result = @schema.static_validator.validate(@query, validate: @validate, timeout: @schema.validate_timeout)
75
+ validation_result = @schema.static_validator.validate(@query, validate: @validate, timeout: @schema.validate_timeout, max_errors: @schema.validate_max_errors)
76
76
  @validation_errors.concat(validation_result[:errors])
77
77
  @internal_representation = validation_result[:irep]
78
78
 
data/lib/graphql/query.rb CHANGED
@@ -117,6 +117,10 @@ module GraphQL
117
117
  raise ArgumentError, "Query should only be provided a query string or a document, not both."
118
118
  end
119
119
 
120
+ if @query_string && !@query_string.is_a?(String)
121
+ raise ArgumentError, "Query string argument should be a String, got #{@query_string.class.name} instead."
122
+ end
123
+
120
124
  # A two-layer cache of type resolution:
121
125
  # { abstract_type => { value => resolved_type } }
122
126
  @resolved_types_cache = Hash.new do |h1, k1|
@@ -151,7 +151,7 @@ module GraphQL
151
151
  input_obj_arg = input_obj_arg.type_class
152
152
  # TODO: this skips input objects whose values were alread replaced with application objects.
153
153
  # See: https://github.com/rmosolgo/graphql-ruby/issues/2633
154
- if value.respond_to?(:key?) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
154
+ if value.is_a?(InputObject) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
155
155
  return false
156
156
  end
157
157
  end
@@ -260,45 +260,88 @@ module GraphQL
260
260
  type.coerce_input(value, context)
261
261
  end
262
262
 
263
- # TODO this should probably be inside after_lazy
264
- if loads && !from_resolver?
265
- loaded_value = if type.list?
266
- loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
- context.schema.after_any_lazies(loaded_values) { |result| result }
268
- else
269
- context.query.with_error_handling do
270
- owner.load_application_object(self, loads, coerced_value, context)
263
+ # If this isn't lazy, then the block returns eagerly and assigns the result here
264
+ # If it _is_ lazy, then we write the lazy to the hash, then update it later
265
+ argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |resolved_coerced_value|
266
+ if loads && !from_resolver?
267
+ loaded_value = context.query.with_error_handling do
268
+ load_and_authorize_value(owner, coerced_value, context)
271
269
  end
272
270
  end
273
- end
274
271
 
275
- coerced_value = if loaded_value
276
- loaded_value
277
- else
278
- coerced_value
279
- end
272
+ maybe_loaded_value = loaded_value || resolved_coerced_value
273
+ context.schema.after_lazy(maybe_loaded_value) do |resolved_loaded_value|
274
+ owner.validate_directive_argument(self, resolved_loaded_value)
275
+ prepared_value = context.schema.error_handler.with_error_handling(context) do
276
+ prepare_value(parent_object, resolved_loaded_value, context: context)
277
+ end
280
278
 
281
- # If this isn't lazy, then the block returns eagerly and assigns the result here
282
- # If it _is_ lazy, then we write the lazy to the hash, then update it later
283
- argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |coerced_value|
284
- owner.validate_directive_argument(self, coerced_value)
285
- prepared_value = context.schema.error_handler.with_error_handling(context) do
286
- prepare_value(parent_object, coerced_value, context: context)
279
+ # TODO code smell to access such a deeply-nested constant in a distant module
280
+ argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
281
+ value: prepared_value,
282
+ definition: self,
283
+ default_used: default_used,
284
+ )
287
285
  end
286
+ end
287
+ end
288
288
 
289
- # TODO code smell to access such a deeply-nested constant in a distant module
290
- argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
291
- value: prepared_value,
292
- definition: self,
293
- default_used: default_used,
294
- )
289
+ def load_and_authorize_value(load_method_owner, coerced_value, context)
290
+ if coerced_value.nil?
291
+ return nil
292
+ end
293
+ arg_load_method = "load_#{keyword}"
294
+ if load_method_owner.respond_to?(arg_load_method)
295
+ custom_loaded_value = if load_method_owner.is_a?(Class)
296
+ load_method_owner.public_send(arg_load_method, coerced_value, context)
297
+ else
298
+ load_method_owner.public_send(arg_load_method, coerced_value)
299
+ end
300
+ context.schema.after_lazy(custom_loaded_value) do |custom_value|
301
+ if loads
302
+ if type.list?
303
+ loaded_values = custom_value.each_with_index.map { |custom_val, idx|
304
+ id = coerced_value[idx]
305
+ load_method_owner.authorize_application_object(self, id, context, custom_val)
306
+ }
307
+ context.schema.after_any_lazies(loaded_values, &:itself)
308
+ else
309
+ load_method_owner.authorize_application_object(self, coerced_value, context, custom_loaded_value)
310
+ end
311
+ else
312
+ custom_value
313
+ end
314
+ end
315
+ elsif loads
316
+ if type.list?
317
+ loaded_values = coerced_value.map { |val| load_method_owner.load_and_authorize_application_object(self, val, context) }
318
+ context.schema.after_any_lazies(loaded_values, &:itself)
319
+ else
320
+ load_method_owner.load_and_authorize_application_object(self, coerced_value, context)
321
+ end
322
+ else
323
+ coerced_value
295
324
  end
296
325
  end
297
326
 
298
327
  # @api private
299
328
  def validate_default_value
300
329
  coerced_default_value = begin
301
- type.coerce_isolated_result(default_value) unless default_value.nil?
330
+ # This is weird, but we should accept single-item default values for list-type arguments.
331
+ # If we used `coerce_isolated_input` below, it would do this for us, but it's not really
332
+ # the right thing here because we expect default values in application format (Ruby values)
333
+ # not GraphQL format (scalar values).
334
+ #
335
+ # But I don't think Schema::List#coerce_result should apply wrapping to single-item lists.
336
+ prepped_default_value = if default_value.nil?
337
+ nil
338
+ elsif (type.kind.list? || (type.kind.non_null? && type.of_type.list?)) && !default_value.respond_to?(:map)
339
+ [default_value]
340
+ else
341
+ default_value
342
+ end
343
+
344
+ type.coerce_isolated_result(prepped_default_value) unless prepped_default_value.nil?
302
345
  rescue GraphQL::Schema::Enum::UnresolvedValueError
303
346
  # It raises this, which is helpful at runtime, but not here...
304
347
  default_value