npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nest-toolbox/typeorm-soft-delete

v1.8.1

Published

Soft delete utilities for TypeORM with enhanced DX

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-metadata

Quick 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 repository
  • criteria: string | number | array | FindOptionsWhere<T> - Entity ID(s) or where clause
  • options?: { 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.