graphql 1.12.23 → 1.13.3

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.

Files changed (134) 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/type_generator.rb +0 -1
  6. data/lib/graphql/analysis/ast/field_usage.rb +2 -2
  7. data/lib/graphql/analysis/ast/query_complexity.rb +10 -14
  8. data/lib/graphql/analysis/ast/visitor.rb +4 -4
  9. data/lib/graphql/backtrace/table.rb +1 -1
  10. data/lib/graphql/base_type.rb +4 -2
  11. data/lib/graphql/boolean_type.rb +1 -1
  12. data/lib/graphql/dataloader.rb +55 -22
  13. data/lib/graphql/directive/deprecated_directive.rb +1 -1
  14. data/lib/graphql/directive/include_directive.rb +1 -1
  15. data/lib/graphql/directive/skip_directive.rb +1 -1
  16. data/lib/graphql/directive.rb +0 -4
  17. data/lib/graphql/enum_type.rb +5 -1
  18. data/lib/graphql/execution/errors.rb +1 -0
  19. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  20. data/lib/graphql/execution/interpreter/arguments_cache.rb +2 -2
  21. data/lib/graphql/execution/interpreter/runtime.rb +31 -19
  22. data/lib/graphql/execution/lookahead.rb +2 -2
  23. data/lib/graphql/execution/multiplex.rb +4 -1
  24. data/lib/graphql/float_type.rb +1 -1
  25. data/lib/graphql/id_type.rb +1 -1
  26. data/lib/graphql/int_type.rb +1 -1
  27. data/lib/graphql/introspection/directive_type.rb +1 -1
  28. data/lib/graphql/introspection/entry_points.rb +2 -2
  29. data/lib/graphql/introspection/enum_value_type.rb +2 -2
  30. data/lib/graphql/introspection/field_type.rb +2 -2
  31. data/lib/graphql/introspection/input_value_type.rb +4 -4
  32. data/lib/graphql/introspection/schema_type.rb +2 -2
  33. data/lib/graphql/introspection/type_type.rb +10 -10
  34. data/lib/graphql/language/block_string.rb +2 -6
  35. data/lib/graphql/language/document_from_schema_definition.rb +10 -4
  36. data/lib/graphql/language/lexer.rb +0 -3
  37. data/lib/graphql/language/lexer.rl +0 -4
  38. data/lib/graphql/language/nodes.rb +3 -2
  39. data/lib/graphql/language/parser.rb +442 -434
  40. data/lib/graphql/language/parser.y +5 -4
  41. data/lib/graphql/language/printer.rb +6 -1
  42. data/lib/graphql/language/sanitized_printer.rb +5 -5
  43. data/lib/graphql/language/token.rb +0 -4
  44. data/lib/graphql/name_validator.rb +0 -4
  45. data/lib/graphql/pagination/active_record_relation_connection.rb +43 -6
  46. data/lib/graphql/pagination/relation_connection.rb +55 -28
  47. data/lib/graphql/query/arguments.rb +1 -1
  48. data/lib/graphql/query/arguments_cache.rb +1 -1
  49. data/lib/graphql/query/context.rb +15 -2
  50. data/lib/graphql/query/literal_input.rb +1 -1
  51. data/lib/graphql/query/null_context.rb +12 -7
  52. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
  53. data/lib/graphql/query/variables.rb +5 -1
  54. data/lib/graphql/relay/edges_instrumentation.rb +0 -1
  55. data/lib/graphql/relay/global_id_resolve.rb +1 -1
  56. data/lib/graphql/relay/page_info.rb +1 -1
  57. data/lib/graphql/rubocop/graphql/base_cop.rb +36 -0
  58. data/lib/graphql/rubocop/graphql/default_null_true.rb +43 -0
  59. data/lib/graphql/rubocop/graphql/default_required_true.rb +43 -0
  60. data/lib/graphql/rubocop.rb +4 -0
  61. data/lib/graphql/schema/addition.rb +37 -28
  62. data/lib/graphql/schema/argument.rb +13 -15
  63. data/lib/graphql/schema/build_from_definition.rb +5 -5
  64. data/lib/graphql/schema/directive/feature.rb +1 -1
  65. data/lib/graphql/schema/directive/flagged.rb +2 -2
  66. data/lib/graphql/schema/directive/include.rb +1 -1
  67. data/lib/graphql/schema/directive/skip.rb +1 -1
  68. data/lib/graphql/schema/directive/transform.rb +1 -1
  69. data/lib/graphql/schema/directive.rb +7 -3
  70. data/lib/graphql/schema/enum.rb +60 -10
  71. data/lib/graphql/schema/enum_value.rb +6 -0
  72. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  73. data/lib/graphql/schema/field.rb +229 -77
  74. data/lib/graphql/schema/field_extension.rb +89 -2
  75. data/lib/graphql/schema/find_inherited_value.rb +1 -0
  76. data/lib/graphql/schema/finder.rb +5 -5
  77. data/lib/graphql/schema/input_object.rb +23 -5
  78. data/lib/graphql/schema/interface.rb +11 -20
  79. data/lib/graphql/schema/introspection_system.rb +1 -1
  80. data/lib/graphql/schema/list.rb +3 -1
  81. data/lib/graphql/schema/member/accepts_definition.rb +15 -3
  82. data/lib/graphql/schema/member/build_type.rb +0 -4
  83. data/lib/graphql/schema/member/cached_graphql_definition.rb +29 -2
  84. data/lib/graphql/schema/member/has_arguments.rb +55 -13
  85. data/lib/graphql/schema/member/has_deprecation_reason.rb +1 -1
  86. data/lib/graphql/schema/member/has_fields.rb +76 -18
  87. data/lib/graphql/schema/member/has_interfaces.rb +90 -0
  88. data/lib/graphql/schema/member.rb +1 -0
  89. data/lib/graphql/schema/non_null.rb +7 -1
  90. data/lib/graphql/schema/object.rb +10 -75
  91. data/lib/graphql/schema/printer.rb +1 -1
  92. data/lib/graphql/schema/relay_classic_mutation.rb +37 -3
  93. data/lib/graphql/schema/resolver/has_payload_type.rb +27 -2
  94. data/lib/graphql/schema/resolver.rb +37 -17
  95. data/lib/graphql/schema/scalar.rb +2 -0
  96. data/lib/graphql/schema/subscription.rb +11 -1
  97. data/lib/graphql/schema/traversal.rb +1 -1
  98. data/lib/graphql/schema/type_expression.rb +1 -1
  99. data/lib/graphql/schema/type_membership.rb +18 -4
  100. data/lib/graphql/schema/union.rb +8 -1
  101. data/lib/graphql/schema/validator/format_validator.rb +0 -4
  102. data/lib/graphql/schema/validator/numericality_validator.rb +1 -0
  103. data/lib/graphql/schema/validator/required_validator.rb +29 -15
  104. data/lib/graphql/schema/validator.rb +4 -7
  105. data/lib/graphql/schema/warden.rb +116 -52
  106. data/lib/graphql/schema.rb +111 -23
  107. data/lib/graphql/static_validation/all_rules.rb +1 -0
  108. data/lib/graphql/static_validation/base_visitor.rb +5 -5
  109. data/lib/graphql/static_validation/definition_dependencies.rb +0 -1
  110. data/lib/graphql/static_validation/literal_validator.rb +1 -1
  111. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  112. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  113. data/lib/graphql/static_validation/rules/query_root_exists.rb +17 -0
  114. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +26 -0
  115. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
  116. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -4
  117. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +13 -7
  118. data/lib/graphql/string_type.rb +1 -1
  119. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +8 -4
  120. data/lib/graphql/subscriptions/event.rb +20 -12
  121. data/lib/graphql/subscriptions/serialize.rb +22 -2
  122. data/lib/graphql/subscriptions.rb +17 -19
  123. data/lib/graphql/tracing/active_support_notifications_tracing.rb +6 -20
  124. data/lib/graphql/tracing/notifications_tracing.rb +59 -0
  125. data/lib/graphql/types/relay/connection_behaviors.rb +26 -9
  126. data/lib/graphql/types/relay/default_relay.rb +5 -1
  127. data/lib/graphql/types/relay/edge_behaviors.rb +13 -2
  128. data/lib/graphql/types/relay/has_node_field.rb +1 -1
  129. data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
  130. data/lib/graphql/types/relay/node_field.rb +14 -3
  131. data/lib/graphql/types/relay/nodes_field.rb +13 -3
  132. data/lib/graphql/version.rb +1 -1
  133. data/lib/graphql.rb +10 -32
  134. metadata +13 -5
