mirror of
https://github.com/directus/directus.git
synced 2026-01-31 00:48:16 -05:00
239 lines
9.3 KiB
Markdown
239 lines
9.3 KiB
Markdown
# Custom API Hooks <small></small>
|
||
|
||
> Custom API Hooks allow running custom logic when a specified event occurs within your project. They can be registered
|
||
> as either "blocking" or immediate.
|
||
|
||
## 1. Create a Hook File
|
||
|
||
Custom hooks are dynamically loaded from within your extensions folder. By default this directory is located at
|
||
`/extensions`, but it can be configured within your project's env file to be located anywhere.
|
||
|
||
### Default Standalone Hook Location
|
||
|
||
```
|
||
/extensions/hooks/<hook-id>/index.js
|
||
```
|
||
|
||
## 2. Define the Event
|
||
|
||
Next, you will want to define your event. You can trigger your custom hook with any of the platform's many API events.
|
||
System events are referenced with the format:
|
||
|
||
```
|
||
<scope>.<action>(.<before>)
|
||
// eg: items.create
|
||
// eg: files.create
|
||
// eg: collections.*
|
||
// eg: users.update.before
|
||
```
|
||
|
||
### Scope
|
||
|
||
The scope determines the API endpoint that is triggered. The `*` wildcard can also be used to include all scopes.
|
||
|
||
::: tip System Scope
|
||
|
||
Currently all system tables are available as event scopes except for `directus_migrations` and `directus_sessions`,
|
||
which don't have relevant endpoints or services.
|
||
|
||
:::
|
||
|
||
### Action
|
||
|
||
Defines the triggering operation within the specified context (see chart below). The `*` wildcard can also be used to
|
||
include all actions available to the scope.
|
||
|
||
### Before
|
||
|
||
Many scopes (see chart below) support an optional `.before` suffix for running a _blocking_ hook prior to the event
|
||
being fired. This allows you to check and/or modify the event's payload before it is processed.
|
||
|
||
- `items.create.before` (Blocking)
|
||
- `items.create` (Non Blocking, also called 'after' implicitly)
|
||
|
||
This also allows you to cancel an event based on the logic within the hook. Below is an example of how you can cancel a
|
||
create event by throwing a standard Directus exception.
|
||
|
||
```js
|
||
module.exports = function registerHook({ exceptions }) {
|
||
const { InvalidPayloadException } = exceptions;
|
||
|
||
return {
|
||
'items.create.before': async function (input) {
|
||
if (LOGIC_TO_CANCEL_EVENT) {
|
||
throw new InvalidPayloadException(WHAT_IS_WRONG);
|
||
}
|
||
|
||
return input;
|
||
},
|
||
};
|
||
};
|
||
```
|
||
|
||
### Event Format Options
|
||
|
||
| Scope | Actions | Before |
|
||
| ------------------------------- | ----------------------------------------------------------- | ---------------- |
|
||
| `cron()` | [See below for configuration](#interval-cron) | No |
|
||
| `server` | `start` and `stop` | Optional |
|
||
| `init` | | Optional |
|
||
| `routes.init` | `before` and `after` | No |
|
||
| `routes.custom.init` | `before` and `after` | No |
|
||
| `middlewares.init` | `before` and `after` | No |
|
||
| `request` | `not_found` | No |
|
||
| `response` | | No<sup>[1]</sup> |
|
||
| `error` | | No |
|
||
| `auth` | `login`, `logout`<sup>[1]</sup> and `refresh`<sup>[1]</sup> | Optional |
|
||
| `oauth.:provider`<sup>[2]</sup> | `login` and `redirect` | Optional |
|
||
| `items` | `read`<sup>[3]</sup>, `create`, `update` and `delete` | Optional |
|
||
| `activity` | `create`, `update` and `delete` | Optional |
|
||
| `collections` | `create`, `update` and `delete` | Optional |
|
||
| `fields` | `create`, `update` and `delete` | Optional |
|
||
| `files` | `upload`<sup>[3]</sup>, `create`, `update` and `delete` | Optional |
|
||
| `folders` | `create`, `update` and `delete` | Optional |
|
||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||
| `presets` | `create`, `update` and `delete` | Optional |
|
||
| `relations` | `create`, `update` and `delete` | Optional |
|
||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||
| `roles` | `create`, `update` and `delete` | Optional |
|
||
| `settings` | `create`, `update` and `delete` | Optional |
|
||
| `users` | `create`, `update` and `delete` | Optional |
|
||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||
|
||
<sup>1</sup> Feature Coming Soon\
|
||
<sup>2</sup> oAuth provider name can replaced with wildcard for any oauth providers `oauth.*.login`\
|
||
<sup>3</sup> Doesn't support `.before` modifier
|
||
|
||
#### Interval (cron)
|
||
|
||
Hooks support running on an interval through [`node-cron`](https://www.npmjs.com/package/node-cron). To set this up,
|
||
provide a cron statement in the event scope as follows: `cron(<statement>)`, for example `cron(15 14 1 * *)` (at 14:15
|
||
on day-of-month 1) or `cron(5 4 * * sun)` (at 04:05 on Sunday). See example below:
|
||
|
||
```js
|
||
const axios = require('axios');
|
||
|
||
module.exports = function registerHook() {
|
||
return {
|
||
'cron(*/15 * * * *)': async function () {
|
||
await axios.post('http://example.com/webhook', { message: 'Another 15 minutes passed...' });
|
||
},
|
||
};
|
||
};
|
||
```
|
||
|
||
## 3. Register your Hook
|
||
|
||
Each custom hook is registered to its event scope using a function with the following format:
|
||
|
||
```js
|
||
const axios = require('axios');
|
||
|
||
module.exports = function registerHook() {
|
||
return {
|
||
'items.create': function () {
|
||
axios.post('http://example.com/webhook');
|
||
},
|
||
};
|
||
};
|
||
```
|
||
|
||
## 4. Develop your Custom Hook
|
||
|
||
> Hooks can impact performance when not carefully implemented. This is especially true for `before` hooks (as these are
|
||
> blocking) and hooks on `read` actions, as a single request can result in a large ammount of database reads.
|
||
|
||
### Register Function
|
||
|
||
The register function (eg: `module.exports = function registerHook()`) must return an object where the key is the event,
|
||
and the value is the handler function itself.
|
||
|
||
The `registerHook` function receives a context parameter with the following properties:
|
||
|
||
- `services` — All API internal services
|
||
- `exceptions` — API exception objects that can be used for throwing "proper" errors
|
||
- `database` — Knex instance that is connected to the current database
|
||
- `getSchema` — Async function that reads the full available schema for use in services
|
||
- `env` — Parsed environment variables
|
||
|
||
### Event Handler Function
|
||
|
||
The event handler function (eg: `'items.create': function()`) receives a context parameter with the following
|
||
properties:
|
||
|
||
- `event` — Full event string
|
||
- `accountability` — Information about the current user
|
||
- `collection` — Collection that is being modified
|
||
- `item` — Primary key(s) of the item(s) being modified
|
||
- `action` — Action that is performed
|
||
- `payload` — Payload of the request
|
||
- `schema` - The current API schema in use
|
||
- `database` - Current database transaction
|
||
|
||
#### Auth
|
||
|
||
The `auth` and `oauth` hooks have the following context properties:
|
||
|
||
- `event` — Full event string
|
||
- `accountability` — Information about the current user
|
||
- `action` — Action that is performed
|
||
- `payload` — Payload of the request
|
||
- `schema` - The current API schema in use
|
||
- `status` - One of `pending`, `success`, `fail`
|
||
- `user` <sup>†</sup> - ID of the user that tried logging in/has logged in
|
||
|
||
<sup>†</sup> Not available in `oauth`
|
||
|
||
## 5. Restart the API
|
||
|
||
To deploy your hook, simply restart the API by running:
|
||
|
||
```bash
|
||
npx directus start
|
||
```
|
||
|
||
## Full Example
|
||
|
||
`extensions/hooks/sync-with-external/index.js`:
|
||
|
||
```js
|
||
const axios = require('axios');
|
||
|
||
module.exports = function registerHook({ services, exceptions }) {
|
||
const { MailService } = services;
|
||
const { ServiceUnavailableException, ForbiddenException } = exceptions;
|
||
|
||
return {
|
||
// Force everything to be admin-only at all times
|
||
'items.*': async function ({ item, accountability }) {
|
||
if (accountability.admin !== true) throw new ForbiddenException();
|
||
},
|
||
// Sync with external recipes service, cancel creation on failure
|
||
'items.create.before': async function (input, { collection, schema }) {
|
||
if (collection !== 'recipes') return input;
|
||
|
||
const mailService = new MailService({ schema });
|
||
|
||
try {
|
||
await axios.post('https://example.com/recipes', input);
|
||
await mailService.send({
|
||
to: 'person@example.com',
|
||
template: {
|
||
name: 'item-created',
|
||
data: {
|
||
collection: collection,
|
||
},
|
||
},
|
||
});
|
||
} catch (error) {
|
||
throw new ServiceUnavailableException(error);
|
||
}
|
||
|
||
input[0].syncedWithExample = true;
|
||
|
||
return input;
|
||
},
|
||
};
|
||
};
|
||
```
|