graphql 2.3.7 → 2.4.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  4. data/lib/generators/graphql/templates/base_resolver.erb +2 -0
  5. data/lib/generators/graphql/type_generator.rb +1 -1
  6. data/lib/graphql/analysis/field_usage.rb +1 -1
  7. data/lib/graphql/analysis/query_complexity.rb +3 -3
  8. data/lib/graphql/analysis/visitor.rb +7 -6
  9. data/lib/graphql/analysis.rb +1 -1
  10. data/lib/graphql/current.rb +52 -0
  11. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  12. data/lib/graphql/dataloader/source.rb +6 -3
  13. data/lib/graphql/dataloader.rb +35 -11
  14. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  15. data/lib/graphql/execution/interpreter/resolve.rb +10 -6
  16. data/lib/graphql/execution/interpreter/runtime.rb +29 -25
  17. data/lib/graphql/execution/interpreter.rb +2 -0
  18. data/lib/graphql/execution/lookahead.rb +10 -10
  19. data/lib/graphql/introspection/directive_type.rb +1 -1
  20. data/lib/graphql/introspection/entry_points.rb +2 -2
  21. data/lib/graphql/introspection/field_type.rb +1 -1
  22. data/lib/graphql/introspection/schema_type.rb +6 -11
  23. data/lib/graphql/introspection/type_type.rb +5 -5
  24. data/lib/graphql/invalid_null_error.rb +1 -1
  25. data/lib/graphql/language/comment.rb +18 -0
  26. data/lib/graphql/language/document_from_schema_definition.rb +55 -28
  27. data/lib/graphql/language/lexer.rb +15 -15
  28. data/lib/graphql/language/nodes.rb +24 -16
  29. data/lib/graphql/language/parser.rb +14 -1
  30. data/lib/graphql/language/printer.rb +23 -7
  31. data/lib/graphql/language/sanitized_printer.rb +1 -1
  32. data/lib/graphql/language.rb +6 -6
  33. data/lib/graphql/query/context.rb +7 -1
  34. data/lib/graphql/query/null_context.rb +2 -2
  35. data/lib/graphql/query.rb +60 -8
  36. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  37. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  38. data/lib/graphql/rubocop.rb +2 -0
  39. data/lib/graphql/schema/addition.rb +1 -0
  40. data/lib/graphql/schema/always_visible.rb +6 -2
  41. data/lib/graphql/schema/argument.rb +14 -1
  42. data/lib/graphql/schema/build_from_definition.rb +9 -1
  43. data/lib/graphql/schema/directive/flagged.rb +1 -1
  44. data/lib/graphql/schema/enum.rb +54 -20
  45. data/lib/graphql/schema/enum_value.rb +9 -1
  46. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  47. data/lib/graphql/schema/field.rb +92 -37
  48. data/lib/graphql/schema/has_single_input_argument.rb +2 -1
  49. data/lib/graphql/schema/input_object.rb +27 -13
  50. data/lib/graphql/schema/interface.rb +21 -4
  51. data/lib/graphql/schema/introspection_system.rb +5 -16
  52. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  53. data/lib/graphql/schema/member/has_arguments.rb +14 -9
  54. data/lib/graphql/schema/member/has_fields.rb +8 -6
  55. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  56. data/lib/graphql/schema/printer.rb +1 -0
  57. data/lib/graphql/schema/resolver.rb +7 -9
  58. data/lib/graphql/schema/type_expression.rb +2 -2
  59. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  60. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  61. data/lib/graphql/schema/validator.rb +2 -0
  62. data/lib/graphql/schema/visibility/migration.rb +186 -0
  63. data/lib/graphql/schema/visibility/profile.rb +523 -0
  64. data/lib/graphql/schema/visibility.rb +75 -0
  65. data/lib/graphql/schema/warden.rb +156 -10
  66. data/lib/graphql/schema.rb +283 -85
  67. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  68. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  69. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  70. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
  71. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  72. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  73. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +3 -2
  74. data/lib/graphql/static_validation/rules/fields_will_merge.rb +8 -7
  75. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  76. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
  77. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  78. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  79. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  80. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -3
  81. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  82. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  83. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  84. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  85. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
  86. data/lib/graphql/static_validation/validation_context.rb +17 -2
  87. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -1
  88. data/lib/graphql/subscriptions/broadcast_analyzer.rb +10 -4
  89. data/lib/graphql/subscriptions/event.rb +1 -1
  90. data/lib/graphql/subscriptions.rb +5 -3
  91. data/lib/graphql/testing/helpers.rb +9 -5
  92. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  93. data/lib/graphql/types/relay/connection_behaviors.rb +10 -0
  94. data/lib/graphql/types/relay/edge_behaviors.rb +10 -0
  95. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  96. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  97. data/lib/graphql/version.rb +1 -1
  98. data/lib/graphql.rb +3 -0
  99. metadata +29 -8
  100. data/lib/graphql/language/token.rb +0 -34
  101. data/lib/graphql/schema/invalid_type_error.rb +0 -7
