graphql 1.12.15 → 1.12.19

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  3. data/lib/graphql/dataloader/source.rb +20 -0
  4. data/lib/graphql/dataloader.rb +39 -16
  5. data/lib/graphql/define/instance_definable.rb +1 -1
  6. data/lib/graphql/deprecated_dsl.rb +11 -3
  7. data/lib/graphql/execution/interpreter/runtime.rb +34 -25
  8. data/lib/graphql/integer_encoding_error.rb +18 -2
  9. data/lib/graphql/introspection/input_value_type.rb +6 -0
  10. data/lib/graphql/pagination/connections.rb +5 -0
  11. data/lib/graphql/query/validation_pipeline.rb +1 -1
  12. data/lib/graphql/query.rb +4 -0
  13. data/lib/graphql/schema/argument.rb +71 -28
  14. data/lib/graphql/schema/field.rb +14 -4
  15. data/lib/graphql/schema/input_object.rb +5 -9
  16. data/lib/graphql/schema/member/has_arguments.rb +90 -44
  17. data/lib/graphql/schema/resolver.rb +24 -59
  18. data/lib/graphql/schema/subscription.rb +25 -7
  19. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  20. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  21. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  22. data/lib/graphql/schema/validator/format_validator.rb +2 -1
  23. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  24. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  25. data/lib/graphql/schema/validator/numericality_validator.rb +12 -2
  26. data/lib/graphql/schema/validator.rb +36 -25
  27. data/lib/graphql/schema.rb +18 -5
  28. data/lib/graphql/static_validation/base_visitor.rb +3 -0
  29. data/lib/graphql/static_validation/rules/fields_will_merge.rb +4 -4
  30. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  31. data/lib/graphql/static_validation/validation_context.rb +6 -1
  32. data/lib/graphql/static_validation/validator.rb +15 -12
  33. data/lib/graphql/string_encoding_error.rb +13 -3
  34. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +7 -1
  35. data/lib/graphql/subscriptions/event.rb +6 -21
  36. data/lib/graphql/subscriptions/serialize.rb +1 -1
  37. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  38. data/lib/graphql/types/big_int.rb +5 -1
  39. data/lib/graphql/types/int.rb +1 -1
  40. data/lib/graphql/types/string.rb +1 -1
  41. data/lib/graphql/unauthorized_error.rb +1 -1
  42. data/lib/graphql/version.rb +1 -1
  43. data/lib/graphql.rb +15 -11
  44. data/readme.md +1 -1
  45. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f858de6eaac686676c159e87ae2f200b6c69b9b6c1da8e7459595b49f4f44e7a
4
- data.tar.gz: afd8686f7897896919bef91076285f0763e49076759036352bd212d41c1bb47f
3
+ metadata.gz: 92aba0a1935a0022b97ea1630cde64884a727ce714fae6a2bca543666c73bde0
4
+ data.tar.gz: 5db896ad77a52ec4a025f526bab4f3c4b3d18206e009c2d8c3e14dd434f12ae6
5
5
  SHA512:
6
- metadata.gz: 6774f423bdf61cf07d842ca00f517068139a86da2d6e0d6b7e15e7970aaf5ce1b07e58d78c0a13a1ccf0a8db87c48d4061e151ca42bc21731e26958ef96ad922
7
- data.tar.gz: 31ca7b94bacf8df5a8c27c0e3c2f831362970e49c65c8b0a284095c1bd392af26347cecc78e60dc91ff4581a9a3d55dc08204eb0230ed641f2907e009a4f7f3e
6
+ metadata.gz: 2985936af9bba4d0feec8263dd4a6c60c09110e39aba1a97359e4d02900732f02900e30d798074932bb6fe2a5ed0e00441be859bb3035ab0c819ccec3c9a0f66
7
+ data.tar.gz: e8e961e540b54a5c54d6d78c444eef422bd55264ee8ffc9c3a231f2614c570ee75de5c2888f3b13974304c48f60072e04bb0d0444ea6e1a13f4a37652dbd2f0b
@@ -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) }
@@ -89,6 +89,26 @@ module GraphQL
89
89
  nil
