Plugin hooks
Lifecycle hooks, execution order, intercepts, and the context every plugin hook receives.
Use plugin hooks to change write data, short-circuit reads, or observe operations from a plugin. Use an intercept when the plugin must wrap the database call itself, as the cache plugin does.
Register hooks
hooks (or ctx.addHook(...) in setup()) registers lifecycle hooks. Every hook can be async.
| Hook | Runs for | Return value |
|---|---|---|
beforeCreate | create, createMany, upsert, upsertMany | any value except undefined replaces data |
beforeUpdate | update, updateMany, updateEach | any value except undefined replaces data |
beforeDelete | delete, deleteMany | any value except undefined skips the database call and becomes the result |
beforeQuery | findMany, findFirst, findOne, findUnique, count, exists, paginate, cursor | any value except undefined skips the database call and becomes the result |
afterCreate / afterUpdate / afterDelete / afterQuery | same kinds as the matching before hook | ignored |
beforeTransaction, afterTransactionCommit, afterTransactionRollback, onTransactionError | transaction(...) | ignored |
beforeRaw, afterRaw, onRawError | $raw, $executeRaw, $rawUnsafe | ignored |
upsert runs the create hooks only, with kind: 'upsert' and data: { create, update }. Plugins have no onError hook. Use onError in client hooks for that.
definePlugin({
id: '@acme/slugs',
hooks: {
beforeCreate(ctx) {
if (ctx.kind !== 'create' || !ctx.model.hasColumn('slug')) return;
const data = ctx.data as Record<string, unknown>;
return { ...data, slug: String(data.title).toLowerCase() };
},
beforeQuery(ctx) {
// short-circuit: count() never hits the database
if (ctx.kind === 'count' && ctx.state.cachedCount !== undefined)
return ctx.state.cachedCount as number;
},
},
});Execution order
For one delegate call:
- plugin before hooks, in plugin order
- plugin transforms, in plugin order
- client before hook
- plugin intercepts, outermost first
- the database call, skipped when a
beforeDeleteorbeforeQueryreturned a value or an intercept returned without callingnext() - client after hook
- plugin after hooks, in plugin order
Every before hook runs even after one returns an override. The last override wins.
Hook contexts are copies
Each hook receives a shallow copy of the operation input. Reassigning
ctx.where inside a hook has no effect. Return new data from
beforeCreate / beforeUpdate, or use a transform to rewrite where,
select, and the other query fields.
Intercepts
Added in 0.3.0intercept(ctx) wraps the execution of every operation kind. It receives the final operation input: before hooks, transforms, and the client before hook have already run. Call ctx.next() to run the next intercept (or the database call), or return a value to skip the call. The cache plugin uses an intercept to serve hits without SQL and to invalidate after writes.
definePlugin({
id: '@acme/timing',
async intercept(ctx) {
const started = performance.now();
const result = await ctx.next();
ctx.annotate('durationMs', performance.now() - started);
return result;
},
});ctx member | Purpose |
|---|---|
next() | run the next intercept, or the operation for the innermost one |
annotate(key, value) | record a value that client and plugin after hooks receive in annotations |
skipAfterHooks() | skip client and plugin after hooks for this call |
params | the values of a prepared statement execution; undefined for regular calls |
Intercepts run in plugin order, so the first plugin is the outermost. When a before hook returned an override, next() resolves to it without SQL. .explain() never runs intercepts. An intercept's return value becomes the result, so a skipped call must return the same shape the operation would.
Before hooks and transforms run once when a read is prepared, while intercepts run on every execute(). During a prepared execution, ctx.args holds param() markers (Drizzle Placeholder instances) and is the same object on every execution, and ctx.params holds the values. An intercept that derives anything from the args, such as a cache key, must also account for ctx.params.
Hook context
CRUD and query hooks (and transforms) receive:
Prop
Type
Transaction hooks receive client (the transaction client), attempt, depth, name, comment, meta, transactionOptions, transactionContext, dialect, models, schema, db, options, isInTransaction: true, afterCommit, and afterRollback. afterTransactionRollback adds reason, and onTransactionError adds error.
Raw hooks receive action ('raw' | 'executeRaw' | 'rawUnsafe'), query (rendered SQL text), sql, rawOptions, name, comment, timeoutMs, signal, map, meta, result (in afterRaw), error (in onRawError), plus dialect, schema, db, options, isInTransaction, transaction, transactionContext, afterCommit, and afterRollback.
Client hooks run before plugin hooks for transaction and raw events.