@@ -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
@@ -212,7 +210,7 @@ module GraphQL
212
210
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
213
211
  # @param validates [Array<Hash>] Configurations for validating this field
214
212
  # @param legacy_edge_class [Class, nil] (DEPRECATED) If present, pass this along to the legacy field definition
215
- 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)
216
214
  if name.nil?
217
215
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
218
216
  end
@@ -220,9 +218,6 @@ module GraphQL
220
218
  if type.nil?
221
219
  raise ArgumentError, "missing second `type` argument or keyword `type:`"
222
220
  end
223
- if null.nil?
224
- raise ArgumentError, "missing keyword argument null:"
225
- end
226
221
  end
227
222
  if (field || function || resolve) && extras.any?
228
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:`"
@@ -281,10 +276,15 @@ module GraphQL
281
276
  @legacy_edge_class = legacy_edge_class
282
277
 
283
278
  arguments.each do |name, arg|
284
- if arg.is_a?(Hash)
279
+ case arg
280
+ when Hash
285
281
  argument(name: name, **arg)
286
- else
282
+ when GraphQL::Schema::Argument
287
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}"
288
288
  end
289
289
  end
290
290
 
@@ -292,6 +292,7 @@ module GraphQL
292
292
  @subscription_scope = subscription_scope
293
293
 
294
294
  @extensions = EMPTY_ARRAY
295
+ @call_after_define = false
295
296
  # This should run before connection extension,
296
297
  # but should it run after the definition block?
297
298
  if scoped?
@@ -324,6 +325,9 @@ module GraphQL
324
325
  instance_eval(&definition_block)
325
326
  end
326
327
  end
328
+
329
+ self.extensions.each(&:after_define_apply)
330
+ @call_after_define = true
327
331
  end
328
332
 
329
333
  # If true, subscription updates with this field can be shared between viewers
@@ -356,27 +360,20 @@ module GraphQL
356
360
  # @example adding an extension with options
357
361
  # extensions([MyExtensionClass, { AnotherExtensionClass => { filter: true } }])
358
362
  #
359
- # @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.
360
364
  # @return [Array<GraphQL::Schema::FieldExtension>] extensions to apply to this field
361
365
  def extensions(new_extensions = nil)
362
- if new_extensions.nil?
363
- # Read the value
364
- @extensions
365
- else
366
- if @extensions.frozen?
367
- @extensions = @extensions.dup
368
- end
369
- new_extensions.each do |extension|
370
- if extension.is_a?(Hash)
371
- extension = extension.to_a[0]
372
- extension_class, options = *extension
373
- @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)
374
371
  else
375
- extension_class = extension
376
- @extensions << extension_class.new(field: self, options: nil)
372
+ self.extension(extension_config)
377
373
  end
378
374
  end
379
375
  end
376
+ @extensions
380
377
  end
381
378
 
382
379
  # Add `extension` to this field, initialized with `options` if provided.
@@ -387,10 +384,19 @@ module GraphQL
387
384
  # @example adding an extension with options
388
385
  # extension(MyExtensionClass, filter: true)
389
386
  #
390
- # @param extension [Class] subclass of {Schema::Fieldextension}
391
- # @param options [Object] if provided, given as `options:` when initializing `extension`.
392
- def extension(extension, options = nil)
393
- 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
394
400
  end
395
401
 
396
402
  # Read extras (as symbols) from this field,
@@ -411,6 +417,62 @@ module GraphQL
411
417
  end
412
418
  end
413
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
+
414
476
  def complexity(new_complexity = nil)
415
477
  case new_complexity
416
478
  when Proc
@@ -439,6 +501,8 @@ module GraphQL
439
501
  # @return [Integer, nil] Applied to connections if {#has_max_page_size?}
440
502
  attr_reader :max_page_size
441
503
 
504
+ prepend Schema::Member::CachedGraphQLDefinition::DeprecatedToGraphQL
505
+
442
506
  # @return [GraphQL::Field]
443
507
  def to_graphql
444
508
  field_defn = if @field
@@ -493,9 +557,9 @@ module GraphQL
493
557
  field_defn.subscription_scope = @subscription_scope
494
558
  field_defn.ast_node = ast_node
495
559
 
496
- arguments.each do |name, defn|
497
- arg_graphql = defn.to_graphql
498
- 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
499
563
  end
500
564
 
501
565
  # Support a passed-in proc, one way or another
@@ -556,13 +620,39 @@ module GraphQL
556
620
 
557
621
  def authorized?(object, args, context)
558
622
  if @resolver_class
559
- # The resolver will check itself during `resolve()`
623
+ # The resolver _instance_ will check itself during `resolve()`
560
624
  @resolver_class.authorized?(object, context)
561
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
562
634
  # Faster than `.any?`
563
- arguments.each_value do |arg|
564
- if args.key?(arg.keyword) && !arg.authorized?(object, args[arg.keyword], context)
565
- 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
566
656
  end
567
657
  end
568
658
  true
@@ -659,7 +749,7 @@ module GraphQL
659
749
  ruby_kwargs = graphql_args.to_kwargs
660
750
  maybe_lazies = []
661
751
  # Apply any `prepare` methods. Not great code organization, can this go somewhere better?
662
- arguments.each do |name, arg_defn|
752
+ arguments(field_ctx).each do |name, arg_defn|
663
753
  ruby_kwargs_key = arg_defn.keyword
664
754
 
665
755
  if ruby_kwargs.key?(ruby_kwargs_key)
@@ -704,51 +794,103 @@ module GraphQL
704
794
 
705
795
  def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
706
796
  with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
707
- if @resolver_class
708
- if obj.is_a?(GraphQL::Schema::Object)
709
- obj = obj.object
710
- end
711
- obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
712
- end
713
-
714
- # Find a way to resolve this field, checking:
715
- #
716
- # - A method on the type instance;
717
- # - Hash keys, if the wrapped object is a hash;
718
- # - A method on the wrapped object;
719
- # - Or, raise not implemented.
720
- #
721
- if obj.respond_to?(@resolver_method)
722
- # Call the method with kwargs, if there are any
723
- if ruby_kwargs.any?
724
- obj.public_send(@resolver_method, **ruby_kwargs)
725
- else
726
- 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)
727
805
  end
728
- elsif obj.object.is_a?(Hash)
729
- inner_object = obj.object
730
- if inner_object.key?(@method_sym)
731
- 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
732
838
  else
733
- 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
734
848
  end
735
- elsif obj.object.respond_to?(@method_sym)
736
- if ruby_kwargs.any?
737
- 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)
738
869
  else
739
- 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."
740
871
  end
741
- else
742
- raise <<-ERR
743
- 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
744
884
 
745
- - `#{obj.class}##{@resolver_method}`, which did not exist
746
- - `#{obj.object.class}##{@method_sym}`, which did not exist
747
- - 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:
748
888
 
