graphql 2.0.11 → 2.0.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/templates/schema.erb +3 -0
  3. data/lib/graphql/dataloader/source.rb +9 -0
  4. data/lib/graphql/execution/interpreter/runtime.rb +10 -8
  5. data/lib/graphql/execution/interpreter.rb +185 -59
  6. data/lib/graphql/execution/lookahead.rb +26 -26
  7. data/lib/graphql/execution/multiplex.rb +1 -116
  8. data/lib/graphql/execution.rb +0 -1
  9. data/lib/graphql/introspection/type_type.rb +7 -0
  10. data/lib/graphql/introspection.rb +2 -1
  11. data/lib/graphql/language/printer.rb +8 -5
  12. data/lib/graphql/query/validation_pipeline.rb +4 -0
  13. data/lib/graphql/query/variable_validation_error.rb +2 -2
  14. data/lib/graphql/query/variables.rb +13 -3
  15. data/lib/graphql/query.rb +11 -2
  16. data/lib/graphql/rake_task/validate.rb +1 -1
  17. data/lib/graphql/schema/argument.rb +25 -13
  18. data/lib/graphql/schema/build_from_definition.rb +1 -2
  19. data/lib/graphql/schema/directive/one_of.rb +12 -0
  20. data/lib/graphql/schema/enum.rb +1 -1
  21. data/lib/graphql/schema/field.rb +12 -11
  22. data/lib/graphql/schema/input_object.rb +36 -1
  23. data/lib/graphql/schema/late_bound_type.rb +4 -0
  24. data/lib/graphql/schema/list.rb +19 -5
  25. data/lib/graphql/schema/member/has_arguments.rb +8 -2
  26. data/lib/graphql/schema/member/validates_input.rb +2 -2
  27. data/lib/graphql/schema/non_null.rb +2 -2
  28. data/lib/graphql/schema/scalar.rb +1 -1
  29. data/lib/graphql/schema.rb +13 -3
  30. data/lib/graphql/static_validation/all_rules.rb +1 -0
  31. data/lib/graphql/static_validation/error.rb +2 -2
  32. data/lib/graphql/static_validation/literal_validator.rb +4 -0
  33. data/lib/graphql/static_validation/rules/directives_are_defined.rb +11 -5
  34. data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb +66 -0
  35. data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid_error.rb +29 -0
  36. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +12 -6
  37. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +1 -1
  38. data/lib/graphql/subscriptions.rb +2 -5
  39. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  40. data/lib/graphql/version.rb +1 -1
  41. metadata +6 -4
  42. data/lib/graphql/execution/instrumentation.rb +0 -92
@@ -17,6 +17,10 @@ module GraphQL
17
17
  @provided_variables = deep_stringify(provided_variables)
18
18
  @errors = []
19
19
  @storage = ast_variables.each_with_object({}) do |ast_variable, memo|
20
+ if schema.validate_max_errors && schema.validate_max_errors <= @errors.count
21
+ add_max_errors_reached_message
22
+ break
23
+ end
20
24
  # Find the right value for this variable:
21
25
  # - First, use the value provided at runtime
22
26
  # - Then, fall back to the default value from the query string
@@ -29,8 +33,9 @@ module GraphQL
29
33
  default_value = ast_variable.default_value
30
34
  provided_value = @provided_variables[variable_name]
31
35
  value_was_provided = @provided_variables.key?(variable_name)
36
+ max_errors = schema.validate_max_errors - @errors.count if schema.validate_max_errors
32
37
  begin
33
- validation_result = variable_type.validate_input(provided_value, ctx)
38
+ validation_result = variable_type.validate_input(provided_value, ctx, max_errors: max_errors)
34
39
  if validation_result.valid?
35
40
  if value_was_provided
36
41
  # Add the variable if a value was provided
@@ -48,8 +53,7 @@ module GraphQL
48
53
  # like InputValidationResults generated by validate_non_null_input but unfortunately we don't
49
54
  # have this information available in the coerce_input call chain. Note this path is the path
50
55
  # that appears under errors.extensions.problems.path and NOT the result path under errors.path.
