graphql 2.3.12 → 2.3.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b47194e443275c95a25db554e05a8aa54672503b91e5cce87e422ff04b036502
4
- data.tar.gz: b8e052d617025f4b2c20b4db7cdc7f2fc69dc2582518ff768f3ff475d5220f29
3
+ metadata.gz: 1a0c7d49f75d9f1740b415ca0c088733fa4ddf380da43dcd469d20654d89ad93
4
+ data.tar.gz: 0bf6ad1c22adc0a8d9e948287a751a561fb7203ca02cb902341b13ad3df40db1
5
5
  SHA512:
6
- metadata.gz: 65ce3a1c3eed524257063672bc87dc72f98af3ab9c398a1cb06d2388ed7199ec22b85c77ae7db82b854efc0a0f4cd2c2a27062e033725f30dc0082b5bbed71c6
7
- data.tar.gz: 04e1f618cf82c8bb08982a2e4ebccba5bfadaff73453e9c09b7c84d22423be2c30a57b9bf61a0ecc15a7f15401e24e72ac4fe33a93be0722f13fcb24f0855891
6
+ metadata.gz: 73583aaf16bda54678168edf89e471ee1b3b54d7a2f1aa1bc0c9b959113000768a8d12a8f3ffeec29561639917fcb694ec2fd8c25f22dd8eaf33c46133f40160
7
+ data.tar.gz: 615f46f473d89526106f9d85d3560052dacfd35f5f31119a037fa2b1fda8f92445bd15c5993df3ebce8af10df9907a2d41576cc4444822df0606a13b0d9ed7c9
@@ -207,22 +207,32 @@ module GraphQL
207
207
  finished_jobs = 0
208
208
  enqueued_jobs = gathered_selections.size
209
209
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
210
- @dataloader.append_job {
211
- evaluate_selection(
212
- result_name, field_ast_nodes_or_ast_node, selections_result
213
- )
214
- finished_jobs += 1
215
- if target_result && finished_jobs == enqueued_jobs
216
- selections_result.merge_into(target_result)
217
- end
218
- }
210
+
219
211
  # Field resolution may pause the fiber,
220
212
  # so it wouldn't get to the `Resolve` call that happens below.
221
213
  # So instead trigger a run from this outer context.
222
214
  if selections_result.graphql_is_eager
223
215
  @dataloader.clear_cache
224
- @dataloader.run
225
- @dataloader.clear_cache
216
+ @dataloader.run_isolated {
217
+ evaluate_selection(
218
+ result_name, field_ast_nodes_or_ast_node, selections_result
219
+ )
220
+ finished_jobs += 1
221
+ if target_result && finished_jobs == enqueued_jobs
222
+ selections_result.merge_into(target_result)
223
+ end
224
+ @dataloader.clear_cache
225
+ }
226
+ else
227
+ @dataloader.append_job {
228
+ evaluate_selection(
229
+ result_name, field_ast_nodes_or_ast_node, selections_result
230
+ )
231
+ finished_jobs += 1
232
+ if target_result && finished_jobs == enqueued_jobs
233
+ selections_result.merge_into(target_result)
234
+ end
235
+ }
226
236
  end
227
237
  end