@@ -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,11 @@ 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 [String] :comment, the GraphQL comment for this value, present in documentation
63
+ # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`)
64
+ # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here
50
65
  # @return [void]
51
66
  # @see {Schema::EnumValue} which handles these inputs by default
52
67
  def value(*args, **kwargs, &block)
@@ -130,7 +145,7 @@ module GraphQL
130
145
  end
131
146
 
132
147
  def validate_non_null_input(value_name, ctx, max_errors: nil)
133
- allowed_values = ctx.warden.enum_values(self)
148
+ allowed_values = ctx.types.enum_values(self)
134
149
  matching_value = allowed_values.find { |v| v.graphql_name == value_name }
135
150
 
136
151
  if matching_value.nil?
@@ -138,35 +153,54 @@ module GraphQL
138
153
  else
139
154
  nil
140
155
  end
156
+ # rescue MissingValuesError
157
+ # nil
141
158
  end
142
159
 
160
+ # Called by the runtime when a field returns a value to give back to the client.
161
+ # This method checks that the incoming {value} matches one of the enum's defined values.
162
+ # @param value [Object] Any value matching the values for this enum.
163
+ # @param ctx [GraphQL::Query::Context]
164
+ # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized.
165
+ # @return [String] The GraphQL-ready string for {value}
143
166
  def coerce_result(value, ctx)
144
- warden = ctx.warden
145
- all_values = warden ? warden.enum_values(self) : values.each_value
167
+ types = ctx.types
168
+ all_values = types ? types.enum_values(self) : values.each_value
146
169
  enum_value = all_values.find { |val| val.value == value }
147
- if enum_value
170
+ if enum_value && (was_authed = enum_value.authorized?(ctx))
148
171
  enum_value.graphql_name
149
172
  else
150
- raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
173
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx, authorized: was_authed)
151
174
  end
152
175
  end
153
176
 
177
+ # Called by the runtime with incoming string representations from a query.
178
+ # It will match the string to a configured by name or by Ruby value.
179
+ # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration
180
+ # @param ctx [GraphQL::Query::Context]
181
+ # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}.
182
+ # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue}
154
183
  def coerce_input(value_name, ctx)
155
- all_values = ctx.warden ? ctx.warden.enum_values(self) : values.each_value
156
-
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
184
+ all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value
185
+
186
+ # This tries matching by incoming GraphQL string, then checks Ruby-defined values
187
+ if v = (all_values.find { |val| val.graphql_name == value_name } || all_values.find { |val| val.value == value_name })
188
+ if v.authorized?(ctx)
189
+ v.value
190
+ else
191
+ raise GraphQL::UnauthorizedEnumValueError.new(type: self, enum_value: v, context: ctx)
192
+ end
163
193
  else
164
194
  nil
165
195
  end
166
196
  end
167
197
 
168
198
  def inherited(child_class)