51
- validation_result = GraphQL::Query::InputValidationResult.new
52
- validation_result.add_problem(ex.message)
56
+ validation_result = GraphQL::Query::InputValidationResult.from_problem(ex.message)
53
57
  end
54
58
 
55
59
  if !validation_result.valid?
@@ -76,6 +80,12 @@ module GraphQL
76
80
  else
77
81
  val
78
82
  end
83
+ end
84
+
85
+ def add_max_errors_reached_message
86
+ message = "Too many errors processing variables, max validation error limit reached. Execution aborted"
87
+ validation_result = GraphQL::Query::InputValidationResult.from_problem(message)
88
+ errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message)
79
89
  end
80
90
  end
81
91
  end
data/lib/graphql/query.rb CHANGED
@@ -34,7 +34,16 @@ module GraphQL
34
34
  attr_accessor :operation_name
35
35
 
36
36
  # @return [Boolean] if false, static validation is skipped (execution behavior for invalid queries is undefined)
37
- attr_accessor :validate
37
+ attr_reader :validate
38
+
39
+ # @param new_validate [Boolean] if false, static validation is skipped. This can't be reasssigned after validation.
40
+ def validate=(new_validate)
41
+ if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated?
42
+ raise ArgumentError, "Can't reassign Query#validate= after validation has run, remove this assignment."
43
+ else
44
+ @validate = new_validate
45
+ end
46
+ end
38
47
 
39
48
  attr_writer :query_string
40
49
 
@@ -187,7 +196,7 @@ module GraphQL
187
196
  # @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
188
197
  def result
189
198
  if !@executed
190
- Execution::Multiplex.run_all(@schema, [self], context: @context)
199
+ Execution::Interpreter.run_all(@schema, [self], context: @context)
191
200
  end
192
201
  @result ||= Query::Result.new(query: self, values: @result_values)
193
202
  end
@@ -15,7 +15,7 @@ module GraphQL
15
15
  puts "Validating graphql-pro v#{version}"
16
16
  puts " - Checking for graphql-pro credentials..."
17
17
 
18
- creds = `bundle config gems.graphql.pro`[/[a-z0-9]{11}:[a-z0-9]{11}/]
18
+ creds = `bundle config gems.graphql.pro --parseable`[/[a-z0-9]{11}:[a-z0-9]{11}/]
19
19
  if creds.nil?
20
20
  puts " #{ex} failed, please set with `bundle config gems.graphql.pro $MY_CREDENTIALS`"
21
21
  exit(1)
@@ -18,8 +18,14 @@ module GraphQL
18
18
  # @return [GraphQL::Schema::Field, Class] The field or input object this argument belongs to
19
19
  attr_reader :owner
20
20
 
21
- # @return [Symbol] A method to call to transform this value before sending it to field resolution method
22
- attr_reader :prepare
21
+ # @param new_prepare [Method, Proc]
22
+ # @return [Symbol] A method or proc to call to transform this value before sending it to field resolution method
23
+ def prepare(new_prepare = NO_DEFAULT)
24
+ if new_prepare != NO_DEFAULT
25
+ @prepare = new_prepare
26
+ end
27
+ @prepare
28
+ end
23
29
 
24
30
  # @return [Symbol] This argument's name in Ruby keyword arguments
25
31
  attr_reader :keyword
@@ -96,8 +102,14 @@ module GraphQL
96
102
  "#<#{self.class} #{path}: #{type.to_type_signature}#{description ? " @description=#{description.inspect}" : ""}>"
97
103
  end
98
104
 
105
+ # @param default_value [Object] The value to use when the client doesn't provide one
99
106
  # @return [Object] the value used when the client doesn't provide a value for this argument
100
- attr_reader :default_value
107
+ def default_value(new_default_value = NO_DEFAULT)
108
+ if new_default_value != NO_DEFAULT
109
+ @default_value = new_default_value
110
+ end
111
+ @default_value
112
+ end
101
113
 
102
114
  # @return [Boolean] True if this argument has a default value
103
115
  def default_value?