228
238
  selections_result
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./base_cop"
3
+
4
+ module GraphQL
5
+ module Rubocop
6
+ module GraphQL
7
+ # Identify (and auto-correct) any field whose type configuration isn't given
8
+ # in the configuration block.
9
+ #
10
+ # @example
11
+ # # bad, immediately causes Rails to load `app/graphql/types/thing.rb`
12
+ # field :thing, Types::Thing
13
+ #
14
+ # # good, defers loading until the file is needed
15
+ # field :thing do
16
+ # type(Types::Thing)
17
+ # end
18
+ #
19
+ class FieldTypeInBlock < BaseCop
20
+ MSG = "type configuration can be moved to a block to defer loading the type's file"
21
+
22
+ BUILT_IN_SCALAR_NAMES = ["Float", "Int", "Integer", "String", "ID", "Boolean"]
23
+ def_node_matcher :field_config_with_inline_type, <<-Pattern
24
+ (
25
+ send {nil? _} :field sym ${const array} ...
26
+ )
27
+ Pattern
28
+
29
+ def_node_matcher :field_config_with_inline_type_and_block, <<-Pattern
30
+ (
31
+ block
32
+ (send {nil? _} :field sym ${const array}) ...
33
+ (args)
34
+ _
35
+
36
+ )
37
+ Pattern
38
+
39
+ def on_block(node)
40
+ field_config_with_inline_type_and_block(node) do |type_const|
41
+ ignore_node(type_const)
42
+ type_const_str = get_type_argument_str(node, type_const)
43
+ if ignore_inline_type_str?(type_const_str)
44
+ # Do nothing ...
45
+ else
46
+ add_offense(type_const) do |corrector|
47
+ cleaned_node_source = delete_type_argument(node, type_const)
48
+ field_indent = determine_field_indent(node)
49
+ cleaned_node_source.sub!(/(\{|do)/, "\\1\n#{field_indent} type #{type_const_str}")
50
+ corrector.replace(node, cleaned_node_source)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def on_send(node)
57
+ field_config_with_inline_type(node) do |type_const|
58
+ return if ignored_node?(type_const)
59
+ type_const_str = get_type_argument_str(node, type_const)
60
+ if ignore_inline_type_str?(type_const_str)
61
+ # Do nothing -- not loading from another file
62
+ else
63
+ add_offense(type_const) do |corrector|
64
+ cleaned_node_source = delete_type_argument(node, type_const)
65
+ field_indent = determine_field_indent(node)
66
+ cleaned_node_source += " do\n#{field_indent} type #{type_const_str}\n#{field_indent}end"
67
+ corrector.replace(node, cleaned_node_source)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ private
75
+
76
+ def ignore_inline_type_str?(type_str)
77
+ BUILT_IN_SCALAR_NAMES.include?(type_str)
78
+ end
79
+
80
+ def get_type_argument_str(send_node, type_const)
81
+ first_pos = type_const.location.expression.begin_pos
82
+ end_pos = type_const.location.expression.end_pos
83
+ node_source = send_node.source_range.source
84
+ node_first_pos = send_node.location.expression.begin_pos
85
+
86
+ relative_first_pos = first_pos - node_first_pos
87
+ end_removal_pos = end_pos - node_first_pos
88
+
89
+ node_source[relative_first_pos...end_removal_pos]
90
+ end
91
+
92
+ def delete_type_argument(send_node, type_const)
93
+ first_pos = type_const.location.expression.begin_pos
94
+ end_pos = type_const.location.expression.end_pos
95
+ node_source = send_node.source_range.source
96
+ node_first_pos = send_node.location.expression.begin_pos
97
+
98
+ relative_first_pos = first_pos - node_first_pos
99
+ end_removal_pos = end_pos - node_first_pos
100
+
101
+ begin_removal_pos = relative_first_pos
102
+ while node_source[begin_removal_pos] != ","
103
+ begin_removal_pos -= 1
104
+ if begin_removal_pos < 1
105
+ raise "Invariant: somehow backtracked to beginning of node looking for a comma (node source: #{node_source.inspect})"
106
+ end
107
+ end
108
+
109
+ node_source[0...begin_removal_pos] + node_source[end_removal_pos..-1]
110
+ end
111
+
112
+ def determine_field_indent(send_node)
113
+ surrounding_node = send_node.parent.parent
114
+ surrounding_source = surrounding_node.source
115
+ indent_test_idx = send_node.location.expression.begin_pos - surrounding_node.source_range.begin_pos - 1
116
+ field_indent = "".dup
117
+ while surrounding_source[indent_test_idx] == " "
118
+ field_indent << " "
119
+ indent_test_idx -= 1
120
+ if indent_test_idx == 0
121
+ raise "Invariant: somehow backtracted to beginning of class when looking for field indent (source: #{node_source.inspect})"
122
+ end
123
+ end
124
+ field_indent
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./base_cop"
3
+
4
+ module GraphQL
5
+ module Rubocop
6
+ module GraphQL
7
+ # Identify (and auto-correct) any root types in your schema file.
8
+ #
9
+ # @example
10
+ # # bad, immediately causes Rails to load `app/graphql/types/query.rb`
11
+ # query Types::Query
12
+ #
13
+ # # good, defers loading until the file is needed
14
+ # query { Types::Query }
15
+ #
16
+ class RootTypesInBlock < BaseCop
17
+ MSG = "type configuration can be moved to a block to defer loading the type's file"
18
+
19
+ def_node_matcher :root_type_config_without_block, <<-Pattern
20
+ (
21
+ send nil? {:query :mutation :subscription} const
22
+ )
23
+ Pattern
24
+
25
+ def on_send(node)
26
+ root_type_config_without_block(node) do
27
+ add_offense(node) do |corrector|
28
+ new_node_source = node.source_range.source
29
+ new_node_source.sub!(/(query|mutation|subscription)/, '\1 {')
30
+ new_node_source << " }"
31
+ corrector.replace(node, new_node_source)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,3 +2,5 @@
2
2
 
3
3
  require "graphql/rubocop/graphql/default_null_true"
4
4
  require "graphql/rubocop/graphql/default_required_true"
5
+ require "graphql/rubocop/graphql/field_type_in_block"
6
+ require "graphql/rubocop/graphql/root_types_in_block"
@@ -189,6 +189,7 @@ module GraphQL
189
189
  add_directives_from(type)
190
190
  if type.kind.fields?
191
191
  type.all_field_definitions.each do |field|
192
+ field.ensure_loaded
192
193
  name = field.graphql_name
193
194
  field_type = field.type.unwrap
194
195
  if !field_type.is_a?(GraphQL::Schema::LateBoundType)
@@ -22,9 +22,21 @@ module GraphQL
22
22
  class Enum < GraphQL::Schema::Member
23
23
  extend GraphQL::Schema::Member::ValidatesInput
24
24
 
25
+ # This is raised when either:
26
+ #
27
+ # - A resolver returns a value which doesn't match any of the enum's configured values;
28
+ # - Or, the resolver returns a value which matches a value, but that value's `authorized?` check returns false.
29
+ #
30
+ # In either case, the field should be modified so that the invalid value isn't returned.
31
+ #
32
+ # {GraphQL::Schema::Enum} subclasses get their own subclass of this error, so that bug trackers can better show where they came from.
25
33
  class UnresolvedValueError < GraphQL::Error
26
- def initialize(value:, enum:, context:)
27
- fix_message = ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
34
+ def initialize(value:, enum:, context:, authorized:)
35
+ fix_message = if authorized == false
36
+ ", but this value was unauthorized. Update the field or resolver to return a different value in this case (or return `nil`)."
37
+ else
38
+ ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
39
+ end
28
40
  message = if (cp = context[:current_path]) && (cf = context[:current_field])
29
41
  "`#{cf.path}` returned `#{value.inspect}` at `#{cp.join(".")}`#{fix_message}"
30
42
  else
@@ -34,6 +46,8 @@ module GraphQL
34
46
  end
35
47
  end
36
48
 
49
+ # Raised when a {GraphQL::Schema::Enum} is defined to have no values.
50
+ # This can also happen when all values return false for `.visible?`.
37
51
  class MissingValuesError < GraphQL::Error
38
52
  def initialize(enum_type)
39
53
  @enum_type = enum_type
@@ -43,10 +57,10 @@ module GraphQL
43
57
 
44
58
  class << self
45
59
  # Define a value for this enum
46
- # @param graphql_name [String, Symbol] the GraphQL value for this, usually `SCREAMING_CASE`
47
- # @param description [String], the GraphQL description for this value, present in documentation
48
- # @param value [Object], the translated Ruby value for this object (defaults to `graphql_name`)
49
- # @param deprecation_reason [String] if this object is deprecated, include a message here
60
+ # @option kwargs [String, Symbol] :graphql_name the GraphQL value for this, usually `SCREAMING_CASE`
61
+ # @option kwargs [String] :description, the GraphQL description for this value, present in documentation
62
+ # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`)
63
+ # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here
50
64
  # @return [void]
51
65
  # @see {Schema::EnumValue} which handles these inputs by default
52
66
  def value(*args, **kwargs, &block)
@@ -140,26 +154,39 @@ module GraphQL
140
154
  end
141
155
  end
142
156
 
157
+ # Called by the runtime when a field returns a value to give back to the client.
158
+ # This method checks that the incoming {value} matches one of the enum's defined values.
159
+ # @param value [Object] Any value matching the values for this enum.
160
+ # @param ctx [GraphQL::Query::Context]
161
+ # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized.
162
+ # @return [String] The GraphQL-ready string for {value}
143
163
  def coerce_result(value, ctx)
144
164
  types = ctx.types
145
165
  all_values = types ? types.enum_values(self) : values.each_value
146
166
  enum_value = all_values.find { |val| val.value == value }
147
- if enum_value
167
+ if enum_value && (was_authed = enum_value.authorized?(ctx))
148
168
  enum_value.graphql_name
149
169
  else
150
- raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
170
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx, authorized: was_authed)
151
171
  end
