graphql 1.13.2 → 1.13.6

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/ast/field_usage.rb +6 -2
  3. data/lib/graphql/date_encoding_error.rb +16 -0
  4. data/lib/graphql/execution/interpreter/arguments_cache.rb +4 -2
  5. data/lib/graphql/execution/interpreter/runtime.rb +33 -17
  6. data/lib/graphql/introspection/directive_location_enum.rb +2 -2
  7. data/lib/graphql/introspection/directive_type.rb +2 -0
  8. data/lib/graphql/introspection/schema_type.rb +5 -0
  9. data/lib/graphql/introspection/type_type.rb +9 -3
  10. data/lib/graphql/introspection.rb +3 -0
  11. data/lib/graphql/language/document_from_schema_definition.rb +8 -3
  12. data/lib/graphql/language/lexer.rb +50 -25
  13. data/lib/graphql/language/lexer.rl +2 -0
  14. data/lib/graphql/language/nodes.rb +2 -2
  15. data/lib/graphql/language/parser.rb +829 -816
  16. data/lib/graphql/language/parser.y +8 -2
  17. data/lib/graphql/language/printer.rb +4 -0
  18. data/lib/graphql/pagination/active_record_relation_connection.rb +43 -6
  19. data/lib/graphql/pagination/relation_connection.rb +59 -29
  20. data/lib/graphql/schema/argument.rb +6 -10
  21. data/lib/graphql/schema/build_from_definition.rb +1 -0
  22. data/lib/graphql/schema/directive.rb +15 -0
  23. data/lib/graphql/schema/field.rb +103 -39
  24. data/lib/graphql/schema/field_extension.rb +37 -0
  25. data/lib/graphql/schema/input_object.rb +15 -0
  26. data/lib/graphql/schema/loader.rb +3 -0
  27. data/lib/graphql/schema/non_null.rb +4 -0
  28. data/lib/graphql/schema/scalar.rb +12 -0
  29. data/lib/graphql/schema/validator/required_validator.rb +29 -15
  30. data/lib/graphql/schema.rb +16 -1
  31. data/lib/graphql/static_validation/all_rules.rb +1 -0
  32. data/lib/graphql/static_validation/base_visitor.rb +1 -1
  33. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  34. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
  35. data/lib/graphql/static_validation/rules/query_root_exists.rb +17 -0
  36. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +26 -0
  37. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  38. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  39. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +6 -0
  40. data/lib/graphql/static_validation/validation_context.rb +4 -0
  41. data/lib/graphql/subscriptions/serialize.rb +22 -2
  42. data/lib/graphql/tracing/active_support_notifications_tracing.rb +6 -20
  43. data/lib/graphql/tracing/data_dog_tracing.rb +6 -1
  44. data/lib/graphql/tracing/notifications_tracing.rb +59 -0
  45. data/lib/graphql/tracing/platform_tracing.rb +11 -6
  46. data/lib/graphql/types/iso_8601_date.rb +13 -5
  47. data/lib/graphql/types/relay/node_field.rb +2 -3
  48. data/lib/graphql/types/relay/nodes_field.rb +19 -3
  49. data/lib/graphql/version.rb +1 -1
  50. data/lib/graphql.rb +1 -0
  51. metadata +10 -6
@@ -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
@@ -47,16 +47,17 @@ module GraphQL
47
47
  def cursor_for(item)
48
48
  load_nodes
49
49
  # index in nodes + existing offset + 1 (because it's offset, not index)
50
- offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
50
+ offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) - (relation_offset(items) || 0)
51
51
  encode(offset.to_s)
52
52
  end
53
53
 
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
119
+ next_offset = relation_offset(items) || 0
120
120
  if after_offset
121
- previous_offset = relation_offset(items) || 0
122
- paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
121
+ next_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 = next_offset
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)
@@ -90,6 +90,11 @@ module GraphQL
90
90
  yield
91
91
  end
92
92
 
93
+ # Continuing is passed as a block, yield to continue.
94
+ def resolve_each(object, arguments, context)
95
+ yield
96
+ end
97
+
93
98
  def on_field?
94
99
  locations.include?(FIELD)
95
100
  end
@@ -101,6 +106,14 @@ module GraphQL
101
106
  def on_operation?
102
107
  locations.include?(QUERY) && locations.include?(MUTATION) && locations.include?(SUBSCRIPTION)
103
108
  end
109
+
110
+ def repeatable?
111
+ !!@repeatable
112
+ end
113
+
114
+ def repeatable(new_value)
115
+ @repeatable = new_value
116
+ end
104
117
  end
105
118
 
106
119
  # @return [GraphQL::Schema::Field, GraphQL::Schema::Argument, Class, Module]
@@ -139,6 +152,7 @@ module GraphQL
139
152
  ENUM_VALUE = :ENUM_VALUE,
140
153
  INPUT_OBJECT = :INPUT_OBJECT,
141
154
  INPUT_FIELD_DEFINITION = :INPUT_FIELD_DEFINITION,
