graphql 1.12.10 → 1.13.4

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +3 -1
  3. data/lib/generators/graphql/install_generator.rb +9 -2
  4. data/lib/generators/graphql/mutation_generator.rb +1 -1
  5. data/lib/generators/graphql/object_generator.rb +2 -1
  6. data/lib/generators/graphql/relay.rb +19 -11
  7. data/lib/generators/graphql/templates/schema.erb +14 -2
  8. data/lib/generators/graphql/type_generator.rb +0 -1
  9. data/lib/graphql/analysis/ast/field_usage.rb +28 -1
  10. data/lib/graphql/analysis/ast/query_complexity.rb +10 -14
  11. data/lib/graphql/analysis/ast/visitor.rb +4 -4
  12. data/lib/graphql/backtrace/table.rb +15 -3
  13. data/lib/graphql/backtrace/tracer.rb +7 -4
  14. data/lib/graphql/base_type.rb +4 -2
  15. data/lib/graphql/boolean_type.rb +1 -1
  16. data/lib/graphql/dataloader/null_dataloader.rb +1 -0
  17. data/lib/graphql/dataloader/source.rb +50 -2
  18. data/lib/graphql/dataloader.rb +110 -41
  19. data/lib/graphql/define/instance_definable.rb +1 -1
  20. data/lib/graphql/deprecated_dsl.rb +11 -3
  21. data/lib/graphql/deprecation.rb +1 -5
  22. data/lib/graphql/directive/deprecated_directive.rb +1 -1
  23. data/lib/graphql/directive/include_directive.rb +1 -1
  24. data/lib/graphql/directive/skip_directive.rb +1 -1
  25. data/lib/graphql/directive.rb +0 -4
  26. data/lib/graphql/enum_type.rb +5 -1
  27. data/lib/graphql/execution/errors.rb +1 -0
  28. data/lib/graphql/execution/execute.rb +1 -1
  29. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  30. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -4
  31. data/lib/graphql/execution/interpreter/resolve.rb +6 -2
  32. data/lib/graphql/execution/interpreter/runtime.rb +513 -213
  33. data/lib/graphql/execution/interpreter.rb +4 -8
  34. data/lib/graphql/execution/lazy.rb +5 -1
  35. data/lib/graphql/execution/lookahead.rb +2 -2
  36. data/lib/graphql/execution/multiplex.rb +4 -1
  37. data/lib/graphql/float_type.rb +1 -1
  38. data/lib/graphql/id_type.rb +1 -1
  39. data/lib/graphql/int_type.rb +1 -1
  40. data/lib/graphql/integer_encoding_error.rb +18 -2
  41. data/lib/graphql/introspection/directive_type.rb +1 -1
  42. data/lib/graphql/introspection/entry_points.rb +2 -2
  43. data/lib/graphql/introspection/enum_value_type.rb +2 -2
  44. data/lib/graphql/introspection/field_type.rb +2 -2
  45. data/lib/graphql/introspection/input_value_type.rb +10 -4
  46. data/lib/graphql/introspection/schema_type.rb +3 -3
  47. data/lib/graphql/introspection/type_type.rb +10 -10
  48. data/lib/graphql/language/block_string.rb +2 -6
  49. data/lib/graphql/language/document_from_schema_definition.rb +10 -4
  50. data/lib/graphql/language/lexer.rb +0 -3
  51. data/lib/graphql/language/lexer.rl +0 -4
  52. data/lib/graphql/language/nodes.rb +13 -3
  53. data/lib/graphql/language/parser.rb +442 -434
  54. data/lib/graphql/language/parser.y +5 -4
  55. data/lib/graphql/language/printer.rb +6 -1
  56. data/lib/graphql/language/sanitized_printer.rb +5 -5
  57. data/lib/graphql/language/token.rb +0 -4
  58. data/lib/graphql/name_validator.rb +0 -4
  59. data/lib/graphql/pagination/active_record_relation_connection.rb +43 -6
  60. data/lib/graphql/pagination/connections.rb +40 -16
  61. data/lib/graphql/pagination/relation_connection.rb +57 -27
  62. data/lib/graphql/query/arguments.rb +1 -1
  63. data/lib/graphql/query/arguments_cache.rb +1 -1
  64. data/lib/graphql/query/context.rb +15 -2
  65. data/lib/graphql/query/literal_input.rb +1 -1
  66. data/lib/graphql/query/null_context.rb +12 -7
  67. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
  68. data/lib/graphql/query/validation_pipeline.rb +1 -1
  69. data/lib/graphql/query/variables.rb +5 -1
  70. data/lib/graphql/query.rb +5 -1
  71. data/lib/graphql/relay/edges_instrumentation.rb +0 -1
  72. data/lib/graphql/relay/global_id_resolve.rb +1 -1
  73. data/lib/graphql/relay/page_info.rb +1 -1
  74. data/lib/graphql/rubocop/graphql/base_cop.rb +36 -0
  75. data/lib/graphql/rubocop/graphql/default_null_true.rb +43 -0
  76. data/lib/graphql/rubocop/graphql/default_required_true.rb +43 -0
  77. data/lib/graphql/rubocop.rb +4 -0
  78. data/lib/graphql/schema/addition.rb +247 -0
  79. data/lib/graphql/schema/argument.rb +103 -45
  80. data/lib/graphql/schema/build_from_definition.rb +13 -7
  81. data/lib/graphql/schema/directive/feature.rb +1 -1
  82. data/lib/graphql/schema/directive/flagged.rb +2 -2
  83. data/lib/graphql/schema/directive/include.rb +1 -1
  84. data/lib/graphql/schema/directive/skip.rb +1 -1
  85. data/lib/graphql/schema/directive/transform.rb +14 -2
  86. data/lib/graphql/schema/directive.rb +7 -3
  87. data/lib/graphql/schema/enum.rb +70 -11
  88. data/lib/graphql/schema/enum_value.rb +6 -0
  89. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  90. data/lib/graphql/schema/field.rb +243 -81
  91. data/lib/graphql/schema/field_extension.rb +89 -2
  92. data/lib/graphql/schema/find_inherited_value.rb +1 -0
  93. data/lib/graphql/schema/finder.rb +5 -5
  94. data/lib/graphql/schema/input_object.rb +39 -29
  95. data/lib/graphql/schema/interface.rb +11 -20
  96. data/lib/graphql/schema/introspection_system.rb +1 -1
  97. data/lib/graphql/schema/list.rb +3 -1
  98. data/lib/graphql/schema/member/accepts_definition.rb +15 -3
  99. data/lib/graphql/schema/member/build_type.rb +1 -4
  100. data/lib/graphql/schema/member/cached_graphql_definition.rb +29 -2
  101. data/lib/graphql/schema/member/has_arguments.rb +145 -57
  102. data/lib/graphql/schema/member/has_deprecation_reason.rb +1 -1
  103. data/lib/graphql/schema/member/has_fields.rb +76 -18
  104. data/lib/graphql/schema/member/has_interfaces.rb +90 -0
  105. data/lib/graphql/schema/member.rb +1 -0
  106. data/lib/graphql/schema/non_null.rb +7 -1
  107. data/lib/graphql/schema/object.rb +10 -75
  108. data/lib/graphql/schema/printer.rb +12 -17
  109. data/lib/graphql/schema/relay_classic_mutation.rb +37 -3
  110. data/lib/graphql/schema/resolver/has_payload_type.rb +27 -2
  111. data/lib/graphql/schema/resolver.rb +75 -65
  112. data/lib/graphql/schema/scalar.rb +2 -0
  113. data/lib/graphql/schema/subscription.rb +36 -8
  114. data/lib/graphql/schema/traversal.rb +1 -1
  115. data/lib/graphql/schema/type_expression.rb +1 -1
  116. data/lib/graphql/schema/type_membership.rb +18 -4
  117. data/lib/graphql/schema/union.rb +8 -1
  118. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  119. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  120. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  121. data/lib/graphql/schema/validator/format_validator.rb +4 -5
  122. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  123. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  124. data/lib/graphql/schema/validator/numericality_validator.rb +13 -2
  125. data/lib/graphql/schema/validator/required_validator.rb +29 -15
  126. data/lib/graphql/schema/validator.rb +33 -25
  127. data/lib/graphql/schema/warden.rb +116 -52
  128. data/lib/graphql/schema.rb +162 -227
  129. data/lib/graphql/static_validation/all_rules.rb +1 -0
  130. data/lib/graphql/static_validation/base_visitor.rb +8 -5
  131. data/lib/graphql/static_validation/definition_dependencies.rb +0 -1
  132. data/lib/graphql/static_validation/error.rb +3 -1
  133. data/lib/graphql/static_validation/literal_validator.rb +1 -1
  134. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  135. data/lib/graphql/static_validation/rules/fields_will_merge.rb +52 -26
  136. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  137. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  138. data/lib/graphql/static_validation/rules/query_root_exists.rb +17 -0
  139. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +26 -0
  140. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -1
  141. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -4
  142. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +13 -7
  143. data/lib/graphql/static_validation/validation_context.rb +8 -2
  144. data/lib/graphql/static_validation/validator.rb +15 -12
  145. data/lib/graphql/string_encoding_error.rb +13 -3
  146. data/lib/graphql/string_type.rb +1 -1
  147. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +36 -6
  148. data/lib/graphql/subscriptions/event.rb +68 -31
  149. data/lib/graphql/subscriptions/serialize.rb +23 -3
  150. data/lib/graphql/subscriptions.rb +17 -19
  151. data/lib/graphql/tracing/active_support_notifications_tracing.rb +6 -20
  152. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  153. data/lib/graphql/tracing/notifications_tracing.rb +59 -0
  154. data/lib/graphql/types/big_int.rb +5 -1
  155. data/lib/graphql/types/int.rb +1 -1
  156. data/lib/graphql/types/relay/connection_behaviors.rb +26 -9
  157. data/lib/graphql/types/relay/default_relay.rb +5 -1
  158. data/lib/graphql/types/relay/edge_behaviors.rb +13 -2
  159. data/lib/graphql/types/relay/has_node_field.rb +2 -2
  160. data/lib/graphql/types/relay/has_nodes_field.rb +2 -2
  161. data/lib/graphql/types/relay/node_field.rb +15 -4
  162. data/lib/graphql/types/relay/nodes_field.rb +14 -4
  163. data/lib/graphql/types/string.rb +1 -1
  164. data/lib/graphql/unauthorized_error.rb +1 -1
  165. data/lib/graphql/version.rb +1 -1
  166. data/lib/graphql.rb +10 -28
  167. data/readme.md +1 -4
  168. metadata +17 -21
  169. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -5,10 +5,6 @@ require "graphql/schema/field/scope_extension"
