NestboltNestbolt

@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 full SoftDeleteService API.
  • 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.