graphql 2.5.21 → 2.5.23

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/execution/interpreter/runtime.rb +3 -2
  3. data/lib/graphql/execution/interpreter.rb +6 -9
  4. data/lib/graphql/execution/lazy.rb +1 -1
  5. data/lib/graphql/execution/next/field_resolve_step.rb +114 -61
  6. data/lib/graphql/execution/next/load_argument_step.rb +5 -1
  7. data/lib/graphql/execution/next/prepare_object_step.rb +2 -2
  8. data/lib/graphql/execution/next/runner.rb +48 -26
  9. data/lib/graphql/execution/next.rb +5 -2
  10. data/lib/graphql/execution.rb +7 -4
  11. data/lib/graphql/execution_error.rb +5 -1
  12. data/lib/graphql/query/context.rb +1 -1
  13. data/lib/graphql/schema/field.rb +8 -5
  14. data/lib/graphql/schema/list.rb +1 -1
  15. data/lib/graphql/schema/member/has_dataloader.rb +20 -0
  16. data/lib/graphql/schema/member/has_fields.rb +6 -1
  17. data/lib/graphql/schema/non_null.rb +1 -1
  18. data/lib/graphql/schema/resolver.rb +18 -3
  19. data/lib/graphql/schema/subscription.rb +0 -2
  20. data/lib/graphql/schema/visibility/profile.rb +68 -49
  21. data/lib/graphql/schema/wrapper.rb +7 -1
  22. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  23. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  24. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  25. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  26. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  27. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  28. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -2
  29. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  30. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  31. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  32. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  33. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  34. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  35. data/lib/graphql/static_validation/validation_context.rb +1 -1
  36. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  37. data/lib/graphql/subscriptions/event.rb +1 -0
  38. data/lib/graphql/subscriptions.rb +20 -0
  39. data/lib/graphql/tracing/perfetto_trace.rb +2 -2
  40. data/lib/graphql/unauthorized_error.rb +4 -0
  41. data/lib/graphql/version.rb +1 -1
  42. metadata +3 -3
@@ -197,6 +197,7 @@ module GraphQL
197
197
  # @param resolve_batch [Symbol, true, nil] Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
198
198
  # @param resolve_each [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
199
199
  # @param resolve_legacy_instance_method [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
200
+ # @param dataload [Class, Hash] Shorthand for making dataloader calls
200
201
  # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
201
202
  # @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
202
203
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
@@ -219,7 +220,7 @@ module GraphQL
219
220
  # @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
220
221
  # @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field
221
222
  # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
222
- def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_legacy_instance_method: nil, resolve_static: nil, resolve_each: nil, resolve_batch: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
223
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_legacy_instance_method: nil, resolve_static: nil, resolve_each: nil, resolve_batch: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, dataload: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
223
224
  if name.nil?
224
225
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
225
226
  end
@@ -289,6 +290,9 @@ module GraphQL
289
290
  elsif resolve_legacy_instance_method
290
291
  @execution_next_mode = :resolve_legacy_instance_method
291
292
  @execution_next_mode_key = resolve_legacy_instance_method == true ? @method_sym : resolve_legacy_instance_method
293
+ elsif dataload
294
+ @execution_next_mode = :dataload
295
+ @execution_next_mode_key = dataload
292
296
  else
293
297
  @execution_next_mode = :direct_send
294
298
  @execution_next_mode_key = @method_sym
@@ -661,10 +665,9 @@ module GraphQL
661
665
  end
662
666
 
663
667
  def authorizes?(context)
664
- method(:authorized?).owner != GraphQL::Schema::Field || (
665
- (args = context.types.arguments(self)) &&
666
- (args.any? { |a| a.authorizes?(context) })
667
- )
668
+ method(:authorized?).owner != GraphQL::Schema::Field ||
669
+ ((args = context.types.arguments(self)) && (args.any? { |a| a.authorizes?(context) })) ||
670
+ (@resolver_class&.authorizes?(context)) || false
668
671
  end
