Skip to content

Soft Delete

Soft-delete allows you to mark records as “deleted” instead of physically removing them from the database. This is essential for auditing, data recovery, and maintaining relational integrity.

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()
id?: number;
@Field()
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;
}

When soft-delete is enabled, UQL automatically transforms delete operations into update operations and adds filters to all find operations.

You write
await querier.deleteOneById(User, 1);
Generated SQL
-- Only already-live rows are stamped
UPDATE "User" SET "deletedAt" = $1 WHERE "id" = $2 AND "deletedAt" IS NULL

By default, soft-deleted records are excluded from all queries.

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

Under 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.

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 querier.findMany(User, { $where: { deletedAt: { $ne: null } } });
// generates: ... WHERE deletedAt IS NOT NULL

Live + 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 querier.findMany(User, { $where: { /* ... */ } }, withDeleted());

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 querier.restoreOneById(User, 1);
await querier.restoreMany(User, { $where: { companyId: 5 } });

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 querier.deleteOneById(User, 1, { hardDelete: true });
await querier.deleteMany(User, { $where: { companyId: 5 } }, { hardDelete: true });

Soft-delete restore does not cascade - restore related entities explicitly if needed.