graphql 1.12.23 → 1.13.3

Sign up to get free protection for your applications and to get access to all the features.
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?