Hooks
Hooks give developers a way to add custom logic to the framework of lists, fields and operations Keystone provides.
This document provides an overview of the concepts, patterns and function of the Keystone hook system. The Hooks API docs describe the specific arguments and usage information.
Conceptual organisation
There are several categorisations that can be applied to hooks and are useful for understanding what is run and when.
Note: the concepts listed here have some exceptions. See the Gotchas section.
Stage
Keystone defines several stages within the hook execution order. These stages are intended to be used for different purposes; they help organise your hook functionality.
- Input resolution - modify the values supplied
- Data validation - check the values are valid
- Before operation - perform side effects before the primary operation
- After operation - perform side effects after the primary operation
Operation
Hooks are available for these core operations:
create
update
delete
authenticate
unauthenticate
These operations are used for both "single" and "many" modes.
E.g. the deleteUser
(singular) and deleteUsers
(plural) mutations are both considered to be delete
operations.
Hooks for these operations have different signatures due to the nature of the operations being performed. See the Hook API docs for specifics.
Note: Keystone does not currently implement
read
hooks.
Hook type
A hook type is defined by where it is attached. Keystone recognises three types of hook:
- Field Type hooks - Field Type hooks are associated with a particular field type and are applied to all fields of that type across all lists.
- Field hooks -
Field hooks can be defined by the app developer by specifying the
hooks
attribute of a field configuration when callingcreateList()
. - List hooks -
List hooks can be defined by the app developer by specifying the
hooks
attribute of a list configuration when callingcreateList()
.
Hook set
For most stage and operation combinations, different functions (hooks) can be supplied for each hook type. This group of distinct but related hooks are referred to as a hook set.
E.g. a beforeDelete
function could be supplied for a list, several specific fields on the list and a field type used by the list.
All hooks in a hook set share the same functional signature but are invoked at different times.
See the Hooks API docs and Intra-Hook Execution Order section for more information.
Putting it together
In total there are 13 hook sets available. This table shows the hook set relevant to each combination of stage and operation:
Stage | create | update | delete | authenticate | unauthenticate |
---|---|---|---|---|---|
Input resolution | resolveInput | resolveInput | n/a | resolveAuthInput | |
Data validation | validateInput | validateInput | validateDelete | validateAuthInput | |
Before operation | beforeChange | beforeChange | beforeDelete | beforeAuth | beforeUnauth |
After operation | afterChange | afterChange | afterDelete | afterAuth | afterUnauth |
The create
, update
and delete
hook sets can be attached as list, field or field type hooks.
The authenticate
and unauthenticate
hook sets are unique in that they can only be defined when creating an authentication strategy.
Due to their similarity, the create
and update
operations share a single set of hooks.
To implement different logic for these operations make it conditional on either the operation
or existingItem
arguments;
for create operations existingItem
will be undefined
.
See the Hooks API docs for argument details and usage.
Execution order
The hooks are invoked in a specific order during an operation. For full details of the mutation lifecycle, and where hooks fit within this, see the Mutation Lifecycle Guide.
Create/Update
- Access control checks
- Field defaults applied
resolveInput
called on all fields, even if they are not defined in the supplied datavalidateInput
called on all fields which have a resolved value (after allresolveInput
calls have returned)beforeChange
called on all fields which have a resolved value (after allvalidateInput
calls have returned)- Database operation
afterChange
called on all fields, even if their value was not changed
Delete
- Access control checks
validateDelete
called on all fieldsbeforeDelete
called on all fields (after allvalidateDelete
calls have returned)- Database operation (after all
beforeDelete
calls have returned) afterDelete
called on all fields (after the DB operation has completed)
Authentication
- Access control checks
resolveAuthInput
called for the listvalidateAuthInput
called for the listbeforeAuth
called for the list- Auth strategy
validate()
is called afterAuth
called for the list
Unauthentication
- Access control checks
beforeAuth
called for the listcontext.endAuthedSession()
is calledafterAuth
called for the list
Intra-hook execution order
Within each hook set, the different hook types are invoked in a specific order.
- All relevant and defined field type hooks are invoked in parallel
- All relevant and defined field hooks are invoked in parallel
- If defined the list hook is invoked
Gotchas
The hook system is powerful but its breadth and flexibility introduce some complexity. A few of the main stumbling blocks are:
- The
create
andupdate
operations share a single set of hooks. To implement different logic for these operations make it conditional on either theoperation
orexistingItem
arguments; for create operationsexistingItem
will beundefined
. - As per the table above, the
delete
operations have no hook set for the input resolution stage. This operation doesn't accept any input (other than the target IDs). - Keystone does not currently implement
read
hooks. - Field type hooks and field hooks are run in parallel.
- The
authenticate
andunauthenticate
hook sets are unique in that they can only be defined when creating an authentication strategy.
These nuances aren't bugs per se -- they generally exist for good reason -- but they can make understanding the hook system difficult.