45 lines
3.7 KiB
Markdown
45 lines
3.7 KiB
Markdown
# 12 — Soft Deletes, Retention & Culling
|
|
|
|
Deletes in SoloPM are soft (`deleted_at`) so mistakes are recoverable. The **culling job** is the only code path that hard-deletes, and it is responsible for cascades **and disk files** — which is why DB-level `ON DELETE CASCADE` is deliberately absent.
|
|
|
|
## Lifecycle
|
|
|
|
- [ ] Soft-deleted rows are invisible to every normal query (`deleted_at IS NULL` filters, [01-architecture.md](01-architecture.md)) and leave the search index immediately ([10-search.md](10-search.md))
|
|
- [ ] Rows older than `CULL_AFTER_DAYS` (default 30) since `deleted_at` are hard-deleted by the cull job
|
|
- [ ] Restore before cull = clearing `deleted_at` (no UI in v1; document the SQL in ops notes)
|
|
|
|
## Cull job
|
|
|
|
- [ ] Runs as a `cull` job scheduled daily ([03-domain-events.md](03-domain-events.md)) + CLI `solopm cull [--dry-run]` + `POST /admin/cull?dry_run=`
|
|
- [ ] **Dry-run mode**: full traversal, returns/logs the would-delete report `{resource_type: count}` + file list, deletes nothing — required before trusting the job
|
|
- [ ] Deletion order respects FK dependencies (children first)
|
|
- [ ] Cascade scope per culled resource:
|
|
- [ ] **project** → its epics, issues (and their cascade), labels, members, favorites, dependencies (both directions), wiki pages, links, attachments (+files), webhooks + deliveries, receipts
|
|
- [ ] **issue** → its comments (and their cascade), issue_labels, watchers, blocker edges (both directions), sub-issue links (children's `parent_issue_id` → NULL), links, attachments (+files)
|
|
- [ ] **epic** → member issues' `epic_id`/`epic_rank` → NULL (issues survive)
|
|
- [ ] **comment** → its links, attachments (+files)
|
|
- [ ] **label** → its issue_labels pivots
|
|
- [ ] **wiki page** → row only
|
|
- [ ] **attachment** → row **and the file on disk** — file deletion and row deletion happen together; a failed file unlink (other than not-exists) aborts that row's cull and is logged
|
|
- [ ] **User culling** (soft-deleted users past retention):
|
|
- [ ] Reassign authored content to the **zombie user (id 2)**: issues.creator_id/assignee_id, comments.author_id, epics.creator_id, labels.created_by, attachments.uploader_id, links.created_by, wiki created_by/updated_by, project creator_id
|
|
- [ ] Precondition (already enforced at delete time): not sole owner of any live project
|
|
- [ ] Delete their favorites, watches, notifications, oauth_accounts, api_tokens, sessions
|
|
- [ ] `audit_log.initiated_by` keeps the original user id — audit is immutable; UI renders unknown ids as "Deleted User"
|
|
- [ ] Non-soft-delete retention in the same job:
|
|
- [ ] `notifications` older than 90 days → hard delete
|
|
- [ ] `events` processed > 30 days ago → hard delete
|
|
- [ ] `jobs` done/dead > 30 days ago → hard delete
|
|
- [ ] `incoming_webhook_receipts` > 90 days → hard delete
|
|
- [ ] `outgoing_webhook_deliveries` > 90 days → hard delete
|
|
- [ ] `audit_log` → **kept forever in v1** ([17-decisions.md](17-decisions.md))
|
|
- [ ] expired `sessions` (scs handles) and revoked/expired `api_tokens` > 30 days → hard delete
|
|
- [ ] The job logs a summary line per run (counts per resource type, duration, dry-run flag) and records a `cull` job row with the report in `payload`
|
|
|
|
## Safety checklist
|
|
|
|
- [ ] Dry-run output reviewed before first real run in any environment
|
|
- [ ] File deletions restricted to paths under `UPLOAD_DIR` matching the stored relative path (no traversal)
|
|
- [ ] Whole-project cull wrapped in one transaction per project (files unlinked after commit; unlink failures logged for manual sweep)
|
|
- [ ] Integration tests: cull a project → zero orphan rows (assert per table) and zero orphan files; cull a user → zombie owns their content, audit rows untouched
|