Skip to content
NewFrom Mongoose to SQL6 min read

Soft Delete

A soft delete stamps a row as deleted instead of removing it. The row drops out of queries, but you can still restore or audit it, and rows pointing at it stay valid. Mark one field softDelete to enable it:

import { Entity, Id, Field } from 'uql-orm';
@Entity()
export class User {
@Id({ type: Number }) id?: number;
@Field({ type: String }) name?: string | null;
@Field({ type: 'timestamptz', softDelete: true }) deletedAt?: Date | null;
}

softDelete: true stamps new Date(). To stamp something else, pass a value, SQL such as currentTimestamp, or a callback, like () => Date.now() for an epoch-millis column. An entity has at most one soft-delete field.

A delete becomes an update that stamps only rows not already deleted:

You write
await pool.deleteOneById(User, 1);
UPDATE "User" SET "deletedAt" = $1 WHERE "id" = $2 AND "deletedAt" IS NULL

Every read leaves deleted rows out, through the built-in softDelete query filter, which combines with any filters you define:

You write
const users = await pool.findMany(User, { $select: { id: true, name: true } });
SELECT "id", "name" FROM "User" WHERE "deletedAt" IS NULL

The filter steps aside when your $where names the field, so reading only deleted rows is a plain query, which works over HTTP too. To read live and deleted rows together, pass withDeleted(), a server-side option that never travels over the wire. It covers the read’s own rows; populated relations still hide their deleted ones. Given options, withDeleted(opts) returns them with soft-deleted rows let in.

import { withDeleted } from 'uql-orm';
const trashed = await pool.findMany(User, {
$where: { deletedAt: { $ne: null } },
});
const all = await pool.findMany(User, {}, withDeleted());

restoreOneById and restoreMany set the field back to null, finding deleted rows despite the filter. { hardDelete: true } removes rows for good, already-deleted ones included:

await pool.restoreMany(User, { $where: { companyId: 5 } });
await pool.deleteOneById(User, 1, { hardDelete: true });

A soft delete cascades only to related entities that also soft-delete, and keeps the rest. A restore does not cascade, so restore related rows yourself.