graphql 2.0.11 → 2.0.14

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.
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}"