169
- child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
199
+ if child_class.name
200
+ # Don't assign a custom error class to anonymous classes
201
+ # because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers
202
+ child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
203
+ end
170
204
  super
171
205
  end
172
206
 
@@ -30,10 +30,11 @@ module GraphQL
30
30
  # @return [Class] The enum type that owns this value
31
31
  attr_reader :owner
32
32
 
33
- def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block)
33
+ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, comment: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block)
34
34
  @graphql_name = graphql_name.to_s
35
35
  GraphQL::NameValidator.validate!(@graphql_name)
36
36
  @description = desc || description
37
+ @comment = comment
37
38
  @value = value == NOT_CONFIGURED ? @graphql_name : value
38
39
  if deprecation_reason
39
40
  self.deprecation_reason = deprecation_reason
@@ -58,6 +59,13 @@ module GraphQL
58
59
  @description
59
60
  end
60
61
 
62
+ def comment(new_comment = nil)
63
+ if new_comment
64
+ @comment = new_comment
65
+ end
66
+ @comment
67
+ end
68
+
61
69
  def value(new_val = nil)
62
70
  unless new_val.nil?
63
71
  @value = new_val
@@ -50,7 +50,7 @@ module GraphQL
50
50
  if field.has_default_page_size? && !value.has_default_page_size_override?
51
51
  value.default_page_size = field.default_page_size
52
52
  end
53
- if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
53
+ if (custom_t = context.schema.connections.edge_class_for_field(@field))
54
54
  value.edge_class = custom_t
55
55
  end
56
56
  value
@@ -106,7 +106,7 @@ module GraphQL
106
106
  # @param subscription [Class] A {GraphQL::Schema::Subscription} class to use for field configuration
107
107
  # @return [GraphQL::Schema:Field] an instance of `self`
108
108
  # @see {.initialize} for other options
109
- def self.from_options(name = nil, type = nil, desc = nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
109
+ def self.from_options(name = nil, type = nil, desc = nil, comment: nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
110
110
  if (resolver_class = resolver || mutation || subscription)
111
111
  # Add a reference to that parent class
112
112
  kwargs[:resolver_class] = resolver_class
@@ -116,6 +116,10 @@ module GraphQL
116
116
  kwargs[:name] = name
117
117
  end
118
118
 
119
+ if comment
120
+ kwargs[:comment] = comment
121
+ end
122
+
119
123
  if !type.nil?
120
124
  if desc
121
125
  if kwargs[:description]
@@ -146,11 +150,16 @@ module GraphQL
146
150
  Member::BuildType.to_type_name(@return_type_expr)
147
151
  elsif @resolver_class && @resolver_class.type
148
152
  Member::BuildType.to_type_name(@resolver_class.type)
149
- else
153
+ elsif type
150
154
  # As a last ditch, try to force loading the return type:
151
155
  type.unwrap.name
152
156
  end
153
- @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
157
+ if return_type_name
158
+ @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
159
+ else
160
+ # TODO set this when type is set by method
161
+ false # not loaded yet?
162
+ end
154
163
  else
155
164
  @connection
156
165
  end
@@ -207,6 +216,7 @@ module GraphQL
207
216
  # @param owner [Class] The type that this field belongs to
208
217
  # @param null [Boolean] (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
209
218
  # @param description [String] Field description
219
+ # @param comment [String] Field comment
210
220
  # @param deprecation_reason [String] If present, the field is marked "deprecated" with this message
211
221
  # @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`)
212
222
  # @param hash_key [String, Symbol] The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
@@ -231,13 +241,13 @@ module GraphQL
231
241
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
232
242
  # @param validates [Array<Hash>] Configurations for validating this field
233
243
  # @param fallback_value [Object] A fallback value if the method is not defined
234
- def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, deprecation_reason: nil, method: 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)
244
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: 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)
235
245
  if name.nil?
236
246
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
237
247
  end
238
248
  if !(resolver_class)