@@ -259,26 +271,26 @@ module GraphQL
259
271
  # If this isn't lazy, then the block returns eagerly and assigns the result here
260
272
  # If it _is_ lazy, then we write the lazy to the hash, then update it later
261
273
  argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |resolved_coerced_value|
274
+ owner.validate_directive_argument(self, resolved_coerced_value)
275
+ prepared_value = begin
276
+ prepare_value(parent_object, resolved_coerced_value, context: context)
277
+ rescue StandardError => err
278
+ context.schema.handle_or_reraise(context, err)
279
+ end
280
+
262
281
  if loads && !from_resolver?
263
282
  loaded_value = begin
264
- load_and_authorize_value(owner, coerced_value, context)
283
+ load_and_authorize_value(owner, prepared_value, context)
265
284
  rescue StandardError => err
266
285
  context.schema.handle_or_reraise(context, err)
267
286
  end
268
287
  end
269
288
 
270
- maybe_loaded_value = loaded_value || resolved_coerced_value
289
+ maybe_loaded_value = loaded_value || prepared_value
271
290
  context.schema.after_lazy(maybe_loaded_value) do |resolved_loaded_value|
272
- owner.validate_directive_argument(self, resolved_loaded_value)
273
- prepared_value = begin
274
- prepare_value(parent_object, resolved_loaded_value, context: context)
275
- rescue StandardError => err
276
- context.schema.handle_or_reraise(context, err)
277
- end
278
-
279
291
  # TODO code smell to access such a deeply-nested constant in a distant module
280
292
  argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
281
- value: prepared_value,
293
+ value: resolved_loaded_value,
282
294
  definition: self,
283
295
  default_used: default_used,
284
296
  )
@@ -55,14 +55,13 @@ module GraphQL
55
55
  end
56
56
  })
57
57
 
58
+ directives.merge!(GraphQL::Schema.default_directives)
58
59
  document.definitions.each do |definition|
59
60
  if definition.is_a?(GraphQL::Language::Nodes::DirectiveDefinition)
60
61
  directives[definition.name] = build_directive(definition, directive_type_resolver)
61
62
  end
62
63
  end
63
64
 
64
- directives = GraphQL::Schema.default_directives.merge(directives)
65
-
66
65
  # In case any directives referenced built-in types for their arguments:
67
66
  replace_late_bound_types_with_built_in(types)
68
67
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Directive < GraphQL::Schema::Member
5
+ class OneOf < GraphQL::Schema::Directive
6
+ description "Requires that exactly one field must be supplied and that field must not be `null`."
7
+ locations(GraphQL::Schema::Directive::INPUT_OBJECT)
8
+ default_directive true
9
+ end
10
+ end
11
+ end
12
+ end
@@ -122,7 +122,7 @@ module GraphQL
122
122
  GraphQL::TypeKinds::ENUM
123
123
  end
124
124
 
125
- def validate_non_null_input(value_name, ctx)
125
+ def validate_non_null_input(value_name, ctx, max_errors: nil)
126
126
  allowed_values = ctx.warden.enum_values(self)
127
127
  matching_value = allowed_values.find { |v| v.graphql_name == value_name }
128
128
 
@@ -666,7 +666,18 @@ module GraphQL
666
666
  inner_object = obj.object
667
667
 
668
668
  if defined?(@hash_key)
669
- inner_object[@hash_key] || inner_object[@hash_key_str] || (@fallback_value != :not_given ? @fallback_value : nil)
669
+ hash_value = if inner_object.is_a?(Hash)
670
+ inner_object.key?(@hash_key) ? inner_object[@hash_key] : inner_object[@hash_key_str]
671
+ elsif inner_object.respond_to?(:[])
672
+ inner_object[@hash_key]
673
+ else
674
+ nil
675
+ end
676
+ if hash_value == false
677
+ hash_value
678
+ else
679
+ hash_value || (@fallback_value != :not_given ? @fallback_value : nil)
680
+ end
670
681
  elsif obj.respond_to?(resolver_method)
671
682
  method_to_call = resolver_method
672
683
  method_receiver = obj
