Directives
Redwood Directives are a powerful feature, supercharging your GraphQL-backed Services.
You can think of directives like "middleware" that let you run reusable code during GraphQL execution to perform tasks like authentication and formatting.
Redwood uses them to make it a snap to protect your API Services from unauthorized access.
Here we call those types of directives Validators.
You can also use them to transform the output of your query result to modify string values, format dates, shield sensitive data, and more! We call those types of directives Transformers.
You'll recognize a directive as being 1) preceded by @
(e.g. @myDirective
) and 2) declared alongside a field:
type Bar {
name: String! @myDirective
}
or a Query or a Mutation:
type Query {
bars: [Bar!]! @myDirective
}
type Mutation {
createBar(input: CreateBarInput!): Bar! @myDirective
}
You can also define arguments that can be extracted and used when evaluating the directive:
type Bar {
field: String! @myDirective(roles: ["ADMIN"])
}
or a Query or Mutation:
type Query {
bars: [Bar!]! @myDirective(roles: ["ADMIN"])
}
You can also use directives on relations:
type Baz {
name: String!
}
type Bar {
name: String!
bazzes: [Baz]! @myDirective
}
There are many ways to write directives using GraphQL tools and libraries. Believe us, it can get complicated fast.
But, don't fret: Redwood provides an easy and ergonomic way to generate and write your own directives so that you can focus on the implementation logic and not the GraphQL plumbing.
What is a Redwood Directive?
Redwood directives are purposeful. They come in two flavors: Validators and Transformers.
Whatever flavor of directive you want, all Redwood directives must have the following properties:
- be in the
api/src/directives/{directiveName}
directory wheredirectiveName
is the directive directory - must have a file named
{directiveName}.{js,ts}
(e.g.maskedEmail.ts
) - must export a
schema
and implement either avalidate
ortransform
function
Understanding the Directive Flow
Since it helps to know a little about the GraphQL phases—specifically the Execution phase—and how Redwood Directives fit in the data-fetching and authentication flow, let's have a quick look at some diagrams.
First, we see the built-in @requireAuth
Validator directive that can allow or deny access to a Service (a.k.a. a resolver) based on Redwood authentication.
In this example, the post(id: Int!)
query is protected using the @requireAuth
directive.
If the request's context has a currentUser
and the app's auth.{js|ts}
determines it isAuthenticated()
, then the execution phase proceeds to get resolved (for example, the post({ id })
Service is executed and queries the database using Prisma) and returns the data in the resulting response when execution is done.
In this second example, we add the Transformer directive @welcome
to the title
field on Post
in the SDL.
The GraphQL Execution phase proceeds the same as the prior example (because the post
query is still protected and we'll want to fetch the user's name) and then the title
field is resolved based on the data fetch query in the service.
Finally after execution is done, then the directive can inspect the resolvedValue
(here "Welcome to the blog!") and replace the value by inserting the current user's name—"Welcome, Tom, to the blog!"
Validators
Validators integrate with Redwood's authentication to evaluate whether or not a field, query, or mutation is permitted—that is, if the request context's currentUser
is authenticated or belongs to one of the permitted roles.
Validators should throw an Error such as AuthenticationError
or ForbiddenError
to deny access and simply return to allow.
Here the @isSubscriber
validator directive checks if the currentUser exists (and therefore is authenticated) and whether or not they have the SUBSCRIBER
role. If they don't, then access is denied by throwing an error.
import {
AuthenticationError,
ForbiddenError,
createValidatorDirective,
ValidatorDirectiveFunc,
} from '@redwoodjs/graphql-server'
import { hasRole } from 'src/lib/auth'
export const schema = gql`
directive @isSubscriber on FIELD_DEFINITION
`
const validate: ValidatorDirectiveFunc = ({ context }) => {
if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!context.currentUser.roles?.includes('SUBSCRIBER')) {
throw new ForbiddenError("You don't have access to do that.")
}
}
const isSubscriber = createValidatorDirective(schema, validate)
export default isSubscriber
Since validator directives can access arguments (such as roles
), you can quickly provide RBAC (Role-based Access Control) to fields, queries and mutations.
import gql from 'graphql-tag'
import { createValidatorDirective } from '@redwoodjs/graphql-server'
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
import { logger } from 'src/lib/logger'
export const schema = gql`
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
`
const validate = ({ directiveArgs }) => {
const { roles } = directiveArgs
applicationRequireAuth({ roles })
}
const requireAuth = createValidatorDirective(schema, validate)
export default requireAuth
All Redwood apps come with two built-in validator directives: @requireAuth
and @skipAuth
.
The @requireAuth
directive takes optional roles.
You may use these to protect against unwanted GraphQL access to your data.
Or explicitly allow public access.
Note: Validators evaluate prior to resolving the field value, so you cannot modify the value and any return value is ignored.
Transformers
Transformers can access the resolved field value to modify and then replace it in the response.
Transformers apply to both single fields (such as a User
's email
) and collections (such as a set of Posts
that belong to User
s) or is the result of a query. As such, Transformers cannot be applied to Mutations.
In the first case of a single field, the directive would return the modified field value. In the latter case, the directive could iterate each Post
and modify the title
in each. In all cases, the directive must return the same expected "shape" of the data the SDL expects.
Note: you can chain directives to first validate and then transform, such as
@requireAuth @maskedEmail
. Or even combine transformations to cascade formatting a value (you could use@uppercase
together with@truncate
to uppercase a title and shorten to 10 characters).
Since transformer directives can access arguments (such as roles
or maxLength
) you may fetch those values and use them when applying (or to check if you even should apply) your transformation.
That means that a transformer directive could consider the permittedRoles
in:
type user {
email: String! @maskedEmail(permittedRoles: ["ADMIN"])
}
and if the currentUser
is an ADMIN
, then skip the masking transform and simply return the original resolved field value:
import { createTransformerDirective, TransformerDirectiveFunc } from '@redwoodjs/graphql-server'
export const schema = gql`
directive @maskedEmail(permittedRoles: [String]) on FIELD_DEFINITION
`
const transform: TransformerDirectiveFunc = ({ context, resolvedValue }) => {
return resolvedValue.replace(/[a-zA-Z0-9]/i, '*')
}
const maskedEmail = createTransformerDirective(schema, transform)
export default maskedEmail
and you would use it in your SDLs like this:
type UserExample {
id: Int!
email: String! @maskedEmail # 👈 will replace alphanumeric characters with asterisks in the response!
name: String
}
Where can I use a Redwood Directive?
A directive can only appear in certain locations in a GraphQL schema or operation. These locations are listed in the directive's definition.
In the example below, the @maskedEmail
example, the directive can only appear in the FIELD_DEFINITION
location.
An example of a FIELD_DEFINITION
location is a field that exists on a Type
:
type UserExample {
id: Int!
email: String! @requireAuth
name: String @maskedEmail # 👈 will maskedEmail name in the response!
}
type Query {
userExamples: [UserExample!]! @requireAuth 👈 will enforce auth when fetching all users
userExamples(id: Int!): UserExample @requireAuth 👈 will enforce auth when fetching a single user
}
Note: Even though GraphQL supports
FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
locations, RedwoodDirectives can only be declared on aFIELD_DEFINITION
— that is, you cannot declare a directive in anInput type
:input UserExampleInput {
email: String! @maskedEmail # 👈 🙅 not allowed on an input
name: String! @requireAuth # 👈 🙅 also not allowed on an input
}
When Should I Use a Redwood Directive?
As noted in the GraphQL spec:
Directives can be useful to get out of situations where you otherwise would need to do string manipulation to add and remove fields in your query. Server implementations may also add experimental features by defining completely new directives.
Here's a helpful guide for deciding when you should use one of Redwood's Validator or Transformer directives:
Use | Directive | Custom? | Type | |
---|---|---|---|---|
✅ | Check if the request is authenticated? | @requireAuth | Built-in | Validator |
✅ | Check if the user belongs to a role? | @requireAuth(roles: ["AUTHOR"]) | Built-in | Validator |
✅ | Only allow admins to see emails, but others get a masked value like "###@######.###" | @maskedEmail(roles: ["ADMIN"]) | Custom | Transformer |
🙅 | Know if the logged in user can edit the record, and/or values | N/A - Instead do this check in your service | ||
🙅 | Is my input a valid email address format? | N/A - Instead do this check in your service using Service Validations or consider GraphQL Scalars | ||