5
5
  module GraphQL
6
6
  class Schema
7
7
  class Field
8
- if !String.method_defined?(:-@)
9
- using GraphQL::StringDedupBackport
10
- end
11
-
12
8
  include GraphQL::Schema::Member::CachedGraphQLDefinition
13
9
  include GraphQL::Schema::Member::AcceptsDefinition
14
10
  include GraphQL::Schema::Member::HasArguments
@@ -20,6 +16,8 @@ module GraphQL
20
16
  include GraphQL::Schema::Member::HasDirectives
21
17
  include GraphQL::Schema::Member::HasDeprecationReason
22
18
 
19
+ class FieldImplementationFailed < GraphQL::Error; end
20
+
23
21
  # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided
24
22
  attr_reader :name
25
23
  alias :graphql_name :name
@@ -61,7 +59,7 @@ module GraphQL
61
59
  end
62
60
 
63
61
  def inspect
64
- "#<#{self.class} #{path}#{arguments.any? ? "(...)" : ""}: #{type.to_type_signature}>"
62
+ "#<#{self.class} #{path}#{all_argument_definitions.any? ? "(...)" : ""}: #{type.to_type_signature}>"
65
63
  end
66
64
 
67
65
  alias :mutation :resolver
@@ -122,6 +120,9 @@ module GraphQL
122
120
  else