152
172
  end
153
173
 
174
+ # Called by the runtime with incoming string representations from a query.
175
+ # It will match the string to a configured by name or by Ruby value.
176
+ # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration
177
+ # @param ctx [GraphQL::Query::Context]
178
+ # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}.
179
+ # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue}
154
180
  def coerce_input(value_name, ctx)
155
181
  all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value
156
182
 
157
- if v = all_values.find { |val| val.graphql_name == value_name }
158
- v.value
159
- elsif v = all_values.find { |val| val.value == value_name }
160
- # this is for matching default values, which are "inputs", but they're
161
- # the Ruby value, not the GraphQL string.
162
- v.value
183
+ # This tries matching by incoming GraphQL string, then checks Ruby-defined values
184
+ if v = (all_values.find { |val| val.graphql_name == value_name } || all_values.find { |val| val.value == value_name })
185
+ if v.authorized?(ctx)
186
+ v.value
187
+ else
188
+ raise GraphQL::UnauthorizedEnumValueError.new(type: self, enum_value: v, context: ctx)
189
+ end
163
190
  else
164
191
  nil
165
192
  end
@@ -146,11 +146,16 @@ module GraphQL
146
146
  Member::BuildType.to_type_name(@return_type_expr)
147
147
  elsif @resolver_class && @resolver_class.type
