graphql 2.5.22 → 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 (41) 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 +93 -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 +3 -1
  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 +3 -4
  14. data/lib/graphql/schema/list.rb +1 -1
  15. data/lib/graphql/schema/member/has_fields.rb +5 -1
  16. data/lib/graphql/schema/non_null.rb +1 -1
  17. data/lib/graphql/schema/resolver.rb +18 -3
  18. data/lib/graphql/schema/subscription.rb +0 -2
  19. data/lib/graphql/schema/visibility/profile.rb +68 -49
  20. data/lib/graphql/schema/wrapper.rb +7 -1
  21. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  22. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  23. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  24. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  25. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  26. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  27. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -2
  28. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  29. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  30. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  31. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  32. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  33. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  34. data/lib/graphql/static_validation/validation_context.rb +1 -1
  35. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
  36. data/lib/graphql/subscriptions/event.rb +1 -0
  37. data/lib/graphql/subscriptions.rb +20 -0
  38. data/lib/graphql/tracing/perfetto_trace.rb +2 -2
  39. data/lib/graphql/unauthorized_error.rb +4 -0
  40. data/lib/graphql/version.rb +1 -1
  41. metadata +3 -3
@@ -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,
@@ -10,9 +10,12 @@ module GraphQL
10
10
  if !@types.directive_exists?(node.name)
11
11
  @directives_are_defined_errors_by_name ||= {}
12
12
  error = @directives_are_defined_errors_by_name[node.name] ||= begin
13
- @directive_names ||= @types.directives.map(&:graphql_name)
13
+ suggestion = if @schema.did_you_mean
14
+ @directive_names ||= @types.directives.map(&:graphql_name)
15
+ context.did_you_mean_suggestion(node.name, @directive_names)
16
+ end
14
17
  err = GraphQL::StaticValidation::DirectivesAreDefinedError.new(
15
- "Directive @#{node.name} is not defined#{context.did_you_mean_suggestion(node.name, @directive_names)}",
18
+ "Directive @#{node.name} is not defined#{suggestion}",
16
19
  nodes: [],
17
20
  directive: node.name
18
21
  )
@@ -3,7 +3,7 @@ module GraphQL
3
3
  module StaticValidation
4
4
  module FieldsAreDefinedOnType
5
5
  def on_field(node, parent)
6
- parent_type = @object_types[-2]
6
+ parent_type = @parent_object_type
7
7
  field = context.query.types.field(parent_type, node.name)
8
8
 
9
9
  if field.nil?
@@ -14,8 +14,9 @@ module GraphQL
14
14
  node_name: parent_type.graphql_name
15
15
  ))
16
16
  else
17
- possible_fields = possible_fields(context, parent_type)
18
- suggestion = context.did_you_mean_suggestion(node.name, possible_fields)
17
+ suggestion = if @schema.did_you_mean
18
+ context.did_you_mean_suggestion(node.name, possible_fields(context, parent_type))
19
+ end
19
20
  message = "Field '#{node.name}' doesn't exist on type '#{parent_type.graphql_name}'#{suggestion}"
20
21
  add_error(GraphQL::StaticValidation::FieldsAreDefinedOnTypeError.new(
21
22
  message,
@@ -7,8 +7,7 @@ module GraphQL
7
7
  include GraphQL::StaticValidation::Error::ErrorHelper
8
8
 
9
9
  def on_field(node, parent)
10
- field_defn = field_definition
11
- if validate_field_selections(node, field_defn.type.unwrap)
10
+ if validate_field_selections(node, @current_object_type)
12
11
  super
13
12
  end
14
13
  end
@@ -23,6 +22,17 @@ module GraphQL
23
22
 
24
23
 
25
24
  def validate_field_selections(ast_node, resolved_type)
25
+ # Fast paths for the two most common cases:
26
+ # 1. Leaf type with no selections (scalars, enums) — most fields
27
+ # 2. Non-leaf type with selections (objects, interfaces)
28
+ if resolved_type
29
+ if ast_node.selections.empty?
30
+ return true if resolved_type.kind.leaf?
31
+ else
32
+ return true unless resolved_type.kind.leaf?
33
+ end
34
+ end
35
+
26
36
  msg = if resolved_type.nil?
27
37
  nil
28
38
  elsif resolved_type.kind.leaf?