better-drizzle
Plugins

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.

HookRuns forReturn value
beforeCreatecreate, createMany, upsert, upsertManyany value except undefined replaces data
beforeUpdateupdate, updateMany, updateEachany value except undefined replaces data
beforeDeletedelete, deleteManyany value except undefined skips the database call and becomes the result
beforeQueryfindMany, findFirst, findOne, findUnique, count, exists, paginate, cursorany value except undefined skips the database call and becomes the result
afterCreate / afterUpdate / afterDelete / afterQuerysame kinds as the matching before hookignored
beforeTransaction, afterTransactionCommit, afterTransactionRollback, onTransactionErrortransaction(...)ignored
beforeRaw, afterRaw, onRawError$raw, $executeRaw, $rawUnsafeignored

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:

  1. plugin before hooks, in plugin order
  2. plugin transforms, in plugin order
  3. client before hook
  4. plugin intercepts, outermost first
  5. the database call, skipped when a beforeDelete or beforeQuery returned a value or an intercept returned without calling next()
  6. client after hook
  7. 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.0

intercept(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 memberPurpose
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
paramsthe 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.

On this page