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:

app.module.ts
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:

audit.service.ts
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.enableGlobalHooksOptional

boolean

Hand the module's CrudEventBus to every service so its writes raise events.

Defaults to false

hooks.subscribersOptional

CrudSubscriber[]

Subscribers added to the bus when it is created.
skipEventsOptional

boolean

Call option: raise no events for this call.

Defaults to false

eventsOptional

CrudEventBus

Service config: the bus to publish on. The module sets it; pass your own when you build a service by hand.

What does a subscriber look like?

An object of optional listener methods. beforeWrite and afterWrite see every operation; the named methods see one:

MethodCalled for
beforeWrite, afterWriteevery operation
beforeCreate, afterCreatecreate, massCreate
beforeUpdate, afterUpdateupdate, massUpdate
beforeUpsert, afterUpsertupsert, massUpsert
beforeSoftDelete, afterSoftDeletesoftDelete, massSoftDelete
beforeRestore, afterRestorerestore, massRestore
beforeDelete, afterDeletedelete, forceDelete, massDelete, massForceDelete
beforeAttach / afterAttach, beforeDetach / afterDetach, beforeSync / afterSyncthe 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:

crud-event.ts
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:

cache.service.ts
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:

posts.service.spec.ts
const bus = new CrudEventBus();
bus.subscribe({ afterWrite: (e) => seen.push(e) });
const posts = new PostsService({ ...config, events: bus });

Common questions

Next steps

Last updated View change history

On this page