graphql 1.13.2 → 1.13.6

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