Awesome
graphql-constraint-directive
Allows using @constraint as a directive to validate input data. Inspired by Constraints Directives RFC and OpenAPI
Install
npm install graphql-constraint-directive
For GraphQL v15 and below, use v2 of this package
npm install graphql-constraint-directive@v2
Usage
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.
Schema wrapper
Implementation based on schema wrappers - basic scalars are wrapped as custom scalars with validations.
Benefits
- based on
graphql
library, works everywhere - posibility to also validate GraphQL response data
Caveats
- 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 })
Server plugin
Implementation based on server plugin. Common server plugins are implemented,
function validateQuery(schema, query, variables, operationName)
can be used to implement additional plugins.
Benefits
- schema stays unmodified
Caveats
- runs only in supported servers
- validates only GraphQL query, not response data
Envelop
Use as an Envelop plugin in supported frameworks, e.g. GraphQL Yoga.
Functionality is plugged in execute
phase
This plugin requires the following dependencies installed in your project:
@envelop/core
-^2.0.0
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);
Apollo 3 Server
As an Apollo 3 Server plugin
This plugin requires the following dependencies installed in your project:
- dependencies required for your selected Apollo Server 3 variant
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 })
Apollo 4 Server
As an Apollo 4 Server plugin
This plugin requires the following dependencies installed in your project:
@apollo/server
-^4.0.0
graphql-tag
-^2.0.0
const { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive/apollo4')
const { ApolloServer } = require('@apollo/server')
const { startStandaloneServer } = require('@apollo/server/standalone');
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 = [
createApollo4QueryValidationPlugin()
]
const server = new ApolloServer({
schema,
plugins
})
await startStandaloneServer(server);
Apollo 4 Subgraph 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.
This plugin requires the following dependencies installed in your project:
@apollo/server
-^4.0.0
graphql-tag
-^2.0.0
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()
]
const server = new ApolloServer({
schema,
plugins
});
await startStandaloneServer(server);
Express
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);
Schema documentation
You can use the provided schema transformation to automatically add @constraint
documentation into fields and arguments descriptions. By default directives are not typically present in the exposed introspected schema
const { constraintDirectiveTypeDefs, constraintDirectiveDocumentation } = require('graphql-constraint-directive')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const typeDefs = ...
let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs]
})
schema = constraintDirectiveDocumentation()(schema);
// any constraint directive handler implementation
This transformation appends constraint documentation header
, and then a list of constraint conditions descriptions
to the description of each field and argument where the @constraint
directive is used.
Original schema:
"""
Existing field or argument description.
"""
fieldOrArgument: String @constraint(minLength: 10, maxLength: 50)
Transformed schema:
"""
Existing field or argument description.
*Constraints:*
* Minimum length: `10`
* Maximum length: `50`
"""
fieldOrArgument: String @constraint(minLength: 10, maxLength: 50)
CommonMark is used in the desccription for better readability.
If constraint documentation header
already exists in the field or argument description, then
constraint documentation is not appended. This allows you to override constraint description
when necessary, or use this in a chain of subgraph/supergraph schemes.
Both constraint documentation header
and constraint conditions descriptions
can be customized
during the transformation creation, eg. to localize them.
schema = constraintDirectiveDocumentation(
{
header: '*Changed header:*',
descriptionsMap: {
minLength: 'Changed Minimum length',
maxLength: 'Changed Maximum length',
startsWith: 'Changed Starts with',
endsWith: 'Changed Ends with',
contains: 'Changed Contains',
notContains: 'Changed Doesn\'t contain',
pattern: 'Changed Must match RegEx pattern',
format: 'Changed Must match format',
min: 'Changed Minimum value',
max: 'Changed Maximum value',
exclusiveMin: 'Changed Grater than',
exclusiveMax: 'Changed Less than',
multipleOf: 'Changed Must be a multiple of',
minItems: 'Changed Minimum number of items',
maxItems: 'Changed Maximum number of items'
}
}
)(schema);
API
String
minLength
@constraint(minLength: 5)
Restrict to a minimum length
maxLength
@constraint(maxLength: 5)
Restrict to a maximum length
startsWith
@constraint(startsWith: "foo")
Ensure value starts with foo
endsWith
@constraint(endsWith: "foo")
Ensure value ends with foo
contains
@constraint(contains: "foo")
Ensure value contains foo
notContains
@constraint(notContains: "foo")
Ensure value does not contain foo
pattern
@constraint(pattern: "^[0-9a-zA-Z]*$")
Ensure value matches regex, e.g. alphanumeric
format
@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
Custom Format
You can add your own custom formats by passing a formats
object to the plugin options. See example below.
@constraint(format: "my-custom-format")
const formats = {
'my-custom-format': (value) => {
if (value === 'foo') {
return true
}
throw new GraphQLError('Value must be foo')
}
};
// Envelop
createEnvelopQueryValidationPlugin({ formats })
// Apollo 3 Server
createApolloQueryValidationPlugin({ formats })
// Apollo 4 Server
createApollo4QueryValidationPlugin({ formats })
Int/Float
min
@constraint(min: 3)
Ensure value is greater than or equal to
max
@constraint(max: 3)
Ensure value is less than or equal to
exclusiveMin
@constraint(exclusiveMin: 3)
Ensure value is greater than
exclusiveMax
@constraint(exclusiveMax: 3)
Ensure value is less than
multipleOf
@constraint(multipleOf: 10)
Ensure value is a multiple
Array/List
minItems
@constraint(minItems: 3)
Restrict array/List to a minimum length
maxItems
@constraint(maxItems: 3)
Restrict array/List to a maximum length
ConstraintDirectiveError
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 }))
Apollo Server 3
Throws a UserInputError
for each validation error.
Apollo Server 4
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.
Envelop
The Envelop plugin throws a prefilled GraphQLError
for each validation error.
uniqueTypeName
@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.