90
90
  end
91
91
 
92
+ # These arguments are given to `dataloader.with(source_class, ...)`. The object
93
+ # returned from this method is used to de-duplicate batch loads under the hood
94
+ # by using it as a Hash key.
95
+ #
96
+ # By default, the arguments are all put in an Array. To customize how this source's
97
+ # batches are merged, override this method to return something else.
98
+ #
99
+ # For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override
100
+ # this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply
101
+ # to equivalent relations.
102
+ #
103
+ # @param batch_args [Array<Object>]
104
+ # @param batch_kwargs [Hash]
105
+ # @return [Object]
106
+ def self.batch_key_for(*batch_args, **batch_kwargs)
107
+ [*batch_args, **batch_kwargs]
108
+ end
109
+
110
+ attr_reader :pending_keys
111
+
92
112
  private
93
113
 
94
114
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
@@ -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
@@ -10,7 +10,19 @@ module GraphQL
10
10
  class Runtime
11
11
 
12
12
  module GraphQLResult
13
- attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
13
+ def initialize(result_name, parent_result)
14
+ @graphql_parent = parent_result
15
+ if parent_result && parent_result.graphql_dead
16
+ @graphql_dead = true
17
+ end
18
+ @graphql_result_name = result_name
19
+ # Jump through some hoops to avoid creating this duplicate storage if at all possible.
20
+ @graphql_metadata = nil
21
+ end
22
+
23
+ attr_accessor :graphql_dead
24
+ attr_reader :graphql_parent, :graphql_result_name
25
+
14
26
  # Although these are used by only one of the Result classes,
15
27
  # it's handy to have the methods implemented on both (even though they just return `nil`)
16
28
  # because it makes it easy to check if anything is assigned.
@@ -24,9 +36,8 @@ module GraphQL
24
36
  end
25
37
 
26
38
  class GraphQLResultHash
27
- def initialize
28
- # Jump through some hoops to avoid creating this duplicate hash if at all possible.
29
- @graphql_metadata = nil
39
+ def initialize(_result_name, _parent_result)
40
+ super
30
41
  @graphql_result_data = {}
31
42
  end
32
43
 
@@ -86,10 +97,8 @@ module GraphQL
86
97
  class GraphQLResultArray
87
98
  include GraphQLResult
88
99
 
89
- def initialize
90
- # Avoid this duplicate allocation if possible -
91
- # but it will require some work to keep it up-to-date if it's created.
92
- @graphql_metadata = nil
100
+ def initialize(_result_name, _parent_result)
101
+ super
93
102
  @graphql_result_data = []
94
103
  end
95
104
 
@@ -146,7 +155,7 @@ module GraphQL
146
155
  @context = query.context
147
156
  @multiplex_context = query.multiplex.context
148
157
  @interpreter_context = @context.namespace(:interpreter)
149
- @response = GraphQLResultHash.new
158
+ @response = GraphQLResultHash.new(nil, nil)
150
159
  # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
151
160
  @runtime_directive_names = []
152
161
  noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@@ -208,7 +217,7 @@ module GraphQL
208
217
  # directly evaluated and the results can be written right into the main response hash.
209
218
  tap_or_each(gathered_selections) do |selections, is_selection_array|
210
219
  if is_selection_array
211
- selection_response = GraphQLResultHash.new
220
+ selection_response = GraphQLResultHash.new(nil, nil)
212
221
  final_response = @response
213
222
  else
214
223
  selection_response = @response
@@ -227,6 +236,7 @@ module GraphQL
227
236
  selections,
228
237
  selection_response,
229
238
  final_response,
239
+ nil,
230
240
  )
231
241
  end
232
242
  }
@@ -338,7 +348,7 @@ module GraphQL
338
348
  NO_ARGS = {}.freeze
