graphql 1.12.17 → 1.12.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/object_generator.rb +2 -1
  3. data/lib/generators/graphql/relay.rb +19 -11
  4. data/lib/generators/graphql/templates/schema.erb +14 -2
  5. data/lib/graphql/analysis/ast/field_usage.rb +7 -3
  6. data/lib/graphql/dataloader/source.rb +32 -2
  7. data/lib/graphql/dataloader.rb +13 -0
  8. data/lib/graphql/deprecated_dsl.rb +11 -3
  9. data/lib/graphql/deprecation.rb +1 -5
  10. data/lib/graphql/integer_encoding_error.rb +18 -2
  11. data/lib/graphql/language/nodes.rb +13 -1
  12. data/lib/graphql/pagination/connections.rb +35 -16
  13. data/lib/graphql/query/validation_pipeline.rb +1 -1
  14. data/lib/graphql/schema/argument.rb +58 -31
  15. data/lib/graphql/schema/build_from_definition.rb +8 -7
  16. data/lib/graphql/schema/directive.rb +4 -0
  17. data/lib/graphql/schema/enum_value.rb +1 -1
  18. data/lib/graphql/schema/field.rb +15 -4
  19. data/lib/graphql/schema/input_object.rb +10 -11
  20. data/lib/graphql/schema/member/has_arguments.rb +90 -44
  21. data/lib/graphql/schema/resolver.rb +20 -57
  22. data/lib/graphql/schema/subscription.rb +4 -4
  23. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  24. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  25. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  26. data/lib/graphql/schema/validator/format_validator.rb +4 -1
  27. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  28. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  29. data/lib/graphql/schema/validator/numericality_validator.rb +7 -1
  30. data/lib/graphql/schema/validator.rb +36 -25
  31. data/lib/graphql/schema.rb +18 -5
  32. data/lib/graphql/static_validation/base_visitor.rb +3 -0
  33. data/lib/graphql/static_validation/error.rb +3 -1
  34. data/lib/graphql/static_validation/rules/fields_will_merge.rb +51 -25
  35. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  36. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  37. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -0
  38. data/lib/graphql/static_validation/validation_context.rb +8 -2
  39. data/lib/graphql/static_validation/validator.rb +15 -12
  40. data/lib/graphql/string_encoding_error.rb +13 -3
  41. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +7 -1
  42. data/lib/graphql/subscriptions/event.rb +47 -2
  43. data/lib/graphql/subscriptions/serialize.rb +1 -1
  44. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  45. data/lib/graphql/types/int.rb +1 -1
  46. data/lib/graphql/types/string.rb +1 -1
  47. data/lib/graphql/unauthorized_error.rb +1 -1
  48. data/lib/graphql/version.rb +1 -1
  49. metadata +5 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c9089e4578454f473996553a4771e4086e51b2b2db17fc31a579ab28019bd39
4
- data.tar.gz: f45a70c81394f35c86d1610491f106e39899bc2c927a6a6d13e9e6533bc3bc85
3
+ metadata.gz: 582e38c72afb32d38c7a256d1eb811a7ef0e13b8984c10370e278f038c639343
4
+ data.tar.gz: 2fc62afb65909e4e08d5ad42c2c5c40a48a7acedca8afa5ac512a4aa60284a8e
5
5
  SHA512:
6
- metadata.gz: b7231b5e00a336439d15d469a383cd7e211e305961d4a4b431e8a97f5e483819bbd87de3d7cfffc9b6677b8175f700e012cd9ee4a0c92fa47005455a14893d3c
7
- data.tar.gz: 98495c2b24d65ef90d7f5e1036956349ea79d03ea192de6ed06e517657f49b4e0e9c748c8a28d54aac24e96b42f52eea5d4cbfbca801a4389b1515804296831a
6
+ metadata.gz: 922e24767106339ff4e441cc9597a6a974814371180aa5bdaab2db7608ac590610199af451f0dfc85019dd4a531bb7934e57a60f7cf976be1c095f3722866474
7
+ data.tar.gz: dd2b9fb0f9f26dd8ad3b3841516eadd66b05765de3f0490eafce49fc29599cc6894f1a0d376b3f3bef47d850fd3948ff7b03256721575a2ae6b06f6fd6741736
@@ -12,7 +12,8 @@ module Graphql
12
12
  #
