graphql 1.13.1 → 1.13.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/ast/field_usage.rb +6 -2
  3. data/lib/graphql/execution/interpreter/arguments_cache.rb +4 -2
  4. data/lib/graphql/execution/interpreter/runtime.rb +5 -4
  5. data/lib/graphql/language/document_from_schema_definition.rb +8 -3
  6. data/lib/graphql/language/lexer.rb +50 -25
  7. data/lib/graphql/language/lexer.rl +2 -0
  8. data/lib/graphql/language/nodes.rb +2 -2
  9. data/lib/graphql/language/parser.rb +829 -816
  10. data/lib/graphql/language/parser.y +8 -2
  11. data/lib/graphql/language/printer.rb +4 -0
  12. data/lib/graphql/pagination/active_record_relation_connection.rb +43 -6
  13. data/lib/graphql/pagination/relation_connection.rb +57 -27
  14. data/lib/graphql/schema/argument.rb +6 -10
  15. data/lib/graphql/schema/build_from_definition.rb +1 -0
  16. data/lib/graphql/schema/directive.rb +8 -0
  17. data/lib/graphql/schema/field.rb +104 -43
  18. data/lib/graphql/schema/field_extension.rb +37 -0
  19. data/lib/graphql/schema/input_object.rb +15 -0
  20. data/lib/graphql/schema/non_null.rb +4 -0
  21. data/lib/graphql/schema/relay_classic_mutation.rb +8 -0
  22. data/lib/graphql/schema/resolver.rb +19 -13
  23. data/lib/graphql/schema/validator/required_validator.rb +29 -15
  24. data/lib/graphql/schema.rb +5 -1
  25. data/lib/graphql/static_validation/all_rules.rb +1 -0
  26. data/lib/graphql/static_validation/base_visitor.rb +1 -1
  27. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  28. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
  29. data/lib/graphql/static_validation/rules/query_root_exists.rb +17 -0
  30. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +26 -0
  31. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  32. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  33. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +6 -0
  34. data/lib/graphql/static_validation/validation_context.rb +4 -0
  35. data/lib/graphql/subscriptions/serialize.rb +22 -2
  36. data/lib/graphql/tracing/active_support_notifications_tracing.rb +6 -20
  37. data/lib/graphql/tracing/data_dog_tracing.rb +6 -1
  38. data/lib/graphql/tracing/notifications_tracing.rb +59 -0
  39. data/lib/graphql/tracing/platform_tracing.rb +11 -6
  40. data/lib/graphql/types/relay/node_field.rb +2 -3
  41. data/lib/graphql/types/relay/nodes_field.rb +19 -3
  42. data/lib/graphql/version.rb +1 -1
  43. metadata +6 -3
@@ -147,6 +147,7 @@ rule
147
147
  name_without_on:
148
148
  IDENTIFIER
149
149
  | FRAGMENT
150
+ | REPEATABLE
150
151
  | TRUE
151
152
  | FALSE
152
153
  | operation_type
@@ -155,6 +156,7 @@ rule
155
156
  enum_name: /* any identifier, but not "true", "false" or "null" */
156
157
  IDENTIFIER
157
158
  | FRAGMENT
159
+ | REPEATABLE
158
160
  | ON
159
161
  | operation_type
160
162
  | schema_keyword
@@ -422,10 +424,14 @@ rule
422
424
  }
423
425
 
424
426
  directive_definition:
