Skip to main content

@nest-toolbox/typeorm-soft-delete

npm version

Soft delete utilities for TypeORM with enhanced developer experience — function-based API, restore support, query helpers, and pagination integration.

Installation

npm install @nest-toolbox/typeorm-soft-delete typeorm reflect-metadata

Peer dependencies: typeorm, reflect-metadata

Quick Start

1. Add @DeleteDateColumn to your entity

import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
email: string;

@DeleteDateColumn()
deletedAt?: Date;
}

2. Use the functions

import { softDelete, restore, findOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}

async deleteUser(id: number) {
await softDelete(this.userRepository, id);
}

async restoreUser(id: number) {
await restore(this.userRepository, id);
}

async getDeletedUsers() {
return findOnlyDeleted(this.userRepository);
}
}

Features

  • 🎯 Function-based API — Import and call, no class extension required
  • 🔧 Optional repository wrapperwithSoftDelete() for method-based DX
  • 🔍 Query utilitiesfindOnlyDeleted(), findWithDeleted(), isSoftDeleted()
  • 🛡️ Safety checksforceDelete() only hard-deletes already soft-deleted records
  • 📊 Pagination integration — Works seamlessly with @nest-toolbox/typeorm-paginate
  • Validation mode — Throw errors when no entities are affected
  • 🎨 TypeScript-first — Full type safety with exported types

API Reference

Core Functions

softDelete(repository, criteria, options?)

Soft delete entities by setting deletedAt to the current timestamp. Only affects records where deletedAt IS NULL.

import { softDelete } from '@nest-toolbox/typeorm-soft-delete';

// Single ID
await softDelete(repo, 123);

// Multiple IDs
await softDelete(repo, [1, 2, 3]);

// Where clause
await softDelete(repo, { email: 'old@example.com' });

// With validation (throws if nothing affected)
await softDelete(repo, 123, { validateExists: true });

Parameters:

ParameterTypeDescription
repositoryRepository<T>TypeORM repository
criteriaSoftDeleteCriteria<T>ID, array of IDs, or FindOptionsWhere<T>
optionsSoftDeleteOptionsOptional — { validateExists?: boolean }

Returns: Promise<SoftDeleteResult>{ affected: number }


restore(repository, criteria, options?)

Restore soft-deleted entities by setting deletedAt back to null. Only affects records where deletedAt IS NOT NULL.

import { restore } from '@nest-toolbox/typeorm-soft-delete';

await restore(repo, 123);
await restore(repo, [1, 2, 3]);
await restore(repo, 123, { validateExists: true });

Parameters: Same as softDelete().

Returns: Promise<SoftDeleteResult>{ affected: number }


forceDelete(repository, criteria)

Permanently delete entities from the database. Safety feature: only deletes records where deletedAt IS NOT NULL — you can't accidentally hard-delete active records.

import { forceDelete } from '@nest-toolbox/typeorm-soft-delete';

await forceDelete(repo, 123);
await forceDelete(repo, [1, 2, 3]);

Parameters:

ParameterTypeDescription
repositoryRepository<T>TypeORM repository
criteriaSoftDeleteCriteria<T>ID, array of IDs, or FindOptionsWhere<T>

Returns: Promise<SoftDeleteResult>{ affected: number }


findWithDeleted(repository, options?)

Find entities including soft-deleted ones.

import { findWithDeleted } from '@nest-toolbox/typeorm-soft-delete';

const allUsers = await findWithDeleted(repo);

const admins = await findWithDeleted(repo, {
where: { role: 'admin' },
take: 10,
});

Returns: Promise<T[]>


findOnlyDeleted(repository, options?)

Find only soft-deleted entities.

import { findOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';

const deletedUsers = await findOnlyDeleted(repo);

const deletedAdmins = await findOnlyDeleted(repo, {
where: { role: 'admin' },
});

Returns: Promise<T[]>


count(repository, options?)

Count entities with optional inclusion of soft-deleted records.

import { count } from '@nest-toolbox/typeorm-soft-delete';

const activeCount = await count(repo);
const totalCount = await count(repo, { includeDeleted: true });
const adminCount = await count(repo, {
where: { role: 'admin' },
includeDeleted: false,
});

CountOptions<T>:

OptionTypeDefaultDescription
includeDeletedbooleanfalseInclude soft-deleted in count
whereFindOptionsWhere<T>Additional filter conditions

Returns: Promise<number>


isSoftDeleted(repository, id)

Check if a specific entity is soft-deleted. Throws an error if the entity doesn't exist at all.

import { isSoftDeleted } from '@nest-toolbox/typeorm-soft-delete';

if (await isSoftDeleted(repo, 123)) {
console.log('User is soft-deleted');
}

Returns: Promise<boolean>


Pagination Integration

Async generators that combine soft delete awareness with @nest-toolbox/typeorm-paginate-style iteration.

rowsWithDeleted(options)

Paginate through entities with soft delete awareness.

import { rowsWithDeleted } from '@nest-toolbox/typeorm-soft-delete';

// Default: excludes soft-deleted
for await (const row of rowsWithDeleted({
repository: userRepo,
where: {},
limit: 100,
})) {
console.log(row.data.name, row.progress);
}

// Include soft-deleted
for await (const row of rowsWithDeleted({
repository: userRepo,
where: {},
includeDeleted: true,
})) {
console.log(row.data.name, row.data.deletedAt);
}

PaginationWithDeletedOptions<T>:

OptionTypeDefaultDescription
repositoryRepository<T>requiredTypeORM repository
whereFindOptionsWhere<T> | FindOptionsWhere<T>[]requiredFilter conditions
limitnumber100Records per page
offsetnumber0Starting offset
includeDeletedbooleanfalseInclude soft-deleted records

Yields PaginatedRow<T>:

PropertyTypeDescription
dataTThe entity
indexnumberZero-based index
progressnumberProgress ratio (0–1)

rowsOnlyDeleted(options)

Paginate through only soft-deleted entities.

import { rowsOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';

for await (const row of rowsOnlyDeleted({
repository: userRepo,
where: {},
})) {
console.log('Deleted:', row.data.name, row.data.deletedAt);
}

Decorator

@SoftDeletable(config?)

Optional decorator to mark an entity as soft-deletable and store metadata.

import { SoftDeletable } from '@nest-toolbox/typeorm-soft-delete';

@Entity()
@SoftDeletable({ columnName: 'deletedAt', allowHardDelete: false })
export class User {
@DeleteDateColumn()
deletedAt?: Date;
}
OptionTypeDefaultDescription
columnNamestring'deletedAt'Name of the delete date column
allowHardDeletebooleanfalseWhether to allow hard deletion

Helper functions:

  • isSoftDeletable(entityClass) — Check if entity has @SoftDeletable
  • getSoftDeleteConfig(entityClass) — Get the decorator config

Repository Wrapper (Optional)

For those who prefer a method-based API, wrap your repository with withSoftDelete().

import { withSoftDelete, SoftDeleteRepository } from '@nest-toolbox/typeorm-soft-delete';

@Injectable()
export class UserService {
private userRepo: SoftDeleteRepository<User>;

constructor(@InjectRepository(User) repository: Repository<User>) {
this.userRepo = withSoftDelete(repository);
}

async example() {
await this.userRepo.softDelete(123);
await this.userRepo.restore(123);
await this.userRepo.forceDelete(123);

const deleted = await this.userRepo.findOnlyDeleted();
const all = await this.userRepo.findWithDeleted();
const isDeleted = await this.userRepo.isSoftDeleted(123);
const activeCount = await this.userRepo.count();

// Standard repository methods still work
const users = await this.userRepo.find({ where: { active: true } });
await this.userRepo.save(newUser);
}
}

Utility Functions

import {
supportsSoftDelete,
validateSoftDeleteSupport,
getDeleteDateColumnName,
} from '@nest-toolbox/typeorm-soft-delete';

// Check if a repository supports soft delete
if (supportsSoftDelete(repo)) { /* ... */ }

// Throws descriptive error if not supported
validateSoftDeleteSupport(repo);

// Get the database column name for the delete date
const colName = getDeleteDateColumnName(repo); // e.g., 'deleted_at'

Types

All types are exported from the package:

import type {
SoftDeleteCriteria, // string | number | (string | number)[] | FindOptionsWhere<T>
SoftDeleteResult, // { affected: number }
SoftDeleteOptions, // { validateExists?: boolean }
RestoreOptions, // { validateExists?: boolean }
CountOptions, // extends FindManyOptions + { includeDeleted?: boolean }
FindDeletedOptions, // extends FindManyOptions (without withDeleted)
PaginatedRow, // { data: T, index: number, progress: number }
SoftDeleteConfig, // { columnName?: string, allowHardDelete?: boolean }
} from '@nest-toolbox/typeorm-soft-delete';

Examples

Cascading soft delete

async deleteUserWithPosts(userId: number) {
await softDelete(userRepo, userId);
await softDelete(postRepo, { userId });
await softDelete(commentRepo, { userId });
}

Scheduled cleanup of old soft-deleted records

import { Cron } from '@nestjs/schedule';
import { LessThan } from 'typeorm';
import { findOnlyDeleted, forceDelete } from '@nest-toolbox/typeorm-soft-delete';

@Cron('0 0 * * *') // Daily at midnight
async cleanupOldDeleted() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

const oldDeleted = await findOnlyDeleted(userRepo, {
where: { deletedAt: LessThan(thirtyDaysAgo) },
});

for (const user of oldDeleted) {
await forceDelete(userRepo, user.id);
}
}

Conditional soft delete with validation

try {
await softDelete(userRepo, 999, { validateExists: true });
} catch (error) {
// "Entity not found or already deleted"
console.error(error.message);
}

Batch soft delete by condition

import { Like } from 'typeorm';

// Soft delete all users from a specific domain
await softDelete(userRepo, { email: Like('%@old-domain.com') });