155
+ VARIABLE_DEFINITION = :VARIABLE_DEFINITION,
142
156
  ]
143
157
 
144
158
  DEFAULT_DEPRECATION_REASON = 'No longer supported'
@@ -161,6 +175,7 @@ module GraphQL
161
175
  ENUM_VALUE: 'Location adjacent to an enum value definition.',
162
176
  INPUT_OBJECT: 'Location adjacent to an input object type definition.',
163
177
  INPUT_FIELD_DEFINITION: 'Location adjacent to an input object field definition.',
178
+ VARIABLE_DEFINITION: 'Location adjacent to a variable definition.',
164
179
  }
165
180
 
166
181
  private
@@ -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
@@ -792,51 +794,103 @@ module GraphQL
792
794
 
793
795
  def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
794
796
  with_extensions(unextended_obj, unextended_ruby_kwargs, query_ctx) do |obj, ruby_kwargs|
795
- if @resolver_class
796
- if obj.is_a?(GraphQL::Schema::Object)
797
- obj = obj.object
798
- end
799
- obj = @resolver_class.new(object: obj, context: query_ctx, field: self)
800
- end
801
-
802
- # Find a way to resolve this field, checking:
803
- #
804
- # - A method on the type instance;
805
- # - Hash keys, if the wrapped object is a hash;
806
- # - A method on the wrapped object;
807
- # - Or, raise not implemented.
808
- #
809
- if obj.respond_to?(@resolver_method)
810
- # Call the method with kwargs, if there are any
811
- if ruby_kwargs.any?
812
- obj.public_send(@resolver_method, **ruby_kwargs)
813
- else
814
- 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)
815
805
  end
816
- elsif obj.object.is_a?(Hash)
817
- inner_object = obj.object
818
- if inner_object.key?(@method_sym)
819
- 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
820
838
  else
821
- 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
822
848
  end
823
- elsif obj.object.respond_to?(@method_sym)
824
- if ruby_kwargs.any?
825
- 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)
826
869
  else
827
- 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."
828
871
  end
829
- else
830
- raise <<-ERR
831
- 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
832
880
 
833
- - `#{obj.class}##{@resolver_method}`, which did not exist
834
- - `#{obj.object.class}##{@method_sym}`, which did not exist
835
- - 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
836
884
 
837
- To implement this field, define one of the methods above (and check for typos)
838
- ERR
839
- 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
840
894
  end
841
895
  end
842
896
 
@@ -850,8 +904,12 @@ module GraphQL
850
904
  # This is a hack to get the _last_ value for extended obj and args,
851
905
  # in case one of the extensions doesn't `yield`.
852
906
  # (There's another implementation that uses multiple-return, but I'm wary of the perf cost of the extra arrays)
853
- extended = { args: args, obj: obj, memos: nil }
907
+ extended = { args: args, obj: obj, memos: nil, added_extras: nil }
854
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
855
913
  yield(obj, args)
856
914
  end
857
915
 
@@ -880,6 +938,12 @@ module GraphQL
880
938
  memos = extended[:memos] ||= {}
881
939
  memos[idx] = memo
882
940
  end
941
+
942
+ if (extras = extension.added_extras)
943
+ ae = extended[:added_extras] ||= []
944
+ ae.concat(extras)
945
+ end
946
+
883
947
  extended[:obj] = extended_obj
884
948
  extended[:args] = extended_args
885
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
@@ -34,6 +34,7 @@ module GraphQL
34
34
  Class.new(GraphQL::Schema) do
35
35
  orphan_types(types.values)
36
36
  directives(directives)
37
+ description(schema["description"])
37
38
 
38
39
  def self.resolve_type(*)
39
40
  raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects")
@@ -141,6 +142,7 @@ module GraphQL
141
142
  Class.new(GraphQL::Schema::Scalar) do
142
143
  graphql_name(type["name"])
143
144
  description(type["description"])
145
+ specified_by_url(type["specifiedByUrl"])
144
146
  end
145
147
  end
146
148
  when "UNION"
@@ -160,6 +162,7 @@ module GraphQL
160
162
  graphql_name(directive["name"])
161
163
  description(directive["description"])
162
164
  locations(*directive["locations"].map(&:to_sym))
165
+ repeatable(directive["isRepeatable"])
163
166
  loader.build_arguments(self, directive["args"], type_resolver)
164
167
  end
165
168
  end
@@ -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
 
@@ -32,6 +32,18 @@ module GraphQL
32
32
  GraphQL::TypeKinds::SCALAR
33
33
  end
34
34
 
35
+ def specified_by_url(new_url = nil)
36
+ if new_url
37
+ @specified_by_url = new_url
38
+ elsif defined?(@specified_by_url)
39
+ @specified_by_url
40
+ elsif superclass.respond_to?(:specified_by_url)
41
+ superclass.specified_by_url
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
35
47
  def default_scalar(is_default = nil)
36
48
  if !is_default.nil?
37
49
  @default_scalar = is_default