123
121
  kwargs[:type] = type
124
122
  end
123
+ if type.is_a?(Class) && type < GraphQL::Schema::Mutation
124
+ raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
125
+ end
125
126
  end
126
127
  new(**kwargs, &block)
127
128
  end
@@ -209,7 +210,7 @@ module GraphQL
209
210
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
210
211
  # @param validates [Array<Hash>] Configurations for validating this field
211
212
  # @param legacy_edge_class [Class, nil] (DEPRECATED) If present, pass this along to the legacy field definition
212
- def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, 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: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, legacy_edge_class: nil, &definition_block)
213
+ def initialize(type: nil, name: nil, owner: nil, null: true, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, 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: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, legacy_edge_class: nil, &definition_block)
213
214
  if name.nil?
214
215
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
215
216
  end
@@ -217,9 +218,6 @@ module GraphQL
217
218
  if type.nil?
218
219
  raise ArgumentError, "missing second `type` argument or keyword `type:`"
219
220
  end
220
- if null.nil?
221
- raise ArgumentError, "missing keyword argument null:"
222
- end
223
221
  end
224
222
  if (field || function || resolve) && extras.any?
225
223
  raise ArgumentError, "keyword `extras:` may only be used with method-based resolve and class-based field such as mutation class, please remove `field:`, `function:` or `resolve:`"
@@ -278,10 +276,15 @@ module GraphQL
278
276
  @legacy_edge_class = legacy_edge_class
279
277
 
280
278
  arguments.each do |name, arg|
281
- if arg.is_a?(Hash)
279
+ case arg
280
+ when Hash
282
281
  argument(name: name, **arg)
283
- else
282
+ when GraphQL::Schema::Argument
284
283
  add_argument(arg)
284
+ when Array
285
+ arg.each { |a| add_argument(a) }
286
+ else
287
+ raise ArgumentError, "Unexpected argument config (#{arg.class}): #{arg.inspect}"
285
288
  end
286
289
  end
287
290
 
@@ -289,6 +292,7 @@ module GraphQL
289
292
  @subscription_scope = subscription_scope
290
293
 
291
294
  @extensions = EMPTY_ARRAY
295
+ @call_after_define = false
292
296
  # This should run before connection extension,
293
297
  # but should it run after the definition block?
294
298
  if scoped?
@@ -321,6 +325,9 @@ module GraphQL
321
325
  instance_eval(&definition_block)
322
326
  end
323
327
  end