749
- To implement this field, define one of the methods above (and check for typos)
750
- ERR
751
- 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
752
894
  end
753
895
  end
754
896
 
@@ -762,8 +904,12 @@ module GraphQL
762
904
  # This is a hack to get the _last_ value for extended obj and args,
763
905
  # in case one of the extensions doesn't `yield`.
764
906
  # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays)
765
- extended = { args: args, obj: obj, memos: nil }
907
+ extended = { args: args, obj: obj, memos: nil, added_extras: nil }
766
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
767
913
  yield(obj, args)
768
914
  end
769
915
 
@@ -792,6 +938,12 @@ module GraphQL
792
938
  memos = extended[:memos] ||= {}
793
939
  memos[idx] = memo
794
940
  end
941
+
942
+ if (extras = extension.added_extras)
943
+ ae = extended[:added_extras] ||= []
944
+ ae.concat(extras)
945
+ end
946
+
795
947
  extended[:obj] = extended_obj
796
948
  extended[:args] = extended_args
797
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}`."
@@ -31,7 +31,7 @@ module GraphQL
31
31
  end
32
32
  # Apply prepares, not great to have it duplicated here.
33
33
  maybe_lazies = []
34
- self.class.arguments.each_value do |arg_defn|
34
+ self.class.arguments(context).each_value do |arg_defn|
35
35
  ruby_kwargs_key = arg_defn.keyword
36
36
 
37
37
  if @ruby_style_hash.key?(ruby_kwargs_key)
@@ -79,6 +79,21 @@ module GraphQL
79
79
  end
80
80
  end
81
81
 
82
+ def self.authorized?(obj, value, ctx)
83
+ # Authorize each argument (but this doesn't apply if `prepare` is implemented):
84
+ if value.is_a?(InputObject)
85
+ arguments(ctx).each do |_name, input_obj_arg|
86
+ input_obj_arg = input_obj_arg.type_class
87
+ if value.key?(input_obj_arg.keyword) &&
88
+ !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
89
+ return false
90
+ end
91
+ end
92
+ end
93
+ # It didn't early-return false:
94
+ true
95
+ end
96
+
82
97
  def unwrap_value(value)
83
98
  case value
84
99
  when Array
@@ -129,8 +144,11 @@ module GraphQL
129
144
  self[#{method_name.inspect}]
130
145
  end
131
146
  RUBY
147
+ argument_defn
132
148
  end
133
149
 
150
+ prepend Schema::Member::CachedGraphQLDefinition::DeprecatedToGraphQL
151
+
134
152
  def to_graphql
135
153
  type_defn = GraphQL::InputObjectType.new
136
154
  type_defn.name = graphql_name
@@ -138,8 +156,8 @@ module GraphQL
138
156
  type_defn.metadata[:type_class] = self
139
157
  type_defn.mutation = mutation
140
158
  type_defn.ast_node = ast_node
141
- arguments.each do |name, arg|
142
- type_defn.arguments[arg.graphql_definition.name] = arg.graphql_definition
159
+ all_argument_definitions.each do |arg|
160
+ type_defn.arguments[arg.graphql_definition(silence_deprecation_warning: true).name] = arg.graphql_definition(silence_deprecation_warning: true) # rubocop:disable Development/ContextIsPassedCop -- legacy-related
143
161
  end
144
162
  # Make a reference to a classic-style Arguments class
145
163
  self.arguments_class = GraphQL::Query::Arguments.construct_arguments_class(type_defn)
@@ -172,7 +190,7 @@ module GraphQL
172
190
  end
173
191
 
174
192
  # Inject missing required arguments
175
- missing_required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)|
193
+ missing_required_inputs = self.arguments(ctx).reduce({}) do |m, (argument_name, argument)|
176
194
  if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
177
195
  m[argument_name] = nil
178
196
  end
@@ -223,7 +241,7 @@ module GraphQL
223
241
 
224
242
  result = {}
225
243
 
226
- arguments.each do |input_key, input_field_defn|
244
+ arguments(ctx).each do |input_key, input_field_defn|
227
245
  input_value = value[input_key]
228
246
  if value.key?(input_key)
229
247
  result[input_key] = if input_value.nil?