239
- if type.nil?
240
- raise ArgumentError, "missing second `type` argument or keyword `type:`"
249
+ if type.nil? && !block_given?
250
+ raise ArgumentError, "missing second `type` argument, keyword `type:`, or a block containing `type(...)`"
241
251
  end
242
252
  end
243
253
  @original_name = name
@@ -247,6 +257,7 @@ module GraphQL
247
257
  @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s)
248
258
 
249
259
  @description = description
260
+ @comment = comment
250
261
  @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
251
262
 
252
263
  self.deprecation_reason = deprecation_reason
@@ -302,6 +313,7 @@ module GraphQL
302
313
  @ast_node = ast_node
303
314
  @method_conflict_warning = method_conflict_warning
304
315
  @fallback_value = fallback_value
316
+ @definition_block = definition_block
305
317
 
306
318
  arguments.each do |name, arg|
307
319
  case arg
@@ -321,18 +333,7 @@ module GraphQL
321
333
 
322
334
  @extensions = EMPTY_ARRAY
323
335
  @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
-
336
+ set_pagination_extensions(connection_extension: connection_extension)
336
337
  # Do this last so we have as much context as possible when initializing them:
337
338
  if extensions.any?
338
339
  self.extensions(extensions)
@@ -352,16 +353,29 @@ module GraphQL
352
353
  self.validates(validates)
353
354
  end
354
355
 
355
- if block_given?
356
- if definition_block.arity == 1
357
- yield self
356
+ if @definition_block.nil?
357
+ self.extensions.each(&:after_define_apply)
358
+ @call_after_define = true
359
+ end
360
+ end
361
+
362
+ # Calls the definition block, if one was given.
363
+ # This is deferred so that references to the return type
364
+ # can be lazily evaluated, reducing Rails boot time.
365
+ # @return [self]
366
+ # @api private
367
+ def ensure_loaded
368
+ if @definition_block
369
+ if @definition_block.arity == 1
370
+ @definition_block.call(self)
358
371
  else
359
- instance_eval(&definition_block)
372
+ instance_eval(&@definition_block)
360
373
  end
374
+ self.extensions.each(&:after_define_apply)
375
+ @call_after_define = true
376
+ @definition_block = nil
361
377
  end
362
-
363
- self.extensions.each(&:after_define_apply)
364
- @call_after_define = true
378
+ self
365
379
  end
366
380
 
367
381
  attr_accessor :dynamic_introspection
@@ -393,6 +407,20 @@ module GraphQL
393
407
  end
394
408
  end
395
409
 
410
+ # @param text [String]
411
+ # @return [String, nil]
412
+ def comment(text = nil)
413
+ if text
414
+ @comment = text
415
+ elsif !NOT_CONFIGURED.equal?(@comment)
416
+ @comment
417
+ elsif @resolver_class
418
+ @resolver_class.comment
419
+ else
420
+ nil
421
+ end
422
+ end
423
+
396
424
  # Read extension instances from this field,
397
425
  # or add new classes/options to be initialized on this field.
398
426
  # Extensions are executed in the order they are added.
@@ -413,7 +441,7 @@ module GraphQL
413
441
  new_extensions.each do |extension_config|
414
442
  if extension_config.is_a?(Hash)
415
443
  extension_class, options = *extension_config.to_a[0]
416
- self.extension(extension_class, options)
444
+ self.extension(extension_class, **options)
417
445
  else
418
446
  self.extension(extension_config)
419
447
  end
@@ -433,7 +461,7 @@ module GraphQL
433
461
  # @param extension_class [Class] subclass of {Schema::FieldExtension}
434
462
  # @param options [Hash] if provided, given as `options:` when initializing `extension`.
435
463
  # @return [void]
436
- def extension(extension_class, options = nil)
464
+ def extension(extension_class, **options)
437
465
  extension_inst = extension_class.new(field: self, options: options)
438
466
  if @extensions.frozen?
439
467
  @extensions = @extensions.dup
@@ -483,7 +511,7 @@ module GraphQL
483
511
  if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
484
512
  max_possible_page_size = arguments[:last]