@@ -679,16 +690,6 @@ module GraphQL
679
690
  elsif inner_object.is_a?(Hash)
680
691
  if @dig_keys
681
692
  inner_object.dig(*@dig_keys)
682
- elsif defined?(@hash_key)
683
- if inner_object.key?(@hash_key)
684
- inner_object[@hash_key]
685
- elsif inner_object.key?(@hash_key_str)
686
- inner_object[@hash_key_str]
687
- elsif @fallback_value != :not_given
688
- @fallback_value
689
- else
690
- nil
691
- end
692
693
  elsif inner_object.key?(@method_sym)
693
694
  inner_object[@method_sym]
694
695
  elsif inner_object.key?(@method_str)
@@ -69,6 +69,19 @@ module GraphQL
69
69
  true
70
70
  end
71
71
 
72
+ def self.one_of
73
+ if !one_of?
74
+ if all_argument_definitions.any? { |arg| arg.type.non_null? }
75
+ raise ArgumentError, "`one_of` may not be used with required arguments -- add `required: false` to argument definitions to use `one_of`"
76
+ end
77
+ directive(GraphQL::Schema::Directive::OneOf)
78
+ end
79
+ end
80
+
81
+ def self.one_of?
82
+ directives.any? { |d| d.is_a?(GraphQL::Schema::Directive::OneOf) }
83
+ end
84
+
72
85
  def unwrap_value(value)
73
86
  case value
74
87
  when Array
@@ -109,6 +122,14 @@ module GraphQL
109
122
  class << self
110
123
  def argument(*args, **kwargs, &block)
111
124
  argument_defn = super(*args, **kwargs, &block)
125
+ if one_of?
126
+ if argument_defn.type.non_null?
127
+ raise ArgumentError, "Argument '#{argument_defn.path}' must be nullable because it is part of a OneOf type, add `required: false`."
128
+ end
129
+ if argument_defn.default_value?
130
+ raise ArgumentError, "Argument '#{argument_defn.path}' cannot have a default value because it is part of a OneOf type, remove `default_value: ...`."
131
+ end
132
+ end
112
133
  # Add a method access
113
134
  method_name = argument_defn.keyword
114
135
  class_eval <<-RUBY, __FILE__, __LINE__
@@ -126,7 +147,7 @@ module GraphQL
126
147
  # @api private
127
148
  INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object responding to `to_h` or `to_unsafe_h`."
128
149
 
129
- def validate_non_null_input(input, ctx)
150
+ def validate_non_null_input(input, ctx, max_errors: nil)
130
151
  warden = ctx.warden
131
152
 
132
153
  if input.is_a?(Array)
@@ -166,6 +187,20 @@ module GraphQL
166
187
  end
167
188
  end
168
189
 
190
+ if one_of?
191
+ if input.size == 1
192
+ input.each do |name, value|
193
+ if value.nil?
194
+ result ||= Query::InputValidationResult.new
195
+ result.add_problem("'#{graphql_name}' requires exactly one argument, but '#{name}' was `null`.")
196
+ end
197
+ end
198
+ else
199
+ result ||= Query::InputValidationResult.new
200
+ result.add_problem("'#{graphql_name}' requires exactly one argument, but #{input.size} were provided.")
201
+ end
202
+ end
203
+
169
204
  result
170
205
  end
171
206
 
@@ -27,6 +27,10 @@ module GraphQL
27
27
  "#<LateBoundType @name=#{name}>"
28
28
  end
29
29
 
30
+ def non_null?
31
+ false
32
+ end
33
+
30
34
  alias :to_s :inspect
31
35
  end
32
36
  end
@@ -45,16 +45,24 @@ module GraphQL
45
45
  end
46
46
  end
47
47
 
48
- def validate_non_null_input(value, ctx)
49
- result = nil
48
+ def validate_non_null_input(value, ctx, max_errors: nil)
49
+ result = GraphQL::Query::InputValidationResult.new
50
50
  ensure_array(value).each_with_index do |item, index|
51
51
  item_result = of_type.validate_input(item, ctx)
