Skip to content

Commit a53bc8e

Browse files
authored
Merge pull request #5368 from rmosolgo/schema-directive-validations
Improve schema directive validations
2 parents 0e8e332 + 4dd3ec7 commit a53bc8e

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,13 @@ def run_directive(method_name, object, directives, idx, &block)
826826
else
827827
dir_defn = @schema_directives.fetch(dir_node.name)
828828
raw_dir_args = arguments(nil, dir_defn, dir_node)
829+
if !raw_dir_args.is_a?(GraphQL::ExecutionError)
830+
begin
831+
dir_defn.validate!(raw_dir_args, context)
832+
rescue GraphQL::ExecutionError => err
833+
raw_dir_args = err
834+
end
835+
end
829836
dir_args = continue_value(
830837
raw_dir_args, # value
831838
nil, # field

lib/graphql/schema/directive.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Schema
99
class Directive < GraphQL::Schema::Member
1010
extend GraphQL::Schema::Member::HasArguments
1111
extend GraphQL::Schema::Member::HasArguments::HasDirectiveArguments
12+
extend GraphQL::Schema::Member::HasValidators
1213

1314
class << self
1415
# Directives aren't types, they don't have kinds.
@@ -75,6 +76,10 @@ def resolve_each(object, arguments, context)
7576
yield
7677
end
7778

79+
def validate!(arguments, context)
80+
Schema::Validator.validate!(validators, self, context, arguments)
81+
end
82+
7883
def on_field?
7984
locations.include?(FIELD)
8085
end
@@ -111,6 +116,9 @@ def inherited(subclass)
111116
# @return [GraphQL::Interpreter::Arguments]
112117
attr_reader :arguments
113118

119+
class InvalidArgumentError < GraphQL::Error
120+
end
121+
114122
def initialize(owner, **arguments)
115123
@owner = owner
116124
assert_valid_owner
@@ -119,7 +127,19 @@ def initialize(owner, **arguments)
119127
# - lazy resolution
120128
# Probably, those won't be needed here, since these are configuration arguments,
121129
# not runtime arguments.
122-
@arguments = self.class.coerce_arguments(nil, arguments, Query::NullContext.instance)
130+
context = Query::NullContext.instance
131+
self.class.all_argument_definitions.each do |arg_defn|
132+
value = arguments[arg_defn.keyword]
133+
result = arg_defn.type.validate_input(value, context)
134+
if !result.valid?
135+
raise InvalidArgumentError, "@#{graphql_name}.#{arg_defn.graphql_name} on #{owner.path} is invalid (#{value.inspect}): #{result.problems.first["explanation"]}"
136+
end
137+
end
138+
self.class.validate!(arguments, context)
139+
@arguments = self.class.coerce_arguments(nil, arguments, context)
140+
if @arguments.is_a?(GraphQL::ExecutionError)
141+
raise @arguments
142+
end
123143
end
124144

125145
def graphql_name

lib/graphql/schema/input_object.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ def validate_non_null_input(input, ctx, max_errors: nil)
182182

183183
input.each do |argument_name, value|
184184
argument = types.argument(self, argument_name)
185+
if argument.nil? && ctx.is_a?(Query::NullContext) && argument_name.is_a?(Symbol)
186+
# Validating definition directive arguments which come in as Symbols
187+
argument = types.arguments(self).find { |arg| arg.keyword == argument_name }
188+
end
185189
# Items in the input that are unexpected
186190
if argument.nil?
187191
result ||= Query::InputValidationResult.new

spec/graphql/schema/directive_spec.rb

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class Thing < GraphQL::Schema::Object
5555
end
5656

5757
it "validates arguments" do
58-
err = assert_raises ArgumentError do
58+
err = assert_raises GraphQL::Schema::Directive::InvalidArgumentError do
5959
GraphQL::Schema::Field.from_options(
6060
name: :something,
6161
type: String,
@@ -65,9 +65,9 @@ class Thing < GraphQL::Schema::Object
6565
)
6666
end
6767

68-
assert_equal "@secret.topSecret is required, but no value was given", err.message
68+
assert_equal "@secret.topSecret on Thing.something is invalid (nil): Expected value to not be null", err.message
6969

70-
err2 = assert_raises ArgumentError do
70+
err2 = assert_raises GraphQL::Schema::Directive::InvalidArgumentError do
7171
GraphQL::Schema::Field.from_options(
7272
name: :something,
7373
type: String,
@@ -77,7 +77,7 @@ class Thing < GraphQL::Schema::Object
7777
)
7878
end
7979

80-
assert_equal "@secret.topSecret is required, but no value was given", err2.message
80+
assert_equal "@secret.topSecret on Thing.something is invalid (12.5): Could not coerce value 12.5 to Boolean", err2.message
8181
end
8282

8383
describe 'repeatable directives' do
@@ -400,4 +400,88 @@ def numbers
400400
enum_value = schema.get_type("Stuff").values["THING"]
401401
assert_equal [["tag", { name: "t7"}], ["tag", { name: "t8"}]], enum_value.directives.map { |dir| [dir.graphql_name, dir.arguments.to_h] }
402402
end
403+
404+
describe "Custom validations on definition directives" do
405+
class DirectiveValidationSchema < GraphQL::Schema
406+
class ValidatedDirective < GraphQL::Schema::Directive
407+
locations OBJECT, FIELD
408+
argument :f, Float, required: false, validates: { numericality: { greater_than: 0 } }
409+
argument :s, String, required: false, validates: { format: { with: /^[a-z]{3}$/ } }
410+
validates required: { one_of: [:f, :s]}
411+
end
412+
413+
class Query < GraphQL::Schema::Object
414+
field :i, Int, fallback_value: 100
415+
end
416+
417+
query(Query)
418+
directive(ValidatedDirective)
419+
end
420+
421+
it "runs custom validation during execution" do
422+
f_err_res = DirectiveValidationSchema.execute("{ i @validatedDirective(f: -10) }")
423+
assert_equal [{"message" => "f must be greater than 0", "locations" => [{"line" => 1, "column" => 5}], "path" => ["i"]}], f_err_res["errors"]
424+
425+
s_err_res = DirectiveValidationSchema.execute("{ i @validatedDirective(s: \"wnrn\") }")
426+
assert_equal [{"message" => "s is invalid", "locations" => [{"line" => 1, "column" => 5}], "path" => ["i"]}], s_err_res["errors"]
427+
428+
f_s_err_res = DirectiveValidationSchema.execute("{ i @validatedDirective }")
429+
assert_equal [{"message" => "validatedDirective must include exactly one of the following arguments: f, s.", "locations" => [{"line" => 1, "column" => 5}], "path" => ["i"]}], f_s_err_res["errors"]
430+
end
431+
432+
it "runs custom validation during definition" do
433+
obj_type = Class.new(GraphQL::Schema::Object)
434+
directive_defn = DirectiveValidationSchema::ValidatedDirective
435+
obj_type.directive(directive_defn, f: 1)
436+
f_err = assert_raises GraphQL::Schema::Validator::ValidationFailedError do
437+
obj_type.directive(directive_defn, f: -1)
438+
end
439+
assert_equal "f must be greater than 0", f_err.message
440+
441+
obj_type.directive(directive_defn, s: "abc")
442+
s_err = assert_raises GraphQL::Schema::Validator::ValidationFailedError do
443+
obj_type.directive(directive_defn, s: "defg")
444+
end
445+
assert_equal "s is invalid", s_err.message
446+
447+
required_err = assert_raises GraphQL::Schema::Validator::ValidationFailedError do
448+
obj_type.directive(directive_defn)
449+
end
450+
assert_equal "validatedDirective must include exactly one of the following arguments: f, s.", required_err.message
451+
end
452+
end
453+
454+
describe "Validating schema directives" do
455+
def build_sdl(size:)
456+
<<~GRAPHQL
457+
directive @tshirt(size: Size!) on INTERFACE | OBJECT
458+
459+
type MyType @tshirt(size: #{size}) {
460+
color: String
461+
}
462+
463+
type Query {
464+
myType: MyType
465+
}
466+
467+
enum Size {
468+
LARGE
469+
MEDIUM
470+
SMALL
471+
}
472+
GRAPHQL
473+
end
474+
475+
it "Raises a nice error for invalid enum values" do
476+
valid_sdl = build_sdl(size: "MEDIUM")
477+
assert_equal valid_sdl, GraphQL::Schema.from_definition(valid_sdl).to_definition
478+
479+
typo_sdl = build_sdl(size: "BLAH")
480+
err = assert_raises GraphQL::Schema::Directive::InvalidArgumentError do
481+
GraphQL::Schema.from_definition(typo_sdl)
482+
end
483+
expected_msg = '@tshirt.size on MyType is invalid ("BLAH"): Expected "BLAH" to be one of: LARGE, MEDIUM, SMALL'
484+
assert_equal expected_msg, err.message
485+
end
486+
end
403487
end

0 commit comments

Comments
 (0)