diff --git a/graphql.go b/graphql.go index 72b13e00..2c3a9d53 100644 --- a/graphql.go +++ b/graphql.go @@ -117,13 +117,12 @@ type Response struct { } // Validate validates the given query with the schema. -func (s *Schema) Validate(queryString string) []*errors.QueryError { +func (s *Schema) Validate(queryString string, variables map[string]interface{}) []*errors.QueryError { doc, qErr := query.Parse(queryString) if qErr != nil { return []*errors.QueryError{qErr} } - - return validation.Validate(s.schema, doc, s.maxDepth) + return validation.Validate(s.schema, doc, variables, s.maxDepth) } // Exec executes the given query with the schema's resolver. It panics if the schema was created @@ -143,7 +142,7 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str } validationFinish := s.validationTracer.TraceValidation() - errs := validation.Validate(s.schema, doc, s.maxDepth) + errs := validation.Validate(s.schema, doc, variables, s.maxDepth) validationFinish(errs) if len(errs) != 0 { return &Response{Errors: errs} diff --git a/internal/validation/testdata/tests.json b/internal/validation/testdata/tests.json index e6d2bc0f..46df80d3 100644 --- a/internal/validation/testdata/tests.json +++ b/internal/validation/testdata/tests.json @@ -1,6 +1,6 @@ { "schemas": [ - "schema {\n query: QueryRoot\n}\n\ndirective @onQuery on QUERY\n\ndirective @onMutation on MUTATION\n\ndirective @onSubscription on SUBSCRIPTION\n\ndirective @onField on FIELD\n\ndirective @onFragmentDefinition on FRAGMENT_DEFINITION\n\ndirective @onFragmentSpread on FRAGMENT_SPREAD\n\ndirective @onInlineFragment on INLINE_FRAGMENT\n\ndirective @onSchema on SCHEMA\n\ndirective @onScalar on SCALAR\n\ndirective @onObject on OBJECT\n\ndirective @onFieldDefinition on FIELD_DEFINITION\n\ndirective @onArgumentDefinition on ARGUMENT_DEFINITION\n\ndirective @onInterface on INTERFACE\n\ndirective @onUnion on UNION\n\ndirective @onEnum on ENUM\n\ndirective @onEnumValue on ENUM_VALUE\n\ndirective @onInputObject on INPUT_OBJECT\n\ndirective @onInputFieldDefinition on INPUT_FIELD_DEFINITION\n\ntype Alien implements Being & Intelligent {\n iq: Int\n name(surname: Boolean): String\n numEyes: Int\n}\n\nscalar Any\n\ninterface Being {\n name(surname: Boolean): String\n}\n\ninterface Canine {\n name(surname: Boolean): String\n}\n\ntype Cat implements Being & Pet {\n name(surname: Boolean): String\n nickname: String\n meows: Boolean\n meowVolume: Int\n furColor: FurColor\n}\n\nunion CatOrDog = Dog | Cat\n\ninput ComplexInput {\n requiredField: Boolean!\n intField: Int\n stringField: String\n booleanField: Boolean\n stringListField: [String]\n}\n\ntype ComplicatedArgs {\n intArgField(intArg: Int): String\n nonNullIntArgField(nonNullIntArg: Int!): String\n stringArgField(stringArg: String): String\n booleanArgField(booleanArg: Boolean): String\n enumArgField(enumArg: FurColor): String\n floatArgField(floatArg: Float): String\n idArgField(idArg: ID): String\n stringListArgField(stringListArg: [String]): String\n stringListNonNullArgField(stringListNonNullArg: [String!]): String\n complexArgField(complexArg: ComplexInput): String\n multipleReqs(req1: Int!, req2: Int!): String\n multipleOpts(opt1: Int = 0, opt2: Int = 0): String\n multipleOptAndReq(req1: Int!, req2: Int!, opt1: Int = 0, opt2: Int = 0): String\n}\n\ntype Dog implements Being & Pet & Canine {\n name(surname: Boolean): String\n nickname: String\n barkVolume: Int\n barks: Boolean\n doesKnowCommand(dogCommand: DogCommand): Boolean\n isHousetrained(atOtherHomes: Boolean = true): Boolean\n isAtLocation(x: Int, y: Int): Boolean\n}\n\nenum DogCommand {\n SIT\n HEEL\n DOWN\n}\n\nunion DogOrHuman = Dog | Human\n\nenum FurColor {\n BROWN\n BLACK\n TAN\n SPOTTED\n NO_FUR\n UNKNOWN\n}\n\ntype Human implements Being & Intelligent {\n name(surname: Boolean): String\n pets: [Pet]\n relatives: [Human]\n iq: Int\n}\n\nunion HumanOrAlien = Human | Alien\n\ninterface Intelligent {\n iq: Int\n}\n\nscalar Invalid\n\ninterface Pet {\n name(surname: Boolean): String\n}\n\ntype QueryRoot {\n human(id: ID): Human\n alien: Alien\n dog: Dog\n cat: Cat\n pet: Pet\n catOrDog: CatOrDog\n dogOrHuman: DogOrHuman\n humanOrAlien: HumanOrAlien\n complicatedArgs: ComplicatedArgs\n invalidArg(arg: Invalid): String\n anyArg(arg: Any): String\n}\n", + "schema {\n query: QueryRoot\n}\n\ndirective @onQuery on QUERY\n\ndirective @onMutation on MUTATION\n\ndirective @onSubscription on SUBSCRIPTION\n\ndirective @onField on FIELD\n\ndirective @onFragmentDefinition on FRAGMENT_DEFINITION\n\ndirective @onFragmentSpread on FRAGMENT_SPREAD\n\ndirective @onInlineFragment on INLINE_FRAGMENT\n\ndirective @onSchema on SCHEMA\n\ndirective @onScalar on SCALAR\n\ndirective @onObject on OBJECT\n\ndirective @onFieldDefinition on FIELD_DEFINITION\n\ndirective @onArgumentDefinition on ARGUMENT_DEFINITION\n\ndirective @onInterface on INTERFACE\n\ndirective @onUnion on UNION\n\ndirective @onEnum on ENUM\n\ndirective @onEnumValue on ENUM_VALUE\n\ndirective @onInputObject on INPUT_OBJECT\n\ndirective @onInputFieldDefinition on INPUT_FIELD_DEFINITION\n\ntype Alien implements Being & Intelligent {\n iq: Int\n name(surname: Boolean): String\n numEyes: Int\n}\n\nscalar Any\n\ninterface Being {\n name(surname: Boolean): String\n}\n\ninterface Canine {\n name(surname: Boolean): String\n}\n\ntype Cat implements Being & Pet {\n name(surname: Boolean): String\n nickname: String\n meows: Boolean\n meowVolume: Int\n furColor: FurColor\n}\n\nunion CatOrDog = Dog | Cat\n\ninput ComplexInput {\n requiredField: Boolean!\n intField: Int\n stringField: String\n booleanField: Boolean\n stringListField: [String]\n enumField: FurColor\n nestedInput: SimpleInput}\n\ntype ComplicatedArgs {\n intArgField(intArg: Int): String\n nonNullIntArgField(nonNullIntArg: Int!): String\n stringArgField(stringArg: String): String\n booleanArgField(booleanArg: Boolean): String\n enumArgField(enumArg: FurColor): String\n enumArrayArgField(enumArrayArg: [FurColor!]!): String\n floatArgField(floatArg: Float): String\n idArgField(idArg: ID): String\n stringListArgField(stringListArg: [String]): String\n stringListNonNullArgField(stringListNonNullArg: [String!]): String\n complexArgField(complexArg: ComplexInput): String\n multipleReqs(req1: Int!, req2: Int!): String\n multipleOpts(opt1: Int = 0, opt2: Int = 0): String\n multipleOptAndReq(req1: Int!, req2: Int!, opt1: Int = 0, opt2: Int = 0): String\n}\n\ntype Dog implements Being & Pet & Canine {\n name(surname: Boolean): String\n nickname: String\n barkVolume: Int\n barks: Boolean\n doesKnowCommand(dogCommand: DogCommand): Boolean\n isHousetrained(atOtherHomes: Boolean = true): Boolean\n isAtLocation(x: Int, y: Int): Boolean\n}\n\nenum DogCommand {\n SIT\n HEEL\n DOWN\n}\n\nunion DogOrHuman = Dog | Human\n\nenum FurColor {\n BROWN\n BLACK\n TAN\n SPOTTED\n NO_FUR\n UNKNOWN\n}\n\ntype Human implements Being & Intelligent {\n name(surname: Boolean): String\n pets: [Pet]\n relatives: [Human]\n iq: Int\n}\n\nunion HumanOrAlien = Human | Alien\n\ninterface Intelligent {\n iq: Int\n}\n\nscalar Invalid\n\ninput SimpleInput {\n stringField: String\n stringListField: [String!]\n}\n\ninterface Pet {\n name(surname: Boolean): String\n}\n\ntype QueryRoot {\n human(id: ID): Human\n alien: Alien\n dog: Dog\n cat: Cat\n pet: Pet\n catOrDog: CatOrDog\n dogOrHuman: DogOrHuman\n humanOrAlien: HumanOrAlien\n complicatedArgs: ComplicatedArgs\n invalidArg(arg: Invalid): String\n anyArg(arg: Any): String\n}\n", "schema {\n query: QueryRoot\n}\n\ntype Connection {\n edges: [Edge]\n}\n\ntype Edge {\n node: Node\n}\n\ntype IntBox implements SomeBox {\n scalar: Int\n deepBox: IntBox\n unrelatedField: String\n listStringBox: [StringBox]\n stringBox: StringBox\n intBox: IntBox\n}\n\ntype Node {\n id: ID\n name: String\n}\n\ninterface NonNullStringBox1 {\n scalar: String!\n}\n\ntype NonNullStringBox1Impl implements SomeBox & NonNullStringBox1 {\n scalar: String!\n unrelatedField: String\n deepBox: SomeBox\n}\n\ninterface NonNullStringBox2 {\n scalar: String!\n}\n\ntype NonNullStringBox2Impl implements SomeBox & NonNullStringBox2 {\n scalar: String!\n unrelatedField: String\n deepBox: SomeBox\n}\n\ntype QueryRoot {\n someBox: SomeBox\n connection: Connection\n}\n\ninterface SomeBox {\n deepBox: SomeBox\n unrelatedField: String\n}\n\ntype StringBox implements SomeBox {\n scalar: String\n deepBox: StringBox\n unrelatedField: String\n listStringBox: [StringBox]\n stringBox: StringBox\n intBox: IntBox\n}\n", "type Foo {\n constructor: String\n}\n\ntype Query {\n foo: Foo\n}\n" ], @@ -1614,6 +1614,267 @@ } ] }, + { + "name": "Validate: Arguments have valid type/valid enum constant in query", + "rule": "ArgumentsOfCorrectType", + "schema": 0, + "query": "\n query Query {\n complicatedArgs {\n enumArgField(enumArg: BROWN)\n }\n }\n ", + "errors": [] + }, + { + "name": "Validate: Arguments have valid type/invalid enum constant in query text", + "rule": "ArgumentsOfCorrectType", + "schema": 0, + "query": "\n query Query {\n complicatedArgs {\n enumArgField(enumArg: RAINBOW)\n }\n }\n ", + "errors": [ + { + "message": "Argument \"enumArg\" has invalid value RAINBOW.\nExpected type \"FurColor\", found RAINBOW.", + "locations": [ + { + "line": 4, + "column": 33 + } + ] + } + ] + }, + { + "name": "Validate: Arguments have valid type/optional enum constant in query", + "rule": "ArgumentsOfCorrectType", + "schema": 0, + "query": "\n query Query {\n complicatedArgs {\n enumArgField()\n }\n }\n ", + "errors": [] + }, + { + "name": "Validate: Variables have valid type/valid enum constant in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($color: FurColor) {\n complicatedArgs {\n enumArgField(enumArg: $color)\n }\n }\n ", + "vars": { + "color": "BROWN" + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/invalid enum constant in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($color: FurColor) {\n complicatedArgs {\n enumArgField(enumArg: $color)\n }\n }\n ", + "vars": { + "color": "RAINBOW" + }, + "errors": [ + { + "message": "Variable \"color\" has invalid value RAINBOW.\nExpected type \"FurColor\", found RAINBOW.", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ] + }, + { + "name": "Validate: Variables have valid type/optional enum constant variable null", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($color: FurColor) {\n complicatedArgs {\n enumArgField(enumArg: $color)\n }\n }\n ", + "vars": { + "color": null + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/list with single value in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($stringListArg: [String!])\n {\n stringListArgField(stringListArg: $stringListArg)\n }\n ", + "vars": { + "stringListArg": "single value" + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/list with list value in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($stringListArg: [String!])\n {\n stringListArgField(stringListArg: $stringListArg)\n }\n ", + "vars": { + "stringListArg": ["first value", "second value"] + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/input type with invalid input in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": "not input" + }, + "errors": [ + { + "message": "Variable \"complexVar\" has invalid type string.\nExpected type \"ComplexInput\", found not input.", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ] + }, + { + "name": "Validate: Variables have valid type/input type with invalid enum constant in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": { + "requiredField": true, + "enumField": "RAINBOW" + } + }, + "errors": [ + { + "message": "Variable \"enumField\" has invalid value RAINBOW.\nExpected type \"FurColor\", found RAINBOW.", + "locations": [ + { + "line": 73, + "column": 3 + } + ] + } + ] + }, + { + "name": "Validate: Variables have valid type/input type with optional enum constant variable null", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": { + "requiredField": true, + "enumField": null + } + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/input type with nested input string from variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": { + "requiredField": true, + "nestedInput": { + "stringField": "something" + } + } + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/input type with nested input string list with single value from variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": { + "requiredField": true, + "nestedInput": { + "stringListField": "something" + } + } + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/input type with nested input string list from variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($complexVar: ComplexInput)\n {\n complicatedArgs {\n complexArgField(complexArg: $complexVar)\n }\n }\n ", + "vars": { + "complexVar": { + "requiredField": true, + "nestedInput": { + "stringListField": ["first", "second"] + } + } + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/number as enum", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($color: FurColor) {\n complicatedArgs {\n enumArgField(enumArg: $color)\n }\n }\n ", + "vars": { + "color": 42 + }, + "errors": [ + { + "message": "Variable \"color\" has invalid type float64.\nExpected type \"FurColor\", found 42.", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ] + }, + { + "name": "Validate: Variables have valid type/valid enum array constant in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($colors: [FurColor!]!) {\n complicatedArgs {\n enumArrayArgField(enumArrayArg: $colors)\n }\n }\n ", + "vars": { + "colors": ["BROWN", "BLACK", "SPOTTED"] + }, + "errors": [] + }, + { + "name": "Validate: Variables have valid type/invalid enum array constant in variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($colors: [FurColor!]!) {\n complicatedArgs {\n enumArrayArgField(enumArrayArg: $colors)\n }\n }\n ", + "vars": { + "colors": ["TEAL", "AUBERGINE"] + }, + "errors": [ + { + "message": "Variable \"colors\" has invalid value TEAL.\nExpected type \"FurColor\", found TEAL.", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + }, + { + "message": "Variable \"colors\" has invalid value AUBERGINE.\nExpected type \"FurColor\", found AUBERGINE.", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ] + }, + { + "name": "Validate: Variables have valid type/string as enum array variable", + "rule": "VariablesOfCorrectType", + "schema": 0, + "query": "\n query Query($colors: [FurColor!]!) {\n complicatedArgs {\n enumArrayArgField(enumArrayArg: $colors)\n }\n }\n ", + "vars": { + "colors": "BROWN" + }, + "errors": [] + }, { "name": "Validate: Overlapping fields can be merged/unique fields", "rule": "OverlappingFieldsCanBeMerged", diff --git a/internal/validation/validation.go b/internal/validation/validation.go index 0b794d04..4a1ca96c 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -63,7 +63,7 @@ func newContext(s *schema.Schema, doc *query.Document, maxDepth int) *context { } } -func Validate(s *schema.Schema, doc *query.Document, maxDepth int) []*errors.QueryError { +func Validate(s *schema.Schema, doc *query.Document, variables map[string]interface{}, maxDepth int) []*errors.QueryError { c := newContext(s, doc, maxDepth) opNames := make(nameSet) @@ -95,6 +95,7 @@ func Validate(s *schema.Schema, doc *query.Document, maxDepth int) []*errors.Que if !canBeInput(t) { c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t) } + validateValue(opc, v, variables[v.Name.Name], t) if v.Default != nil { validateLiteral(opc, v.Default) @@ -178,6 +179,57 @@ func Validate(s *schema.Schema, doc *query.Document, maxDepth int) []*errors.Que return c.errs } +func validateValue(c *opContext, v *common.InputValue, val interface{}, t common.Type) { + switch t := t.(type) { + case *common.NonNull: + if val == nil { + c.addErr(v.Loc, "VariablesOfCorrectType", "Variable \"%s\" has invalid value null.\nExpected type \"%s\", found null.", v.Name.Name, t) + return + } + validateValue(c, v, val, t.OfType) + case *common.List: + if val == nil { + return + } + vv, ok := val.([]interface{}) + if !ok { + c.addErr(v.Loc, "VariablesOfCorrectType", "Variable \"%s\" has invalid type %T.\nExpected type \"%s\", found %v.", v.Name.Name, val, t, val) + return + } + for _, elem := range vv { + validateValue(c, v, elem, t.OfType) + } + case *schema.Enum: + if val == nil { + return + } + e, ok := val.(string) + if !ok { + c.addErr(v.Loc, "VariablesOfCorrectType", "Variable \"%s\" has invalid type %T.\nExpected type \"%s\", found %v.", v.Name.Name, val, t, val) + return + } + for _, option := range t.Values { + if option.Name == e { + return + } + } + c.addErr(v.Loc, "VariablesOfCorrectType", "Variable \"%s\" has invalid value %s.\nExpected type \"%s\", found %s.", v.Name.Name, e, t, e) + case *schema.InputObject: + if val == nil { + return + } + in, ok := val.(map[string]interface{}) + if !ok { + c.addErr(v.Loc, "VariablesOfCorrectType", "Variable \"%s\" has invalid type %T.\nExpected type \"%s\", found %s.", v.Name.Name, val, t, val) + return + } + for _, f := range t.Values { + fieldVal := in[f.Name.Name] + validateValue(c, f, fieldVal, f.Type) + } + } +} + // validates the query doesn't go deeper than maxDepth (if set). Returns whether // or not query validated max depth to avoid excessive recursion. func validateMaxDepth(c *opContext, sels []query.Selection, depth int) bool { @@ -686,6 +738,7 @@ func validateLiteral(c *opContext, l common.Literal) { }) continue } + validateValueType(c, l, resolveType(c.context, v.Type)) c.usedVars[op][v] = struct{}{} } } diff --git a/internal/validation/validation_test.go b/internal/validation/validation_test.go index 3d265a92..2532390d 100644 --- a/internal/validation/validation_test.go +++ b/internal/validation/validation_test.go @@ -19,6 +19,7 @@ type Test struct { Rule string Schema int Query string + Vars map[string]interface{} Errors []*errors.QueryError } @@ -50,7 +51,7 @@ func TestValidate(t *testing.T) { if err != nil { t.Fatal(err) } - errs := validation.Validate(schemas[test.Schema], d, 0) + errs := validation.Validate(schemas[test.Schema], d, test.Vars, 0) got := []*errors.QueryError{} for _, err := range errs { if err.Rule == test.Rule {