52
- if !item_result.valid?
53
- result ||= GraphQL::Query::InputValidationResult.new
52
+ unless item_result.valid?
53
+ if max_errors
54
+ if max_errors == 0
55
+ add_max_errros_reached_message(result)
56
+ break
57
+ end
58
+
59
+ max_errors -= 1
60
+ end
61
+
54
62
  result.merge_result!(index, item_result)
55
63
  end
56
64
  end
57
- result
65
+ result.valid? ? nil : result
58
66
  end
59
67
 
60
68
  private
@@ -67,6 +75,12 @@ module GraphQL
67
75
  [value]
68
76
  end
69
77
  end
78
+
79
+ def add_max_errros_reached_message(result)
80
+ message = "Too many errors processing list variable, max validation error limit reached. Execution aborted"
81
+ item_result = GraphQL::Query::InputValidationResult.from_problem(message)
82
+ result.merge_result!(nil, item_result)
83
+ end
70
84
  end
71
85
  end
72
86
  end
@@ -323,8 +323,14 @@ module GraphQL
323
323
  end
324
324
  # Double-check that the located object is actually of this type
325
325
  # (Don't want to allow arbitrary access to objects this way)
326
- resolved_application_object_type = context.schema.resolve_type(argument.loads, application_object, context)
327
- context.schema.after_lazy(resolved_application_object_type) do |application_object_type|
326
+ maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context)
327
+ context.schema.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result|
328
+ if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
329
+ application_object_type, application_object = resolve_type_result
330
+ else
331
+ application_object_type = resolve_type_result
332
+ # application_object is already assigned
333
+ end
328
334
  possible_object_types = context.warden.possible_types(argument.loads)
329
335
  if !possible_object_types.include?(application_object_type)
330
336
  err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
@@ -8,11 +8,11 @@ module GraphQL
8
8
  validate_input(val, ctx).valid?
9
9
  end
10
10
 
11
- def validate_input(val, ctx)
11
+ def validate_input(val, ctx, max_errors: nil)
12
12
  if val.nil?
13
13
  Query::InputValidationResult::VALID
14
14
  else
15
- validate_non_null_input(val, ctx) || Query::InputValidationResult::VALID
15
+ validate_non_null_input(val, ctx, max_errors: max_errors) || Query::InputValidationResult::VALID
16
16
  end
17
17
  end
18
18
 
@@ -31,13 +31,13 @@ module GraphQL
31
31
  "#<#{self.class.name} @of_type=#{@of_type.inspect}>"
32
32
  end
33
33
 
34
- def validate_input(value, ctx)
34
+ def validate_input(value, ctx, max_errors: nil)
35
35
  if value.nil?
36
36
  result = GraphQL::Query::InputValidationResult.new
37
37
  result.add_problem("Expected value to not be null")
38
38
  result
39
39
  else
40
- of_type.validate_input(value, ctx)
40
+ of_type.validate_input(value, ctx, max_errors: max_errors)
41
41
  end
42
42
  end
43
43
 
@@ -40,7 +40,7 @@ module GraphQL
40
40
  @default_scalar ||= false
41
41
  end
42
42
 
43
- def validate_non_null_input(value, ctx)
43
+ def validate_non_null_input(value, ctx, max_errors: nil)
44
44
  coerced_result = begin
45
45
  coerce_input(value, ctx)
46
46
  rescue GraphQL::CoercionError => err
@@ -31,6 +31,7 @@ require "graphql/schema/union"
31
31
  require "graphql/schema/directive"
32
32
  require "graphql/schema/directive/deprecated"
33
33
  require "graphql/schema/directive/include"
34
+ require "graphql/schema/directive/one_of"
34
35
  require "graphql/schema/directive/skip"
35
36
  require "graphql/schema/directive/feature"
36
37
  require "graphql/schema/directive/flagged"
@@ -754,13 +755,21 @@ module GraphQL
754
755
  # rubocop:disable Lint/DuplicateMethods
755
756
  module ResolveTypeWithType
756
757
  def resolve_type(type, obj, ctx)