485
513
  end
486
- elsif arguments.is_a?(GraphQL::UnauthorizedError)
514
+ elsif arguments.is_a?(GraphQL::ExecutionError) || arguments.is_a?(GraphQL::UnauthorizedError)
487
515
  raise arguments
488
516
  end
489
517
 
@@ -577,16 +605,29 @@ module GraphQL
577
605
  class MissingReturnTypeError < GraphQL::Error; end
578
606
  attr_writer :type
579
607
 
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)"
608
+ # Get or set the return type of this field.
609
+ #
610
+ # It may return nil if no type was configured or if the given definition block wasn't called yet.
611
+ # @param new_type [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List] A GraphQL return type
612
+ # @return [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List, nil] the configured type for this field
613
+ def type(new_type = NOT_CONFIGURED)
614
+ if NOT_CONFIGURED.equal?(new_type)
615
+ if @resolver_class
616
+ return_type = @return_type_expr || @resolver_class.type_expr
617
+ if return_type.nil?
618
+ 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)"
619
+ end
620
+ nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
621
+ Member::BuildType.parse_type(return_type, null: nullable)
622
+ elsif !@return_type_expr.nil?
623
+ @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
585
624
  end
586
- nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
587
- Member::BuildType.parse_type(return_type, null: nullable)
588
625
  else
589
- @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
626
+ @return_type_expr = new_type
627
+ # If `type` is set in the definition block, then the `connection_extension: ...` given as a keyword won't be used, hmm...
628
+ # Also, arguments added by `connection_extension` will clobber anything previously defined,
629
+ # so `type(...)` should go first.
630
+ set_pagination_extensions(connection_extension: self.class.connection_extension)
590
631
  end
591
632
  rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
592
633
  # Let this propagate up
@@ -618,7 +659,7 @@ module GraphQL
618
659
  using_arg_values = false
619
660
  end
620
661
 
621
- args = context.warden.arguments(self)
662
+ args = context.types.arguments(self)
622
663
  args.each do |arg|
623
664
  arg_key = arg.keyword
624
665
  if arg_values.key?(arg_key)
@@ -897,6 +938,20 @@ ERR
897
938
  raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}"
898
939
  end
899
940
  end
941
+
942
+ def set_pagination_extensions(connection_extension:)
943
+ # This should run before connection extension,
944
+ # but should it run after the definition block?
945
+ if scoped?
946
+ self.extension(ScopeExtension, call_after_define: false)
947
+ end
948
+
949
+ # The problem with putting this after the definition_block
950
+ # is that it would override arguments
951
+ if connection? && connection_extension
952
+ self.extension(connection_extension, call_after_define: false)
953
+ end
954
+ end
900
955
  end
901
956
  end
902
957
  end
@@ -149,7 +149,8 @@ module GraphQL
149
149
 
150
150
  def authorize_arguments(args, values)
151
151
  # remove the `input` wrapper to match values
152
- input_args = args["input"].type.unwrap.arguments(context)
152
+ input_type = args.find { |a| a.graphql_name == "input" }.type.unwrap
153
+ input_args = context.types.arguments(input_type)
153
154
  super(input_args, values)
154
155
  end
155
156
  end
@@ -23,7 +23,8 @@ module GraphQL
23
23
  @ruby_style_hash = ruby_kwargs
24
24
  @arguments = arguments
25
25
  # Apply prepares, not great to have it duplicated here.
26
- self.class.arguments(context).each_value do |arg_defn|
26
+ arg_defns = context ? context.types.arguments(self.class) : self.class.arguments(context).each_value
27
+ arg_defns.each do |arg_defn|
27
28
  ruby_kwargs_key = arg_defn.keyword
28
29
  if @ruby_style_hash.key?(ruby_kwargs_key)
29
30
  # Weirdly, procs are applied during coercion, but not methods.
@@ -58,7 +59,7 @@ module GraphQL
58
59
  def self.authorized?(obj, value, ctx)