328
+
329
+ self.extensions.each(&:after_define_apply)
330
+ @call_after_define = true
324
331
  end
325
332
 
326
333
  # If true, subscription updates with this field can be shared between viewers
@@ -353,27 +360,20 @@ module GraphQL
353
360
  # @example adding an extension with options
354
361
  # extensions([MyExtensionClass, { AnotherExtensionClass => { filter: true } }])
355
362
  #
356
- # @param extensions [Array<Class, Hash<Class => Object>>] Add extensions to this field. For hash elements, only the first key/value is used.
363
+ # @param extensions [Array<Class, Hash<Class => Hash>>] Add extensions to this field. For hash elements, only the first key/value is used.
357
364
  # @return [Array<GraphQL::Schema::FieldExtension>] extensions to apply to this field
358
365
  def extensions(new_extensions = nil)
359
- if new_extensions.nil?
360
- # Read the value
361
- @extensions
362
- else
363
- if @extensions.frozen?
364
- @extensions = @extensions.dup
365
- end
366
- new_extensions.each do |extension|
367
- if extension.is_a?(Hash)
368
- extension = extension.to_a[0]
369
- extension_class, options = *extension
370
- @extensions << extension_class.new(field: self, options: options)
366
+ if new_extensions
367
+ new_extensions.each do |extension_config|
368
+ if extension_config.is_a?(Hash)
369
+ extension_class, options = *extension_config.to_a[0]
370
+ self.extension(extension_class, options)
371
371
  else
372
- extension_class = extension
373
- @extensions << extension_class.new(field: self, options: nil)
372
+ self.extension(extension_config)
374
373
  end
375
374
  end
376
375
  end
376
+ @extensions
377
377
  end
378
378
 
379
379
  # Add `extension` to this field, initialized with `options` if provided.
@@ -384,10 +384,19 @@ module GraphQL
384
384
  # @example adding an extension with options
385
385
  # extension(MyExtensionClass, filter: true)
386
386
  #
387
- # @param extension [Class] subclass of {Schema::Fieldextension}
388
- # @param options [Object] if provided, given as `options:` when initializing `extension`.
389
- def extension(extension, options = nil)
390
- extensions([{extension => options}])
387
+ # @param extension_class [Class] subclass of {Schema::FieldExtension}
388
+ # @param options [Hash] if provided, given as `options:` when initializing `extension`.
389
+ # @return [void]
390
+ def extension(extension_class, options = nil)
391
+ extension_inst = extension_class.new(field: self, options: options)
392
+ if @extensions.frozen?
393
+ @extensions = @extensions.dup
394
+ end
395
+ if @call_after_define
396
+ extension_inst.after_define_apply
397
+ end
398
+ @extensions << extension_inst
399
+ nil
391
400
  end
392
401
 
393
402
  # Read extras (as symbols) from this field,
@@ -408,6 +417,62 @@ module GraphQL
408
417
  end
409
418
  end
410
419
 
420
+ def calculate_complexity(query:, nodes:, child_complexity:)
421
+ if respond_to?(:complexity_for)
422
+ lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner)
423
+ complexity_for(child_complexity: child_complexity, query: query, lookahead: lookahead)
424
+ elsif connection?
425
+ arguments = query.arguments_for(nodes.first, self)
426
+ max_possible_page_size = nil
427
+ if arguments[:first]
428
+ max_possible_page_size = arguments[:first]
429
+ end
430
+ if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
431
+ max_possible_page_size = arguments[:last]
432
+ end
433
+
434
+ if max_possible_page_size.nil?
435
+ max_possible_page_size = max_page_size || query.schema.default_max_page_size
436
+ end
437
+
438
+ if max_possible_page_size.nil?
439
+ raise GraphQL::Error, "Can't calculate complexity for #{path}, no `first:`, `last:`, `max_page_size` or `default_max_page_size`"
440
+ else
441
+ metadata_complexity = 0
442
+ lookahead = GraphQL::Execution::Lookahead.new(query: query, field: self, ast_nodes: nodes, owner_type: owner)
443
+
444
+ if (page_info_lookahead = lookahead.selection(:page_info)).selected?
445
+ metadata_complexity += 1 # pageInfo
446
+ metadata_complexity += page_info_lookahead.selections.size # subfields
447
+ end
448
+
449
+ if lookahead.selects?(:total) || lookahead.selects?(:total_count) || lookahead.selects?(:count)
450
+ metadata_complexity += 1
451
+ end
452
+
453
+ nodes_edges_complexity = 0
454
+ nodes_edges_complexity += 1 if lookahead.selects?(:edges)
455
+ nodes_edges_complexity += 1 if lookahead.selects?(:nodes)
456
+
457
+ # Possible bug: selections on `edges` and `nodes` are _both_ multiplied here. Should they be?
458
+ items_complexity = child_complexity - metadata_complexity - nodes_edges_complexity
459
+ # Add 1 for _this_ field
460
+ 1 + (max_possible_page_size * items_complexity) + metadata_complexity + nodes_edges_complexity
461
+ end
462
+ else
463
+ defined_complexity = complexity
464
+ case defined_complexity
465
+ when Proc
466
+ arguments = query.arguments_for(nodes.first, self)
467
+ defined_complexity.call(query.context, arguments.keyword_arguments, child_complexity)
468
+ when Numeric
469
+ defined_complexity + child_complexity
470
+ else
471
+ raise("Invalid complexity: #{defined_complexity.inspect} on #{path} (#{inspect})")
472
+ end
473
+ end
474
+ end
475
+
411
476
  def complexity(new_complexity = nil)