148
148
  Member::BuildType.to_type_name(@resolver_class.type)
149
- else
149
+ elsif type
150
150
  # As a last ditch, try to force loading the return type:
151
151
  type.unwrap.name
152
152
  end
153
- @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
153
+ if return_type_name
154
+ @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
155
+ else
156
+ # TODO set this when type is set by method
157
+ false # not loaded yet?
158
+ end
154
159
  else
155
160
  @connection
156
161
  end
@@ -236,8 +241,8 @@ module GraphQL
236
241
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
237
242
  end
238
243
  if !(resolver_class)
239
- if type.nil?
240
- raise ArgumentError, "missing second `type` argument or keyword `type:`"
244
+ if type.nil? && !block_given?
245
+ raise ArgumentError, "missing second `type` argument, keyword `type:`, or a block containing `type(...)`"
241
246
  end
242
247
  end
243
248
  @original_name = name
@@ -302,6 +307,7 @@ module GraphQL
302
307
  @ast_node = ast_node
303
308
  @method_conflict_warning = method_conflict_warning
304
309
  @fallback_value = fallback_value
310
+ @definition_block = nil
305
311
 
306
312
  arguments.each do |name, arg|
307
313
  case arg
@@ -320,26 +326,14 @@ module GraphQL
320
326
  @subscription_scope = subscription_scope
