@marcduez/graphql-validation-directives
v0.5.0
Published
<span class="badge-npmversion"><a href="https://npmjs.org/package/@marcduez/graphql-validation-directives" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@marcduez/graphql-validation-directives.svg" alt="NPM version" /></a></span>
Downloads
7
Maintainers
Readme
graphql-validation-directives
Library for adding input validation to GraphQL services, using schema directives.
This library was heavily inspired by the approach used by @profusion/apollo-validation-directives.
Under the hood, this library:
- Uses the schema visitor logic to copy directive metadata into the extension methods of input objects, input object fields, and field arguments.
- Wraps the resolver functions of all fields with validated arguments, validating all arguments before calling the original resolver. If validation fails, a validation error is returned and the original resolver is never executed.
General usage
import { makeExecutableSchema } from "@graphql-tools/schema"
import {
addValidationToSchema,
ValidListDirective,
ValidObjectDirective,
ValidStringDirective,
} from "@marcduez/graphql-validation-directives"
import gql from "graphql-tag"
const validListDirective = new ValidListDirective()
const validObjectDirective = new ValidObjectDirective()
const validStringDirective = new ValidStringDirective()
const executableSchema = addValidationToSchema(
validListDirective.applyDirectiveToSchema(
validObjectDirective.applyDirectiveToSchema(
validStringDirective.applyDirectiveToSchema(
makeExecutableSchema({
typeDefs: [
validListDirective.typeDefs,
validObjectDirective.typeDefs,
validStringDirective.typeDefs,
gql`
input Mutation1Input
@validObject(equalFields: ["string1", "string2"]) {
list: [[String!]]
@validList(maxItems: 2, listDepth: 0)
@validList(uniqueItems: true, listDepth: 1)
@validString(startsWith: "abc")
string1: String!
string2: String!
}
type Mutation {
mutation1(input: Mutation1Input!): Boolean!
mutation2(
input: String! @validString(maxLength: 255, startsWith: "xyz")
): Boolean!
}
`,
],
resolvers: [
{
Query: {
mutation1: () => true,
mutation2: () => true,
},
},
],
})
)
)
)
)
Null values
Null and undefined values are not validated. Use non-null GraphQL type hints for null checks.
List Depth
The GraphQL type system allows us to define list of lists. To target our validation at the right level of nesting, we need a way to specify the level of nesting. See documentation of BaseValidationDirective#getListDepth method for more details.
Custom directive names
If the default names of the validation directives collide with something in your own type definitions, you can use them with a custom name.
import { makeExecutableSchema } from "@graphql-tools/schema"
import {
addValidationToSchema,
ValidStringDirective,
} from "@marcduez/graphql-validation-directives"
import gql from "graphql-tag"
const validStringDirective = new ValidStringDirective("customDirectiveName")
const executableSchema = addValidationToSchema(
validStringDirective.applyDirectiveToSchema(
makeExecutableSchema({
typeDefs: [
validStringDirective.typeDefs,
gql`
type Mutation {
mutation1(
input: String!
@customDirectiveName(maxLength: 255, startsWith: "xyz")
): Boolean!
}
`,
],
resolvers: [
{
Query: {
mutation1: () => true,
},
},
],
})
)
)
Built-in directives
@validString directive
format
Throws if a string does not match the provided format. Currently allowed values are EMAIL
and UUID
.
input Mutation1Input {
field: String! @validString(format: EMAIL)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(format: EMAIL)): Boolean!
}
maxLength
Throws if the string is longer than the provided value.
input Mutation1Input {
field: String! @validString(maxLength: 255)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(maxLength: 255)): Boolean!
}
minLength
Throws if the string is shorter than the provided value.
input Mutation1Input {
field: String! @validString(minLength: 8)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(minLength: 8)): Boolean!
}
startsWith
Throws if the string does not start with the provided value.
input Mutation1Input {
field: String! @validString(startsWith: "account-")
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(startsWith: "account-")): Boolean!
}
endsWith
Throws if the string does not end with the provided value.
input Mutation1Input {
field: String! @validString(endsWith: "-cad")
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(endsWith: "-cad")): Boolean!
}
includes
Throws if the string does not include the provided value.
input Mutation1Input {
field: String! @validString(includes: "-rrsp-")
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(includes: "-rrsp-")): Boolean!
}
regex and regexFlags
Throws if the string does not match the provided regular expression pattern. If flags are provided, they are used.
input Mutation1Input {
field: String! @validString(regex: "^[a-z0-9]$", regexFlags: "i")
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(regex: "^[a-z0-9]$", regexFlags: "i")): Boolean!
}
oneOf
Throws if the string is not in the provided collection of strings.
input Mutation1Input {
field: String! @validString(oneOf: ["tfsa", "rrsp", "individual"])
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: String! @validString(oneOf: ["tfsa", "rrsp", "individual"])): Boolean!
}
@validInt directive
multipleOf
Throws if the number is not multiple of the provided integer value.
input Mutation1Input {
field: Int! @validInt(multipleOf: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(multipleOf: 2)): Boolean!
}
max
Throws if the number is greater than the provided integer value.
input Mutation1Input {
field: Int! @validInt(max: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(max: 2)): Boolean!
}
min
Throws if the number is less than the provided integer value.
input Mutation1Input {
field: Int! @validInt(min: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(min: 2)): Boolean!
}
exclusiveMax
Throws if the number is greater than or equal to the provided integer value.
input Mutation1Input {
field: Int! @validInt(exclusiveMax: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(exclusiveMax: 2)): Boolean!
}
exclusiveMin
Throws if the number is less than or equal to the provided integer value.
input Mutation1Input {
field: Int! @validInt(exclusiveMin: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(exclusiveMin: 2.2)): Boolean!
}
oneOf
Throws if the number is not in the provided collection of integers.
input Mutation1Input {
field: Int! @validInt(oneOf: [2, 3, 4])
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Int! @validInt(oneOf: [2, 3, 4])): Boolean!
}
@validFloat directive
multipleOf
Throws if the number is not multiple of the provided float value.
input Mutation1Input {
field: Float! @validFloat(multipleOf: 2.2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(multipleOf: 2.2)): Boolean!
}
max
Throws if the number is greater than the provided float value.
input Mutation1Input {
field: Float! @validFloat(max: 2.2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(max: 2.2)): Boolean!
}
min
Throws if the number is less than the provided float value.
input Mutation1Input {
field: Float! @validFloat(min: 2.2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(min: 2.2)): Boolean!
}
exclusiveMax
Throws if the number is greater than or equal to the provided float value.
input Mutation1Input {
field: Float! @validFloat(exclusiveMax: 2.2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(exclusiveMax: 2.2)): Boolean!
}
exclusiveMin
Throws if the number is less than or equal to the provided float value.
input Mutation1Input {
field: Float! @validFloat(exclusiveMin: 2.2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(exclusiveMin: 2.2)): Boolean!
}
oneOf
Throws if the number is not in the provided collection of floats.
input Mutation1Input {
field: Float! @validFloat(oneOf: [2.1, 2.2, 2.3])
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: Float! @validFloat(oneOf: [2.1, 2.2, 2.3])): Boolean!
}
@validList directive
maxItems
Throws if the list has more than the provided number of items.
input Mutation1Input {
field: [String!]! @validList(maxItems: 5)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: [String!]! @validList(maxItems: 5)): Boolean!
}
minItems
Throws if the list has less than the provided number of items.
input Mutation1Input {
field: [String!]! @validList(minItems: 2)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: [String!]! @validList(minItems: 2)): Boolean!
}
uniqueItems
Throws if the list has non-unique items.
input Mutation1Input {
field: [String!]! @validList(uniqueItems: true)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: [String!]! @validList(uniqueItems: true)): Boolean!
}
listDepth
See documentation of BaseValidationDirective#getListDepth method for more details on this feature.
input Mutation1Input {
field: [[String!]!]! @validList(maxItems: 3) @validList(maxItems: 2, listDepth: 1)
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
type Mutation {
someMutation(arg: [[String!]!]! @validList(maxItems: 3) @validList(maxItems: 2, listDepth: 1)): Boolean!
}
@validObject directive
equalFields
Throws if the fields with the provided names have non-equal values.
input Mutation1Input @validObject(equalFields: ["password", "confirmPassword"]) {
password: String!
confirmPassword: String!
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
input Mutation1Input {
password: String!
confirmPassword: String!
}
type Mutation {
someMutation(arg: Mutation1Input! @validObject(equalFields: ["password", "confirmPassword"])): Boolean!
}
nonEqualFields
Throws if the fields with the provided names have equal values.
input Mutation1Input @validObject(nonEqualFields: ["securityAnswer1", "securityAnswer2"]) {
securityAnswer1: String!
securityAnswer2: String!
}
type Mutation {
someMutation(arg: Mutation1Input!): Boolean!
}
OR
input Mutation1Input {
securityAnswer1: String!
securityAnswer2: String!
}
type Mutation {
someMutation(arg: Mutation1Input! @validObject(nonEqualFields: ["securityAnswer1", "securityAnswer2"])): Boolean!
}
Writing custom directives
Below is an example of a custom validation directive that checks whether a string is a valid timezone name.
import { makeExecutableSchema } from "@graphql-tools/schema"
import {
addValidationToSchema,
BaseValidationDirective,
} from "@marcduez/graphql-validation-directives"
import gql from "graphql-tag"
import { getTimezone } from "countries-and-timezones"
class ValidTimezoneDirective extends BaseValidationDirective {
constructor(name = "validTimezone") {
super(name)
}
get typeDefs() {
return gql(`
directive @${this.name} on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
`)
}
validate(_directiveConfig: Record<string, any>, value: any) {
if (!getTimezone(value)) {
throw new Error("Value must be a valid timezone")
}
}
}
const validTimezoneDirective = new ValidTimezoneDirective()
const executableSchema = addValidationToSchema(
validTimezoneDirective.applyDirectiveToSchema(
makeExecutableSchema({
typeDefs: [
validTimezoneDirective.typeDefs,
gql`
type Mutation {
mutation1(input: String! @validTimezone): Boolean!
}
`,
],
resolvers: [
{
Query: {
mutation1: () => true,
},
},
],
})
)
)