412
477
  case new_complexity
413
478
  when Proc
@@ -436,6 +501,8 @@ module GraphQL
436
501
  # @return [Integer, nil] Applied to connections if {#has_max_page_size?}
437
502
  attr_reader :max_page_size
438
503
 
504
+ prepend Schema::Member::CachedGraphQLDefinition::DeprecatedToGraphQL
505
+
439
506
  # @return [GraphQL::Field]
440
507
  def to_graphql
441
508
  field_defn = if @field
@@ -490,9 +557,9 @@ module GraphQL
490
557
  field_defn.subscription_scope = @subscription_scope
491
558
  field_defn.ast_node = ast_node
492
559
 
493
- arguments.each do |name, defn|
494
- arg_graphql = defn.to_graphql
495
- field_defn.arguments[arg_graphql.name] = arg_graphql
560
+ all_argument_definitions.each do |defn|
561
+ arg_graphql = defn.deprecated_to_graphql
562
+ field_defn.arguments[arg_graphql.name] = arg_graphql # rubocop:disable Development/ContextIsPassedCop -- legacy-related
496
563
  end
497
564
 
498
565
  # Support a passed-in proc, one way or another
@@ -510,6 +577,7 @@ module GraphQL
510
577
  field_defn
511
578
  end
512
579
 
580
+ class MissingReturnTypeError < GraphQL::Error; end
513
581
  attr_writer :type
514
582
 
515
583
  def type
@@ -517,14 +585,21 @@ module GraphQL
517
585
  Member::BuildType.parse_type(@function.type, null: false)
518
586
  elsif @field
519
587
  Member::BuildType.parse_type(@field.type, null: false)
588
+ elsif @return_type_expr.nil?
589
+ # Not enough info to determine type
590
+ message = "Can't determine the return type for #{self.path}"
591
+ if @resolver_class
592
+ message += " (it has `resolver: #{@resolver_class}`, consider configuration a `type ...` for that class)"
593
+ end
594
+ raise MissingReturnTypeError, message
520
595
  else
521
596
  Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
522
597
  end
523
- rescue GraphQL::Schema::InvalidDocumentError => err
598
+ rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
524
599
  # Let this propagate up
525
600
  raise err
526
601
  rescue StandardError => err
