Transition hooks
A workflow stage move is not a write, so the two hooks around it carry their own context, and only the first of them can stop the move.
transitionStage() moves a record from one stage to the next without changing
a field on it. beforeTransition and afterTransition sit around that move.
They live in the same .hooks({ ... }) block as everything else, but almost
nothing about their context is the same.
Turning them on
Stage moves are stored as version snapshots, so a workflow needs versioning.
.options({ versioning: { workflow: true } }) // stages: draft, publishedLeave the workflow off and transitionStage() throws, so neither hook ever
fires. Options covers custom stages and the
moves they allow.
The pair
import { ApiError } from "questpie/errors";
.hooks({
beforeTransition: ({ data, toStage }) => {
if (toStage === "published" && !data.publishedAt) {
throw ApiError.badRequest("Set publishedAt before publishing.");
}
},
afterTransition: async ({ data, toStage, queue }) => {
await queue.announce.publish(
{ postId: data.id, stage: toStage },
{ idempotencyKey: `announce:${data.id}:${toStage}` },
);
},
})Both run inside the transaction that writes the snapshot, and both receive the same object.
| Key | What you get |
|---|---|
data | The record, as it stands before the move. |
recordId | Its id. |
fromStage, toStage | The move. |
scheduledAt | Whatever date the caller passed, if they passed one. |
expectedRevision | The revision the caller asserted, under optimistic concurrency. |
locale, accessMode | As on any hook. |
Everything else is your app context: db, collections, queue, session,
your own services. There is no operation, no original, and no
onAfterCommit, so a non-transactional side effect has no safe home here. Put
it in a durable job instead.
Only `beforeTransition` can refuse
Throwing from it aborts the move. An error from afterTransition is caught
and logged, and the transition still commits.
A future date skips both
Pass a scheduledAt in the future and QUESTPIE queues the move rather than
running it. That decision is itself a beforeTransition, and it runs ahead of
yours, so neither of your hooks fires on that call. They fire when the queued
job performs the move.
Related
- Hooks for the write lifecycle.
- Options for declaring the stages.
- Versions and stages for the same pair on a global.
Bulk writes
`updateMany` and `deleteMany` run the write hooks once per row and hand each call the shape of the whole batch, but a `before*` hook and an `after*` hook do not see the same set of rows.
Validation errors
A rejected write comes back as one HTTP 400 body with a field error per Zod issue, and a hook can raise the same shape for rules a single field schema cannot see.