Soft Delete
Archive records instead of permanently deleting them, with built-in recovery.
Mental Model
In SolidX, soft delete turns deletion into a recoverable record lifecycle state.
- Active records remain visible in normal queries.
- Archived records stay in the database with deleted metadata.
- Recovery clears the deleted state and brings the record back into normal application flows.
Soft delete hides records from day-to-day usage without losing the data needed for recovery, audit, or archive workflows.
Soft delete archives a record rather than removing it from the database. The row stays in place with a deleted_at timestamp; all normal queries filter it out automatically. Records can be recovered at any time.
Enabling Soft Delete
Set enableSoftDelete: true in the model's metadata JSON:
{
"modelName": "book",
"enableSoftDelete": true
}Automatic CRUD Support
No service or controller changes needed - the CRUD layer picks this up automatically.
Database Impact
CommonEntity (the base all SolidX entities extend) already declares two columns for this:
| Column | TypeORM decorator | Active | Deleted |
|---|---|---|---|
deleted_at | @DeleteDateColumn | NULL | timestamp |
deleted_tracker | @Column | "not-deleted" | "deleted" |
TypeORM appends WHERE deleted_at IS NULL to all queries by default, so archived records are invisible without extra effort.
Why `deleted_tracker` Exists
deleted_tracker solves a uniqueness problem: SQL unique constraints don't know about TypeORM's soft-delete filter. Without it, soft-deleting a book and then creating a new one with the same ISBN would hit a constraint violation. By including deleted_tracker in composite unique indexes, the DB sees ("isbn-123", "deleted") and ("isbn-123", "not-deleted") as distinct rows.
Code Generation: Composite Unique Indexes
When solidctl generates an entity for a soft-delete-enabled model, any field marked unique gets a composite @Index with deletedTracker instead of a plain @Unique. Example from Book:
@Entity('lm_book')
@Index(['isbn', 'deletedTracker'], { unique: true })
@Index(['bookUserKey', 'deletedTracker'], { unique: true })
export class Book extends CommonEntity {
// ...
}Manual Unique Fields
If you add a unique field to a soft-delete-enabled model manually, follow this same pattern - don't use @Unique or a single-column unique @Index.
Runtime Behaviour
Calls TypeORM's softRemove(), setting deleted_at and flipping deleted_tracker to "deleted".
REST: DELETE /api/{model}/{id} · DELETE /api/{model}/bulk
Service: bookService.delete(id) · bookService.deleteMany([id1, id2])
Use showSoftDeleted on find():
// active only (default)
await bookService.find({});
// active + archived
await bookService.find({ showSoftDeleted: 'inclusive' });
// archived only - e.g. for a trash view
await bookService.find({ showSoftDeleted: 'exclusive' });The same parameter works on the REST API: GET /api/book?showSoftDeleted=inclusive.
Clears deleted_at and resets deleted_tracker to "not-deleted". If a new active record already holds the same unique value as the one being recovered, the service throws a conflict error - resolve the conflict first.
await bookService.recover(id);
await bookService.recoverMany([id1, id2, id3]);REST: POST /api/{model}/recover/{id} · POST /api/{model}/recover/bulk (body: array of IDs)
See Delete endpoint and Recover endpoint for full request/response examples.
UI
When enableSoftDelete: true, the list view gains a "Show Archived Records" checkbox in the settings (cog) menu. Toggling it sends showSoftDeleted: "inclusive" with the list query - archived rows appear greyed out alongside active ones. Each archived row shows a Recover icon instead of the usual context menu; selecting multiple archived rows surfaces a bulk Recover button in the toolbar.