321
327
 
322
328
  @extensions = EMPTY_ARRAY
323
- @call_after_define = false
324
- # This should run before connection extension,
325
- # but should it run after the definition block?
326
- if scoped?
327
- self.extension(ScopeExtension)
328
- end
329
-
330
- # The problem with putting this after the definition_block
331
- # is that it would override arguments
332
- if connection? && connection_extension
333
- self.extension(connection_extension)
334
- end
335
-
329
+ set_pagination_extensions(connection_extension: connection_extension)
336
330
  # Do this last so we have as much context as possible when initializing them:
337
331
  if extensions.any?
338
- self.extensions(extensions)
332
+ self.extensions(extensions, call_after_define: false)
339
333
  end
340
334
 
341
335
  if resolver_class && resolver_class.extensions.any?
342
- self.extensions(resolver_class.extensions)
336
+ self.extensions(resolver_class.extensions, call_after_define: false)
343
337
  end
344
338
 
345
339
  if directives.any?
@@ -353,15 +347,28 @@ module GraphQL
353
347
  end
354
348
 
355
349
  if block_given?
356
- if definition_block.arity == 1
357
- yield self
350
+ @definition_block = definition_block
351
+ else
352
+ self.extensions.each(&:after_define_apply)
353
+ end
354
+ end
355
+
356
+ # Calls the definition block, if one was given.
357
+ # This is deferred so that references to the return type
358
+ # can be lazily evaluated, reducing Rails boot time.
359
+ # @return [self]
360
+ # @api private
361
+ def ensure_loaded
362
+ if @definition_block
363
+ if @definition_block.arity == 1
364
+ @definition_block.call(self)
358
365
  else
359
- instance_eval(&definition_block)
366
+ instance_eval(&@definition_block)
360
367
  end
368
+ self.extensions.each(&:after_define_apply)
369
+ @definition_block = nil
361
370
  end
362
-
363
- self.extensions.each(&:after_define_apply)
364
- @call_after_define = true
371
+ self
365
372
  end
366
373
 
367
374
  attr_accessor :dynamic_introspection
@@ -408,14 +415,14 @@ module GraphQL
408
415
  #
409
416
  # @param extensions [Array<Class, Hash<Class => Hash>>] Add extensions to this field. For hash elements, only the first key/value is used.
410
417
  # @return [Array<GraphQL::Schema::FieldExtension>] extensions to apply to this field
411
- def extensions(new_extensions = nil)
418
+ def extensions(new_extensions = nil, call_after_define: !@definition_block)
412
419
  if new_extensions
413
420
  new_extensions.each do |extension_config|
414
421
  if extension_config.is_a?(Hash)
415
422
  extension_class, options = *extension_config.to_a[0]
416
- self.extension(extension_class, options)
423
+ self.extension(extension_class, call_after_define: call_after_define, **options)
417
424
  else
418
- self.extension(extension_config)
425
+ self.extension(extension_config, call_after_define: call_after_define)
419
426
  end
420
427
  end
421
428
  end
@@ -433,12 +440,12 @@ module GraphQL
433
440
  # @param extension_class [Class] subclass of {Schema::FieldExtension}
434
441
  # @param options [Hash] if provided, given as `options:` when initializing `extension`.
435
442
  # @return [void]
436
- def extension(extension_class, options = nil)
443
+ def extension(extension_class, call_after_define: !@definition_block, **options)
437
444
  extension_inst = extension_class.new(field: self, options: options)
438
445
  if @extensions.frozen?
439
446
  @extensions = @extensions.dup
440
447
  end
441
- if @call_after_define
448
+ if call_after_define
442
449
  extension_inst.after_define_apply
443
450
  end
444
451
  @extensions << extension_inst
@@ -577,16 +584,29 @@ module GraphQL
577
584
  class MissingReturnTypeError < GraphQL::Error; end
578
585
  attr_writer :type
579
586
 
