NestboltNestbolt

@nestbolt/soft-delete

Events

Listen to soft-delete, restore, and force-delete events via @nestjs/event-emitter.

@nestbolt/soft-delete emits events when entities are soft-deleted, restored, or force-deleted. Events are dispatched via @nestjs/event-emitter when it's installed and EventEmitterModule is registered.

Setup

Install and register the event emitter module:

npm install @nestjs/event-emitter
import { EventEmitterModule } from "@nestjs/event-emitter";
import { SoftDeleteModule } from "@nestbolt/soft-delete";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    SoftDeleteModule.forRoot(),
    // ...
  ],
})
export class AppModule {}

If @nestjs/event-emitter is not installed or EventEmitterModule is not registered, the package works normally but no events are emitted. The event emitter is resolved lazily and treated as fully optional.

Events

soft-delete.deleted

Emitted after a successful softDelete() call through the service or the mixin. The repo.remove() interception does not emit this event -- see Reliability below.

import { OnEvent } from "@nestjs/event-emitter";
import { SOFT_DELETE_EVENTS, type SoftDeleteDeletedEvent } from "@nestbolt/soft-delete";

@Injectable()
export class SoftDeleteListener {
  @OnEvent(SOFT_DELETE_EVENTS.DELETED)
  handleDeleted(event: SoftDeleteDeletedEvent) {
    console.log(`${event.entityType}:${event.entityId} was soft-deleted`);
  }
}

Payload: SoftDeleteDeletedEvent

FieldTypeDescription
entityTypestringThe entity class name (Post.name === "Post")
entityIdstringThe ID of the entity that was soft-deleted

soft-delete.restored

Emitted after a successful restore() call.

import { OnEvent } from "@nestjs/event-emitter";
import { SOFT_DELETE_EVENTS, type SoftDeleteRestoredEvent } from "@nestbolt/soft-delete";

@Injectable()
export class SoftDeleteListener {
  @OnEvent(SOFT_DELETE_EVENTS.RESTORED)
  handleRestored(event: SoftDeleteRestoredEvent) {
    console.log(`${event.entityType}:${event.entityId} was restored`);
  }
}

Payload: SoftDeleteRestoredEvent

FieldTypeDescription
entityTypestringThe entity class name
entityIdstringThe ID of the entity that was restored

soft-delete.force-deleted

Emitted after a successful forceDelete() call -- the row is physically removed from the table.

import { OnEvent } from "@nestjs/event-emitter";
import { SOFT_DELETE_EVENTS, type SoftDeleteForceDeletedEvent } from "@nestbolt/soft-delete";

@Injectable()
export class SoftDeleteListener {
  @OnEvent(SOFT_DELETE_EVENTS.FORCE_DELETED)
  handleForceDeleted(event: SoftDeleteForceDeletedEvent) {
    console.log(`${event.entityType}:${event.entityId} was permanently deleted`);
  }
}

Payload: SoftDeleteForceDeletedEvent

FieldTypeDescription
entityTypestringThe entity class name
entityIdstringThe ID of the entity that was force-deleted

Event Constants

Use the SOFT_DELETE_EVENTS constant for type-safe event names:

import { SOFT_DELETE_EVENTS } from "@nestbolt/soft-delete";

SOFT_DELETE_EVENTS.DELETED;        // "soft-delete.deleted"
SOFT_DELETE_EVENTS.RESTORED;       // "soft-delete.restored"
SOFT_DELETE_EVENTS.FORCE_DELETED;  // "soft-delete.force-deleted"

Practical Example: Cleanup on Force Delete

A common use case is tearing down related resources when an entity is permanently removed -- uploaded files, search-index documents, cached projections, etc.:

@Injectable()
export class PostCleanupListener {
  constructor(
    private readonly storage: StorageService,
    private readonly searchIndex: SearchIndexService,
  ) {}

  @OnEvent(SOFT_DELETE_EVENTS.FORCE_DELETED)
  async handleForceDeleted(event: SoftDeleteForceDeletedEvent) {
    if (event.entityType !== "Post") return;

    await Promise.allSettled([
      this.storage.deleteByPostId(event.entityId),
      this.searchIndex.removeDocument("posts", event.entityId),
    ]);
  }
}

Filter on entityType so a single listener can handle one entity type at a time, even though all soft-delete events share the same event names.

Practical Example: Audit Trail

Use soft-delete.deleted and soft-delete.restored to record an audit trail of who/when without coupling each call site to your audit system:

@Injectable()
export class SoftDeleteAuditListener {
  constructor(private readonly audit: AuditLogService) {}

  @OnEvent(SOFT_DELETE_EVENTS.DELETED)
  onDeleted(event: SoftDeleteDeletedEvent) {
    return this.audit.record({
      action: "soft_delete",
      entityType: event.entityType,
      entityId: event.entityId,
    });
  }

  @OnEvent(SOFT_DELETE_EVENTS.RESTORED)
  onRestored(event: SoftDeleteRestoredEvent) {
    return this.audit.record({
      action: "restore",
      entityType: event.entityType,
      entityId: event.entityId,
    });
  }
}

Reliability

Events are emitted after the database write succeeds. If your handler throws, the soft delete / restore / force delete itself is unaffected -- the row state is already committed. Events are best-effort signals, not transactional outbox messages.

The repo.remove() interception in the subscriber does not emit events itself -- it only sets the deleted-at column and saves the entity. If you need events to fire for every soft delete, prefer the explicit softDeleteService.softDelete() or entity.softDelete() API.