669
672
 
670
673
  def authorized?(object, args, context)
@@ -19,7 +19,7 @@ module GraphQL
19
19
  end
20
20
 
21
21
  def to_type_signature
22
- "[#{@of_type.to_type_signature}]"
22
+ @type_signature ||= -"[#{@of_type.to_type_signature}]"
23
23
  end
24
24
 
25
25
  # This is for introspection, where it's expected the name will be `null`
@@ -56,6 +56,16 @@ module GraphQL
56
56
  source.load(find_by_value)
57
57
  end
58
58
 
59
+ # @see dataload_record Like `dataload_record`, but accepts an Array of `find_by_values`
60
+ def dataload_all_records(model, find_by_values, find_by: nil)
61
+ source = if find_by
62
+ dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
63
+ else
64
+ dataloader.with(Dataloader::ActiveRecordSource, model)
65
+ end
66
+ source.load_all(find_by_values)
67
+ end
68
+
59
69
  # Look up an associated record using a Rails association (via {Dataloader::ActiveRecordAssociationSource})
60
70
  # @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
61
71
  # @param record [ActiveRecord::Base] The object that the association belongs to.
@@ -73,6 +83,16 @@ module GraphQL
73
83
  end
74
84
  source.load(record)
75
85
  end
86
+
87
+ # @see dataload_association Like `dataload_assocation` but accepts an Array of records (required param)
88
+ def dataload_all_associations(records, association_name, scope: nil)
89
+ source = if scope
90
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
91
+ else
92
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name)
93
+ end
94
+ source.load_all(records)
95
+ end
76
96
  end
77
97
  end
78
98
  end
@@ -48,6 +48,7 @@ module GraphQL
48
48
  # @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
49
49
  # @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
50
50
  # @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
51
+ # @option kwargs [Class, Hash] :dataload Shorthand for dataloader lookups
51
52
  # @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
52
53
  # @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
53
54
  # @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
@@ -149,10 +150,14 @@ module GraphQL
149
150
 
150
151
  def global_id_field(field_name, **kwargs)
151
152
  type = self
152
- field field_name, "ID", **kwargs, null: false
153
+ field field_name, "ID", **kwargs, null: false, resolve_each: true
153
154
  define_method(field_name) do
154
155
  context.schema.id_from_object(object, type, context)
155
156
  end
157
+
158
+ define_singleton_method(field_name) do |object, context|
159
+ context.schema.id_from_object(object, type, context)
160
+ end
156
161
  end
157
162
 
158
163
  # @param new_has_no_fields [Boolean] Call with `true` to make this Object type ignore the requirement to have any defined fields.
@@ -24,7 +24,7 @@ module GraphQL
24
24
  end
25
25
 
26
26
  def to_type_signature
27
- "#{@of_type.to_type_signature}!"
27
+ @type_signature ||= -"#{@of_type.to_type_signature}!"
28
28
  end
29
29
 
30
30
  def inspect
@@ -46,7 +46,7 @@ module GraphQL
46
46
  @prepared_arguments = nil
47
47
  end
48
48
 
49
- attr_accessor :exec_result, :exec_index, :field_resolve_step
49
+ attr_accessor :exec_result, :exec_index, :field_resolve_step, :raw_arguments
50
50
 
51
51
  # @return [Object] The application object this field is being resolved on
52
52
  attr_accessor :object
@@ -66,7 +66,12 @@ module GraphQL
66
66
  q = context.query
67
67
  trace_objs = [object]
68
68
  q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
69
- is_authed, new_return_value = authorized?(**@prepared_arguments)
69
+ begin
70
+ is_authed, new_return_value = authorized?(**@prepared_arguments)
71
+ rescue GraphQL::UnauthorizedError => err
72
+ new_return_value = q.schema.unauthorized_object(err)
73
+ is_authed = true # the error was handled
74
+ end
70
75
 
