@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-emitterimport { 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
| Field | Type | Description |
|---|---|---|
entityType | string | The entity class name (Post.name === "Post") |
entityId | string | The 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
| Field | Type | Description |
|---|---|---|
entityType | string | The entity class name |
entityId | string | The 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
| Field | Type | Description |
|---|---|---|
entityType | string | The entity class name |
entityId | string | The 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.