339
349
 
340
350
  # @return [void]
341
- 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
342
352
  set_all_interpreter_context(owner_object, nil, nil, path)
343
353
 
344
354
  finished_jobs = 0
@@ -346,7 +356,7 @@ module GraphQL
346
356
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
347
357
  @dataloader.append_job {
348
358
  evaluate_selection(
349
- 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
350
360
  )
351
361
  finished_jobs += 1
352
362
  if target_result && finished_jobs == enqueued_jobs
@@ -361,7 +371,8 @@ module GraphQL
361
371
  attr_reader :progress_path
362
372
 
363
373
  # @return [void]
364
- 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
375
+ return if dead_result?(selections_result)
365
376
  # As a performance optimization, the hash key will be a `Node` if
366
377
  # there's only one selection of the field. But if there are multiple
367
378
  # selections of the field, it will be an Array of nodes
@@ -411,16 +422,16 @@ module GraphQL
411
422
  total_args_count = field_defn.arguments.size
412
423
  if total_args_count == 0
413
424
  kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
414
- 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)
415
426
  else
416
427
  # TODO remove all arguments(...) usages?
417
428
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
418
- 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)
419
430
  end
420
431
  end
421
432
  end
422
433
 
423
- 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
424
435
  context.scoped_context = scoped_context
425
436
  return_type = field_defn.type
426
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|
@@ -462,6 +473,8 @@ module GraphQL
462
473
  # This is used by `__typename` in order to support the legacy runtime,
463
474
  # but it has no use here (and it's always `nil`).
464
475
  # Stop adding it here to avoid the overhead of `.merge_extras` below.
476
+ when :parent
477
+ extra_args[:parent] = parent_object
465
478
  else
466
479
  extra_args[extra] = field_defn.fetch_extra(extra, context)
467
480
  end
@@ -693,9 +706,7 @@ module GraphQL
693
706
  after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
694
707
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
695
708
  if HALT != continue_value
696
- response_hash = GraphQLResultHash.new
697
- response_hash.graphql_parent = selection_result
698
- response_hash.graphql_result_name = result_name
709
+ response_hash = GraphQLResultHash.new(result_name, selection_result)
699
710
  set_result(selection_result, result_name, response_hash)
700
711
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
701
712
  # There are two possibilities for `gathered_selections`:
@@ -708,9 +719,7 @@ module GraphQL
708
719
  # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
709
720
  tap_or_each(gathered_selections) do |selections, is_selection_array|
710
721
  if is_selection_array
711
- this_result = GraphQLResultHash.new
712
- this_result.graphql_parent = selection_result
713
- this_result.graphql_result_name = result_name
722
+ this_result = GraphQLResultHash.new(result_name, selection_result)
714
723
  final_result = response_hash
715
724
  else
716
725
  this_result = response_hash
@@ -727,6 +736,7 @@ module GraphQL
727
736
  selections,
728
737
  this_result,
729
738
  final_result,
739
+ owner_object.object,
730
740
  )
731
741
  this_result
732
742
  end
@@ -735,16 +745,15 @@ module GraphQL
735
745
  end
736
746
  when "LIST"
737
747
  inner_type = current_type.of_type
738
- response_list = GraphQLResultArray.new
748
+ response_list = GraphQLResultArray.new(result_name, selection_result)
739
749
  response_list.graphql_non_null_list_items = inner_type.non_null?
740
- response_list.graphql_parent = selection_result
741
- response_list.graphql_result_name = result_name
742
750
  set_result(selection_result, result_name, response_list)
743
751
 
744
752
  idx = 0
745
753
  scoped_context = context.scoped_context
746
754
  begin
747
755
  value.each do |inner_value|
756
+ break if dead_result?(response_list)
748
757
  next_path = path.dup
749
758
  next_path << idx
750
759
  this_idx = idx
@@ -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
@@ -130,6 +130,11 @@ module GraphQL
130
130
  if defined?(Mongoid::Association::Referenced::HasMany::Targets::Enumerable)
