graphql 2.3.11 → 2.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fd32c95924aac8818df81050ba408953617ee69edf0b1e20a16b4927e198230
4
- data.tar.gz: a938a1fbde00c6ba934e63f40248b890bc7cbb6d0374d51dd138a88acbc05465
3
+ metadata.gz: c1fd7e53d9729765a9c020960a968e478cf43efca04789f73b64ac226c5bf3c2
4
+ data.tar.gz: ed151811454729c3ab5fd4c2877bc4cbb19093e0e85faa630b4472b486061d7b
5
5
  SHA512:
6
- metadata.gz: 595f05e3c28c15e05e65543d48493e101d92ac1238deb0ec9682b2b853293041438652ccdab3dbb9c4242d9b93b29e10da82851f7985ac286916a77697850e0d
7
- data.tar.gz: 90c3315b9d03b6c8eb4cee5e44578ec6b968cfe6de5feeaf160ecdbce2428a6a774edff7e06d7e2afaae4a5e33faeb809b629fae486ae1737bcf8bd9d298a0b1
6
+ metadata.gz: 564d90c0336b0b7d812843bbd3c7db78855820e6fdeaf8d786149c06dcdf2025aabb11dd31b3621ae0631d8d3fd3c4e222c712fa652e2a24a96af9b2d594d660
7
+ data.tar.gz: b705e105a0c18dd578bac21978bca5d1229e33cef6d6a627d6a47b2e4fafe711aa9b4a1fc956eece47e8e1c67280af05c74cfa01aa367b3e58d38cd5c7b78cd9
@@ -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,60 @@ 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
465
478
  else
466
- @subscription_object = new_subscription_object
467
- add_subscription_extension_if_necessary
468
- add_type_and_traverse(new_subscription_object, root: true) unless use_schema_subset?
469
- nil
479
+ @subscription_object = new_subscription_object || lazy_load_block.call
480
+ add_type_and_traverse(@subscription_object, root: true)
470
481
  end
482
+ nil
483
+ elsif @subscription_object.is_a?(Proc)
484
+ @subscription_object = @subscription_object.call
485
+ add_subscription_extension_if_necessary
486
+ @subscription_object
471
487
  else
472
488
  @subscription_object || find_inherited_value(:subscription)
473
489
  end
@@ -1373,7 +1389,8 @@ module GraphQL
1373
1389
 
1374
1390
  # @api private
1375
1391
  def add_subscription_extension_if_necessary
1376
- if !defined?(@subscription_extension_added) && subscription && self.subscriptions
1392
+ # TODO: when there's a proper API for extending root types, migrat this to use it.
1393
+ if !defined?(@subscription_extension_added) && @subscription_object.is_a?(Class) && self.subscriptions
1377
1394
  @subscription_extension_added = true
1378
1395
  subscription.all_field_definitions.each do |field|
1379
1396
  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.11"
3
+ VERSION = "2.3.13"
4
4
  end
data/lib/graphql.rb CHANGED
@@ -4,6 +4,7 @@ require "json"
4
4
  require "set"
5
5
  require "singleton"
6
6
  require "forwardable"
7
+ require "fiber/storage"
7
8
 
8
9
  module GraphQL
9
10
  class Error < StandardError
@@ -118,6 +119,7 @@ require "graphql/parse_error"
118
119
  require "graphql/backtrace"
119
120
 
120
121
  require "graphql/unauthorized_error"
122
+ require "graphql/unauthorized_enum_value_error"
121
123
  require "graphql/unauthorized_field_error"
122
124
  require "graphql/load_application_object_failed_error"
123
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.11
4
+ version: 2.3.13
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-02 00:00:00.000000000 Z
11
+ date: 2024-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fiber-storage
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: benchmark-ips
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -408,6 +422,8 @@ files:
408
422
  - lib/graphql/rubocop/graphql/base_cop.rb
409
423
  - lib/graphql/rubocop/graphql/default_null_true.rb
410
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
411
427
  - lib/graphql/runtime_type_error.rb
412
428
  - lib/graphql/schema.rb
413
429
  - lib/graphql/schema/addition.rb
@@ -616,6 +632,7 @@ files:
616
632
  - lib/graphql/types/relay/page_info.rb
617
633
  - lib/graphql/types/relay/page_info_behaviors.rb
618
634
  - lib/graphql/types/string.rb
635
+ - lib/graphql/unauthorized_enum_value_error.rb
619
636
  - lib/graphql/unauthorized_error.rb
620
637
  - lib/graphql/unauthorized_field_error.rb
621
638
  - lib/graphql/unresolved_type_error.rb
@@ -646,7 +663,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
646
663
  - !ruby/object:Gem::Version
647
664
  version: '0'
648
665
  requirements: []
649
- rubygems_version: 3.5.12
666
+ rubygems_version: 3.3.7
650
667
  signing_key:
651
668
  specification_version: 4
652
669
  summary: A GraphQL language and runtime for Ruby