425
- description_opt DIRECTIVE DIR_SIGN name arguments_definitions_opt ON directive_locations {
426
- result = make_node(:DirectiveDefinition, name: val[3], arguments: val[4], locations: val[6], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
427
+ description_opt DIRECTIVE DIR_SIGN name arguments_definitions_opt directive_repeatable_opt ON directive_locations {
428
+ result = make_node(:DirectiveDefinition, name: val[3], arguments: val[4], locations: val[7], repeatable: !!val[5], description: val[0] || get_description(val[1]), definition_line: val[1].line, position_source: val[0] || val[1])
427
429
  }
428
430
 
431
+ directive_repeatable_opt:
432
+ /* nothing */
433
+ | REPEATABLE
434
+
429
435
  directive_locations:
430
436
  name { result = [make_node(:DirectiveLocation, name: val[0].to_s, position_source: val[0])] }
431
437
  | directive_locations PIPE name { val[0] << make_node(:DirectiveLocation, name: val[2].to_s, position_source: val[2]) }
@@ -252,6 +252,10 @@ module GraphQL
252
252
  out << print_arguments(directive.arguments)
253
253
  end
254
254
 
255
+ if directive.repeatable
256
+ out << " repeatable"
257
+ end
258
+
255
259
  out << " on #{directive.locations.map(&:name).join(' | ')}"
256
260
  end
257
261
 
@@ -7,13 +7,18 @@ module GraphQL
7
7
  class ActiveRecordRelationConnection < Pagination::RelationConnection
8
8
  private
9
9
 
10
- def relation_larger_than(relation, size)
11
- initial_offset = relation.offset_value || 0
12
- relation.offset(initial_offset + size).exists?
10
+ def relation_larger_than(relation, initial_offset, size)
11
+ if already_loaded?(relation)
12
+ (relation.size + initial_offset) > size
13
+ else
14
+ set_offset(sliced_nodes, initial_offset + size).exists?
15
+ end
13
16
  end
14
17
 
15
18
  def relation_count(relation)
16
- int_or_hash = if relation.respond_to?(:unscope)
19
+ int_or_hash = if already_loaded?(relation)
20
+ relation.size
21
+ elsif relation.respond_to?(:unscope)
17
22
  relation.unscope(:order).count(:all)
18
23
  else
19
24
  # Rails 3
@@ -28,11 +33,19 @@ module GraphQL
28
33
  end
29
34
 
30
35
  def relation_limit(relation)
31
- relation.limit_value
36
+ if relation.is_a?(Array)
37
+ nil
38
+ else
39
+ relation.limit_value
40
+ end
32
41
  end
33
42
 
34
43
  def relation_offset(relation)
35
- relation.offset_value
44
+ if relation.is_a?(Array)
45
+ nil
46
+ else
47
+ relation.offset_value
48
+ end
36
49
  end
37
50
 
38
51
  def null_relation(relation)
@@ -43,6 +56,30 @@ module GraphQL
43
56
  relation.where("1=2")
44
57
  end
45
58
  end
59
+
60
+ def set_limit(nodes, limit)
61
+ if already_loaded?(nodes)
62
+ nodes.take(limit)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ def set_offset(nodes, offset)
69
+ if already_loaded?(nodes)
70
+ # If the client sent a bogus cursor beyond the size of the relation,
71
+ # it might get `nil` from `#[...]`, so return an empty array in that case
72
+ nodes[offset..-1] || []
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def already_loaded?(relation)
81
+ relation.is_a?(Array) || relation.loaded?
82
+ end
46
83
  end
47
84
  end
48
85
  end
@@ -35,7 +35,7 @@ module GraphQL
35
35
  if @nodes && @nodes.count < first
36
36
  false
37
37
  else
38
- relation_larger_than(sliced_nodes, first)
38
+ relation_larger_than(sliced_nodes, @sliced_nodes_offset, first)
39
39
  end
40
40
  else
41
41
  false
@@ -54,9 +54,10 @@ module GraphQL
54
54
  private
55
55
 
56
56
  # @param relation [Object] A database query object
57
+ # @param _initial_offset [Integer] The number of items already excluded from the relation
57
58
  # @param size [Integer] The value against which we check the relation size
58
59
  # @return [Boolean] True if the number of items in this relation is larger than `size`
59
- def relation_larger_than(relation, size)
60
+ def relation_larger_than(relation, _initial_offset, size)
60
61
  relation_count(set_limit(relation, size + 1)) == size + 1
61
62
  end
62
63
 
@@ -111,30 +112,51 @@ module GraphQL
111
112
  end
112
113
  end
113
114
 
114
- # Apply `before` and `after` to the underlying `items`,
115
- # returning a new relation.
116
- def sliced_nodes
117
- @sliced_nodes ||= begin
118
- paginated_nodes = items
119
-
115
+ def calculate_sliced_nodes_parameters
116
+ if defined?(@sliced_nodes_limit)
117
+ return
118
+ else
120
119
  if after_offset
121
120
  previous_offset = relation_offset(items) || 0
122
- paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
121
+ relation_offset = previous_offset + after_offset
123
122
  end
124
123
 
125
124
  if before_offset && after_offset
126
125
  if after_offset < before_offset
127
126
  # Get the number of items between the two cursors
128
127
  space_between = before_offset - after_offset - 1
129
- paginated_nodes = set_limit(paginated_nodes, space_between)
128
+ relation_limit = space_between
130
129
  else
131
- # TODO I think this is untested
132
130
  # The cursors overextend one another to an empty set
133
- paginated_nodes = null_relation(paginated_nodes)
131
+ @sliced_nodes_null_relation = true
134
132
  end
135
133
  elsif before_offset
136
134
  # Use limit to cut off the tail of the relation
137
- paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
135
+ relation_limit = before_offset - 1
136
+ end
137
+
138
+ @sliced_nodes_limit = relation_limit
139
+ @sliced_nodes_offset = relation_offset || 0
140
+ end
141
+ end
142
+
143
+ # Apply `before` and `after` to the underlying `items`,
144
+ # returning a new relation.
145
+ def sliced_nodes
146
+ @sliced_nodes ||= begin
147
+ calculate_sliced_nodes_parameters
148
+ paginated_nodes = items
149
+
150
+ if @sliced_nodes_null_relation
151
+ paginated_nodes = null_relation(paginated_nodes)
152
+ else
153
+ if @sliced_nodes_limit
154
+ paginated_nodes = set_limit(paginated_nodes, @sliced_nodes_limit)
155
+ end
156
+
157
+ if @sliced_nodes_offset
158
+ paginated_nodes = set_offset(paginated_nodes, @sliced_nodes_offset)
159
+ end
138
160
  end
139
161
 
140
162
  paginated_nodes
@@ -155,32 +177,40 @@ module GraphQL
155
177
  # returning a new relation
156
178
  def limited_nodes
157
179
  @limited_nodes ||= begin
158
- paginated_nodes = sliced_nodes
159
- previous_limit = relation_limit(paginated_nodes)
180
+ calculate_sliced_nodes_parameters
181
+ if @sliced_nodes_null_relation
182
+ # it's an empty set
183
+ return sliced_nodes
184
+ end
185
+ relation_limit = @sliced_nodes_limit
186
+ relation_offset = @sliced_nodes_offset
160
187
 
161
- if first && (previous_limit.nil? || previous_limit > first)
188
+ if first && (relation_limit.nil? || relation_limit > first)
162
189
  # `first` would create a stricter limit that the one already applied, so add it
163
- paginated_nodes = set_limit(paginated_nodes, first)
190
+ relation_limit = first
164
191
  end
165
192
 
166
193
  if last
167
- if (lv = relation_limit(paginated_nodes))
168
- if last <= lv
194
+ if relation_limit
195
+ if last <= relation_limit
169
196
  # `last` is a smaller slice than the current limit, so apply it
170
- offset = (relation_offset(paginated_nodes) || 0) + (lv - last)
171
- paginated_nodes = set_offset(paginated_nodes, offset)
172
- paginated_nodes = set_limit(paginated_nodes, last)
197
+ relation_offset += (relation_limit - last)
198
+ relation_limit = last
173
199
  end
174
200
  else
175
201
  # No limit, so get the last items
176
- sliced_nodes_count = relation_count(@sliced_nodes)
177
- offset = (relation_offset(paginated_nodes) || 0) + sliced_nodes_count - [last, sliced_nodes_count].min
178
- paginated_nodes = set_offset(paginated_nodes, offset)
179
- paginated_nodes = set_limit(paginated_nodes, last)
202
+ sliced_nodes_count = relation_count(sliced_nodes)
203
+ relation_offset += (sliced_nodes_count - [last, sliced_nodes_count].min)
204
+ relation_limit = last
180
205
  end
181
206
  end
182
207
 
183
- @paged_nodes_offset = relation_offset(paginated_nodes)
208
+ @paged_nodes_offset = relation_offset
209
+ paginated_nodes = items
210
+ paginated_nodes = set_offset(paginated_nodes, relation_offset)
211
+ if relation_limit
212
+ paginated_nodes = set_limit(paginated_nodes, relation_limit)
213
+ end
184
214
  paginated_nodes
185
215
  end
186
216
  end
@@ -37,7 +37,7 @@ module GraphQL
37
37
  # @param arg_name [Symbol]
38
38
  # @param type_expr
39
39
  # @param desc [String]
40
- # @param required [Boolean] if true, this argument is non-null; if false, this argument is nullable
40
+ # @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
41
41
  # @param description [String]
42
42
  # @param default_value [Object]
43
43
  # @param as [Symbol] Override the keyword name when passed to a method
@@ -53,7 +53,7 @@ module GraphQL
53
53
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
54
54
  @type_expr = type_expr || type
55
55
  @description = desc || description
56
- @null = !required
56
+ @null = required != true
57
57
  @default_value = default_value
58
58
  @owner = owner
59
59
  @as = as
@@ -72,6 +72,9 @@ module GraphQL
72
72
  end
73
73
 
74
74
  self.validates(validates)
75
+ if required == :nullable
76
+ self.owner.validates(required: { argument: arg_name })
77
+ end
75
78
 
76
79
  if definition_block
77
80
  if definition_block.arity == 1
@@ -147,14 +150,7 @@ module GraphQL
147
150
  end
148
151
  end
149
152
  elsif as_type.kind.input_object?
150
- as_type.arguments(ctx).each do |_name, input_obj_arg|
151
- input_obj_arg = input_obj_arg.type_class
152
- # TODO: this skips input objects whose values were alread replaced with application objects.
153
- # See: https://github.com/rmosolgo/graphql-ruby/issues/2633
154
- if value.is_a?(InputObject) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
155
- return false
156
- end
157
- end
153
+ return as_type.authorized?(obj, value, ctx)
158
154
  end
159
155
  # None of the early-return conditions were activated,
160
156
  # so this is authorized.
@@ -377,6 +377,7 @@ module GraphQL
377
377
  Class.new(GraphQL::Schema::Directive) do
378
378
  graphql_name(directive_definition.name)
379
379
  description(directive_definition.description)
380
+ repeatable(directive_definition.repeatable)
380
381
  locations(*directive_definition.locations.map { |location| location.name.to_sym })
381
382
  ast_node(directive_definition)
382
383
  builder.build_arguments(self, directive_definition.arguments, type_resolver)
@@ -101,6 +101,14 @@ module GraphQL
101
101
  def on_operation?
102
102
  locations.include?(QUERY) && locations.include?(MUTATION) && locations.include?(SUBSCRIPTION)
103
103
  end
104
+
105
+ def repeatable?
106
+ !!@repeatable
107
+ end
108
+
109
+ def repeatable(new_value)
110
+ @repeatable = new_value
111
+ end
104
112
  end
105
113
 
106
114
  # @return [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class, Module]
@@ -16,6 +16,8 @@ module GraphQL
16
16
  include GraphQL::Schema::Member::HasDirectives
17
17
  include GraphQL::Schema::Member::HasDeprecationReason
18
18
 
19
+ class FieldImplementationFailed < GraphQL::Error; end
20
+
19
21
  # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided
20
22
  attr_reader :name
21
23
  alias :graphql_name :name
@@ -617,13 +619,10 @@ module GraphQL
617
619
  end
618
620
 
619
621
  def authorized?(object, args, context)
620
- resolver_authed = if @resolver_class
622
+ if @resolver_class
621
623
  # The resolver _instance_ will check itself during `resolve()`
622
624
  @resolver_class.authorized?(object, context)
623
625
  else
624
- true
625
- end
626
- resolver_authed && begin
627
626
  if (arg_values = context[:current_arguments])
628
627
  # ^^ that's provided by the interpreter at runtime, and includes info about whether the default value was used or not.
629
628
  using_arg_values = true
@@ -795,51 +794,103 @@ module GraphQL
795
794
 
796
795
  def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
797
796
  with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
798
- if @resolver_class
799
- if obj.is_a?(GraphQL::Schema::Object)
800
- obj = obj.object
801
- end
802
- obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
803
- end
804
-
805
- # Find a way to resolve this field, checking:
806
- #
807
- # - A method on the type instance;
808
- # - Hash keys, if the wrapped object is a hash;
809
- # - A method on the wrapped object;
810
- # - Or, raise not implemented.
811
- #
812
- if obj.respond_to?(@resolver_method)
813
- # Call the method with kwargs, if there are any
814
- if ruby_kwargs.any?
815
- obj.public_send(@resolver_method, **ruby_kwargs)
816
- else
817
- 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)
818
805
  end
819
- elsif obj.object.is_a?(Hash)
820
- inner_object = obj.object
821
- if inner_object.key?(@method_sym)
822
- 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
823
838
  else
824
- 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
825
848
  end
826
- elsif obj.object.respond_to?(@method_sym)
827
- if ruby_kwargs.any?
828
- 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)
829
869
  else