757
- first_resolved_type, resolved_value = if type.is_a?(Module) && type.respond_to?(:resolve_type)
758
+ maybe_lazy_resolve_type_result = if type.is_a?(Module) && type.respond_to?(:resolve_type)
758
759
  type.resolve_type(obj, ctx)
759
760
  else
760
761
  super
761
762
  end
762
763
 
763
- after_lazy(first_resolved_type) do |resolved_type|
764
+ after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result|
765
+ if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
766
+ resolved_type = resolve_type_result[0]
767
+ resolved_value = resolve_type_result[1]
768
+ else
769
+ resolved_type = resolve_type_result
770
+ resolved_value = obj
771
+ end
772
+
764
773
  if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind))
765
774
  if resolved_value
766
775
  [resolved_type, resolved_value]
@@ -905,6 +914,7 @@ module GraphQL
905
914
  "include" => GraphQL::Schema::Directive::Include,
906
915
  "skip" => GraphQL::Schema::Directive::Skip,
907
916
  "deprecated" => GraphQL::Schema::Directive::Deprecated,
917
+ "oneOf" => GraphQL::Schema::Directive::OneOf,
908
918
  }.freeze
909
919
  end
910
920
 
@@ -982,7 +992,7 @@ module GraphQL
982
992
  # @param context [Hash] Multiplex-level context
983
993
  # @return [Array<Hash>] One result for each query in the input
984
994
  def multiplex(queries, **kwargs)
985
- GraphQL::Execution::Multiplex.run_all(self, queries, **kwargs)
995
+ GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
986
996
  end
987
997
 
988
998
  def instrumenters
@@ -36,6 +36,7 @@ module GraphQL
36
36
  GraphQL::StaticValidation::QueryRootExists,
37
37
  GraphQL::StaticValidation::SubscriptionRootExists,
38
38
  GraphQL::StaticValidation::InputObjectNamesAreUnique,
39
+ GraphQL::StaticValidation::OneOfInputObjectsAreValid,
39
40
  ]
40
41
  end
41
42
  end
@@ -30,10 +30,10 @@ module GraphQL
30
30
  }.tap { |h| h["path"] = path unless path.nil? }
31
31
  end
32
32
 
33
- private
34
-
35
33
  attr_reader :nodes
36
34
 
35
+ private
36
+
37
37
  def locations
38
38
  nodes.map do |node|
39
39
  h = {"line" => node.line, "column" => node.col}
@@ -108,6 +108,10 @@ module GraphQL
108
108
  arg_type = @warden.get_argument(type, name).type
109
109
  recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
110
110
  end
111
+
112
+ if type.one_of? && ast_node.arguments.size != 1
113
+ results << Query::InputValidationResult.from_problem("`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{ast_node.arguments.size})")
114
+ end
111
115
  merge_results(results)
112
116
  end
113
117
  end
@@ -9,11 +9,17 @@ module GraphQL
9
9
 
10
10
  def on_directive(node, parent)
11
11
  if !@directive_names.include?(node.name)
12
- add_error(GraphQL::StaticValidation::DirectivesAreDefinedError.new(
13
- "Directive @#{node.name} is not defined",
14
- nodes: node,
15
- directive: node.name
16
- ))
12
+ @directives_are_defined_errors_by_name ||= {}
13
+ error = @directives_are_defined_errors_by_name[node.name] ||= begin
14
+ err = GraphQL::StaticValidation::DirectivesAreDefinedError.new(
15
+ "Directive @#{node.name} is not defined",
16
+ nodes: [],
17
+ directive: node.name
18
+ )
19
+ add_error(err)
20
+ err
21
+ end
22
+ error.nodes << node
17
23
  else
18
24
  super