580
- def type
581
- if @resolver_class
582
- return_type = @return_type_expr || @resolver_class.type_expr
583
- if return_type.nil?
584
- raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
587
+ # Get or set the return type of this field.
588
+ #
589
+ # It may return nil if no type was configured or if the given definition block wasn't called yet.
590
+ # @param new_type [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List] A GraphQL return type
591
+ # @return [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List, nil] the configured type for this field
592
+ def type(new_type = NOT_CONFIGURED)
593
+ if NOT_CONFIGURED.equal?(new_type)
594
+ if @resolver_class
595
+ return_type = @return_type_expr || @resolver_class.type_expr
596
+ if return_type.nil?
597
+ raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
598
+ end
599
+ nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
600
+ Member::BuildType.parse_type(return_type, null: nullable)
601
+ elsif !@return_type_expr.nil?
602
+ @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
585
603
  end
586
- nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
587
- Member::BuildType.parse_type(return_type, null: nullable)
588
604
  else
589
- @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
605
+ @return_type_expr = new_type
606
+ # If `type` is set in the definition block, then the `connection_extension: ...` given as a keyword won't be used, hmm...
607
+ # Also, arguments added by `connection_extension` will clobber anything previously defined,
608
+ # so `type(...)` should go first.
609
+ set_pagination_extensions(connection_extension: self.class.connection_extension)
590
610
  end
591
611
  rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
592
612
  # Let this propagate up
@@ -897,6 +917,20 @@ ERR
897
917
  raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}"
898
918
  end
899
919
  end
920
+
921
+ def set_pagination_extensions(connection_extension:)
922
+ # This should run before connection extension,
923
+ # but should it run after the definition block?
924
+ if scoped?
925
+ self.extension(ScopeExtension, call_after_define: false)
926
+ end
927
+
928
+ # The problem with putting this after the definition_block
929
+ # is that it would override arguments
930
+ if connection? && connection_extension
931
+ self.extension(connection_extension, call_after_define: false)
932
+ end
933
+ end
900
934
  end
901
935
  end
902
936
  end
@@ -121,7 +121,7 @@ module GraphQL
121
121
  # Choose the most local definition that passes `.visible?` --
122
122
  # stop checking for fields by name once one has been found.
123
123
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
124
- visible_fields[field_name] = f
124
+ visible_fields[field_name] = f.ensure_loaded
125
125
  end
126
126
  end
127
127
  end
@@ -142,7 +142,7 @@ module GraphQL
142
142
  visible_interface_implementation?(ancestor, context, warden) &&
143
143
  (f_entry = ancestor.own_fields[field_name]) &&
144
144
  (skip_visible || (f_entry = Warden.visible_entry?(:visible_field?, f_entry, context, warden)))
145
- return f_entry
145
+ return (skip_visible ? f_entry : f_entry.ensure_loaded)
146
146
  end
147
147
  i += 1
148
148
  end
@@ -161,7 +161,7 @@ module GraphQL
161
161
  # Choose the most local definition that passes `.visible?` --
162
162
  # stop checking for fields by name once one has been found.
163
163
  if !visible_fields.key?(field_name) && (f = Warden.visible_entry?(:visible_field?, fields_entry, context, warden))
164
- visible_fields[field_name] = f
164
+ visible_fields[field_name] = f.ensure_loaded
165
165
  end
166
166
  end
167
167
  end
@@ -129,7 +129,7 @@ module GraphQL
129
129
  end.compare_by_identity
130
130
 
131
131
  @cached_fields = Hash.new do |h, owner|
132
- h[owner] = non_duplicate_items(owner.all_field_definitions, @cached_visible_fields[owner])
132
+ h[owner] = non_duplicate_items(owner.all_field_definitions.each(&:ensure_loaded), @cached_visible_fields[owner])
133
133
  end.compare_by_identity
134
134
 
135
135
  @cached_arguments = Hash.new do |h, owner|
@@ -213,13 +213,11 @@ module GraphQL
213
213
  end
214
214
  end
215
215
  end