131
131
  add(Mongoid::Association::Referenced::HasMany::Targets::Enumerable, Pagination::MongoidRelationConnection)
132
132
  end
133
+
134
+ # Mongoid 7.3+
135
+ if defined?(Mongoid::Association::Referenced::HasMany::Enumerable)
136
+ add(Mongoid::Association::Referenced::HasMany::Enumerable, Pagination::MongoidRelationConnection)
137
+ end
133
138
  end
134
139
  end
135
140
  end
@@ -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
@@ -122,6 +122,9 @@ module GraphQL
122
122
  else
123
123
  kwargs[:type] = type
124
124
  end
125
+ if type.is_a?(Class) && type < GraphQL::Schema::Mutation
126
+ raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
127
+ end
125
128
  end
126
129
  new(**kwargs, &block)
127
130
  end
@@ -510,6 +513,7 @@ module GraphQL
510
513
  field_defn
511
514
  end
512
515
 
516
+ class MissingReturnTypeError < GraphQL::Error; end
513
517
  attr_writer :type
514
518
 
515
519
  def type
@@ -517,14 +521,21 @@ module GraphQL
517
521
  Member::BuildType.parse_type(@function.type, null: false)
518
522
  elsif @field
519
523
  Member::BuildType.parse_type(@field.type, null: false)
524
+ elsif @return_type_expr.nil?
525
+ # Not enough info to determine type
526
+ message = "Can't determine the return type for #{self.path}"
527
+ if @resolver_class
528
+ message += " (it has `resolver: #{@resolver_class}`, consider configuration a `type ...` for that class)"
529
+ end
530
+ raise MissingReturnTypeError, message
520
531
  else
521
532
  Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
522
533
  end
523
- rescue GraphQL::Schema::InvalidDocumentError => err
534
+ rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
524
535
  # Let this propagate up
525
536
  raise err
526
537
  rescue StandardError => err
527
- raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace
538
+ raise MissingReturnTypeError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace
528
539
  end
529
540
 
530
541
  def visible?(context)
@@ -608,8 +619,7 @@ module GraphQL
608
619
  if is_authorized
609
620
  public_send_field(object, args, ctx)
610
621
  else
611
- err = GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
612
- ctx.schema.unauthorized_field(err)
622
+ raise GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
613
623
  end
614
624
  end
615
625
  rescue GraphQL::UnauthorizedFieldError => err
@@ -40,11 +40,7 @@ module GraphQL
40
40
  # With the interpreter, it's done during `coerce_arguments`
41
41
  if loads && !arg_defn.from_resolver? && !context.interpreter?
42
42
  value = @ruby_style_hash[ruby_kwargs_key]
43
- loaded_value = if arg_defn.type.list?
44
- value.map { |val| load_application_object(arg_defn, loads, val, context) }
45
- else
46
- load_application_object(arg_defn, loads, value, context)
47
- end
43
+ loaded_value = arg_defn.load_and_authorize_value(self, value, context)
48
44
  maybe_lazies << context.schema.after_lazy(loaded_value) do |loaded_value|
49
45
  overwrite_argument(ruby_kwargs_key, loaded_value)
50
46
  end
@@ -71,11 +67,11 @@ module GraphQL
71
67
  end
72
68
 
73
69
  def prepare
74
- if context
75
- context.schema.after_any_lazies(@maybe_lazies) do
76
- object = context[:current_object]
70
+ if @context
71
+ @context.schema.after_any_lazies(@maybe_lazies) do
72
+ object = @context[:current_object]
77
73
  # Pass this object's class with `as` so that messages are rendered correctly from inherited validators
78
- Schema::Validator.validate!(self.class.validators, object, context, @ruby_style_hash, as: self.class)
74
+ Schema::Validator.validate!(self.class.validators, object, @context, @ruby_style_hash, as: self.class)
79
75
  self
80
76
  end
81
77
  else