13
13
  # Add the Node interface with `--node`.
14
14
  class ObjectGenerator < TypeGeneratorBase
15
- desc "Create a GraphQL::ObjectType with the given name and fields"
15
+ desc "Create a GraphQL::ObjectType with the given name and fields." \
16
+ "If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns."
16
17
  source_root File.expand_path('../templates', __FILE__)
17
18
 
18
19
  argument :custom_fields,
@@ -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, context)
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
- # TODO: Implement this function
11
- # to return the correct object type for `obj`
22
+ # TODO: Implement this method
23
+ # to return the correct GraphQL object type for `obj`
12
24
  raise(GraphQL::RequiredImplementationMissingError)
13
25
  end
14
26
  end
@@ -15,8 +15,12 @@ module GraphQL
15
15
  field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
16
16
  @used_fields << field
17
17
  @used_deprecated_fields << field if field_defn.deprecation_reason
18
-
19
- extract_deprecated_arguments(visitor.query.arguments_for(node, visitor.field_definition).argument_values)
18
+ arguments = visitor.query.arguments_for(node, visitor.field_definition)
19
+ # If there was an error when preparing this argument object,
20
+ # then this might be an error or something:
21
+ if arguments.respond_to?(:argument_values)
22
+ extract_deprecated_arguments(arguments.argument_values)
23
+ end
20
24
  end
21
25
 
22
26
  def result
@@ -37,7 +41,7 @@ module GraphQL
37
41
 
38
42
  if argument.definition.type.kind.input_object?
39
43
  extract_deprecated_arguments(argument.value.arguments.argument_values)
40
- elsif argument.definition.type.list?
44
+ elsif argument.definition.type.list? && !argument.value.nil?
41
45
  argument
42
46
  .value
43
47
  .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,29 +68,46 @@ 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
 
92
113
  # These arguments are given to `dataloader.with(source_class, ...)`. The object
@@ -107,6 +128,8 @@ module GraphQL
107
128
  [*batch_args, **batch_kwargs]
108
129
  end
109
130
 
131
+ attr_reader :pending_keys
132
+
110
133
  private
111
134
 
112
135
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
@@ -114,6 +137,13 @@ module GraphQL
114
137
  # @return [Object] The result from {#fetch} for `key`.
115
138
  # @api private
116
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
117
147
  result = @results[key]
118
148
 
119
149
  raise result if result.class <= StandardError
@@ -90,6 +90,16 @@ module GraphQL
90
90
  # Use a self-contained queue for the work in the block.
91
91
  def run_isolated
92
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
+
93
103
  @pending_jobs = []
94
104
  res = nil
95
105
  # Make sure the block is inside a Fiber, so it can `Fiber.yield`
@@ -100,6 +110,9 @@ module GraphQL
100
110
  res
101
111
  ensure
102
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
103
116
  end
104
117
 
105
118
  # @api private Move along, move along
@@ -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
@@ -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
@@ -133,6 +133,8 @@ module GraphQL
133
133
  end
134
134
 
135
135
  class << self
136
+ # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
137
+
136
138
  # Add a default `#visit_method` and `#children_method_name` using the class name
137
139
  def inherited(child_class)
138
140
  super
@@ -197,7 +199,16 @@ module GraphQL
197
199
  else
198
200
  module_eval <<-RUBY, __FILE__, __LINE__
199
201
  def children
200
- @children ||= (#{children_of_type.keys.map { |k| "@#{k}" }.join(" + ")}).freeze
202
+ @children ||= begin
203
+ if #{children_of_type.keys.map { |k| "@#{k}.any?" }.join(" || ")}
204
+ new_children = []
205
+ #{children_of_type.keys.map { |k| "new_children.concat(@#{k})" }.join("; ")}
206
+ new_children.freeze
207
+ new_children
208
+ else
209
+ NO_CHILDREN
210
+ end
211
+ end
201
212
  end
202
213
  RUBY
203
214
  end
@@ -267,6 +278,7 @@ module GraphQL
267
278
  RUBY
268
279
  end
269
280
  end
281
+ # rubocop:enable Development/NoEvalCop
270
282
  end
271
283
  end
272
284
 
@@ -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
 
@@ -55,6 +55,7 @@ module GraphQL
55
55
  def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, validates: nil, directives: nil, deprecation_reason: nil, &definition_block)
