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

@thenkei/prisma-soft-delete-extension

v1.1.0

Published

Soft-delete (paranoid) support for Prisma v7+ via client extensions

Downloads

282

Readme

@thenkei/prisma-soft-delete-extension

Soft-delete support for Prisma v7+ via client extensions.

This extension turns configured delete() / deleteMany() calls into timestamp updates, filters soft-deleted rows from the normal Prisma query surface, adds lifecycle helpers for restore and intentional hard delete, and blocks write paths that would otherwise mutate deleted rows silently.

Installation

npm install @thenkei/prisma-soft-delete-extension

Peer dependencies:

  • @prisma/client >=7.0.0
  • prisma >=7.0.0

Quick Start

Add a nullable DateTime field to each soft-deleted model:

model User {
  id        Int       @id @default(autoincrement())
  name      String
  deletedAt DateTime?
}

Extend your Prisma client:

import { PrismaClient } from '@prisma/client';
import {
  createSoftDeleteExtension,
  defineSoftDeleteConfig,
  withSoftDeleteTypes,
} from '@thenkei/prisma-soft-delete-extension';

const config = defineSoftDeleteConfig({
  models: {
    User: true,
  },
});

const prisma = withSoftDeleteTypes(
  new PrismaClient().$extends(createSoftDeleteExtension(config)),
  config
);

Basic lifecycle:

const user = await prisma.user.create({ data: { name: 'Alice' } });

await prisma.user.delete({ where: { id: user.id } });

const hidden = await prisma.user.findUnique({ where: { id: user.id } });
// hidden === null

await prisma.user.restore({ where: { id: user.id } });

await prisma.user.delete({ where: { id: user.id } });
await prisma.user.hardDelete({ where: { id: user.id } });

Configuration

field

Global soft-delete field name. Defaults to deletedAt.

createSoftDeleteExtension({
  field: 'deletedAt',
  models: { User: true },
});

models

Model names must match the Prisma schema exactly.

  • true: use the global field
  • { field: 'customField' }: override the field for one model
createSoftDeleteExtension({
  models: {
    User: true,
    Post: { field: 'archivedAt' },
    Comment: { field: 'removedAt' },
  },
});

Models omitted from models are full passthrough.

If you want model-selective TypeScript support for includeSoftDeleted, preserve the config's model-name literals with either satisfies SoftDeleteConfig or defineSoftDeleteConfig():

import {
  createSoftDeleteExtension,
  defineSoftDeleteConfig,
  type SoftDeleteConfig,
} from '@thenkei/prisma-soft-delete-extension';

const configWithSatisfies = {
  models: {
    User: true,
    Post: { field: 'archivedAt' },
  },
} satisfies SoftDeleteConfig;

const configWithHelper = defineSoftDeleteConfig({
  models: {
    User: true,
    Post: { field: 'archivedAt' },
  },
});

createSoftDeleteExtension(configWithSatisfies);
createSoftDeleteExtension(configWithHelper);

Behavior Matrix

Delete

| Operation | Configured model | Unconfigured model | |-----------|------------------|--------------------| | delete() | Sets the soft-delete field to the current timestamp | Physical delete | | deleteMany() | Sets the soft-delete field on matching rows | Physical delete |

Lifecycle

| Operation | Configured model | Unconfigured model | |-----------|------------------|--------------------| | restore() | Restores one previously deleted row; throws P2025 if the row is active or missing | Throws configuration error | | restoreMany() | Restores only deleted matching rows; returns { count: 0 } when none match | Throws configuration error | | hardDelete() | Physically deletes one previously deleted row; throws P2025 if the row is active or missing | Throws configuration error | | hardDeleteMany() | Physically deletes only deleted matching rows; returns { count: 0 } when none match | Throws configuration error |

Query

| Operation | Configured model behavior | |-----------|---------------------------| | findMany() | Excludes soft-deleted rows | | findFirst() | Excludes soft-deleted rows | | findFirstOrThrow() | Excludes soft-deleted rows | | findUnique() | Returns null for a soft-deleted row | | findUniqueOrThrow() | Throws P2025 for a soft-deleted row | | count() | Excludes soft-deleted rows | | aggregate() | Excludes soft-deleted rows | | groupBy() | Excludes soft-deleted rows through where; having stays caller-controlled |

Unconfigured models are passthrough for all query operations.

Update / Upsert

| Operation | Configured model behavior | |-----------|---------------------------| | root update() | Active-only by default; explicit deletedAt predicates override | | root updateMany() | Active-only by default; explicit deletedAt predicates override | | root updateManyAndReturn() | Active-only by default; explicit deletedAt predicates override | | root upsert() | Throws if the target row exists and is soft-deleted | | nested toMany update() | Active-only by default; explicit deletedAt predicates override | | nested toMany updateMany() | Active-only by default; explicit deletedAt predicates override | | nested toMany delete() | Throws to prevent accidental hard delete | | nested toMany deleteMany() | Throws to prevent accidental hard delete | | nested toMany upsert() | Throws if the target row exists and is soft-deleted | | nested toOne delete() | Throws to prevent accidental hard delete | | nested toOne update() | Throws | | nested toOne upsert() | Throws |

