Plugins

Soft delete

Turn delete() into a recoverable state change, filter deleted rows by default, and add restore helpers.

@better-drizzle/soft-delete extends the built-in methods with typed soft-delete controls, default visibility filtering, and restore helpers.

Install

npm install @better-drizzle/soft-delete
pnpm add @better-drizzle/soft-delete
yarn add @better-drizzle/soft-delete
bun add @better-drizzle/soft-delete

Usage

import { better } from 'better-drizzle';
import { softDelete } from '@better-drizzle/soft-delete';

const client = better(db, {
	schema,
	plugins: [
		softDelete({
			column: 'deletedAt',
			deletedByColumn: 'deletedById',
			defaults: {
				mode: 'soft',
				visibility: 'without',
			},
		}),
	],
});

Every option is optional. Defaults:

  • column: 'deletedAt'
  • deletedByColumn: 'deletedById'
  • defaults.mode: 'soft'
  • defaults.visibility: 'without'

Reads exclude deleted rows by default

findMany, findFirst, count, and exists filter out soft-deleted rows automatically:

await client.users.findMany(); // visible rows only

Opt in per query with the typed deleted arg:

await client.users.findMany({ deleted: 'with' }); // include deleted
await client.users.findMany({ deleted: 'only' }); // only deleted
await client.users.count({ deleted: 'with' });

Deleting and restoring

// Soft delete (default)
await client.users.delete({ where: { id: 1 } });

// Record who deleted it (when the column exists)
await client.users.delete({ where: { id: 1 }, deletedBy: 'admin_42' });

// Force a real, physical delete
await client.users.delete({ where: { id: 1 }, mode: 'hard' });

// Restore
await client.users.restore({ where: { id: 1 } });
await client.users.restoreById(1);

Behavior details

  • Models without the configured column are ignored automatically.
  • deletedBy is written only when the configured column exists on the model.
  • restore() and restoreById() bypass plugin transforms so they can always clear the soft-delete fields directly.
  • mode: 'hard' falls back to the built-in physical delete.

Need a one-off hard delete elsewhere?

Any delegate can bypass plugins for a single call with $withoutPlugins() — handy for repair scripts that must delete beneath the soft-delete layer.

Where it fits best

  • audit-sensitive systems
  • admin tools where deleted rows must remain inspectable
  • apps where "delete" should default to reversible

On this page