NestJS Drizzle CRUD events and audit logs
NestJS Drizzle CRUD events fire before and after every write on one CrudEventBus, once per call, bulk included. Use them for audit logs and cache busting.
NestJS Drizzle CRUD events in nestjs-drizzle-crud are module-level notifications raised before and after every write. Turn on hooks.enableGlobalHooks, and every service publishes a before event and an after event on one CrudEventBus for create, update, upsert, soft delete, restore, delete, attach, detach and sync. A mass write raises one pair for the whole batch. Subscribe once and you see every entity, which is what an audit log or a cache needs.
Off by default
With enableGlobalHooks off, no events fire. If you set hooks.subscribers without it, the module logs a warning at startup.
How do I turn on CRUD events?
Enable global hooks in forRoot
Set hooks.enableGlobalHooks and, optionally, list subscribers inline:
DrizzleCrudModule.forRoot({
dialect: "postgresql",
connectionString: process.env.DATABASE_URL,
hooks: {
enableGlobalHooks: true,
subscribers: [
{ afterWrite: (e) => console.log(e.entity, e.operation, e.ids) },
],
},
});In forRootAsync, the factory can return hooks.subscribers built from injected services.
Subscribe from a provider
Inject CrudEventBus and call subscribe. It returns a function that removes the subscriber again:
import { Inject, Injectable, type OnModuleInit } from "@nestjs/common";
import { CrudEventBus, DRIZZLE_DB } from "@avytrix/nestjs-drizzle-crud";
import { auditLog } from "./db/schema";
@Injectable()
export class AuditService implements OnModuleInit {
constructor(
private readonly bus: CrudEventBus,
@Inject(DRIZZLE_DB) private readonly db: any,
) {}
onModuleInit() {
this.bus.subscribe({
afterWrite: async (event) => {
const db = event.transaction ?? this.db;
await db.insert(auditLog).values({
entity: event.entity,
operation: event.operation,
ids: event.ids.map(String),
});
},
});
}
}Writing through event.transaction puts the audit row in the same transaction as the write, so both commit or both roll back.
hooks.enableGlobalHooksOptionalboolean
Defaults to false
hooks.subscribersOptionalCrudSubscriber[]
skipEventsOptionalboolean
Defaults to false
eventsOptionalCrudEventBus
What does a subscriber look like?
An object of optional listener methods. beforeWrite and afterWrite see every operation; the named methods see one:
| Method | Called for |
|---|---|
beforeWrite, afterWrite | every operation |
beforeCreate, afterCreate | create, massCreate |
beforeUpdate, afterUpdate | update, massUpdate |
beforeUpsert, afterUpsert | upsert, massUpsert |
beforeSoftDelete, afterSoftDelete | softDelete, massSoftDelete |
beforeRestore, afterRestore | restore, massRestore |
beforeDelete, afterDelete | delete, forceDelete, massDelete, massForceDelete |
beforeAttach / afterAttach, beforeDetach / afterDetach, beforeSync / afterSync | the many-to-many link methods |
entities: ["posts", "authors"] limits a subscriber to those table names. Subscribers run one after another in the order they subscribed, and every listener is awaited. Within one subscriber, beforeWrite or afterWrite runs before the named method.
What is in an event?
Every listener receives one CrudEvent:
interface CrudEvent<T = any> {
entity: string; // the table name
operation: "create" | "update" | "upsert" | "softDelete" | "restore"
| "delete" | "attach" | "detach" | "sync";
phase: "before" | "after";
bulk: boolean; // true for the mass methods
ids: unknown[]; // primary keys; empty before a create or upsert
input?: unknown; // the DTO, the array of DTOs, or the related ids
rows: T[]; // after: the rows as stored; empty for deletes
transaction?: unknown; // the transaction the write runs in, if any
context?: Record<string, unknown>; // options.context or CrudContext
}A force delete is reported as delete. context is the call's options.context, or the CrudContext of the request, so an audit row can record the tenant or user.
How do bulk writes raise events?
Once per call. A mass write raises one before event and one after event with bulk: true, every id in ids and every written row in rows, from inside its transaction. A massUpdate of 500 rows is two events, not a thousand.
With bulkMode: "perItem" each item goes through the single-row method instead, so you get one pair of events per item with bulk: false. See Bulk operations.
Can a listener stop a write?
Yes, a before listener can. If it throws, nothing is written and the caller gets the error (a Nest HttpException keeps its status). An after listener that throws inside a transaction rolls the write back. Every mass write runs in a transaction; a single write only does when you pass one in options.transaction.
How do I bust a cache on write?
Subscribe to the entity and drop the keys its ids touch:
this.bus.subscribe({
entities: ["products"],
afterWrite: (e) => {
for (const id of e.ids) this.cache.delete(`product:${id}`);
this.cache.delete("products:list");
},
});How do events relate to the per-service hooks?
They are separate and both run. The protected hooks on a service (beforeCreate, afterUpdate and the rest) run per row and follow options.hooks.skipBefore / skipAfter. Events run once per call and follow options.skipEvents instead, so skipping a hook never silently skips the audit trail. See Drizzle hooks and validation in NestJS.
How do I use events without the module?
Pass a bus in the config when you build a service by hand, as in tests:
const bus = new CrudEventBus();
bus.subscribe({ afterWrite: (e) => seen.push(e) });
const posts = new PostsService({ ...config, events: bus });Common questions
No. A mass write raises one before and one after event for the whole call, with every id in ids and bulk: true. Only bulkMode: "perItem" raises one pair per item.
No. Events cover writes only: create, update, upsert, soft delete, restore, delete, attach, detach and sync.
No. hooks.skipBefore and hooks.skipAfter skip the per-service hooks only. Pass skipEvents: true to skip the events of a call.
Insert through event.transaction when it is set. The audit row then commits or rolls back with the write it records.
Next steps
Drizzle hooks and validation in NestJS
Per-service hooks that validate, map and react to each row.
Bulk insert and update with Drizzle
Mass writes in one statement, one transaction and one event pair.
forRoot
Every project-wide option, hooks included.
Drizzle hooks and validation in NestJS
Add Drizzle hooks and validation to a NestJS service by overriding validateCreate, beforeCreate, afterUpdate and the DTO mappers on SqlBaseCrudService.
Error handling in NestJS with Drizzle
NestJS Drizzle error handling without a custom filter. Each exception extends a Nest HTTP exception, so a missing row is a 404 and a duplicate a 409.