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:
createupdatedeleteauthenticateunauthenticate
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
readhooks.
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
hooksattribute of a field configuration when callingcreateList(). - List hooks -
List hooks can be defined by the app developer by specifying the
hooksattribute 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
resolveInputcalled on all fields, even if they are not defined in the supplied datavalidateInputcalled on all fields which have a resolved value (after allresolveInputcalls have returned)beforeChangecalled on all fields which have a resolved value (after allvalidateInputcalls have returned)- Database operation
afterChangecalled on all fields, even if their value was not changed
Delete
- Access control checks
validateDeletecalled on all fieldsbeforeDeletecalled on all fields (after allvalidateDeletecalls have returned)- Database operation (after all
beforeDeletecalls have returned) afterDeletecalled on all fields (after the DB operation has completed)
Authentication
- Access control checks
resolveAuthInputcalled for the listvalidateAuthInputcalled for the listbeforeAuthcalled for the list- Auth strategy
validate()is called afterAuthcalled for the list
Unauthentication
- Access control checks
beforeAuthcalled for the listcontext.endAuthedSession()is calledafterAuthcalled 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
createandupdateoperations share a single set of hooks. To implement different logic for these operations make it conditional on either theoperationorexistingItemarguments; for create operationsexistingItemwill beundefined. - As per the table above, the
deleteoperations 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
readhooks. - Field type hooks and field hooks are run in parallel.
- The
authenticateandunauthenticatehook 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.