@nest-toolbox/typeorm-soft-delete
v1.8.1
Published
Soft delete utilities for TypeORM with enhanced DX
Maintainers
Readme
@nest-toolbox/typeorm-soft-delete
Soft delete utilities for TypeORM with enhanced developer experience.
Features
- 🎯 Function-based API following TypeORM patterns
- 🔧 Optional method-based wrapper for enhanced DX
- 🔍 Query utilities for finding deleted records
- 📊 Pagination integration with
@nest-toolbox/typeorm-paginate - 🎨 TypeScript-first with full type safety
- ✅ Well-tested with 90%+ coverage
Installation
npm install @nest-toolbox/typeorm-soft-delete typeorm reflect-metadataQuick Start
1. Setup Your Entity
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';
import { SoftDeletable } from '@nest-toolbox/typeorm-soft-delete';
@Entity()
@SoftDeletable() // Optional decorator for metadata
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@DeleteDateColumn() // Required - TypeORM's built-in soft delete column
deletedAt?: Date;
}2. Use Soft Delete Functions (Recommended)
import { softDelete, restore, findOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async deleteUser(id: number) {
// Soft delete by ID
const result = await softDelete(this.userRepository, id);
console.log(`Soft deleted ${result.affected} user(s)`);
}
async restoreUser(id: number) {
// Restore soft-deleted user
await restore(this.userRepository, id);
}
async getDeletedUsers() {
// Find only deleted users
return findOnlyDeleted(this.userRepository);
}
}3. Alternative: Method-Based API (Optional)
import { withSoftDelete } from '@nest-toolbox/typeorm-soft-delete';
@Injectable()
export class UserService {
private userRepo: SoftDeleteRepository<User>;
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {
this.userRepo = withSoftDelete(userRepository);
}
async deleteUser(id: number) {
await this.userRepo.softDelete(id);
}
async restoreUser(id: number) {
await this.userRepo.restore(id);
}
}API Reference
Core Functions
softDelete(repository, criteria, options?)
Soft delete entities by setting deletedAt to current timestamp.
Parameters:
repository: Repository<T>- TypeORM repositorycriteria: string | number | array | FindOptionsWhere<T>- Entity ID(s) or where clauseoptions?: { validateExists?: boolean }- Optional validation
Returns: Promise<{ affected: number }>
Examples:
// Single ID
await softDelete(repo, 123);
// Multiple IDs
await softDelete(repo, [1, 2, 3]);
// Where clause
await softDelete(repo, { email: '[email protected]' });
// With validation
await softDelete(repo, 123, { validateExists: true });restore(repository, criteria, options?)
Restore soft-deleted entities by setting deletedAt back to null.
Parameters: Same as softDelete()
Returns: Promise<{ affected: number }>
Examples:
// Restore single user
await restore(userRepo, 123);
// Restore multiple users
await restore(userRepo, [1, 2, 3]);
// Restore with validation
await restore(userRepo, 123, { validateExists: true });forceDelete(repository, criteria)
Permanently delete soft-deleted entities (hard delete). Only deletes records where deletedAt IS NOT NULL.
Safety: This prevents accidentally hard-deleting active records.
Examples:
// Permanently delete soft-deleted user
await forceDelete(userRepo, 123);
// Permanently delete multiple
await forceDelete(userRepo, [1, 2, 3]);findWithDeleted(repository, options?)
Find entities including soft-deleted ones.
Examples:
// Find all users including deleted
const allUsers = await findWithDeleted(userRepo);
// Find with additional options
const users = await findWithDeleted(userRepo, {
where: { role: 'admin' },
take: 10,
});findOnlyDeleted(repository, options?)
Find only soft-deleted entities.
Examples:
// Find all deleted users
const deletedUsers = await findOnlyDeleted(userRepo);
// Find deleted with filter
const deletedAdmins = await findOnlyDeleted(userRepo, {
where: { role: 'admin' },
});count(repository, options?)
Count entities with optional inclusion of soft-deleted.
Examples:
// Count active users
const activeCount = await count(userRepo);
// Count all users including deleted
const totalCount = await count(userRepo, { includeDeleted: true });
// Count with filter
const adminCount = await count(userRepo, {
where: { role: 'admin' },
includeDeleted: false,
});isSoftDeleted(repository, id)
Check if entity is soft-deleted.
Examples:
if (await isSoftDeleted(userRepo, 123)) {
console.log('User is deleted');
}Pagination Integration
Works seamlessly with @nest-toolbox/typeorm-paginate:
import { rowsWithDeleted, rowsOnlyDeleted } from '@nest-toolbox/typeorm-soft-delete';
// Paginate excluding deleted (default)
for await (const row of rowsWithDeleted({
repository: userRepo,
where: {},
limit: 100,
})) {
console.log(row.data, row.progress);
}
// Paginate including deleted
for await (const row of rowsWithDeleted({
repository: userRepo,
where: {},
includeDeleted: true,
})) {
console.log(row.data);
}
// Paginate only deleted
for await (const row of rowsOnlyDeleted({
repository: userRepo,
where: {},
})) {
console.log('Deleted:', row.data);
}Optional Repository Wrapper
For those who prefer method-based API:
import { withSoftDelete, SoftDeleteRepository } from '@nest-toolbox/typeorm-soft-delete';
const userRepo: SoftDeleteRepository<User> = withSoftDelete(userRepository);
// All soft delete methods
await userRepo.softDelete(123);
await userRepo.restore(123);
await userRepo.forceDelete(123);
const deleted = await userRepo.findOnlyDeleted();
const all = await userRepo.findWithDeleted();
const isDeleted = await userRepo.isSoftDeleted(123);
// Standard repository methods still work
const users = await userRepo.find({ where: { active: true } });
const user = await userRepo.findOne({ where: { id: 1 } });
await userRepo.save(newUser);Database Setup
Ensure your database table has a deletedAt column:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
deleted_at TIMESTAMP NULL, -- NULL = active, NOT NULL = deleted
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Recommended: Index for performance
CREATE INDEX idx_users_deleted_at ON users(deleted_at);TypeORM will automatically handle the column when you use @DeleteDateColumn():
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@DeleteDateColumn() // Creates nullable timestamp column
deletedAt?: Date;
}Best Practices
1. Always Use @DeleteDateColumn
The package requires entities to have @DeleteDateColumn() decorator:
@Entity()
export class User {
@DeleteDateColumn()
deletedAt?: Date; // Required for soft delete functionality
}2. Add Database Index
Index the deletedAt column for better query performance:
CREATE INDEX idx_table_deleted_at ON your_table(deleted_at);3. Use forceDelete() Carefully
Permanent deletion cannot be undone. The package only allows force deletion of already soft-deleted records as a safety measure:
// Safe: Only permanently deletes records that are already soft-deleted
await forceDelete(repo, id);4. Consider Cascading
Soft delete related entities to maintain referential integrity:
async deleteUserWithPosts(userId: number) {
// Soft delete user
await softDelete(userRepo, userId);
// Soft delete user's posts
await softDelete(postRepo, { userId });
}5. Regular Cleanup
Schedule jobs to permanently delete old soft-deleted records:
@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);
}
}Migration from TypeORM Built-in
TypeORM has basic soft delete support via repository.softDelete(). This package provides enhanced functionality:
TypeORM Built-in:
- ✅ Basic soft delete with
softDelete(id) - ❌ No restore functionality
- ❌ No query utilities for deleted records
- ❌ No pagination integration
- ❌ Limited type safety
This Package:
- ✅ Enhanced soft delete operations
- ✅ Restore functionality
- ✅ Query utilities (
findOnlyDeleted,findWithDeleted) - ✅ Pagination integration
- ✅ Better type safety and validation
- ✅ Safety checks (forceDelete only deletes soft-deleted records)
Before:
await repository.softDelete(id);
const deleted = await repository.find({ withDeleted: true });After:
await softDelete(repository, id);
const deleted = await findOnlyDeleted(repository);
// Or restore them
await restore(repository, id);Advanced Usage
Batch Operations
Process large numbers of records efficiently:
const userIds = [1, 2, 3, 4, 5, /* ... hundreds more */];
// Soft delete in bulk
const result = await softDelete(userRepo, userIds);
console.log(`Deleted ${result.affected} users`);Conditional Soft Delete
Use FindOptionsWhere for complex conditions:
// Soft delete all inactive users
await softDelete(userRepo, { active: false });
// Soft delete users by email domain
await softDelete(userRepo, { email: Like('%@old-domain.com') });Validation
Ensure entities exist before deletion:
try {
await softDelete(userRepo, 999, { validateExists: true });
} catch (error) {
console.error('User not found or already deleted');
}TypeScript Support
Full TypeScript support with strict typing:
import type {
SoftDeleteCriteria,
SoftDeleteResult,
SoftDeleteOptions,
RestoreOptions,
CountOptions,
PaginatedRow,
} from '@nest-toolbox/typeorm-soft-delete';
// Type-safe operations
const result: SoftDeleteResult = await softDelete(repo, 1);
console.log(`Affected: ${result.affected}`);Error Handling
The package provides clear error messages:
// Missing @DeleteDateColumn
await softDelete(repoWithoutColumn, 1);
// Error: Entity User does not have @DeleteDateColumn.
// Soft delete requires a column decorated with @DeleteDateColumn.
// Entity not found (with validation)
await restore(repo, 999, { validateExists: true });
// Error: Entity not found or not soft-deleted
// Check before operations
if (!supportsSoftDelete(repo)) {
console.error('Repository does not support soft delete');
}Testing
Mock repositories in your tests:
import { vi } from 'vitest';
const mockRepo = {
metadata: {
deleteDateColumn: { databaseName: 'deletedAt' },
primaryColumns: [{ propertyName: 'id' }],
},
createQueryBuilder: vi.fn().mockReturnValue({
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
andWhere: vi.fn().mockReturnThis(),
execute: vi.fn().mockResolvedValue({ affected: 1 }),
}),
} as any;
const result = await softDelete(mockRepo, 1);
expect(result.affected).toBe(1);Contributing
Contributions are welcome! Please check out the main repository for contributing guidelines.
License
MIT
Support
For issues and questions, please visit the GitHub Issues page.