527
- raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace
602
+ raise MissingReturnTypeError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: (#{err.class}) #{err.message}", err.backtrace
528
603
  end
529
604
 
530
605
  def visible?(context)
@@ -545,13 +620,39 @@ module GraphQL
545
620
 
546
621
  def authorized?(object, args, context)
547
622
  if @resolver_class
548
- # The resolver will check itself during `resolve()`
623
+ # The resolver _instance_ will check itself during `resolve()`
549
624
  @resolver_class.authorized?(object, context)
550
625
  else
626
+ if (arg_values = context[:current_arguments])
627
+ # ^^ that's provided by the interpreter at runtime, and includes info about whether the default value was used or not.
628
+ using_arg_values = true
629
+ arg_values = arg_values.argument_values
630
+ else
631
+ arg_values = args
632
+ using_arg_values = false
633
+ end
551
634
  # Faster than `.any?`
552
- arguments.each_value do |arg|
553
- if args.key?(arg.keyword) && !arg.authorized?(object, args[arg.keyword], context)
554
- return false
635
+ arguments(context).each_value do |arg|
636
+ arg_key = arg.keyword
637
+ if arg_values.key?(arg_key)
638
+ arg_value = arg_values[arg_key]
639
+ if using_arg_values
640
+ if arg_value.default_used?
641
+ # pass -- no auth required for default used
642
+ next
643
+ else
644
+ application_arg_value = arg_value.value
645
+ if application_arg_value.is_a?(GraphQL::Execution::Interpreter::Arguments)
646
+ application_arg_value.keyword_arguments
647
+ end
648
+ end
649
+ else
650
+ application_arg_value = arg_value
651
+ end
652
+
653
+ if !arg.authorized?(object, application_arg_value, context)
654
+ return false
655
+ end
555
656
  end
556
657
  end
557
658
  true
@@ -608,8 +709,7 @@ module GraphQL
608
709
  if is_authorized
609
710
  public_send_field(object, args, ctx)
610
711
  else
611
- err = GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
612
- ctx.schema.unauthorized_field(err)
712
+ raise GraphQL::UnauthorizedFieldError.new(object: application_object, type: object.class, context: ctx, field: self)
613
713
  end
614
714
  end
615
715
  rescue GraphQL::UnauthorizedFieldError => err
@@ -649,7 +749,7 @@ module GraphQL
649
749
  ruby_kwargs = graphql_args.to_kwargs
650
750
  maybe_lazies = []
651
751
  # Apply any `prepare` methods. Not great code organization, can this go somewhere better?
652
- arguments.each do |name, arg_defn|
752
+ arguments(field_ctx).each do |name, arg_defn|
653
753
  ruby_kwargs_key = arg_defn.keyword
654
754
 
655
755
  if ruby_kwargs.key?(ruby_kwargs_key)
@@ -694,51 +794,103 @@ module GraphQL
694
794
 
695
795
  def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
696
796
  with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
697
- if @resolver_class
698
- if obj.is_a?(GraphQL::Schema::Object)
699
- obj = obj.object
700
- end
701
- obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
702
- end
703
-
704
- # Find a way to resolve this field, checking:
705
- #
706
- # - A method on the type instance;
707
- # - Hash keys, if the wrapped object is a hash;
708
- # - A method on the wrapped object;
709
- # - Or, raise not implemented.
710
- #
711
- if obj.respond_to?(@resolver_method)
712
- # Call the method with kwargs, if there are any
713
- if ruby_kwargs.any?
714
- obj.public_send(@resolver_method, **ruby_kwargs)
715
- else
716
- obj.public_send(@resolver_method)
797
+ begin
798
+ method_receiver = nil
799
+ method_to_call = nil
800
+ if @resolver_class
801
+ if obj.is_a?(GraphQL::Schema::Object)
802
+ obj = obj.object
803
+ end
804
+ obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
717
805
  end
718
- elsif obj.object.is_a?(Hash)
719
- inner_object = obj.object
720
- if inner_object.key?(@method_sym)
721
- inner_object[@method_sym]
806
+
807
+ # Find a way to resolve this field, checking:
808
+ #
809
+ # - A method on the type instance;
810
+ # - Hash keys, if the wrapped object is a hash;
811
+ # - A method on the wrapped object;
812
+ # - Or, raise not implemented.
813
+ #
814
+ if obj.respond_to?(@resolver_method)
815
+ method_to_call = @resolver_method
816
+ method_receiver = obj
817
+ # Call the method with kwargs, if there are any
818
+ if ruby_kwargs.any?
819
+ obj.public_send(@resolver_method, **ruby_kwargs)
820
+ else
821
+ obj.public_send(@resolver_method)
822
+ end
823
+ elsif obj.object.is_a?(Hash)
824
+ inner_object = obj.object
825
+ if inner_object.key?(@method_sym)
826
+ inner_object[@method_sym]
827
+ else
828
+ inner_object[@method_str]
829
+ end
830
+ elsif obj.object.respond_to?(@method_sym)
831
+ method_to_call = @method_sym
832
+ method_receiver = obj.object
833
+ if ruby_kwargs.any?
834
+ obj.object.public_send(@method_sym, **ruby_kwargs)
835
+ else
836
+ obj.object.public_send(@method_sym)
837
+ end
722
838
  else
723
- inner_object[@method_str]
839
+ raise <<-ERR
840
+ Failed to implement #{@owner.graphql_name}.#{@name}, tried:
841
+
842
+ - `#{obj.class}##{@resolver_method}`, which did not exist
843
+ - `#{obj.object.class}##{@method_sym}`, which did not exist
844
+ - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
845
+
846
+ To implement this field, define one of the methods above (and check for typos)
847
+ ERR
724
848
  end
725
- elsif obj.object.respond_to?(@method_sym)
726
- if ruby_kwargs.any?
727
- obj.object.public_send(@method_sym, **ruby_kwargs)
849
+ rescue ArgumentError
850
+ assert_satisfactory_implementation(method_receiver, method_to_call, ruby_kwargs)
851
+ # if the line above doesn't raise, re-raise
852
+ raise
853
+ end
854
+ end
855
+ end
856
+
857
+ def assert_satisfactory_implementation(receiver, method_name, ruby_kwargs)
858
+ method_defn = receiver.method(method_name)
859
+ unsatisfied_ruby_kwargs = ruby_kwargs.dup
860
+ unsatisfied_method_params = []
861
+ encountered_keyrest = false
862
+ method_defn.parameters.each do |(param_type, param_name)|
863
+ case param_type
864
+ when :key
865
+ unsatisfied_ruby_kwargs.delete(param_name)
866
+ when :keyreq
867
+ if unsatisfied_ruby_kwargs.key?(param_name)
868
+ unsatisfied_ruby_kwargs.delete(param_name)
728
869
  else
729
- obj.object.public_send(@method_sym)
870
+ unsatisfied_method_params << "- `#{param_name}:` is required by Ruby, but not by GraphQL. Consider `#{param_name}: nil` instead, or making this argument required in GraphQL."
730
871
  end
731
- else
732
- raise <<-ERR
733
- Failed to implement #{@owner.graphql_name}.#{@name}, tried:
872
+ when :keyrest
873
+ encountered_keyrest = true
874
+ when :req
875
+ unsatisfied_method_params << "- `#{param_name}` is required by Ruby, but GraphQL doesn't pass positional arguments. If it's meant to be a GraphQL argument, use `#{param_name}:` instead. Otherwise, remove it."
876
+ when :opt, :rest
877
+ # This is fine, although it will never be present
878
+ end
879
+ end
880
+
881
+ if encountered_keyrest
882
+ unsatisfied_ruby_kwargs.clear
883
+ end
734
884
 
735
- - `#{obj.class}##{@resolver_method}`, which did not exist
736
- - `#{obj.object.class}##{@method_sym}`, which did not exist
737
- - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
885
+ if unsatisfied_ruby_kwargs.any? || unsatisfied_method_params.any?
886
+ raise FieldImplementationFailed.new, <<-ERR
887
+ Failed to call #{method_name} on #{receiver.inspect} because the Ruby method params were incompatible with the GraphQL arguments:
738
888
 
739
- To implement this field, define one of the methods above (and check for typos)
740
- ERR
741
- end
889
+ #{ unsatisfied_ruby_kwargs
890
+ .map { |key, value| "- `#{key}: #{value}` was given by GraphQL but not defined in the Ruby method. Add `#{key}:` to the method parameters." }
891
+ .concat(unsatisfied_method_params)
892
+ .join("\n") }
893
+ ERR
742
894
  end
