Skip to content

nestbolt/soft-delete

Repository files navigation

@nestbolt/soft-delete

Soft delete for NestJS with TypeORM — mark entities as deleted instead of removing them.

npm version npm downloads tests license


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

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/soft-delete

Or via yarn:

yarn add @nestbolt/soft-delete

Or via pnpm:

pnpm add @nestbolt/soft-delete

Peer Dependencies

This 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

Quick Start

  1. Register the module in your AppModule:
import { SoftDeleteModule } from '@nestbolt/soft-delete';

@Module({
  imports: [
    TypeOrmModule.forRoot({ /* ... */ }),
    SoftDeleteModule.forRoot(),
  ],
})
export class AppModule {}
  1. 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;
}
  1. 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);

Module Configuration

Static Configuration (forRoot)

SoftDeleteModule.forRoot({
  columnName: 'deleted_at',  // Default column name
})

Async Configuration (forRootAsync)

SoftDeleteModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    columnName: config.get('SOFT_DELETE_COLUMN', 'deleted_at'),
  }),
})

The module is registered as globalSoftDeleteService is available everywhere without re-importing.

Using the Decorator

The @SoftDeletable() class decorator marks an entity as soft-deletable:

@SoftDeletable()                           // Uses default column 'deleted_at'
@SoftDeletable({ columnName: 'removed_at' }) // Custom column name

Using the Mixin

The 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

Using the Service Directly

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

Query Helpers

// 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();

Custom Column Name

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.

Events

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 }

Configuration Options

Option Type Default Description
columnName string 'deleted_at' Default column name for soft-delete timestamp

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

About

Soft delete for NestJS with TypeORM — mark entities as deleted instead of removing.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors