Extend GraphQL
with directives
@neoziro
smooth-code.com
I am Greg Bergé
JavaScript, React & GraphQL trainer
@neoziro
!
github.com/neoziro
"
smooth-code.com#
smooth code
What is a directive?
@include(if: Boolean)
Built-in directives
@include(if: Boolean!)
@skip(if: Boolean!)
@deprecated(reason: String)
@include / @skip
•Query directive
•Can be used on fields and fragments
•Control the scope of query using a
parameter
query Person($includeFilms: Boolean = false) { 

person(personID: 1) { 

name 

height 

mass 

filmConnection(first: 10) @include(if: $includeFilms) { 

edges { 

node { 

title 

} 

} 

} 

} 

}
query Person($skipFilms: Boolean = false) { 

person(personID: 1) { 

name 

height 

mass 

filmConnection(first: 10) @skip(if: $skipFilms) { 

edges { 

node { 

title 

} 

} 

} 

} 

}
@deprecated
•Schema directive
•Can be used on field definitions and
enums
•Marks an element of a GraphQL
schema as no longer supported
type Book { 

name: String 

longName: String @deprecated(reason: "use name instead") 

}
I love it 🤩
I want to create directives!
But why?
•Resolved server-side
→ less computation on client
•Most of GraphQL clients cache queries
→ avoid computation
•Declarative
→ understand code by reading query
query { 

user { 

name @upperCase 

} 

}
Text Formatting
Date Formatting
query { 

user { 

birthDate @dateFormat(format: "DD/MM/YYYY") 

} 

}
Restrictions
type User { 

privateNote: String @requireAuth 

}
Let's do it!
😒
🤔
🤔🤔
🤔🤔🤔
Sorry @leebyron 🤭
Behind a directive
/** 

* Used to conditionally include fields or fragments. 

*/ 

export const GraphQLIncludeDirective = new GraphQLDirective({ 

name: 'include', 

description: 

'Directs the executor to include this field or fragment only when ' + 

'the `if` argument is true.', 

locations: [ 

DirectiveLocation.FIELD, 

DirectiveLocation.FRAGMENT_SPREAD, 

DirectiveLocation.INLINE_FRAGMENT, 

], 

args: { 

if: { 

type: new GraphQLNonNull(GraphQLBoolean), 

description: 'Included when true.', 

}, 

}, 

})
/** 

* The set of allowed directive location values. 

*/ 

export const DirectiveLocation = { 

// Request Definitions 

QUERY: 'QUERY', 

MUTATION: 'MUTATION', 

SUBSCRIPTION: 'SUBSCRIPTION', 

FIELD: 'FIELD', 

FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION', 

FRAGMENT_SPREAD: 'FRAGMENT_SPREAD', 

INLINE_FRAGMENT: 'INLINE_FRAGMENT', 

// Type System Definitions 

SCHEMA: 'SCHEMA', 

SCALAR: 'SCALAR', 

OBJECT: 'OBJECT', 

FIELD_DEFINITION: 'FIELD_DEFINITION', 

ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION', 

INTERFACE: 'INTERFACE', 

UNION: 'UNION', 

ENUM: 'ENUM', 

ENUM_VALUE: 'ENUM_VALUE', 

INPUT_OBJECT: 'INPUT_OBJECT', 

INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION', 

}

🤩
We can put directive
everywhere!
How to declare a
directive?
# GraphQL Schema 