216
- visible_f
216
+ visible_f.ensure_loaded
217
+ elsif f && @cached_visible_fields[owner][f.ensure_loaded]
218
+ f
217
219
  else
218
- if f && @cached_visible_fields[owner][f]
219
- f
220
- else
221
- nil
222
- end
220
+ nil
223
221
  end
224
222
  end
225
223
 
@@ -446,6 +444,7 @@ module GraphQL
446
444
  # recurse into visible fields
447
445
  t_f = type.all_field_definitions
448
446
  t_f.each do |field|
447
+ field.ensure_loaded
449
448
  if @cached_visible[field]
450
449
  visit_directives(field)
451
450
  field_type = field.type.unwrap
@@ -165,6 +165,8 @@ module GraphQL
165
165
  equivalent_schema_members?(inner_member1, inner_member2)
166
166
  end
167
167
  when GraphQL::Schema::Field
168
+ member1.ensure_loaded
169
+ member2.ensure_loaded
168
170
  if member1.introspection? && member2.introspection?
169
171
  member1.inspect == member2.inspect
170
172
  else
@@ -28,6 +28,8 @@ module GraphQL
28
28
  end
29
29
 
30
30
  def validate(object, context, value)
31
+ return EMPTY_ARRAY if permitted_empty_value?(value)
32
+
31
33
  all_errors = EMPTY_ARRAY
32
34
 
33
35
  value.each do |subvalue|
@@ -430,44 +430,62 @@ module GraphQL
430
430
  end
431
431
  end
432
432
 
433
- def query(new_query_object = nil)
434
- if new_query_object
433
+ def query(new_query_object = nil, &lazy_load_block)
434
+ if new_query_object || block_given?
435
435
  if @query_object
436
- raise GraphQL::Error, "Second definition of `query(...)` (#{new_query_object.inspect}) is invalid, already configured with #{@query_object.inspect}"
436
+ dup_defn = new_query_object || yield
437
+ raise GraphQL::Error, "Second definition of `query(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@query_object.inspect}"
438
+ elsif use_schema_subset?
439
+ @query_object = block_given? ? lazy_load_block : new_query_object
437
440
  else
438
- @query_object = new_query_object
439
- add_type_and_traverse(new_query_object, root: true) unless use_schema_subset?
440
- nil
441
+ @query_object = new_query_object || lazy_load_block.call
442
+ add_type_and_traverse(@query_object, root: true)
441
443
  end
444
+ nil
445
+ elsif @query_object.is_a?(Proc)
446
+ @query_object = @query_object.call
442
447
  else
443
448
  @query_object || find_inherited_value(:query)
444
449
  end
445
450
  end
446
451
 
447
- def mutation(new_mutation_object = nil)
448
- if new_mutation_object
452
+ def mutation(new_mutation_object = nil, &lazy_load_block)
453
+ if new_mutation_object || block_given?
449
454
  if @mutation_object
450
- raise GraphQL::Error, "Second definition of `mutation(...)` (#{new_mutation_object.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
455
+ dup_defn = new_mutation_object || yield
456
+ raise GraphQL::Error, "Second definition of `mutation(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
457
+ elsif use_schema_subset?
458
+ @mutation_object = block_given? ? lazy_load_block : new_mutation_object
451
459
  else
452
- @mutation_object = new_mutation_object
453
- add_type_and_traverse(new_mutation_object, root: true) unless use_schema_subset?
454
- nil
460
+ @mutation_object = new_mutation_object || lazy_load_block.call
461
+ add_type_and_traverse(@mutation_object, root: true)
455
462
  end
463
+ nil
464
+ elsif @mutation_object.is_a?(Proc)
465
+ @mutation_object = @mutation_object.call
456
466
  else
457
467
  @mutation_object || find_inherited_value(:mutation)
458
468
  end
459
469
  end
460
470
 
461
- def subscription(new_subscription_object = nil)
462
- if new_subscription_object
471
+ def subscription(new_subscription_object = nil, &lazy_load_block)
472
+ if new_subscription_object || block_given?
463
473
  if @subscription_object