56
56
  arg_name ||= name
57
57
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
58
+ NameValidator.validate!(@name)
58
59
  @type_expr = type_expr || type
59
60
  @description = desc || description
60
61
  @null = !required
@@ -78,11 +79,8 @@ module GraphQL
78
79
  self.validates(validates)
79
80
 
80
81
  if definition_block
81
- if definition_block.arity == 1
82
- instance_exec(self, &definition_block)
83
- else
84
- instance_eval(&definition_block)
85
- end
82
+ # `self` will still be self, it will also be the first argument to the block:
83
+ instance_exec(self, &definition_block)
86
84
  end
87
85
  end
88
86
 
@@ -260,38 +258,67 @@ module GraphQL
260
258
  type.coerce_input(value, context)
261
259
  end
262
260
 
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)
261
+ # If this isn't lazy, then the block returns eagerly and assigns the result here
262
+ # If it _is_ lazy, then we write the lazy to the hash, then update it later
263
+ argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |resolved_coerced_value|
264
+ if loads && !from_resolver?
265
+ loaded_value = context.query.with_error_handling do
266
+ load_and_authorize_value(owner, coerced_value, context)
271
267
  end
272
268
  end
273
- end
274
269
 
275
- coerced_value = if loaded_value
276
- loaded_value
277
- else
278
- coerced_value
279
- end
270
+ maybe_loaded_value = loaded_value || resolved_coerced_value
271
+ context.schema.after_lazy(maybe_loaded_value) do |resolved_loaded_value|
272
+ owner.validate_directive_argument(self, resolved_loaded_value)
273
+ prepared_value = context.schema.error_handler.with_error_handling(context) do
274
+ prepare_value(parent_object, resolved_loaded_value, context: context)
275
+ end
280
276
 
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)
277
+ # TODO code smell to access such a deeply-nested constant in a distant module
278
+ argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
279
+ value: prepared_value,
280
+ definition: self,
281
+ default_used: default_used,
282
+ )
287
283
  end
284
+ end
285
+ end
288
286
 
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
- )
287
+ def load_and_authorize_value(load_method_owner, coerced_value, context)
288
+ if coerced_value.nil?
289
+ return nil
290
+ end
291
+ arg_load_method = "load_#{keyword}"
292
+ if load_method_owner.respond_to?(arg_load_method)
293
+ custom_loaded_value = if load_method_owner.is_a?(Class)
294
+ load_method_owner.public_send(arg_load_method, coerced_value, context)
295
+ else
296
+ load_method_owner.public_send(arg_load_method, coerced_value)
297
+ end
298
+ context.schema.after_lazy(custom_loaded_value) do |custom_value|
299
+ if loads
300
+ if type.list?
301
+ loaded_values = custom_value.each_with_index.map { |custom_val, idx|
302
+ id = coerced_value[idx]
303
+ load_method_owner.authorize_application_object(self, id, context, custom_val)
304
+ }
305
+ context.schema.after_any_lazies(loaded_values, &:itself)
306
+ else
307
+ load_method_owner.authorize_application_object(self, coerced_value, context, custom_loaded_value)
308
+ end
309
+ else
310
+ custom_value
311
+ end
312
+ end
313
+ elsif loads
314
+ if type.list?
315
+ loaded_values = coerced_value.map { |val| load_method_owner.load_and_authorize_application_object(self, val, context) }
316
+ context.schema.after_any_lazies(loaded_values, &:itself)
317
+ else
318
+ load_method_owner.load_and_authorize_application_object(self, coerced_value, context)
319
+ end
320
+ else
321
+ coerced_value
295
322
  end
296
323
  end
297
324
 
@@ -426,17 +426,18 @@ module GraphQL
426
426
 
427
427
  # Don't do this for interfaces
428
428
  if default_resolve