directive @dateFormat(format: String!) on FIELD | FIELD_DEFINITION
How to use it?
I can use my own
directive 😎
But it doesn't work 😓
I need a directive
resolver!
Wait...
How to define a
directive resolver?
Not supported 😨
Let's take a look to a
resolver signature
Logging info...
{ fieldName: 'birthDate',
fieldNodes:
[ { kind: 'Field',
alias: null,
name: [Object],
arguments: [],
directives: [Array],
selectionSet: null,
loc: [Object] } ],
returnType: DateTime,
parentType: Human,
path:
{ prev: { prev: undefined, key: 'character' },
key: 'birthDate' },
schema:
GraphQLSchema {
_queryType: Query,
_mutationType: Mutation,
_subscriptionType: Subscription,
_directives:
[ [GraphQLDirective],
[GraphQLDirective],
[GraphQLDirective],
[GraphQLDirective] ],
astNode:
{ kind: 'SchemaDefinition',
SPOTED
[ { kind: 'Directive',
name: { kind: 'Name', value: 'dateFormat', loc: [Object] },
arguments: [ [Object] ],
loc: { start: 37, end: 62 } } ]
Let's write a directive
resolver!
import format from 'date-fns/format' 



const resolvers = { 

Character: { 

birthDate(obj, args, context, info) { 

const directive = info.fieldNodes[0].directives.find( 

directive => directive.name.value === 'dateFormat', 

) 

if (directive) { 

const formatArg = directive.arguments.find( 

arg => arg.name.value === 'format', 

) 

return formatDate(obj.birthDate, formatArg.value.value) 

} 

return obj.birthDate 

}, 

} 

}
I can use my own
directive and it works 😎
But it is not generic 😓
Introducing
graphql-directive
import { buildSchema } from 'graphql' 

import { addDirectiveResolveFunctionsToSchema } from 'graphql-directive' 

import format from 'date-fns/format' 



// Schema 

const schema = buildSchema(` 

directive @dateFormat(format: String) on FIELD_DEFINITION | FIELD 

`) 



// Resolver 

addDirectiveResolveFunctionsToSchema(schema, { 

async dateFormat(resolve, source, args) { 

const value = await resolve() 

return format(new Date(value), args.format) 

}, 

})
graphql-directive
•Easy to use
•Supports FIELD and FIELD_DEFINITION
locations
•Gives new power 🚀
Wait, I use it in my project
and it is random 🤔
The problem
query { 

myField @upperCase 

}
Query
Apollo Cache
{ 

"myField": "MYRESULT" 

}
With directive
query { 

myField
}
{ 

"myField": "myResult" 

}
Without directive
Same result 😱
The fix
query { 

myField @upperCase 

}
Query
Apollo Cache
{ 

"myField@upperCase": "MYRESULT" 

}
With directive
query { 

myField
}
{ 

"myField": "myResult" 

}
Without directive
Include directive
in cache key ✨
THANKS
I will tweet slides after the talk @neoziro

Extend GraphQL with directives

  • 1.
  • 2.
    I am GregBergé JavaScript, React & GraphQL trainer @neoziro ! github.com/neoziro " smooth-code.com#
  • 3.
  • 4.
    What is adirective?
  • 5.
  • 6.
  • 7.
  • 8.
    @include / @skip •Querydirective •Can be used on fields and fragments •Control the scope of query using a parameter
  • 9.
    query Person($includeFilms: Boolean= false) { 
 person(personID: 1) { 
 name 
 height 
 mass 
 filmConnection(first: 10) @include(if: $includeFilms) { 
 edges { 
 node { 
 title 
 } 
 } 
 } 
 } 
 }
  • 10.
    query Person($skipFilms: Boolean= false) { 
 person(personID: 1) { 
 name 
 height 
 mass 
 filmConnection(first: 10) @skip(if: $skipFilms) { 
 edges { 
 node { 
 title 
 } 
 } 
 } 
 } 
 }
  • 13.
    @deprecated •Schema directive •Can beused on field definitions and enums •Marks an element of a GraphQL schema as no longer supported
  • 14.
    type Book {
 name: String 
 longName: String @deprecated(reason: "use name instead") 
 }
  • 17.
    I love it🤩 I want to create directives!
  • 18.
  • 19.
    •Resolved server-side → lesscomputation on client •Most of GraphQL clients cache queries → avoid computation •Declarative → understand code by reading query
  • 20.
    query { 
 user{ 
 name @upperCase 
 } 
 } Text Formatting
  • 21.
    Date Formatting query {
 user { 
 birthDate @dateFormat(format: "DD/MM/YYYY") 
 } 
 }
  • 22.
    Restrictions type User {
 privateNote: String @requireAuth 
 }
  • 23.
  • 25.
  • 27.
  • 29.
  • 31.
  • 32.
  • 33.
  • 34.
    /** 
 * Usedto conditionally include fields or fragments. 
 */ 
 export const GraphQLIncludeDirective = new GraphQLDirective({ 
 name: 'include', 
 description: 
 'Directs the executor to include this field or fragment only when ' + 
 'the `if` argument is true.', 
 locations: [ 
 DirectiveLocation.FIELD, 
 DirectiveLocation.FRAGMENT_SPREAD, 
 DirectiveLocation.INLINE_FRAGMENT, 
 ], 
 args: { 
 if: { 
 type: new GraphQLNonNull(GraphQLBoolean), 
 description: 'Included when true.', 
 }, 
 }, 
 })
  • 35.
    /** 
 * Theset of allowed directive location values. 
 */ 
 export const DirectiveLocation = { 
 // Request Definitions 
 QUERY: 'QUERY', 
 MUTATION: 'MUTATION', 
 SUBSCRIPTION: 'SUBSCRIPTION', 
 FIELD: 'FIELD', 
 FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION', 
 FRAGMENT_SPREAD: 'FRAGMENT_SPREAD', 
 INLINE_FRAGMENT: 'INLINE_FRAGMENT', 
 // Type System Definitions 
 SCHEMA: 'SCHEMA', 
 SCALAR: 'SCALAR', 
 OBJECT: 'OBJECT', 
 FIELD_DEFINITION: 'FIELD_DEFINITION', 
 ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION', 
 INTERFACE: 'INTERFACE', 
 UNION: 'UNION', 
 ENUM: 'ENUM', 
 ENUM_VALUE: 'ENUM_VALUE', 
 INPUT_OBJECT: 'INPUT_OBJECT', 
 INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION', 
 }

  • 36.
    🤩 We can putdirective everywhere!
  • 37.
    How to declarea directive?
  • 38.
    # GraphQL Schema
 directive @dateFormat(format: String!) on FIELD | FIELD_DEFINITION
  • 39.
  • 41.
    I can usemy own directive 😎
  • 43.
    But it doesn'twork 😓
  • 44.
    I need adirective resolver!
  • 45.
  • 46.
    How to definea directive resolver?
  • 47.
  • 48.
    Let's take alook to a resolver signature
  • 50.
  • 51.
    { fieldName: 'birthDate', fieldNodes: [{ kind: 'Field', alias: null, name: [Object], arguments: [], directives: [Array], selectionSet: null, loc: [Object] } ], returnType: DateTime, parentType: Human, path: { prev: { prev: undefined, key: 'character' }, key: 'birthDate' }, schema: GraphQLSchema { _queryType: Query, _mutationType: Mutation, _subscriptionType: Subscription, _directives: [ [GraphQLDirective], [GraphQLDirective], [GraphQLDirective], [GraphQLDirective] ], astNode: { kind: 'SchemaDefinition', SPOTED
  • 52.
    [ { kind:'Directive', name: { kind: 'Name', value: 'dateFormat', loc: [Object] }, arguments: [ [Object] ], loc: { start: 37, end: 62 } } ]
  • 53.
    Let's write adirective resolver!
  • 54.
    import format from'date-fns/format' 
 
 const resolvers = { 
 Character: { 
 birthDate(obj, args, context, info) { 
 const directive = info.fieldNodes[0].directives.find( 
 directive => directive.name.value === 'dateFormat', 
 ) 
 if (directive) { 
 const formatArg = directive.arguments.find( 
 arg => arg.name.value === 'format', 
 ) 
 return formatDate(obj.birthDate, formatArg.value.value) 
 } 
 return obj.birthDate 
 }, 
 } 
 }
  • 56.
    I can usemy own directive and it works 😎
  • 57.
    But it isnot generic 😓
  • 59.
  • 61.
    import { buildSchema} from 'graphql' 
 import { addDirectiveResolveFunctionsToSchema } from 'graphql-directive' 
 import format from 'date-fns/format' 
 
 // Schema 
 const schema = buildSchema(` 
 directive @dateFormat(format: String) on FIELD_DEFINITION | FIELD 
 `) 
 
 // Resolver 
 addDirectiveResolveFunctionsToSchema(schema, { 
 async dateFormat(resolve, source, args) { 
 const value = await resolve() 
 return format(new Date(value), args.format) 
 }, 
 })
  • 62.
    graphql-directive •Easy to use •SupportsFIELD and FIELD_DEFINITION locations •Gives new power 🚀
  • 63.
    Wait, I useit in my project and it is random 🤔
  • 64.
  • 65.
    query { 
 myField@upperCase 
 } Query Apollo Cache { 
 "myField": "MYRESULT" 
 } With directive query { 
 myField } { 
 "myField": "myResult" 
 } Without directive Same result 😱
  • 66.
  • 67.
    query { 
 myField@upperCase 
 } Query Apollo Cache { 
 "myField@upperCase": "MYRESULT" 
 } With directive query { 
 myField } { 
 "myField": "myResult" 
 } Without directive Include directive in cache key ✨
  • 69.
    THANKS I will tweetslides after the talk @neoziro