Finally an easy way to create custom GraphQL directives! With this package creating a custom schema directive is as easy as writing any other Apollo resolver.

This library aims to resolve this quote, and commonly shared opinion, from the Schema Directives docs:

...some of the examples may seem quite complicated. No matter how many tools and best practices you have at your disposal, it can be difficult to implement a non-trivial schema directive in a reliable, reusable way.

concept

Implementing a custom schema directive used to be a very tedious and confusing process. With the addition of the graphql-tools SchemaVisitor class a big leap in the direction of usability was made. But there was still a lot of uncertainty about how it could be used, especially for beginners to GraphQL. Many authors opted for simpler alternatives like higher order function resolver wrappers that behaved like directives. These wrappers, while simple, are undocumented in the schema and often require repetitive application and upkeep throughout the codebase.

What are the benefits of implementing directives vs using higher order resolver wrappers?

your directives are officially documented as part of the schema itself

write its resolver once and use it any number of times by simply @directive tagging Types and Type Fields in your schema that you want to apply it to

tagging Types and Type Fields in your schema that you want to apply it to no more concerns of forgetting to wrap a resolver leading to unexpected behavior

there is no "hidden" magic that requires digging throughout the resolvers to understand

This library makes implementing directives as simple as writing any other resolver in your Apollo Server. For those authors who are currently using higher order resolver wrappers transitioning to using directives is trivial.

current support

directive targets (covers the vast majority of use cases): OBJECT : directives applied to Type definitions the directive is applied to all the fields of the Object Type it is tagged on FIELD_DEFINITION : directives applied to Type.field definitions the directive is applied only to the specific Object Type Field it is tagged on note this includes Query.queryName and Mutation.mutationName because Query and Mutation are considered Object Types

directive arguments

unit and integration tests are available in the tests/ directory. the integration tests also serve as example implementations and can be run with

$ npm test $ npm run test :integration

usage

$ npm install apollo-directive

once you have written the directive type def you can implement its resolver using one of the two package utilities: createDirective or createSchemaDirectives

or both tools make use of a directiveConfig object

const directiveConfig = { name : string , resolverReplacer : function , hooks : { function , ... }, };

resolverReplacer and directiveResolver

const resolverReplacer = ( originalResolver , directiveContext ) => async function directiveResolver ( ... resolverArgs ) { const [ root , args , context , info ] = resolverArgs ; const { name , objectType , field , args : directiveArgs , } = directiveContext ; const result = originalResolver . apply ( this , args ) ; const result = await originalResolver . apply ( this , args ) ; return resolvedValue ; } ;

the resolverReplacer and directiveResolver functions are used in a higher order function chain that returns a resolvedValue resolverReplacer -> directiveResolver -> resolvedValue

and functions are used in a higher order function chain that returns a this sounds complicated but as seen above the implementation on your end is as intuitive as writing any other resolver

resolverReplacer is used internally to replace the original resolver with your directiveResolver used as a bridge between apollo-directive and your directiveResolver brings the originalResolver and directiveContext parameters into the scope of your directiveResolver

is used internally to replace the original resolver with your the directiveResolver function receives the original field resolver's arguments (root, args, context, info) these can be abbreviated into an array as (...resolverArgs) to make using the apply() syntax easier (see below)

function receives the original field resolver's arguments the directiveResolver must be a function declaration not an arrow function

executing the originalResolver must be done using the apply syntax

result = originalResolver . apply ( this , resolverArgs ) ; result = await originalResolver . apply ( this , resolverArgs ) ; result = originalResolver . apply ( this , [ root , args , context , info ] ) ;

boilerplates to get going quickly

module . exports = { name , resolverReplacer : ( originalResolver , directiveContext ) => async function directiveResolver ( ... resolverArgs ) { } , } ; module . exports = createDirective ( { name , resolverReplacer : ( originalResolver , directiveContext ) => async function directiveResolver ( ... resolverArgs ) { } , } ) ;

using createDirective

use for creating a single directive resolver

add the resolver to the Apollo Server serverConfig.schemaDirectives object the name must match the <directive name> from the corresponding directive type definition in the schema

object