71
76
  if (runner = @field_resolve_step.runner).resolves_lazies && runner.schema.lazy?(is_authed)
72
77
  is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
@@ -74,13 +79,19 @@ module GraphQL
74
79
 
75
80
  result = if is_authed
76
81
  Schema::Validator.validate!(self.class.validators, object, context, @prepared_arguments, as: @field)
82
+ if q.subscription? && @field.owner == context.schema.subscription
83
+ # This needs to use arguments without `loads:`
84
+ @original_arguments = @field_resolve_step.coerce_arguments(@field, @field_resolve_step.ast_node.arguments, false)
85
+ end
77
86
  call_resolve(@prepared_arguments)
87
+ elsif new_return_value.nil?
88
+ err = UnauthorizedFieldError.new(object: object, type: @field_resolve_step.parent_type, context: context, field: @field)
89
+ context.schema.unauthorized_field(err)
78
90
  else
79
91
  new_return_value
80
92
  end
81
93
  q = context.query
82
94
  q.current_trace.end_execute_field(field, @prepared_arguments, trace_objs, q, [result])
83
-
84
95
  exec_result[exec_index] = result
85
96
  rescue RuntimeError => err
86
97
  exec_result[exec_index] = err
@@ -198,6 +209,10 @@ module GraphQL
198
209
  authorize_arguments(args, inputs)
199
210
  end
200
211
 
212
+ def self.authorizes?(context)
213
+ self.instance_method(:authorized?).owner != GraphQL::Schema::Resolver
214
+ end
215
+
201
216
  # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
202
217
  #
203
218
  # By default, the error is re-raised and passed along to {{Schema.unauthorized_object}}.
@@ -15,8 +15,6 @@ module GraphQL
15
15
  extend GraphQL::Schema::Resolver::HasPayloadType
16
16
  extend GraphQL::Schema::Member::HasFields
17
17
  NO_UPDATE = :no_update
18
- # The generated payload type is required; If there's no payload,
19
- # propagate null.
20
18
  null false
21
19
 
22
20
  # @api private
@@ -54,6 +54,9 @@ module GraphQL
54
54
  @cached_fields.default_proc = nil
55
55
  @cached_arguments.default_proc = nil
56
56
  @loadable_possible_types.default_proc = nil
57
+ @cached_field_result.default_proc = nil
58
+ @cached_field_result.each { |_, h| h.default_proc = nil }
59
+ @cached_type_result.default_proc = nil
57
60
  super
58
61
  end
59
62
 
@@ -122,6 +125,14 @@ module GraphQL
122
125
  end.compare_by_identity
123
126
 
124
127
  @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity
128
+
129
+ # Combined cache for field(owner, field_name) — avoids repeated kind check + parent lookup + visibility check
130
+ @cached_field_result = Hash.new { |h, owner|
131
+ h[owner] = Hash.new { |h2, field_name| h2[field_name] = compute_field(owner, field_name) }
132
+ }.compare_by_identity
133
+
134
+ # Cache for type(type_name) — avoids repeated get_type + visibility + referenced? checks
135
+ @cached_type_result = Hash.new { |h, type_name| h[type_name] = compute_type(type_name) }
125
136
  end
126
137
 
127
138
  def field_on_visible_interface?(field, owner)
@@ -149,58 +160,11 @@ module GraphQL
149
160
  end
150
161
 
151
162
  def type(type_name)
152
- t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
153
- if t
154
- if t.is_a?(Array)
155
- vis_t = nil
156
- t.each do |t_defn|
157
- if @cached_visible[t_defn] && referenced?(t_defn)
158
- if vis_t.nil?
159
- vis_t = t_defn
160
- else
161
- raise_duplicate_definition(vis_t, t_defn)
162
- end
163
- end
164
- end
165
- vis_t
166
- else
167
- if t && @cached_visible[t] && referenced?(t)
168
- t
169
- else
170
- nil
171
- end
172
- end
173
- end
163
+ @cached_type_result[type_name]
174
164
  end