464
- raise GraphQL::Error, "Second definition of `subscription(...)` (#{new_subscription_object.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
474
+ dup_defn = new_subscription_object || yield
475
+ raise GraphQL::Error, "Second definition of `subscription(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
476
+ elsif use_schema_subset?
477
+ @subscription_object = block_given? ? lazy_load_block : new_subscription_object
478
+ add_subscription_extension_if_necessary
465
479
  else
466
- @subscription_object = new_subscription_object
480
+ @subscription_object = new_subscription_object || lazy_load_block.call
467
481
  add_subscription_extension_if_necessary
468
- add_type_and_traverse(new_subscription_object, root: true) unless use_schema_subset?
469
- nil
482
+ add_type_and_traverse(@subscription_object, root: true)
470
483
  end
484
+ nil
485
+ elsif @subscription_object.is_a?(Proc)
486
+ @subscription_object = @subscription_object.call
487
+ add_subscription_extension_if_necessary
488
+ @subscription_object
471
489
  else
472
490
  @subscription_object || find_inherited_value(:subscription)
473
491
  end
@@ -1373,7 +1391,8 @@ module GraphQL
1373
1391
 
1374
1392
  # @api private
1375
1393
  def add_subscription_extension_if_necessary
1376
- if !defined?(@subscription_extension_added) && subscription && self.subscriptions
1394
+ # TODO: when there's a proper API for extending root types, migrat this to use it.
1395
+ if !defined?(@subscription_extension_added) && @subscription_object.is_a?(Class) && self.subscriptions
1377
1396
  @subscription_extension_added = true
1378
1397
  subscription.all_field_definitions.each do |field|
1379
1398
  if !field.extensions.any? { |ext| ext.is_a?(Subscriptions::DefaultSubscriptionResolveExtension) }
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class UnauthorizedEnumValueError < GraphQL::UnauthorizedError
4
+ # @return [GraphQL::Schema::EnumValue] The value whose `#authorized?` check returned false
5
+ attr_accessor :enum_value
6
+
7
+ def initialize(type:, context:, enum_value:)
8
+ @enum_value = enum_value
9
+ message ||= "#{enum_value.path} failed authorization"
10
+ super(message, object: enum_value.value, type: type, context: context)
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.3.12"
3
+ VERSION = "2.3.14"
4
4
  end
data/lib/graphql.rb CHANGED
@@ -119,6 +119,7 @@ require "graphql/parse_error"
119
119
  require "graphql/backtrace"
120
120
 
121
121
  require "graphql/unauthorized_error"
122
+ require "graphql/unauthorized_enum_value_error"
122
123
  require "graphql/unauthorized_field_error"
123
124
  require "graphql/load_application_object_failed_error"
124
125
  require "graphql/testing"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.12
4
+ version: 2.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-05 00:00:00.000000000 Z
11
+ date: 2024-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -422,6 +422,8 @@ files:
422
422
  - lib/graphql/rubocop/graphql/base_cop.rb
423
423
  - lib/graphql/rubocop/graphql/default_null_true.rb
424
424
  - lib/graphql/rubocop/graphql/default_required_true.rb
425
+ - lib/graphql/rubocop/graphql/field_type_in_block.rb
426
+ - lib/graphql/rubocop/graphql/root_types_in_block.rb
425
427
  - lib/graphql/runtime_type_error.rb
426
428
  - lib/graphql/schema.rb
427
429
  - lib/graphql/schema/addition.rb
@@ -630,6 +632,7 @@ files:
630
632
  - lib/graphql/types/relay/page_info.rb
631
633
  - lib/graphql/types/relay/page_info_behaviors.rb
632
634
  - lib/graphql/types/string.rb
635
+ - lib/graphql/unauthorized_enum_value_error.rb
633
636
  - lib/graphql/unauthorized_error.rb
634
637
  - lib/graphql/unauthorized_field_error.rb
635
638
  - lib/graphql/unresolved_type_error.rb