Skip to content

Events, Testing & API Reference ​

Events ​

Lifecycle events are notification-only. Use them for metrics, cache invalidation, and downstream notifications, not as authoritative proof that a database transaction committed. For atomic lifecycle evidence, use the 0.7 bridge with auditLifecycle: 'atomic-required' and withAuditTransaction().

Enable events and install @nestjs/event-emitter:

bash
npm install @nestjs/event-emitter
typescript
// app.module.ts
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    SoftDeleteModule.forRoot({
      softDeleteModels: ['User', 'Post'],
      enableEvents: true,
      prismaServiceToken: PrismaService,
    }),
  ],
})
export class AppModule {}

Listen to events with @OnEvent():

typescript
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { SoftDeletedEvent, RestoredEvent, PurgedEvent } from '@nestarc/soft-delete';

@Injectable()
export class LifecycleListener {
  @OnEvent(SoftDeletedEvent.EVENT_NAME)
  onDeleted(event: SoftDeletedEvent) {
    console.log(`${event.model} soft-deleted by ${event.actorId} at ${event.deletedAt}`);
  }

  @OnEvent(RestoredEvent.EVENT_NAME)
  onRestored(event: RestoredEvent) {
    console.log(`${event.model} restored by ${event.actorId}`);
  }

  @OnEvent(PurgedEvent.EVENT_NAME)
  onPurged(event: PurgedEvent) {
    console.log(`${event.count} ${event.model} records purged (older than ${event.olderThan})`);
  }
}
Event classEVENT_NAMEPayload fields
SoftDeletedEventsoft-delete.deletedmodel, where, deletedAt, actorId, count?
RestoredEventsoft-delete.restoredmodel, where, actorId, count?
PurgedEventsoft-delete.purgedmodel, count, olderThan

Version 0.5 populates the optional count for bulk delete and bulk restore operations. Single-row soft deletes report count: 1; single-row restores keep the field optional for backward compatibility.

typescript
@OnEvent(RestoredEvent.EVENT_NAME)
onRestored(event: RestoredEvent) {
  metrics.count('soft_delete.restored', event.count ?? 1, {
    model: event.model,
  });
}

Notification-only contract ​

@nestjs/event-emitter dispatches these events synchronously by default, but an event may still be observed before an outer Prisma transaction commits. Listener failure, asynchronous scheduling, process termination, or a later rollback can therefore make event delivery diverge from durable database state. Do not call AuditService.log() from a listener and treat the result as atomic evidence.

When evidence must commit with the lifecycle write, install @nestarc/audit-log at its accepted optional peer range (^0.4.1 || ^0.5.0), apply extensions in tenancy → audit-log → soft-delete order, and inject that exact composed client into SoftDeleteModule. Configure auditLifecycle: 'atomic-required', bound bulk conversion with auditMaxBatchRecords, and run delete, restore, purge, cascade, and bulk operations inside withAuditTransaction(). The bridge fails before mutation when any part of that contract is absent. See the atomic lifecycle setup.


Testing ​

Import TestSoftDeleteModule from @nestarc/soft-delete/testing in your unit or integration tests.

typescript
import { Test } from '@nestjs/testing';
import { TestSoftDeleteModule, expectSoftDeleted, expectNotSoftDeleted, expectCascadeSoftDeleted } from '@nestarc/soft-delete/testing';
import { SoftDeleteService } from '@nestarc/soft-delete';
import { createPrismaSoftDeleteExtension } from '@nestarc/soft-delete';
import { basePrisma } from './prisma';
import { prismaDmmf } from './prisma.dmmf';

