Typing a hook
An inline hook is typed from the collection above it. A handler in its own file has to declare what it takes, and which type that is depends on where the file sits.
Three collections that all stamp updatedBy should share one function. The
moment you lift that function out of the collection file, the compiler stops
inferring its argument for you.
Inline needs nothing
Written on .hooks({ ... }), data, original and operation are already
narrowed to this collection and this stage.
// inside collection("posts").hooks({ afterChange: (ctx) => { ... } })
// ctx.data CollectionDoc<"posts">, the saved row
// ctx.original CollectionDoc<"posts">, on update only
// ctx.operation "create" | "update"A file the collection imports
Take the package-level HookContext. It is cycle-safe, which is the whole
reason it exists.
import type { HookContext } from "questpie";
export function stampSlug(ctx: HookContext<{ title?: string; slug?: string }>) {
if (ctx.operation === "create" && ctx.data.title && !ctx.data.slug) {
ctx.data.slug = ctx.data.title.toLowerCase().replace(/\s+/g, "-");
}
}HookContext<TData, TOriginal, TOperation> takes the row type, the preimage
type and the operation union, in that order. Give it only the first and
operation stays the full five-way union, so branch on it.
A file the collection does not import
Routes, services, jobs and scripts can use the generated HookRuleContext<K>
instead, where K is a collection key. It resolves data to that collection's
row and types session and collections through your own app.
import type { HookRuleContext } from "#questpie";
export function isFirstPublish(ctx: HookRuleContext<"posts">) {
return ctx.operation === "update" && ctx.data.status === "published";
}Do not cross the two
Importing HookRuleContext into a file that a collection imports sends the
generated barrel back through itself, and codegen fails with a type cycle. A
handler you register on .hooks() is imported by that collection, so it takes
the package-level HookContext. AccessRuleContext follows the same rule.
Composing them
Any stage takes an array, and the handlers run in order.
.hooks({
beforeChange: [normalizeTitle, computeSlug, stampUpdatedBy],
})Because a second .hooks() call appends rather than replaces, splitting them
across calls does the same thing. Both forms are how a module contributes a
handler to a collection it did not declare.
questpie also exports HookFunction<TData, TOriginal, TOperation>, the
function type itself, for annotating a factory that builds handlers.
Related
- Hooks for the stages.
- Writing rules for the same split on the access side.
- Codegen for what
#questpiecontains.
Beyond CRUD
Four more operations carry their own access rule, and each one falls back somewhere different when you leave it out.
Side effects
The `after*` write hooks run inside the transaction that made the write, so what you may safely do in one depends on whether the work can join that transaction or has to wait for the commit.