Soft Delete
Soft-delete marks a record as deleted instead of “physically” removing the row, which is what keeps an audit trail readable, a mistaken delete recoverable, and the rows pointing at it from going dangling.
Configuration
Section titled “Configuration”Mark one field with softDelete in its @Field options. Its presence makes the entity soft-deletable - there’s no separate @Entity flag. The value controls what gets stamped on delete: pass true to stamp the current timestamp (new Date()), or a callback (e.g. () => Date.now() for an epoch-millis column) to stamp anything else. An entity may have at most one soft-delete field.
import { Entity, Id, Field } from 'uql-orm';
@Entity()export class User { @Id({ type: Number }) id?: number;
@Field({ type: String }) name?: string;
/** * `softDelete` marks this as the field stamped on delete. `softDelete: true` stamps the * current timestamp (`new Date()`) - so for this column it's equivalent to the callback below; * pass a callback only when you need a different value (e.g. `() => Date.now()`). */ @Field({ type: 'timestamptz', softDelete: true, }) deletedAt?: Date;}Automatic Transformation
Section titled “Automatic Transformation”When soft-delete is enabled, UQL automatically transforms delete operations into update operations and adds filters to all find operations.
1. Deleting a record
Section titled “1. Deleting a record”await pool.deleteOneById(User, 1);-- Only already-live rows are stampedUPDATE "User" SET "deletedAt" = $1 WHERE "id" = $2 AND "deletedAt" IS NULL2. Querying records
Section titled “2. Querying records”By default, soft-deleted records are excluded from all queries.
const users = await pool.findMany(User, { $select: { id: true, name: true } });SELECT "id", "name" FROM "User" WHERE "deletedAt" IS NULLUnder the hood, soft-delete is the built-in softDelete query filter (auto-registered from the @Field({ softDelete }) above), so it composes with any other filters you define.
Reading trashed rows
Section titled “Reading trashed rows”Only trashed - just constrain the field in your $where. The soft-delete filter steps aside for any key you set, so this is a plain, serializable query (no helper, no special option):
const trashed = await pool.findMany(User, { $where: { deletedAt: { $ne: null } },});// generates: ... WHERE deletedAt IS NOT NULLLive + trashed - this fully turns the filter off, so it’s a server-side option (withDeleted()), not part of the serializable query - the same reason filter bypass never crosses the HTTP wire:
import { withDeleted } from 'uql-orm';
const all = await pool.findMany(User, { $where: {/* ... */} }, withDeleted());Restoring
Section titled “Restoring”restore un-deletes rows by setting the soft-delete field back to null (it finds trashed rows even though the filter is normally on):
await pool.restoreOneById(User, 1);await pool.restoreMany(User, { $where: { companyId: 5 } });Hard Delete
Section titled “Hard Delete”Pass { hardDelete: true } to permanently remove rows instead of stamping them (this ignores the soft-delete filter, so already-deleted rows are removed too):
await pool.deleteOneById(User, 1, { hardDelete: true });await pool.deleteMany(User, { $where: { companyId: 5 } }, { hardDelete: true });Soft-delete restore does not cascade - restore related entities explicitly if needed.