describe('UsersService', () => {
  let softDelete: SoftDeleteService;
  let prisma: any; // your extended PrismaClient in tests

  beforeAll(async () => {
    prisma = basePrisma.$extends(
      createPrismaSoftDeleteExtension({
        softDeleteModels: ['User', 'Post'],
        cascade: { User: ['Post'] },
        dmmf: prismaDmmf,
      }),
    );

    const module = await Test.createTestingModule({
      imports: [
        TestSoftDeleteModule.register(
          {
            softDeleteModels: ['User', 'Post'],
            cascade: { User: ['Post'] },
            dmmf: prismaDmmf,
          },
          prisma,
        ),
      ],
    }).compile();

    softDelete = module.get(SoftDeleteService);
  });

  it('soft-deletes a user', async () => {
    await prisma.user.delete({ where: { id: 1 } });
    await expectSoftDeleted(prisma.user, { id: 1 });
  });

  it('restores a user', async () => {
    await softDelete.restore('User', { id: 1 });
    await expectNotSoftDeleted(prisma.user, { id: 1 });
  });

  it('cascades soft-delete to posts', async () => {
    await prisma.user.delete({ where: { id: 2 } });
    await expectCascadeSoftDeleted(prisma, 'User', { id: 2 }, ['Post']);
  });
});

basePrisma is your generated, adapter-backed Prisma 7 client. Because this test enables cascade, load prismaDmmf as shown in the DMMF setup.

Assertion helpers ​

HelperDescription
expectSoftDeleted(delegate, where, deletedAtField?)Asserts the record exists and deletedAt is non-null.
expectNotSoftDeleted(delegate, where, deletedAtField?)Asserts the record exists and deletedAt is null.
expectCascadeSoftDeleted(prisma, parentModel, where, childModels, deletedAtField?)Asserts the parent and all listed child models have soft-deleted records.

API Reference ​

@nestarc/soft-delete ​

ExportKindDescription
SoftDeleteModuleModuleNestJS dynamic module. Use .forRoot() or .forRootAsync().
SoftDeleteServiceServicerestore(), restoreMany(), forceDelete(), purge(), withDeleted(), onlyDeleted().
SoftDeleteContextServiceAsyncLocalStorage context for filter mode.
createPrismaSoftDeleteExtensionFunctionCreates a Prisma client extension for standalone use.
WithDeletedDecoratorInclude soft-deleted records in the route handler's queries.
OnlyDeletedDecoratorReturn only soft-deleted records in the route handler's queries.
SkipSoftDeleteDecoratorBypass soft-delete logic in the route handler.
WithDeletedRelationsDecoratorInclude deleted rows for exact to-many relation paths when relation filtering is enabled.
SoftDeleteFilterInterceptorInterceptorReads route metadata and sets the SoftDeleteContext. Auto-registered.
SoftDeletedEventClassEvent emitted after a soft-delete. EVENT_NAME = 'soft-delete.deleted'.
RestoredEventClassEvent emitted after a restore. EVENT_NAME = 'soft-delete.restored'.
PurgedEventClassEvent emitted after a purge. EVENT_NAME = 'soft-delete.purged'.
SoftDeleteEventEmitterServiceInternal emitter; exposed for advanced use.
SoftDeleteFieldMissingErrorErrorExported error reserved for missing-field validation; current runtime paths surface missing fields as Prisma errors.
CascadeRelationNotFoundErrorErrorThrown when a cascade relation cannot be resolved.
CascadeDmmfMissingErrorErrorThrown when cascade is configured without available DMMF metadata.
RelationDmmfMissingErrorErrorThrown when relation filters are enabled without available DMMF metadata.
SoftDeleteModuleOptionsInterfaceOptions for forRoot().
SoftDeleteModuleAsyncOptionsInterfaceOptions for forRootAsync().
SoftDeleteExtensionOptionsInterfaceOptions for createPrismaSoftDeleteExtension().
RelationFilterOptionsInterfaceEnables relation filtering and bounds traversal depth.
PrismaDmmfLikeInterfaceMinimal DMMF shape accepted by explicit configuration.

@nestarc/soft-delete/testing ​

ExportKindDescription
TestSoftDeleteModuleModuleLightweight test module. Use .register(options, prisma?).
expectSoftDeletedFunctionAssert a record is soft-deleted.
expectNotSoftDeletedFunctionAssert a record is not soft-deleted.
expectCascadeSoftDeletedFunctionAssert a parent and its children are all soft-deleted.

For complete signatures, see the generated @nestarc/soft-delete API and testing API.

Released under the MIT License.