const { ApolloServer } = require ( " apollo-server-X " ) ; const { createDirective } = require ( " apollo-directives " ) ; const adminDirectiveConfig = { name : " admin " , resolverReplacer : requireAdminReplacer , hooks : { } } ; const adminDirective = createDirective ( adminDirectiveConfig ) ; const server = new ApolloServer ( { ... schemaDirectives : { admin : adminDirective , } , } ) ;

using createSchemaDirectives

accepts an array of directive config objects in config.directiveConfigs

assign the result to serverConfig.schemaDirectives in the Apollo Server constructor

in the Apollo Server constructor creates each directive and provides them as the schemaDirectives object in { name: directiveResolver, ... } form

const { ApolloServer } = require ( " apollo-server-X " ) ; const { createSchemaDirectives } = require ( " apollo-directives " ) ; const adminDirectiveConfig = { name : " admin " , resolverReplacer : requireAdminReplacer , hooks : { } } ; const server = new ApolloServer ( { ... schemaDirectives : createSchemaDirectives ( { directiveConfigs : [ adminDirectiveConfig ] , } ) , } ) ;

directive config

directiveConfig is validated and will throw an Error for missing or invalid properties

is validated and will throw an Error for missing or invalid properties shape

const directiveConfig = { name : string , resolverReplacer : function , hooks : { function , ... }, };

resolverReplacer

a higher order function used to bridge information between createDirective and the directive logic in the directiveResolver

and the directive logic in the used in createDirective config parameter

parameter may not be async

must return a function that implements the directiveResolver signature (the same as the standard Apollo resolver)

(the same as the standard Apollo resolver) signature

resolverReplacer ( originalResolver , directiveContext ) - > directiveResolver ( root , args , context , info ) - > resolved value

directiveContext

the directiveContext object provides access to information about the directive itself

object provides access to information about the directive itself you can use this information in the directiveResolver as needed

as needed see the [objectType] and [field] shapes

const { name , objectType , field , args : directiveArgs , } = directiveContext ;

directiveResolver

a higher order function used to transform the result or behavior of the originalResolver

must be a function declaration not an arrow function

may be async if you need to work with promises

if you need to work with promises must return a valid resolved value (valid according to the schema) for example if your schema dictates that the resolved value may not be null then you must support this rule by not returning undefined or null from the directiveResolver

a valid resolved value (valid according to the schema) signature:

directiveResolver ( root , args , context , info ) - > resolved value directiveResolver ( ... resolverArgs ) - > resolved value

name

the name of the directive (same as the name in the directive type definition in the schema)

used for improving performance when directives are registered on server startup added as _<name>DirectiveApplied property on the objectType you can read more from this Apollo Docs: Schema Directives section

when using the createSchemaDirectives utility used as the directive identifier in the schemaDirectives object ex: directive type def @admin then name = "admin"

utility

hooks

provide access to each step of the process as the directive resolver is applied during server startup

purely observational, nothing returned from these functions is used

can be used for logging or debugging

onVisitObject

called once for each Object Type definition that the directive has been applied to

called before the directive is applied to the Object Type

receives the directiveContext object note that directiveContext.field will be undefined for this hook

signature

onVisitObject ( directiveContext ) - > void

onVisitFieldDefinition

called once for each Object Type field definition that the directive has been applied to

called before the directive is applied to the field

receives the directiveContext object

signature

onvisitFieldDefinition ( directiveContext ) - > void

onApplyDirective

called immediately before the directive is applied directive applied to an Object Type ( on OBJECT ): called once for each field in the Object directive applied to a field ( on FIELD_DEFINITION ): called once for the field called after onVisitObject or onVisitFieldDefinition is executed

receives the directiveContext object

technical note: using the directive name, directiveConfig.name , the internal method applying the directive will exit early for the following case: directives that are applied to both an object and its individual field(s) will exit early to prevent duplicate application of the directive onApplyDirective will not be called a second time for this case due to exiting early this is a performance measure that you can read more about from this Apollo Docs: Schema Directives section

, the internal method applying the directive will exit early for the following case: signature

onApplyDirective ( directiveContext ) - > void ;

schema directive type definitions and usage

learn more about writing directive type defs or see the examples below official GraphQL Schema Directives spec apollo directives examples



