Soft delete for NestJS with TypeORM — mark entities as deleted instead of removing them.
This package provides soft delete functionality for NestJS with TypeORM that lets you mark entities as deleted instead of permanently removing them, with restore, force delete, and query helpers.
Once installed, using it is as simple as:
@SoftDeletable()
@Entity()
class Post extends SoftDeletableMixin(BaseEntity) {
@Column({ name: 'deleted_at', nullable: true }) deletedAt: Date | null;
}
await post.softDelete(); // Marks as deleted
await post.restore(); // Restores
await post.forceDelete(); // Permanently removes- Installation
- Quick Start
- Module Configuration
- Using the Decorator
- Using the Mixin
- Using the Service Directly
- Query Helpers
- Custom Column Name
- Events
- Configuration Options
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install the package via npm:
npm install @nestbolt/soft-deleteOr via yarn:
yarn add @nestbolt/soft-deleteOr via pnpm:
pnpm add @nestbolt/soft-deleteThis package requires the following peer dependencies:
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
typeorm ^0.3.0
reflect-metadata ^0.1.13 || ^0.2.0
Optional:
@nestjs/event-emitter ^2.0.0 || ^3.0.0
- Register the module in your
AppModule:
import { SoftDeleteModule } from '@nestbolt/soft-delete';
@Module({
imports: [
TypeOrmModule.forRoot({ /* ... */ }),
SoftDeleteModule.forRoot(),
],
})
export class AppModule {}- Add the decorator and mixin to your entity:
import { SoftDeletable, SoftDeletableMixin } from '@nestbolt/soft-delete';
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm';
@SoftDeletable()
@Entity('posts')
export class Post extends SoftDeletableMixin(BaseEntity) {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column({ name: 'deleted_at', type: 'datetime', nullable: true, default: null })
deletedAt: Date | null;
}- Use soft delete in your service:
// Via mixin methods
await post.softDelete();
await post.restore();
console.log(post.isDeleted()); // true/false
// Via service
await softDeleteService.softDelete(Post, postId);
await softDeleteService.restore(Post, postId);
await softDeleteService.forceDelete(Post, postId);SoftDeleteModule.forRoot({
columnName: 'deleted_at', // Default column name
})SoftDeleteModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
columnName: config.get('SOFT_DELETE_COLUMN', 'deleted_at'),
}),
})The module is registered as global — SoftDeleteService is available everywhere without re-importing.
The @SoftDeletable() class decorator marks an entity as soft-deletable:
@SoftDeletable() // Uses default column 'deleted_at'
@SoftDeletable({ columnName: 'removed_at' }) // Custom column nameThe SoftDeletableMixin() adds instance methods to your entity:
| Method | Returns | Description |
|---|---|---|
softDelete() |
Promise<void> |
Soft-delete this entity |
restore() |
Promise<void> |
Restore this entity |
forceDelete() |
Promise<void> |
Permanently delete from DB |
isDeleted() |
boolean |
Check if soft-deleted |
isTrashed() |
boolean |
Alias for isDeleted() |
getDeletedAt() |
Date | null |
Get deletion timestamp |
Inject SoftDeleteService for programmatic control:
| Method | Returns | Description |
|---|---|---|
softDelete<T>(Entity, id) |
Promise<void> |
Set deletedAt to now |
restore<T>(Entity, id) |
Promise<void> |
Set deletedAt to null |
forceDelete<T>(Entity, id) |
Promise<void> |
Permanently DELETE |
withTrashed<T>(Entity, alias?) |
SelectQueryBuilder<T> |
Query including deleted |
onlyTrashed<T>(Entity, alias?) |
SelectQueryBuilder<T> |
Query only deleted |
isSoftDeletable(Entity) |
boolean |
Check if decorated |
getColumnName(Entity?) |
string |
Resolve column name |
// Get all posts including soft-deleted
const all = await softDeleteService.withTrashed(Post).getMany();
// Get only soft-deleted posts
const trashed = await softDeleteService.onlyTrashed(Post).getMany();
// Add conditions to trashed query
const old = await softDeleteService
.onlyTrashed(Post, 'post')
.andWhere('post.createdAt < :date', { date: someDate })
.getMany();Override the column name per entity or globally:
// Per entity
@SoftDeletable({ columnName: 'removed_at' })
// Globally
SoftDeleteModule.forRoot({ columnName: 'removed_at' })Entity-level options take priority over module-level options.
When @nestjs/event-emitter is installed, the following events are emitted:
| Event | Payload |
|---|---|
soft-delete.deleted |
{ entityType, entityId } |
soft-delete.restored |
{ entityType, entityId } |
soft-delete.force-deleted |
{ entityType, entityId } |
| Option | Type | Default | Description |
|---|---|---|---|
columnName |
string |
'deleted_at' |
Default column name for soft-delete timestamp |
npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
- Inspired by spatie/laravel-model-cleanup and Laravel's built-in
SoftDeletestrait
The MIT License (MIT). Please see License File for more information.