chore: document project-wide soft delete pattern and add deleted_by column to all tables

This commit is contained in:
2026-06-11 20:36:47 +00:00
parent d0e2075e6d
commit 9a93debfc9
37 changed files with 1690 additions and 266 deletions
+13 -1
View File
@@ -187,7 +187,19 @@ The `comments` table uses `(target_type, target_uid)` so the same `_comment_sect
## Media (attachments) soft delete
The profile **Media** tab lists every attachment a user uploaded (`get_user_media`); a single upload can be removed via `POST /media/{uid}/delete` (owner or admin) as a **soft delete** - it stamps a `deleted_at` column WITHOUT touching the `target_type`/`target_uid` relation or unlinking the file, so the item disappears from every display surface (the gallery AND its parent post/project) yet stays restorable. The three user-facing read paths (`get_attachments`, `get_attachments_batch`, `get_user_media`) filter `deleted_at IS NULL`; the hard-delete cascades (`delete_attachments_for`, `delete_target_attachments`) stay UNFILTERED. The only hard deletes from the UI are admin **Media** trash purge (`/admin/media`, `POST /admin/media/{uid}/purge`) and parent-object deletion. See `AGENTS.md` -> "Profile media gallery and soft-deleted attachments".
The profile **Media** tab lists every attachment a user uploaded (`get_user_media`); a single upload can be removed via `POST /media/{uid}/delete` (owner or admin) as a **soft delete** - it stamps `deleted_at`/`deleted_by` WITHOUT touching the `target_type`/`target_uid` relation or unlinking the file, so the item disappears from every display surface (the gallery AND its parent post/project) yet stays restorable. The three user-facing read paths (`get_attachments`, `get_attachments_batch`, `get_user_media`) filter `deleted_at IS NULL`; the hard-delete cascades (`delete_attachments_for`, `delete_target_attachments`) stay UNFILTERED. The only hard deletes from the UI are admin trash purge and parent-object deletion. See `AGENTS.md` -> "Profile media gallery and soft-deleted attachments".
## Project-wide soft delete (hard rule)
Attachments are one instance of a platform-wide model: **every removal is a soft delete; only garbage collection is a hard delete.** Removable rows carry `deleted_at` (ISO timestamp) + `deleted_by` (actor uid or `system`); a live row has `deleted_at = NULL` and every list/count read filters `deleted_at IS NULL`. The table set is `database.SOFT_DELETE_TABLES` (content, engagement toggles, sessions, container instances/schedules, Devii task/lesson/virtual-tool stores, customizations, fork relations); `init_db` ensures both columns + an `idx_<t>_deleted` index per table. Core primitives in `database.py`: `soft_delete`, `soft_delete_in`, `restore`, `purge`, `list_deleted`/`count_deleted`, and `restore_event`/`purge_event` (act across all tables sharing one `deleted_at` stamp). `resolve_by_slug(..., include_deleted=)` and `paginate(...)` filter conditionally; `seo._collect` too.
Rules when touching this area:
- **Every INSERT into a soft-deletable table MUST write `deleted_at: None, deleted_by: None`.** `dataset.find(deleted_at=None)` on a table missing the column matches NOTHING (false predicate), silently hiding all rows on a fresh DB - the born-live insert is what creates the column.
- **Any new read** (find/count/query) of a soft-deletable table MUST filter `deleted_at IS NULL`. Use the central helpers/chokepoints instead of inline deletes.
- **Toggles revive, never duplicate:** look up the physical row ignoring `deleted_at`, stamp on toggle-off, clear on re-toggle.
- **Cascades share one `stamp`** (`content.delete_content_item`) so the event restores/purges atomically; pass `deleted_by = actor`.
- **GC stays HARD** (job sweep, metrics ring, usage-ledger prune/reset, expired-session cleanup, fork rollback, news-sync image refresh, admin Purge). Logout is soft (auditable); only expiry GC is hard.
- Admin **Trash** at `/admin/trash` (`AdminTrashOut`, `admin_trash.html`) restores/purges by event. Admin-only docs: `docs/soft-delete.html`. See `AGENTS.md` -> "Project-wide soft delete".
## Testing