creating schema directive type defs

# only able to tag Object Type Fields directive @<directive name> on FIELD_DEFINITION # only able to tag Object Types directive @<directive name> on OBJECT # able to tag Object Types and Object Type Fields directive @<directive name> on FIELD_DEFINITION | OBJECT # alternate accepted syntax directive @<directive name> on | FIELD_DEFINITION | OBJECT # adding a description to a directive """ directive description (can be multi-line) """ directive @<directive name> on FIELD_DEFINITION | OBJECT

using directives in your schema type defs

applying directives is as simple as "tagging" them onto an Object Type or one of its fields

# tagging an Object Type Field type SomeType { # the directive resolver is executed when access to the tagged field(s) is made aTaggedField: String @<directive name> } type Query { queryName: ReturnType @<directive name> } # tagging an Object Type type SomeType @<directive name> { # the directive is applied to every field in this Type # the directive resolver is executed when any access to this Type's fields (through queries / mutations / nesting) are made } # multiple directives can be tagged, space-separated type SomeType @firstDirective @secondDirective { # applying a directive to a list type must be to the right of the closing bracket aTaggedField: [TypeName] @<directive name> }

example of defining and using a schema directive

a basic example

""" returns all String scalar values in upper case """ directive @upperCase on FIELD_DEFINITION | OBJECT # the Object Type itself is tagged # all of the fields in this object will have the @upperCase directive applied type User @upperCase { id: ID! username: String! friends: [User!]! } type Dog { id: ID! # only Dog.streetAddress will have the directive applied streetAddress: String! @upperCase }

a more complex example of an authentication / authorization directive

this directive can receive a requires argument with an array of Role enum elements directives argument(s) are available in the directiveResolver through directiveContext.args

argument with an array of enum elements the requires argument has a default value set as [ADMIN] if no argument is provided (just @auth ) then this default argument will be provided as ["ADMIN"]

argument has a default value set as

# example of a directive to enforce authentication / authorization # you can provide a default value just like arguments to any other definition directive @auth(requires: [Role] = [ADMIN]) on FIELD_DEFINITION | OBJECT # assumes a ROLE enum has been defined enum Role { USER # any authenticated user SELF # the authenticated user only ADMIN # admins only } # apply the directive to an entire Object Type # because no argument is provided the default ([ADMIN]) is used type PaymentInfo @auth { # all of the fields in this Object Type will have the directive applied requiring ADMIN permissions } type User { # authorization for the authenticated user themself or an admin email: EmailAddress! @auth(requires: [SELF, ADMIN]) }

what targets should the directive be applied to?

note that queries and resolver type definitions are considered fields of the Query and Mutation Object Types

and Object Types directive needs to transform the result of a resolver tag the directive on a field any access to the field will execute the directive examples upper case a value translate a value format a date string

directive needs to do some auxiliary behavior in a resolver tag the directive on a field, object, or both any queries that request values (directly or through nesting) from the tagged object and / or field will execute the directive examples enforcing authentication / authorization logging



examples

annotated example from Apollo Docs: Schema Directives - Uppercase String

corresponds to the following directive type def

directive @upperCase on FIELD_DEFINITION | OBJECT

const upperCaseReplacer = ( originalResolver , directiveContext ) => async function upperCaseResolver ( ... resolverArgs ) { const result = await originalResolver . apply ( this , resolverArgs ) ; if ( typeof result === " string " ) { return result . toUpperCase ( ) ; } return result ; } ; module . exports = upperCaseReplacer ;

the objectType and field shapes

these two objects can be found in the directiveContext object

object provide access to information about the Object Type or Object Type Field the directive is being applied to

use the following shapes as a guide or use the hooks to log these in more detail as needed to expand the objects (incluidng AST nodes) in your log use JSON.stringify(objectType | field, null, 2)



objectType

Object Type information

const { name , type , description , isDeprecated , deprecationReason , astNode , _fields , } = objectType ;

field

Object Type Field information

const { name , type , args : [ { name , type , description , defaultValue , astNode , } , ... ] , description , isDeprecated , deprecationReason , astNode , } = field ;

astNode

it is unlikely you will need to access this property

this is a parsed object of the AST for the Object Type or Object Type Field