Nested Read Rules

  • Relation filters in where are rewritten recursively to exclude soft-deleted configured models.
  • include and select on configured toMany relations automatically add where: { deletedAt: null } unless you already supply a deletedAt predicate.
  • Included or selected configured toOne relations become null when the related row is soft-deleted.
  • Compound-unique findUnique() and findUniqueOrThrow() stay soft-delete aware.

includeSoftDeleted

Pass includeSoftDeleted: true to these operations:

  • findMany()
  • findFirst()
  • findFirstOrThrow()
  • findUnique()
  • findUniqueOrThrow()
  • count()
  • aggregate()
  • groupBy()

For TypeScript projects, wrap the final extended client in withSoftDeleteTypes(client, config) to expose includeSoftDeleted only on configured soft-delete models. Omitting the second argument keeps the previous broad typing behavior and widens all model delegates.

Examples:

const allUsers = await prisma.user.findMany({
  includeSoftDeleted: true,
});

const totalUsers = await prisma.user.count({
  includeSoftDeleted: true,
});

const groupedUsers = await prisma.user.groupBy({
  by: ['name'],
  _count: { _all: true },
  includeSoftDeleted: true,
});

// Property 'includeSoftDeleted' does not exist on Tag args because Tag is not configured.
// await prisma.tag.findMany({ includeSoftDeleted: true });

When includeSoftDeleted: true is set, nested relation filtering is skipped for that operation as well.

This option does not affect lifecycle methods, raw queries, or write hardening.

Nested Delete Guardrails

Nested relation delete and deleteMany calls against configured soft-delete models are rejected. Allowing those Prisma envelopes through would physically remove rows and bypass the extension's safety guarantees.

For configured models:

  • use the model delegate delete() / deleteMany() to perform a soft delete
  • use hardDelete() / hardDeleteMany() only when you intend a permanent removal

Unconfigured models remain passthrough, including nested deletes.

Restore and Hard Delete Examples

Restore one row:

await prisma.user.restore({
  where: { id: 1 },
});

Restore many deleted rows:

await prisma.user.restoreMany({
  where: { name: 'archived-user' },
});

Hard-delete one row that has already been soft-deleted:

await prisma.user.hardDelete({
  where: { id: 1 },
});

Hard-delete many deleted rows:

await prisma.user.hardDeleteMany({
  where: {
    deletedAt: { not: null },
  },
});

Raw Queries

These APIs are explicit passthrough and receive no soft-delete behavior:

  • $queryRaw
  • $queryRawUnsafe
  • $executeRaw
  • $executeRawUnsafe

findRaw is also unsupported by this package. The extension targets Prisma's relational model query surface, not provider-specific raw document APIs.

Schema Requirements

Soft-delete fields must be nullable DateTime fields with no default:

model User {
  id        Int       @id @default(autoincrement())
  name      String
  deletedAt DateTime?
}

If a configured model can be returned through a toOne relation that you include or select, make that relation optional in Prisma so runtime nulling is type-safe:

model Comment {
  id       Int    @id @default(autoincrement())
  authorId Int?
  author   User?  @relation(fields: [authorId], references: [id])
}

Prisma Version

This package requires Prisma v7+ and uses Prisma.defineExtension.

Release Process

Versioning and publishing are managed by GitHub Actions and semantic-release.

  • Pull requests into main run CI.
  • Pushes to main run CI and then semantic-release.
  • Do not manually bump package.json versions or hand-edit CHANGELOG.md for releases.
  • Use conventional commits so semantic-release can determine the correct version bump:
    • fix: for patch releases
    • feat: for minor releases
    • BREAKING CHANGE: or ! for major releases

Local release verification:

npm run ci
npm pack

See CHANGELOG.md for release history and UPGRADE.md for 0.x to 1.0.0 migration notes. The release workflow publishes to npm using secrets.NPM_TOKEN and creates the matching GitHub release automatically.

License

MIT

Contributing

  1. Install dependencies with npm install.
  2. Run npm run ci before you open a pull request.
  3. Use conventional commits so semantic-release can calculate the next version correctly.
  4. Do not manually bump package.json versions or hand-edit release entries in CHANGELOG.md.
  5. Keep changes small and focused, and update README.md or UPGRADE.md when public behavior changes.

Patch and minor releases are cut automatically from main by semantic-release after CI passes, so the only supported release input is a correctly formatted commit history.