# Hooks

> Every hook of a GoLocomotive app, its event fields and its default action, plus routes, middlewares and the request event.

A hook is `app.<hook>(handler, ...collections)`. No collection means every collection. Handlers run in the order they were bound, and each one must `await e.next()` for the default action to run. A handler that skips `next()` stops the chain and answers with `e.json(...)` itself. How to choose between them: [Server: hooks, routes, crons](https://golocomotive.fr/docs/developers/server.md#the-hook-chain).

## Record request hooks

They fire on an HTTP request only, never on a write made by a hook or a script. They run BEFORE the rule check of the write.

| hook | event, beside `e.next()` | default action |
|---|---|---|
| `onRecordsListRequest` | request, `collection`, `result` | answer `result` |
| `onRecordViewRequest` | request, `collection`, `record` | answer `record` |
| `onRecordCreateRequest` | request, `collection`, `data` (mutable), `record` (set by `next`) | the rule, the write, the answer |
| `onRecordUpdateRequest` | request, `collection`, `original` (stored, rule not checked yet), `data`, `record` | the rule, the write, the answer |
| `onRecordDeleteRequest` | request, `collection`, `record` (stored) | the rule, the delete, 204 |

## After success hooks

They fire after the commit, for EVERY write: an HTTP request, a hook, a script, a cascade. A throw is logged and the write stays.

| hook | event |
|---|---|
| `onRecordAfterCreateSuccess` | `collection`, `record` |
| `onRecordAfterUpdateSuccess` | `collection`, `record`, `original` |
| `onRecordAfterDeleteSuccess` | `collection`, `record` |

## Auth hooks

| hook | event | default action |
|---|---|---|
| `onRecordAuthRequest` | request, `collection`, `record`, `token`, `authMethod` (`password`, `otp`, or `''` on a refresh) | the second factor, the alert, then answer `{ token, record }` |
| `onRecordAuthWithPasswordRequest` | request, `collection`, `identity`, `password`, `record` (null when unknown) | check the password, then auth |
| `onRecordAuthWithOTPRequest` | request, `collection`, `record`, `otpId` | use the code, then auth |
| `onRecordRequestOTPRequest` | request, `collection`, `email`, `record` | make and mail the code |
| `onRecordAuthRefreshRequest` | request, `collection`, `record` | answer a fresh token |
| `onRecordRequestPasswordResetRequest` | request, `collection`, `record` | mail the link |
| `onRecordConfirmPasswordResetRequest` | request, `collection`, `record` | set the password |
| `onRecordRequestEmailChangeRequest` | request, `collection`, `record`, `newEmail` | mail the link to the new address |
| `onRecordConfirmEmailChangeRequest` | request, `collection`, `record`, `newEmail` | set `email` and `verified` |

## Mail and backup hooks

| hook | event | default action |
|---|---|---|
| `onMailerSend` | `message` (mutable); `kind` and `record` on auth mails | send |
| `onBackupCreate` | `name` (mutable, `''` for the default name), `exclude` (mutable: entries of the data folder to leave out) | write the zip into the store |
| `onBackupRestore` | `name`, `exclude` (mutable: entries to keep in place) | stage, stop, swap, restart |

`app.db` is always in a backup. `backups` and the temp folder are always out. Code after `next()` in `onBackupRestore` runs only when the restore failed before it stopped the app: on success the process restarts.

## The request event

| member | what |
|---|---|
| `request` | the `Request` |
| `url` | the parsed `URL` |
| `params` | path parameters |
| `auth` | the signed in record, or null |
| `isSuperuser` | true for a `_superusers` record |
| `realIp` | the client IP, read from `trustedProxy` headers, else the socket's |
| `remoteIp` | the socket's address, always |
| `body()` | the JSON or multipart body, parsed once |
| `json(status, data)` | answer JSON |
| `noContent()` | answer 204 |
| `response` | what was answered, after `next()` |

## Routes and middlewares

```ts
app.route('POST', '/api/orders/:id/close', async (e) => {
    const order = await app.records.getOne('orders', e.params.id ?? '')
    if (order.owner !== e.auth?.id) throw new ApiError(403, 'Not your order.')
    await app.records.update('orders', order.id, { status: 'done' })
    return e.noContent()
}, requireAuth('users'))
```

- `app.route(method, path, handler, ...middlewares)`. Paths take `:name` and a `*` wildcard.
- `requireAuth(...collections)`: 401 without a signed in record of those collections (any auth collection when none is named).
- `requireSuperuser()`: 403 for anyone but a superuser.
- `bodyLimit(bytes)`: this route's body limit; `0` is none; a function of the app and the path parameters is read per request.
- A middleware is a handler that calls `e.next()`. A handler can also return a `Response`.
- `ApiError(status, message, data?)` thrown anywhere becomes PocketBase's error shape.

## Crons

| call | what |
|---|---|
| `app.cron(name, spec, fn)` | five fields (`* , - /`) or `@yearly @monthly @weekly @daily @hourly`, UTC, checked each minute. The same name replaces. A throw is logged, the schedule goes on |
| `__loco_cleanup` | built in, hourly: expired codes and two factor sessions, idle rate limit clients |
| `__loco_backup` | built in when `backups.cron` is set |