175
165
 
176
166
  def field(owner, field_name)
177
- f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
178
- field
179
- elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
180
- entry_point_field
181
- elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
182
- dynamic_field
183
- else
184
- nil
185
- end
186
- if f.is_a?(Array)
187
- visible_f = nil
188
- f.each do |f_defn|
189
- if @cached_visible_fields[owner][f_defn]
190
-
191
- if visible_f.nil?
192
- visible_f = f_defn
193
- else
194
- raise_duplicate_definition(visible_f, f_defn)
195
- end
196
- end
197
- end
198
- visible_f&.ensure_loaded
199
- elsif f && @cached_visible_fields[owner][f.ensure_loaded]
200
- f
201
- else
202
- nil
203
- end
167
+ @cached_field_result[owner][field_name]
204
168
  end
205
169
 
206
170
  def fields(owner)
@@ -306,6 +270,7 @@ module GraphQL
306
270
  def preload
307
271
  load_all_types
308
272
  @all_types.each do |type_name, type_defn|
273
+ type(type_name)
309
274
  if type_defn.kind.fields?
310
275
  fields(type_defn).each do |f|
311
276
  field(type_defn, f.graphql_name)
@@ -341,6 +306,60 @@ module GraphQL
341
306
 
342
307
  private
343
308
 
309
+ def compute_type(type_name)
310
+ t = @visibility.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
311
+ if t
312
+ if t.is_a?(Array)
313
+ vis_t = nil
314
+ t.each do |t_defn|
315
+ if @cached_visible[t_defn] && referenced?(t_defn)
316
+ if vis_t.nil?
317
+ vis_t = t_defn
318
+ else
319
+ raise_duplicate_definition(vis_t, t_defn)
320
+ end
321
+ end
322
+ end
323
+ vis_t
324
+ else
325
+ if t && @cached_visible[t] && referenced?(t)
326
+ t
327
+ else
328
+ nil
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def compute_field(owner, field_name)
335
+ f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
336
+ field
337
+ elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
338
+ entry_point_field
339
+ elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
340
+ dynamic_field
341
+ else
342
+ nil
343
+ end
344
+ if f.is_a?(Array)
345
+ visible_f = nil
346
+ f.each do |f_defn|
347
+ if @cached_visible_fields[owner][f_defn]
348
+ if visible_f.nil?
349
+ visible_f = f_defn
350
+ else
351
+ raise_duplicate_definition(visible_f, f_defn)
352
+ end
353
+ end
354
+ end
355
+ visible_f&.ensure_loaded
356
+ elsif f && @cached_visible_fields[owner][f.ensure_loaded]
357
+ f
358
+ else
359
+ nil
360
+ end
361
+ end
362
+
344
363
  def non_duplicate_items(definitions, visibility_cache)
345
364
  non_dups = []
346
365
  names = Set.new
@@ -13,7 +13,13 @@ module GraphQL
13
13
  end
14
14
 
15
15
  def unwrap
16
- @of_type.unwrap
16
+ @unwrapped ||= @of_type.unwrap
17
+ end
18
+
19
+ def freeze
20
+ unwrap
21
+ to_type_signature
22
+ super
17
23
  end
18
24
 
19
25
  def ==(other)
@@ -4,25 +4,26 @@ module GraphQL
4
4
  class BaseVisitor < GraphQL::Language::StaticVisitor
5
5
  def initialize(document, context)
6
6
  @path = []
7
- @object_types = []
8
- @directives = []
9
- @field_definitions = []
10
- @argument_definitions = []
11
- @directive_definitions = []
7
+ @path_depth = 0
8
+ @current_object_type = nil
9
+ @parent_object_type = nil
10
+ @current_field_definition = nil
11
+ @current_argument_definition = nil
12
+ @parent_argument_definition = nil
13
+ @current_directive_definition = nil
12
14
  @context = context