743
895
  end
744
896
 
@@ -752,8 +904,12 @@ module GraphQL
752
904
  # This is a hack to get the _last_ value for extended obj and args,
753
905
  # in case one of the extensions doesn't `yield`.
754
906
  # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays)
755
- extended = { args: args, obj: obj, memos: nil }
907
+ extended = { args: args, obj: obj, memos: nil, added_extras: nil }
756
908
  value = run_extensions_before_resolve(obj, args, ctx, extended) do |obj, args|
909
+ if (added_extras = extended[:added_extras])
910
+ args = args.dup
911
+ added_extras.each { |e| args.delete(e) }
912
+ end
757
913
  yield(obj, args)
758
914
  end
759
915
 
@@ -782,6 +938,12 @@ module GraphQL
782
938
  memos = extended[:memos] ||= {}
783
939
  memos[idx] = memo
784
940
  end
941
+
942
+ if (extras = extension.added_extras)
943
+ ae = extended[:added_extras] ||= []
944
+ ae.concat(extras)
945
+ end
946
+
785
947
  extended[:obj] = extended_obj
786
948
  extended[:args] = extended_args
787
949
  run_extensions_before_resolve(extended_obj, extended_args, ctx, extended, idx: idx + 1) { |o, a| yield(o, a) }
@@ -15,23 +15,110 @@ module GraphQL
15
15
  # @return [Object]
16
16
  attr_reader :options
17
17
 
18
+ # @return [Array<Symbol>, nil] `default_argument`s added, if any were added (otherwise, `nil`)
19
+ attr_reader :added_default_arguments
20
+
18
21
  # Called when the extension is mounted with `extension(name, options)`.
19
- # The instance is frozen to avoid improper use of state during execution.
22
+ # The instance will be frozen to avoid improper use of state during execution.
20
23
  # @param field [GraphQL::Schema::Field] The field where this extension was mounted
21
24
  # @param options [Object] The second argument to `extension`, or `{}` if nothing was passed.
22
25
  def initialize(field:, options:)
23
26
  @field = field
24
27
  @options = options || {}
28
+ @added_default_arguments = nil
25
29
  apply
26
- freeze
27
30
  end
28
31
 
32
+ class << self
33
+ # @return [Array(Array, Hash), nil] A list of default argument configs, or `nil` if there aren't any
34
+ def default_argument_configurations
35
+ args = superclass.respond_to?(:default_argument_configurations) ? superclass.default_argument_configurations : nil
36
+ if @own_default_argument_configurations
37
+ if args
38
+ args.concat(@own_default_argument_configurations)
39
+ else
40
+ args = @own_default_argument_configurations.dup
41
+ end
42
+ end
43
+ args
44
+ end
45
+
46
+ # @see Argument#initialize
47
+ # @see HasArguments#argument
48
+ def default_argument(*argument_args, **argument_kwargs)
49
+ configs = @own_default_argument_configurations ||= []
50
+ configs << [argument_args, argument_kwargs]
51
+ end
52
+
53
+ # If configured, these `extras` will be added to the field if they aren't already present,
54
+ # but removed by from `arguments` before the field's `resolve` is called.
55
+ # (The extras _will_ be present for other extensions, though.)
56
+ #
57
+ # @param new_extras [Array<Symbol>] If provided, assign extras used by this extension
58
+ # @return [Array<Symbol>] any extras assigned to this extension
59
+ def extras(new_extras = nil)
60
+ if new_extras
61
+ @own_extras = new_extras
62
+ end
63
+
64
+ inherited_extras = self.superclass.respond_to?(:extras) ? superclass.extras : nil
65
+ if @own_extras
66
+ if inherited_extras
67
+ inherited_extras + @own_extras
68
+ else
69
+ @own_extras
70
+ end
71
+ elsif inherited_extras
72
+ inherited_extras
73
+ else
74
+ NO_EXTRAS
75
+ end
76
+ end
77
+ end
78
+
79
+ NO_EXTRAS = [].freeze
80
+ private_constant :NO_EXTRAS
81
+
29
82
  # Called when this extension is attached to a field.