59
60
  # Authorize each argument (but this doesn't apply if `prepare` is implemented):
60
61
  if value.respond_to?(:key?)
61
- arguments(ctx).each do |_name, input_obj_arg|
62
+ ctx.types.arguments(self).each do |input_obj_arg|
62
63
  if value.key?(input_obj_arg.keyword) &&
63
64
  !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
64
65
  return false
@@ -132,12 +133,14 @@ module GraphQL
132
133
  end
133
134
  # Add a method access
134
135
  method_name = argument_defn.keyword
135
- class_eval <<-RUBY, __FILE__, __LINE__
136
- def #{method_name}
137
- self[#{method_name.inspect}]
138
- end
139
- alias_method :#{method_name}, :#{method_name}
140
- RUBY
136
+ suppress_redefinition_warning do
137
+ class_eval <<-RUBY, __FILE__, __LINE__
138
+ def #{method_name}
139
+ self[#{method_name.inspect}]
140
+ end
141
+ alias_method :#{method_name}, :#{method_name}
142
+ RUBY
143
+ end
141
144
  argument_defn
142
145
  end
143
146
 
@@ -149,7 +152,7 @@ module GraphQL
149
152
  INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object."
150
153
 
151
154
  def validate_non_null_input(input, ctx, max_errors: nil)
152
- warden = ctx.warden
155
+ types = ctx.types
153
156
 
154
157
  if input.is_a?(Array)
155
158
  return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
@@ -161,9 +164,9 @@ module GraphQL
161
164
  end
162
165
 
163
166
  # Inject missing required arguments
164
- missing_required_inputs = self.arguments(ctx).reduce({}) do |m, (argument_name, argument)|
165
- if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
166
- m[argument_name] = nil
167
+ missing_required_inputs = ctx.types.arguments(self).reduce({}) do |m, (argument)|
168
+ if !input.key?(argument.graphql_name) && argument.type.non_null? && !argument.default_value? && types.argument(self, argument.graphql_name)
169
+ m[argument.graphql_name] = nil
167
170
  end
168
171
 
169
172
  m
@@ -172,7 +175,7 @@ module GraphQL
172
175
  result = nil
173
176
  [input, missing_required_inputs].each do |args_to_validate|
174
177
  args_to_validate.each do |argument_name, value|
175
- argument = warden.get_argument(self, argument_name)
178
+ argument = types.argument(self, argument_name)
176
179
  # Items in the input that are unexpected
177
180
  if argument.nil?
178
181
  result ||= Query::InputValidationResult.new
@@ -242,6 +245,17 @@ module GraphQL
242
245
 
243
246
  result
244
247
  end
248
+
249
+ private
250
+
251
+ # Suppress redefinition warning for objectId arguments
252
+ def suppress_redefinition_warning
253
+ verbose = $VERBOSE
254
+ $VERBOSE = nil
255
+ yield
256
+ ensure
257
+ $VERBOSE = verbose
258
+ end
245
259
  end
246
260
 
247
261
  private
@@ -63,6 +63,7 @@ module GraphQL
63
63
 
64
64
  child_class.introspection(introspection)
65
65
  child_class.description(description)
66
+ child_class.comment(nil)
66
67
  # If interfaces are mixed into each other, only define this class once
67
68
  if !child_class.const_defined?(:UnresolvedTypeError, false)
68
69
  add_unresolved_type_error(child_class)
@@ -82,13 +83,29 @@ module GraphQL
82
83
  super
83
84
  end
84
85
 
86
+ # Register other Interface or Object types as implementers of this Interface.
87
+ #
88
+ # When those Interfaces or Objects aren't used as the return values of fields,
89
+ # they may have to be registered using this method so that GraphQL-Ruby can find them.
90
+ # @param types [Class, Module]
91
+ # @return [Array<Module, Class>] Implementers of this interface, if they're registered
85
92
  def orphan_types(*types)
86
93
  if types.any?
87
- @orphan_types = types
94
+ @orphan_types ||= []
95
+ @orphan_types.concat(types)
88
96
  else
89
- all_orphan_types = @orphan_types || []
90
- all_orphan_types += super if defined?(super)
91
- all_orphan_types.uniq
97
+ if defined?(@orphan_types)
98
+ all_orphan_types = @orphan_types.dup
99
+ if defined?(super)
100
+ all_orphan_types += super
101
+ all_orphan_types.uniq!
102
+ end
103
+ all_orphan_types
104
+ elsif defined?(super)
105
+ super
106
+ else
107
+ EmptyObjects::EMPTY_ARRAY
108
+ end
92
109
  end
93
110
  end
94
111
 
@@ -25,7 +25,7 @@ module GraphQL
25
25
  load_constant(:DirectiveLocationEnum)
26
26
  ]
27
27
  @types = {}
28
- @possible_types = {}.tap(&:compare_by_identity)
28
+ @possible_types = {}.compare_by_identity
29
29
  type_defns.each do |t|
30
30
  @types[t.graphql_name] = t
31
31
  @possible_types[t] = [t]
@@ -69,7 +69,7 @@ module GraphQL
69
69
  def resolve_late_bindings
70
70
  @types.each do |name, t|
71
71
  if t.kind.fields?
72
- t.fields.each do |_name, field_defn|
72
+ t.all_field_definitions.each do |field_defn|
73
73
  field_defn.type = resolve_late_binding(field_defn.type)
74
74
  end
75
75
  end
@@ -90,7 +90,8 @@ module GraphQL
90
90
  def resolve_late_binding(late_bound_type)
91
91
  case late_bound_type
92
92
  when GraphQL::Schema::LateBoundType
93
- @schema.get_type(late_bound_type.name)
93
+ type_name = late_bound_type.name
94
+ @types[type_name] || @schema.get_type(type_name)
94
95
  when GraphQL::Schema::List
95
96
  resolve_late_binding(late_bound_type.of_type).to_list_type
96
97
  when GraphQL::Schema::NonNull
@@ -113,19 +114,7 @@ module GraphQL
113
114
 
114
115
  def get_fields_from_class(class_sym:)
115
116
  object_type_defn = load_constant(class_sym)
116
-
117
- if object_type_defn.is_a?(Module)
118
- object_type_defn.fields
119
- else
120
- extracted_field_defns = {}
121
- object_class = object_type_defn.metadata[:type_class]
122
- object_type_defn.all_fields.each do |field_defn|
123
- inner_resolve = field_defn.resolve_proc
124
- resolve_with_instantiate = PerFieldProxyResolve.new(object_class: object_class, inner_resolve: inner_resolve)
125
- extracted_field_defns[field_defn.name] = field_defn.redefine(resolve: resolve_with_instantiate)
126
- end
127
- extracted_field_defns
128
- end
117
+ object_type_defn.fields
129
118
  end
130
119
 
131
120
  # This is probably not 100% robust -- but it has to be good enough to avoid modifying the built-in introspection types
@@ -50,12 +50,27 @@ module GraphQL
50
50
  end
51
51
  end
52
52
 
53
+ # Call this method to provide a new comment; OR
54
+ # call it without an argument to get the comment
55
+ # @param new_comment [String]
56
+ # @return [String, nil]
57
+ def comment(new_comment = NOT_CONFIGURED)
58
+ if !NOT_CONFIGURED.equal?(new_comment)
59
+ @comment = new_comment
60
+ elsif defined?(@comment)
61
+ @comment
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
53
67
  # This pushes some configurations _down_ the inheritance tree,
54
68
  # in order to prevent repetitive lookups at runtime.
55
69
  module ConfigurationExtension
56
70
  def inherited(child_class)
57
71
  child_class.introspection(introspection)
58
72
  child_class.description(description)
73
+ child_class.comment(nil)
59
74
  child_class.default_graphql_name = nil
60
75
 
61
76
  if defined?(@graphql_name) && @graphql_name && (self.name.nil? || graphql_name != default_graphql_name)