13
15
  @types = context.query.types
14
16
  @schema = context.schema
17
+ @inline_fragment_paths = {}
18
+ @field_unwrapped_types = {}.compare_by_identity
15
19
  super(document)
16
20
  end
17
21
 
18
22
  attr_reader :context
19
23
 
20
- # @return [Array<GraphQL::ObjectType>] Types whose scope we've entered
21
- attr_reader :object_types
22
-
23
24
  # @return [Array<String>] The nesting of the current position in the AST
24
25
  def path
25
- @path.dup
26
+ @path[0, @path_depth]
26
27
  end
27
28
 
28
29
  # Build a class to visit the AST and perform validation,
@@ -55,86 +56,125 @@ module GraphQL
55
56
  module ContextMethods
56
57
  def on_operation_definition(node, parent)
57
58
  object_type = @schema.root_type_for_operation(node.operation_type)
58
- push_type(object_type)
59
- @path.push("#{node.operation_type}#{node.name ? " #{node.name}" : ""}")
59
+ prev_parent_ot = @parent_object_type
60
+ @parent_object_type = @current_object_type
61
+ @current_object_type = object_type
62
+ @path[@path_depth] = "#{node.operation_type}#{node.name ? " #{node.name}" : ""}"
63
+ @path_depth += 1
60
64
  super
61
- @object_types.pop
62
- @path.pop
65
+ @current_object_type = @parent_object_type
66
+ @parent_object_type = prev_parent_ot
67
+ @path_depth -= 1
63
68
  end
64
69
 
65
70
  def on_fragment_definition(node, parent)
66
- on_fragment_with_type(node) do
67
- @path.push("fragment #{node.name}")
68
- super
71
+ object_type = if node.type
72
+ @types.type(node.type.name)
73
+ else
74
+ @current_object_type
69
75
  end
76
+ prev_parent_ot = @parent_object_type
77
+ @parent_object_type = @current_object_type
78
+ @current_object_type = object_type
79
+ @path[@path_depth] = "fragment #{node.name}"
80
+ @path_depth += 1
81
+ super
82
+ @current_object_type = @parent_object_type
83
+ @parent_object_type = prev_parent_ot
84
+ @path_depth -= 1
70
85
  end
71
86
 
87
+ INLINE_FRAGMENT_NO_TYPE = "..."
88
+
72
89
  def on_inline_fragment(node, parent)
73
- on_fragment_with_type(node) do
74
- @path.push("...#{node.type ? " on #{node.type.to_query_string}" : ""}")
75
- super
90
+ if node.type
91
+ object_type = @types.type(node.type.name)
92
+ @path[@path_depth] = @inline_fragment_paths[node.type.name] ||= -"... on #{node.type.to_query_string}"
93
+ @path_depth += 1
94
+ else
95
+ object_type = @current_object_type
96
+ @path[@path_depth] = INLINE_FRAGMENT_NO_TYPE
97
+ @path_depth += 1
76
98
  end
99
+ prev_parent_ot = @parent_object_type
100
+ @parent_object_type = @current_object_type
101
+ @current_object_type = object_type
102
+ super
103
+ @current_object_type = @parent_object_type
104
+ @parent_object_type = prev_parent_ot
105
+ @path_depth -= 1
77
106
  end
78
107
 
79
108
  def on_field(node, parent)
80
- parent_type = @object_types.last
109
+ parent_type = @current_object_type
81
110
  field_definition = @types.field(parent_type, node.name)
82
- @field_definitions.push(field_definition)
83
- if !field_definition.nil?
84
- next_object_type = field_definition.type.unwrap
85
- push_type(next_object_type)
111
+ prev_field_definition = @current_field_definition
112
+ @current_field_definition = field_definition
113
+ prev_parent_ot = @parent_object_type
114
+ @parent_object_type = @current_object_type
115
+ if field_definition
116
+ @current_object_type = @field_unwrapped_types[field_definition] ||= field_definition.type.unwrap
86
117
  else
87
- push_type(nil)
118
+ @current_object_type = nil
88
119
  end
89
- @path.push(node.alias || node.name)
120
+ @path[@path_depth] = node.alias || node.name
121
+ @path_depth += 1
90
122
  super
91
- @field_definitions.pop
92
- @object_types.pop
93
- @path.pop
123
+ @current_field_definition = prev_field_definition
124
+ @current_object_type = @parent_object_type
125
+ @parent_object_type = prev_parent_ot
126
+ @path_depth -= 1
94
127
  end
95
128
 
96
129
  def on_directive(node, parent)
97
130
  directive_defn = @context.schema_directives[node.name]
98
- @directive_definitions.push(directive_defn)
131
+ prev_directive_definition = @current_directive_definition
132
+ @current_directive_definition = directive_defn
99
133
  super
100
- @directive_definitions.pop
134
+ @current_directive_definition = prev_directive_definition
101
135
  end
102
136
 
103
137
  def on_argument(node, parent)
104
- argument_defn = if (arg = @argument_definitions.last)
138
+ argument_defn = if (arg = @current_argument_definition)
105
139
  arg_type = arg.type.unwrap
106
140
  if arg_type.kind.input_object?
107
141
  @types.argument(arg_type, node.name)
108
142
  else
109
143
  nil
110
144
  end
111
- elsif (directive_defn = @directive_definitions.last)
145
+ elsif (directive_defn = @current_directive_definition)
112
146
  @types.argument(directive_defn, node.name)
113
- elsif (field_defn = @field_definitions.last)
147
+ elsif (field_defn = @current_field_definition)
114
148
  @types.argument(field_defn, node.name)
115
149
  else
116
150
  nil
117
151
  end
118
152
 
119
- @argument_definitions.push(argument_defn)
120
- @path.push(node.name)
153
+ prev_parent = @parent_argument_definition
154
+ @parent_argument_definition = @current_argument_definition
155
+ @current_argument_definition = argument_defn
156
+ @path[@path_depth] = node.name
157
+ @path_depth += 1
121
158
  super
122
- @argument_definitions.pop
123
- @path.pop
159
+ @current_argument_definition = @parent_argument_definition
160
+ @parent_argument_definition = prev_parent
161
+ @path_depth -= 1
124
162
  end
125
163
 
126
164
  def on_fragment_spread(node, parent)
127
- @path.push("... #{node.name}")
165
+ @path[@path_depth] = "... #{node.name}"
166
+ @path_depth += 1
128
167
  super
129
- @path.pop
168
+ @path_depth -= 1
130
169
  end
131
170
 
132
171
  def on_input_object(node, parent)
133
- arg_defn = @argument_definitions.last
172
+ arg_defn = @current_argument_definition
134
173
  if arg_defn && arg_defn.type.list?
135
- @path.push(parent.children.index(node))
174
+ @path[@path_depth] = parent.children.index(node)
175
+ @path_depth += 1
136
176
  super
137
- @path.pop
177
+ @path_depth -= 1
138
178
  else
139
179
  super
140
180
  end
@@ -142,48 +182,32 @@ module GraphQL
142
182
 
143
183
  # @return [GraphQL::BaseType] The current object type
144
184
  def type_definition
145
- @object_types.last
185
+ @current_object_type
146
186
  end
147
187
 
148
188
  # @return [GraphQL::BaseType] The type which the current type came from
149
189
  def parent_type_definition
150
- @object_types[-2]
190
+ @parent_object_type
151
191
  end
152
192
 
153
193
  # @return [GraphQL::Field, nil] The most-recently-entered GraphQL::Field, if currently inside one
154
194
  def field_definition
155
- @field_definitions.last
195
+ @current_field_definition
156
196
  end
157
197
 
158
198
  # @return [GraphQL::Directive, nil] The most-recently-entered GraphQL::Directive, if currently inside one
159
199
  def directive_definition
160
- @directive_definitions.last
200
+ @current_directive_definition
161
201
  end
162
202
 
163
203
  # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one
164
204
  def argument_definition
165
- # Don't get the _last_ one because that's the current one.
166
- # Get the second-to-last one, which is the parent of the current one.
167
- @argument_definitions[-2]
205
+ # Return the parent argument definition (not the current one).
206
+ @parent_argument_definition
168
207
  end
169
208
 
170
209
  private
171
210
 
172
- def on_fragment_with_type(node)
173
- object_type = if node.type
174
- @types.type(node.type.name)
175
- else
176
- @object_types.last
177
- end
178
- push_type(object_type)
179
- yield(node)
180
- @object_types.pop
181
- @path.pop
182
- end
183
-
184
- def push_type(t)
185
- @object_types.push(t)
186
- end
187
211
  end
188
212
 
189
213
  private
@@ -192,7 +216,7 @@ module GraphQL
192
216
  if @context.too_many_errors?
193
217
  throw :too_many_validation_errors
194
218
  end
195
- error.path ||= (path || @path.dup)
219
+ error.path ||= (path || @path[0, @path_depth])
196
220
  context.errors << error
197
221
  end
198
222
 
@@ -12,7 +12,7 @@ module GraphQL
12
12
  return
13
13
  end
14
14
 
15
- if @context.schema.error_bubbling || context.errors.none? { |err| err.path.take(@path.size) == @path }
15
+ if @context.schema.error_bubbling || context.errors.none? { |err| err.path.take(@path_depth) == @path[0, @path_depth] }
16
16
  parent_defn = parent_definition(parent)
17
17
 
18
18
  if parent_defn && (arg_defn = @types.argument(parent_defn, node.name))
@@ -16,12 +16,24 @@ module GraphQL
16
16
 
17
17
  def validate_arguments(node)
18
18
  argument_defns = node.arguments
19
- if !argument_defns.empty?
20
- args_by_name = Hash.new { |h, k| h[k] = [] }
21
- argument_defns.each { |a| args_by_name[a.name] << a }
22
- args_by_name.each do |name, defns|
23
- if defns.size > 1
24
- add_error(GraphQL::StaticValidation::ArgumentNamesAreUniqueError.new("There can be only one argument named \"#{name}\"", nodes: defns, name: name))
19
+ if argument_defns.size > 1
20
+ seen = {}
21
+ argument_defns.each do |a|
22
+ name = a.name
23
+ if seen.key?(name)
24
+ prev = seen[name]
25
+ if prev.is_a?(Array)
26
+ prev << a
27
+ else
28
+ seen[name] = [prev, a]
29
+ end
30
+ else
31
+ seen[name] = a
32
+ end
33
+ end
34
+ seen.each do |name, val|
35
+ if val.is_a?(Array)
36
+ add_error(GraphQL::StaticValidation::ArgumentNamesAreUniqueError.new("There can be only one argument named \"#{name}\"", nodes: val, name: name))
25
37
  end
26
38
  end
27
39
  end
@@ -10,9 +10,12 @@ module GraphQL
10
10
  elsif parent_defn
11
11
  kind_of_node = node_type(parent)
12
12
  error_arg_name = parent_name(parent, parent_defn)
13
- arg_names = context.types.arguments(parent_defn).map(&:graphql_name)
13
+ suggestion = if @schema.did_you_mean
14
+ arg_names = context.types.arguments(parent_defn).map(&:graphql_name)
15
+ context.did_you_mean_suggestion(node.name, arg_names)
16
+ end
14
17
  add_error(GraphQL::StaticValidation::ArgumentsAreDefinedError.new(
15
- "#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'#{context.did_you_mean_suggestion(node.name, arg_names)}",
18
+ "#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'#{suggestion}",
16
19
  nodes: node,
17
20
  name: error_arg_name,
18
21
  type: kind_of_node,