30
83
  # The field definition may be extended during this method.
31
84
  # @return [void]
32
85
  def apply
33
86
  end
34
87
 
88
+ # Called after the field's definition block has been executed.
89
+ # (Any arguments from the block are present on `field`)
90
+ # @return [void]
91
+ def after_define
92
+ end
93
+
94
+ # @api private
95
+ def after_define_apply
96
+ after_define
97
+ if (configs = self.class.default_argument_configurations)
98
+ existing_keywords = field.all_argument_definitions.map(&:keyword)
99
+ existing_keywords.uniq!
100
+ @added_default_arguments = []
101
+ configs.each do |config|
102
+ argument_args, argument_kwargs = config
103
+ arg_name = argument_args[0]
104
+ if !existing_keywords.include?(arg_name)
105
+ @added_default_arguments << arg_name
106
+ field.argument(*argument_args, **argument_kwargs)
107
+ end
108
+ end
109
+ end
110
+ if (extras = self.class.extras).any?
111
+ @added_extras = extras - field.extras
112
+ field.extras(@added_extras)
113
+ else
114
+ @added_extras = nil
115
+ end
116
+ freeze
117
+ end
118
+
119
+ # @api private
120
+ attr_reader :added_extras
121
+
35
122
  # Called before resolving {#field}. It should either:
36
123
  #
37
124
  # - `yield` values to continue execution; OR
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module GraphQL
2
3
  class Schema
3
4
  module FindInheritedValue
@@ -38,7 +38,7 @@ module GraphQL
38
38
 
39
39
  find_in_directive(directive, path: path)
40
40
  else
41
- type = schema.get_type(type_or_directive)
41
+ type = schema.get_type(type_or_directive) # rubocop:disable Development/ContextIsPassedCop -- build-time
42
42
 
43
43
  if type.nil?
44
44
  raise MemberNotFoundError, "Could not find type `#{type_or_directive}` in schema."
@@ -56,7 +56,7 @@ module GraphQL
56
56
 
57
57
  def find_in_directive(directive, path:)
58
58
  argument_name = path.shift
59
- argument = directive.arguments[argument_name]
59
+ argument = directive.get_argument(argument_name) # rubocop:disable Development/ContextIsPassedCop -- build-time
60
60
 
61
61
  if argument.nil?
62
62
  raise MemberNotFoundError, "Could not find argument `#{argument_name}` on directive #{directive}."
@@ -102,7 +102,7 @@ module GraphQL
102
102
 
103
103
  def find_in_field(field, path:)
104
104
  argument_name = path.shift
105
- argument = field.arguments[argument_name]
105
+ argument = field.get_argument(argument_name) # rubocop:disable Development/ContextIsPassedCop -- build-time
106
106
 
107
107
  if argument.nil?
108
108
  raise MemberNotFoundError, "Could not find argument `#{argument_name}` on field `#{field.name}`."
@@ -119,7 +119,7 @@ module GraphQL
119
119
 
120
120
  def find_in_input_object(input_object, path:)
121
121
  field_name = path.shift
122
- input_field = input_object.arguments[field_name]
122
+ input_field = input_object.get_argument(field_name) # rubocop:disable Development/ContextIsPassedCop -- build-time
123
123
 
124
124
  if input_field.nil?
125
125
  raise MemberNotFoundError, "Could not find input field `#{field_name}` on input object type `#{input_object.graphql_name}`."
@@ -136,7 +136,7 @@ module GraphQL
136
136
 
137
137
  def find_in_enum_type(enum_type, path:)
138
138
  value_name = path.shift
139
- enum_value = enum_type.values[value_name]
139
+ enum_value = enum_type.enum_values.find { |v| v.graphql_name == value_name } # rubocop:disable Development/ContextIsPassedCop -- build-time, not runtime
140
140
 
141
141
  if enum_value.nil?
142
142
  raise MemberNotFoundError, "Could not find enum value `#{value_name}` on enum type `#{enum_type.graphql_name}`."