830
- 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."
831
871
  end
832
- else
833
- raise <<-ERR
834
- 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
835
880
 
836
- - `#{obj.class}##{@resolver_method}`, which did not exist
837
- - `#{obj.object.class}##{@method_sym}`, which did not exist
838
- - Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
881
+ if encountered_keyrest
882
+ unsatisfied_ruby_kwargs.clear
883
+ end
839
884
 
840
- To implement this field, define one of the methods above (and check for typos)
841
- ERR
842
- end
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:
888
+
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
843
894
  end
844
895
  end
845
896
 
@@ -853,8 +904,12 @@ module GraphQL
853
904
  # This is a hack to get the _last_ value for extended obj and args,
854
905
  # in case one of the extensions doesn't `yield`.
855
906
  # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays)
856
- extended = { args: args, obj: obj, memos: nil }
907
+ extended = { args: args, obj: obj, memos: nil, added_extras: nil }
857
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
858
913
  yield(obj, args)
859
914
  end
860
915
 
@@ -883,6 +938,12 @@ module GraphQL
883
938
  memos = extended[:memos] ||= {}
884
939
  memos[idx] = memo
885
940
  end
941
+
942
+ if (extras = extension.added_extras)
943
+ ae = extended[:added_extras] ||= []
944
+ ae.concat(extras)
945
+ end
946
+
886
947
  extended[:obj] = extended_obj