19
25
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ module OneOfInputObjectsAreValid
5
+ def on_input_object(node, parent)
6
+ return super unless parent.is_a?(GraphQL::Language::Nodes::Argument)
7
+
8
+ parent_type = get_parent_type(context, parent)
9
+ return super unless parent_type && parent_type.kind.input_object? && parent_type.one_of?
10
+
11
+ validate_one_of_input_object(node, context, parent_type)
12
+ super
13
+ end
14
+
15
+ private
16
+
17
+ def validate_one_of_input_object(ast_node, context, parent_type)
18
+ present_fields = ast_node.arguments.map(&:name)
19
+ input_object_type = parent_type.to_type_signature
20
+
21
+ if present_fields.count != 1
22
+ add_error(
23
+ OneOfInputObjectsAreValidError.new(
24
+ "OneOf Input Object '#{input_object_type}' must specify exactly one key.",
25
+ path: context.path,
26
+ nodes: ast_node,
27
+ input_object_type: input_object_type
28
+ )
29
+ )
30
+ return
31
+ end
32
+
33
+ field = present_fields.first
34
+ value = ast_node.arguments.first.value
35
+
36
+ if value.is_a?(GraphQL::Language::Nodes::NullValue)
37
+ add_error(
38
+ OneOfInputObjectsAreValidError.new(
39
+ "Argument '#{input_object_type}.#{field}' must be non-null.",
40
+ path: [*context.path, field],
41
+ nodes: ast_node.arguments.first,
42
+ input_object_type: input_object_type
43
+ )
44
+ )
45
+ return
46
+ end
47
+
48
+ if value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
49
+ variable_name = value.name
50
+ variable_type = @declared_variables[variable_name].type
51
+
52
+ unless variable_type.is_a?(GraphQL::Language::Nodes::NonNullType)
53
+ add_error(
54
+ OneOfInputObjectsAreValidError.new(
55
+ "Variable '#{variable_name}' must be non-nullable to be used for OneOf Input Object '#{input_object_type}'.",
56
+ path: [*context.path, field],
57
+ nodes: ast_node,
58
+ input_object_type: input_object_type
59
+ )
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class OneOfInputObjectsAreValidError < StaticValidation::Error
5
+ attr_reader :input_object_type
6
+
7
+ def initialize(message, path:, nodes:, input_object_type:)
8
+ super(message, path: path, nodes: nodes)
9
+ @input_object_type = input_object_type
10
+ end
11
+
12
+ # A hash representation of this Message
13
+ def to_h
14
+ extensions = {
15
+ "code" => code,
16
+ "inputObjectType" => input_object_type
17
+ }
18
+
19
+ super.merge({
20
+ "extensions" => extensions
21
+ })
22
+ end
23
+
24
+ def code
25
+ "invalidOneOfInputObject"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -34,12 +34,18 @@ module GraphQL
34
34
  used_directives = {}
35
35
  node.directives.each do |ast_directive|
36
36
  directive_name = ast_directive.name
37
- if used_directives[directive_name]
38
- add_error(GraphQL::StaticValidation::UniqueDirectivesPerLocationError.new(
39
- "The directive \"#{directive_name}\" can only be used once at this location.",
40
- nodes: [used_directives[directive_name], ast_directive],
41
- directive: directive_name,
42
- ))
37
+ if (first_node = used_directives[directive_name])
38
+ @directives_are_unique_errors_by_first_node ||= {}
39
+ err = @directives_are_unique_errors_by_first_node[first_node] ||= begin
40
+ error = GraphQL::StaticValidation::UniqueDirectivesPerLocationError.new(
41
+ "The directive \"#{directive_name}\" can only be used once at this location.",
42
+ nodes: [used_directives[directive_name]],
43
+ directive: directive_name,
44
+ )
45
+ add_error(error)
46
+ error
47
+ end
48
+ err.nodes << ast_directive
43
49
  elsif !((dir_defn = context.schema_directives[directive_name]) && dir_defn.repeatable?)
44
50
  used_directives[directive_name] = ast_directive
45
51
  end
@@ -23,7 +23,7 @@ module GraphQL
23
23
  problems = validation_result.problems
24
24
  first_problem = problems && problems.first
25
25
  if first_problem
26
- error_message = first_problem["message"]
26
+ error_message = first_problem["explanation"]
27
27
  end
28
28
 
29
29
  error_message ||= "Default value for $#{node.name} doesn't match type #{type.to_type_signature}"