@nestbolt/soft-delete
Quick Start
Set up the module, decorate an entity, and soft-delete your first record in under five minutes.
This guide walks you through the minimal setup to start soft-deleting entities.
1. Register the Module
Add SoftDeleteModule.forRoot() to your root module. The module is registered globally, so you only need to import it once:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { SoftDeleteModule } from "@nestbolt/soft-delete";
import { Post } from "./entities/post.entity";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
// ... your database config
autoLoadEntities: true,
synchronize: true,
}),
SoftDeleteModule.forRoot(),
TypeOrmModule.forFeature([Post]),
],
})
export class AppModule {}The module registers SoftDeleteService and SoftDeleteSubscriber globally.
2. Decorate Your Entity
Add the @SoftDeletable() decorator and a nullable deletedAt column to your entity. To use the entity-level mixin methods (post.softDelete(), post.restore(), etc.), extend from SoftDeletableMixin():
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { SoftDeletable, SoftDeletableMixin } from "@nestbolt/soft-delete";
@SoftDeletable()
@Entity("posts")
export class Post extends SoftDeletableMixin(class { id!: string }) {
@PrimaryGeneratedColumn("uuid")
declare id: string;
@Column()
title!: string;
@Column({ name: "deleted_at", type: "datetime", nullable: true, default: null })
deletedAt!: Date | null;
}The column name and the entity property name don't have to match -- the service maps the configured column name (deleted_at) back to the property (deletedAt) using TypeORM metadata.
3. Soft Delete, Restore, Force Delete
Once the entity is loaded from the database, you can call the mixin methods directly:
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post) private readonly repo: Repository<Post>,
) {}
async softDelete(postId: string) {
const post = await this.repo.findOneByOrFail({ id: postId });
await post.softDelete();
return { deletedAt: post.getDeletedAt() };
}
async restore(postId: string) {
const post = await this.repo.findOneByOrFail({ id: postId });
await post.restore();
}
async forceDelete(postId: string) {
const post = await this.repo.findOneByOrFail({ id: postId });
await post.forceDelete();
}
}After softDelete(), the row is still in the table but deleted_at is set. After restore(), deleted_at is NULL again. After forceDelete(), the row is physically removed.
4. Use the Service Directly
If you don't want the mixin, inject SoftDeleteService and pass the entity constructor + id:
import { SoftDeleteService } from "@nestbolt/soft-delete";
@Injectable()
export class PostsService {
constructor(private readonly softDelete: SoftDeleteService) {}
remove(postId: string) {
return this.softDelete.softDelete(Post, postId);
}
restore(postId: string) {
return this.softDelete.restore(Post, postId);
}
destroy(postId: string) {
return this.softDelete.forceDelete(Post, postId);
}
}5. Query Trashed Rows
By default, regular repo.find() calls still return soft-deleted rows -- this package doesn't add an automatic global filter. Use the withTrashed() and onlyTrashed() query helpers when you want to be explicit about what you're including:
@Injectable()
export class PostsService {
constructor(private readonly softDelete: SoftDeleteService) {}
// All posts, including soft-deleted ones (uses an alias of "post")
listAll() {
return this.softDelete.withTrashed(Post, "post").getMany();
}
// Only the soft-deleted posts
listTrashed() {
return this.softDelete.onlyTrashed(Post, "post").getMany();
}
// Trashed posts older than a date
listOldTrashed(cutoff: Date) {
return this.softDelete
.onlyTrashed(Post, "post")
.andWhere("post.createdAt < :cutoff", { cutoff })
.getMany();
}
}If you want regular reads to exclude trashed rows, add an explicit WHERE post.deleted_at IS NULL to your queries -- the package leaves that policy decision to you.
6. Intercept repo.remove()
If you call repository.remove(post) on a @SoftDeletable() entity, the bundled subscriber best-effort intercepts the call and turns it into a soft delete. This is convenient for code that already uses TypeORM's remove() API, but it isn't a hard guarantee -- see the Subscriber page for the caveats. For reliable soft-delete behavior, prefer the explicit service or mixin methods.
Next Steps
- Configuration --
forRoot,forRootAsync, and the fullSoftDeleteServiceAPI. - Decorator --
@SoftDeletable()and@IncludeDeleted()options reference. - Mixin -- entity-instance methods and composing with other mixins.
- Subscriber -- how
repo.remove()interception works and its limits. - Events -- listen to
soft-delete.deleted,soft-delete.restored,soft-delete.force-deleted.