887
948
  extended[:args] = extended_args
888
949
  run_extensions_before_resolve(extended_obj, extended_args, ctx, extended, idx: idx + 1) { |o, a| yield(o, a) }
@@ -49,8 +49,36 @@ module GraphQL
49
49
  configs = @own_default_argument_configurations ||= []
50
50
  configs << [argument_args, argument_kwargs]
51
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
52
77
  end
53
78
 
79
+ NO_EXTRAS = [].freeze
80
+ private_constant :NO_EXTRAS
81
+
54
82
  # Called when this extension is attached to a field.
55
83
  # The field definition may be extended during this method.
56
84
  # @return [void]
@@ -79,9 +107,18 @@ module GraphQL
79
107
  end
80
108
  end
81
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
82
116
  freeze
83
117
  end
84
118
 
119
+ # @api private
120
+ attr_reader :added_extras
121
+
85
122
  # Called before resolving {#field}. It should either:
86
123
  #
87
124
  # - `yield` values to continue execution; OR
@@ -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
@@ -53,6 +53,10 @@ module GraphQL
53
53
  end
54
54
 
55
55
  def coerce_input(value, ctx)
56
+ # `.validate_input` above is used for variables, but this method is used for arguments
57
+ if value.nil?
58
+ raise GraphQL::ExecutionError, "`null` is not a valid input for `#{to_type_signature}`, please provide a value for this argument."
59
+ end
56
60
  of_type.coerce_input(value, ctx)
