Allows using @constraint as a directive to validate input data. Inspired by Constraints Directives RFC and OpenAPI
npm install graphql-constraint-directive
For GraphQL v15 and below, use v2 of this package
npm install graphql-constraint-directive@v2
There are multiple ways to make use of the constraint directive in your project. Below outlines the benefits and caveats. Please choose the most appropriate to your use case.
Implementation based on schema wrappers - basic scalars are wrapped as custom scalars with validations.
- based on
graphql
library, works everywhere - posibility to also validate GraphQL response data
- modifies GraphQL schema, basic scalars (Int, Float, String) are replaced by custom scalars
const { constraintDirective, constraintDirectiveTypeDefs } = require('graphql-constraint-directive')
const express = require('express')
const { ApolloServer } = require('apollo-server-express')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const typeDefs = `
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}`
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
})
schema = constraintDirective()(schema)
const app = express()
const server = new ApolloServer({ schema })
await server.start()
server.applyMiddleware({ app })
Implementation based on server plugin. Common server plugins are implemented,
function validateQuery(schema, query, variables, operationName)
can be used to implement additional plugins.
- schema stays unmodified
- runs only in supported servers
- validates only GraphQL query, not response data
Use as an Envelop plugin in supported frameworks, e.g. GraphQL Yoga.
Functionality is plugged in execute
phase
const { createEnvelopQueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive')
const express = require('express')
const { createServer } = require('@graphql-yoga/node')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const typeDefs = `
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}`
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
})
const app = express()
const yoga = createServer({
schema,
plugins: [createEnvelopQueryValidationPlugin()],
graphiql: false
})
app.use('/', yoga)
app.listen(4000);
As an Apollo 3 Server plugin
const { createApolloQueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive')
const express = require('express')
const { ApolloServer } = require('apollo-server-express')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const typeDefs = `
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}`
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
})
const plugins = [
createApolloQueryValidationPlugin({
schema
})
]
const app = express()
const server = new ApolloServer({
schema,
plugins
})
await server.start()
server.applyMiddleware({ app })
As an Apollo 4 Server plugin
const { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive/apollo4')
const express = require('express')
const { ApolloServer } = require('@apollo/server')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const cors = require('cors')
const { json } = require('body-parser')
const typeDefs = `
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}`
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
})
const plugins = [
createApollo4QueryValidationPlugin({
schema
})
]
const app = express()
const server = new ApolloServer({
schema,
plugins
})
await server.start()
app.use(
'/',
cors(),
json(),
expressMiddleware(server)
)
There is a small change required to make the Apollo Server quickstart work when trying to build an Apollo Subgraph Server.
We must use the buildSubgraphSchema
function to build a schema that can be passed to an Apollo Gateway/supergraph, instead of makeExecuteableSchema
. This uses makeExecutableSchema
under the hood.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefsGql } from 'graphql-constraint-directive/apollo4';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}
`;
const schema = buildSubgraphSchema({
typeDefs: [constraintDirectiveTypeDefsGql, typeDefs]
});
const plugins = [
createApollo4QueryValidationPlugin({
schema
})
]
const server = new ApolloServer({
schema,
plugins
});
await startStandaloneServer(server);
This implementation is untested now, as express-graphql
module is not maintained anymore.
As a Validation rule when query variables
are available
const { createQueryValidationRule, constraintDirectiveTypeDefs } = require('graphql-constraint-directive')
const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const typeDefs = `
type Query {
books: [Book]
}
type Book {
title: String
}
type Mutation {
createBook(input: BookInput): Book
}
input BookInput {
title: String! @constraint(minLength: 5, format: "email")
}`
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
})
const app = express()
app.use(
'/api',
graphqlHTTP(async (request, response, { variables }) => ({
schema,
validationRules: [
createQueryValidationRule({
variables
})
]
}))
)
app.listen(4000);
@constraint(minLength: 5)
Restrict to a minimum length
@constraint(maxLength: 5)
Restrict to a maximum length
@constraint(startsWith: "foo")
Ensure value starts with foo
@constraint(endsWith: "foo")
Ensure value ends with foo
@constraint(contains: "foo")
Ensure value contains foo
@constraint(notContains: "foo")
Ensure value does not contain foo
@constraint(pattern: "^[0-9a-zA-Z]*$")
Ensure value matches regex, e.g. alphanumeric
@constraint(format: "email")
Ensure value is in a particular format
Supported formats:
- byte: Base64
- date-time: RFC 3339
- date: ISO 8601
- ipv4
- ipv6
- uri
- uuid
@constraint(min: 3)
Ensure value is greater than or equal to
@constraint(max: 3)
Ensure value is less than or equal to
@constraint(exclusiveMin: 3)
Ensure value is greater than
@constraint(exclusiveMax: 3)
Ensure value is less than
@constraint(multipleOf: 10)
Ensure value is a multiple
@constraint(minItems: 3)
Restrict array/List to a minimum length
@constraint(maxItems: 3)
Restrict array/List to a maximum length
Each validation error throws a ConstraintDirectiveError
. Combined with a formatError function, this can be used to customise error messages.
{
code: 'ERR_GRAPHQL_CONSTRAINT_VALIDATION',
fieldName: 'theFieldName',
context: [ { arg: 'argument name which failed', value: 'value of argument' } ]
}
const formatError = function (error) {
const code = error?.originalError?.originalError?.code || error?.originalError?.code || error?.code
if (code === 'ERR_GRAPHQL_CONSTRAINT_VALIDATION') {
// return a custom object
}
return error
}
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, formatError }))
Throws a UserInputError
for each validation error.
Throws a prefilled GraphQLError
with extensions.code
set to BAD_USER_INPUT
and http status code 400
.
In case of more validation errors, top level error is generic with Query is invalid, for details see extensions.validationErrors
message,
detailed errors are stored in extensions.validationErrors
of this error.
The Envelop plugin throws a prefilled GraphQLError
for each validation error.
@constraint(uniqueTypeName: "Unique_Type_Name")
Override the unique type name generate by the library to the one passed as an argument.
Has meaning only for Schema wrapper
implementation.