429
- owner.class_eval <<-RUBY, __FILE__, __LINE__
430
- # frozen_string_literal: true
431
- def #{resolve_method_name}(**args)
432
- field_instance = self.class.get_field("#{field_definition.name}")
433
- context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
434
- end
435
- RUBY
429
+ define_field_resolve_method(owner, resolve_method_name, field_definition.name)
436
430
  end
437
431
  end
438
432
  end
439
433
 
434
+ def define_field_resolve_method(owner, method_name, field_name)
435
+ owner.define_method(method_name) { |**args|
436
+ field_instance = self.class.get_field(field_name)
437
+ context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
438
+ }
439
+ end
440
+
440
441
  def build_resolve_type(lookup_hash, directives, missing_type_handler)
441
442
  resolve_type_proc = nil
442
443
  resolve_type_proc = ->(ast_node) {
@@ -116,6 +116,10 @@ module GraphQL
116
116
  @arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext)
117
117
  end
118
118
 
119
+ def graphql_name
120
+ self.class.graphql_name
121
+ end
122
+
119
123
  LOCATIONS = [
120
124
  QUERY = :QUERY,
121
125
  MUTATION = :MUTATION,
@@ -55,7 +55,7 @@ module GraphQL
55
55
  end
56
56
 
57
57
  if block_given?
58
- instance_eval(&block)
58
+ instance_exec(self, &block)
59
59
  end
60
60
  end
61
61
 
@@ -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
@@ -228,6 +231,7 @@ module GraphQL
228
231
  name_s = -name.to_s
229
232
  @underscored_name = -Member::BuildType.underscore(name_s)
230
233
  @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s)
234
+ NameValidator.validate!(@name)
231
235
  @description = description
232
236
  if field.is_a?(GraphQL::Schema::Field)
233
237
  raise ArgumentError, "Instead of passing a field as `field:`, use `add_field(field)` to add an already-defined field."
@@ -510,6 +514,7 @@ module GraphQL
510
514
  field_defn
511
515
  end
512
516
 
517
+ class MissingReturnTypeError < GraphQL::Error; end
513
518
  attr_writer :type
514
519
 
515
520
  def type
@@ -517,14 +522,21 @@ module GraphQL
517
522
  Member::BuildType.parse_type(@function.type, null: false)
518
523
  elsif @field
519
524
  Member::BuildType.parse_type(@field.type, null: false)
525
+ elsif @return_type_expr.nil?
526
+ # Not enough info to determine type
527
+ message = "Can't determine the return type for #{self.path}"
528
+ if @resolver_class
529
+ message += " (it has `resolver: #{@resolver_class}`, consider configuration a `type ...` for that class)"
530
+ end
531
+ raise MissingReturnTypeError, message
520
532
  else
521
533
  Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
522
534
  end
523
- rescue GraphQL::Schema::InvalidDocumentError => err
535
+ rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
524
536
  # Let this propagate up
525
537
  raise err
526
538
  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
539
+ raise MissingReturnTypeError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace
528
540
  end
529
541
 
530
542
  def visible?(context)
@@ -608,8 +620,7 @@ module GraphQL
608
620
  if is_authorized
609
621
  public_send_field(object, args, ctx)
610
622
  else
611
- err = GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
612
- ctx.schema.unauthorized_field(err)
623
+ raise GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
613
624
  end
614
625
  end
615
626
  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
@@ -127,12 +123,8 @@ module GraphQL
127
123
  def argument(*args, **kwargs, &block)
128
124
  argument_defn = super(*args, **kwargs, &block)
129
125
  # Add a method access
130
- method_name = argument_defn.keyword
131
- class_eval <<-RUBY, __FILE__, __LINE__
132
- def #{method_name}
133
- self[#{method_name.inspect}]
134
- end
135
- RUBY
126
+ define_accessor_method(argument_defn.keyword)
127
+ argument_defn
136
128
  end
137
129
 
138
130
  def to_graphql
@@ -240,6 +232,13 @@ module GraphQL
240
232
 
241
233
  result
242
234
  end
235
+
236
+ private
237
+
238
+ def define_accessor_method(method_name)
239
+ define_method(method_name) { self[method_name] }
240
+ alias_method(method_name, method_name)
241
+ end
243
242
  end
244
243
 
245
244
  private