57
61
  end
58
62
 
@@ -155,6 +155,14 @@ module GraphQL
155
155
  end
156
156
  end
157
157
  end
158
+
159
+ private
160
+
161
+ def authorize_arguments(args, values)
162
+ # remove the `input` wrapper to match values
163
+ input_args = args["input"].type.unwrap.arguments(context)
164
+ super(input_args, values)
165
+ end
158
166
  end
159
167
  end
160
168
  end
@@ -145,19 +145,9 @@ module GraphQL
145
145
  # @raise [GraphQL::UnauthorizedError] To signal an authorization failure
146
146
  # @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)
147
147
  def authorized?(**inputs)
148
- self.class.arguments(context).each_value do |argument|
149
- arg_keyword = argument.keyword
150
- if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
151
- arg_auth, err = argument.authorized?(self, arg_value, context)
152
- if !arg_auth
153
- return arg_auth, err
154
- else
155
- true
156
- end
157
- else
158
- true
159
- end
160
- end
148
+ arg_owner = @field # || self.class
149
+ args = arg_owner.arguments(context)
150
+ authorize_arguments(args, inputs)
161
151
  end
162
152
 
163
153
  # Called when an object loaded by `loads:` fails the `.authorized?` check for its resolved GraphQL object type.
@@ -172,6 +162,22 @@ module GraphQL
172
162
 
173
163
  private
174
164
 
165
+ def authorize_arguments(args, inputs)
166
+ args.each_value do |argument|
167
+ arg_keyword = argument.keyword
168
+ if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
169
+ arg_auth, err = argument.authorized?(self, arg_value, context)
170
+ if !arg_auth
171
+ return arg_auth, err
172
+ else
173
+ true
174
+ end
175
+ else
176
+ true
177
+ end
178
+ end
179
+ end
180
+
175
181
  def load_arguments(args)
176
182
  prepared_args = {}
177
183
  prepare_lazies = []