Skip to content

feat(api): soft-delete projects — recoverable + audited (DELETE /projects/:id, restore) - #257

Merged
thewrz merged 5 commits into
mainfrom
feat/api-project-soft-delete
Jun 24, 2026
Merged

feat(api): soft-delete projects — recoverable + audited (DELETE /projects/:id, restore)#257
thewrz merged 5 commits into
mainfrom
feat/api-project-soft-delete

Conversation

@thewrz

@thewrz thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Why

Projects had no delete at all. You asked for a recoverable delete with an audit (who + when) — not a destructive one. This delivers that, mirroring the spec-withdraw custody pattern (ADR-030).

What

  • DELETE /projects/:id — soft-deletes (tombstone): sets deleted_at + deleted_by, hides the project from GET /projects, but it stays readable by id and recoverable. Body carries the actor: { deletedBy } (required). Idempotent: re-deleting preserves the original who/when.
  • POST /projects/:id/restore — clears the tombstone; the project reappears.
  • deleted_by is caller-supplied free text (no FK) — SpecR has no auth/user model yet (feat(api): Phase 5f — authentication + multi-tenant (JWT, org isolation) #43). When auth lands it populates this from the session. Rationale in ADR-031.
  • Migration 034 (reversible) adds deleted_at/deleted_by; GET /projects/:id surfaces them.

Testing

  • 12 new integration tests (soft-delete→200 + vanishes from list + still GET-able by id; restore→200 + reappears; missing deletedBy→400; malformed id→400; unknown→404; idempotent re-delete preserves who/when)
  • pnpm lint clean; OpenAPI contract gate green
  • CI green
  • Demo wiring (delete/restore button) — follow-up, not in this PR

🤖 Co-authored by Claude Opus 4.8.

thewrz and others added 4 commits June 24, 2026 09:27
Projects are soft-deleted (tombstone deleted_at, hidden from the listing,
reversible via restore), mirroring ADR-030's spec-withdraw custody model and
adding an audit actor. deleted_by is caller-supplied free text (no FK) — there
is no user/auth model yet (#43 deferred); when auth lands it populates from the
session. Audit = who (deleted_by) + when (deleted_at).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migration 034 adds reversible projects.deleted_at/deleted_by (nullable, no FK).
softDeleteProject tombstones idempotently (COALESCE keep-first, so a re-delete
returns the original who/when), restoreProject clears the tombstone (idempotent),
listProjects filters deleted_at IS NULL, and findProjectById surfaces the
tombstone. All parameterized; pg errors wrapped in DatabaseError. Exported from
the db barrel. ADR-031.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…i + tests

Adds the soft-delete (ADR-031) HTTP surface:
- DELETE /projects/:id — body { deletedBy } required (400 if missing/empty),
  malformed id 400, unknown 404; 200 { projectId, deletedAt, deletedBy };
  idempotent re-delete returns the existing tombstone.
- POST /projects/:id/restore — clears the tombstone; idempotent; 400/404 guards.
- openapi.yaml: both operations documented (3.1 nullable type [string, 'null'])
  and deletedAt/deletedBy added to the project response schema.
- Integration tests (TDD, native fetch harness) cover soft-delete + list
  hiding + GET-by-id surfacing, restore, idempotent re-delete, and the
  400/404 guards; contract allowlist + ProjectWithToc mock fixtures updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A second DELETE preserves the original deleted_at/deleted_by via COALESCE but
should not bump updated_at — the project's state didn't change. Only set
updated_at when transitioning a live project to deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thewrz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 10 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f8a9001a-3452-4cd9-9d4a-7581a9c323d9

📥 Commits

Reviewing files that changed from the base of the PR and between 559160e and ebcfe6a.

📒 Files selected for processing (12)
  • docs/adr/031-project-soft-delete-audit.md
  • openapi.yaml
  • src/api/contract.integration.test.ts
  • src/api/generate.test.ts
  • src/api/projects.integration.test.ts
  • src/api/projects.test.ts
  • src/api/projects.ts
  • src/api/references.test.ts
  • src/api/router.ts
  • src/db/index.ts
  • src/db/migrations/034_add_project_soft_delete.ts
  • src/db/queries/projects.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-project-soft-delete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Symmetric to the soft-delete fix: restoring an already-active project (no
tombstone to clear) must not bump updated_at. Only set it when deleted_at was
actually non-null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Review note: CodeRabbit was a rate-limited no-op, so Codex (GPT-5.5 xhigh) ran as the gate. 1 [P3] finding, fixed in ebcfe6a:

  • restore was not idempotent on updated_atPOST /projects/:id/restore on an already-active project bumped updated_at even though no tombstone was cleared. Now guarded (updated_at = CASE WHEN deleted_at IS NOT NULL THEN now() ELSE updated_at END), symmetric to the soft-delete fix. 40/40 project tests green.

@thewrz
thewrz merged commit b6d7533 into main Jun 24, 2026
5 checks passed
@thewrz
thewrz deleted the feat/api-project-soft-delete branch June 24, 2026 18:11
thewrz added a commit that referenced this pull request Jul 1, 2026
#325)

* docs(readme): sync capabilities to last month of merged PRs

Reflect shipped work in the README's "Included Today", "API Surface", and MCP
tool table, validated against the merged diffs and current main:

- PDF ingest (text-layer + OCR + font-encoding recovery) accepted by POST /parse
  (#287, #290, #311)
- coordination / E&O report + submittal register (#241, #269, #277, #282, #283,
  #284) and article-role tagging (#273)
- onboarding pipeline: library import, editability review/override, reclassify,
  finalize/reopen, open-comments (#243, #247, #248, #249, #272)
- spec/project soft-delete + restore (#257, #313), document concurrency (#197),
  revision/addendum manual rendering (#221), numbering profiles (#317, #322)
- add missing MCP tools get_numbering_profile, submittal_register,
  open_comments_report; document GET /docs (Scalar) (#213, #285)
- add Example Client pointer to examples/web_ui_demo (#225)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(roadmap): move shipped work to done; re-date to 2026-07-01

Reconcile the roadmap with merged reality (was stamped 2026-06-17). Moved from
planned/in-progress to Included, each validated against the diff:

- PDF ingest (#287, #290, #311) — remove from "Later"
- deep paragraph nesting pr6/pr7 (#215)
- revision nomenclature (#216) + revision/addendum manual rendering (#221) —
  the two "Near Term" Phase 2e items are done
- coordination / E&O report, required-sections, article-role, submittal register
  (#239, #241, #269, #273, #277, #282, #283, #284) — new "Coordination and
  Semantics" section; removed "coordination report" from planned Phase 4
- onboarding APIs (#243, #247, #248, #249, #272) — API done; UI remains planned
- soft-delete/withdraw (#257, #313), section-number format (#266, #271),
  external-content associations (#242), structural numbering profiles (#317)

Kept as planned (foundation only): header/footer composition (#222, #314) and
keynote surfacing (#315) — DB/AST exist, no resolution/render/export yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(architecture): reflect merged structural changes

Update the architecture spec for shipped work, validated against the diffs and
current schema/routes:

- Tech Stack + Data Flow: Parse — PDF text-layer (unpdf/pdfjs-dist) + OCR
  (tesseract.js/@napi-rs/canvas) path and numberingProfileId override (#287,
  #290, #311, #317; ADR-034, ADR-039)
- DB schema — specs.onboarding_status/withdrawn_at, projects.section_number_format
  /deleted_at/deleted_by, paragraphs.source_facts/classification/
  editability_override; "Additional tables" summary for editing_conventions,
  paragraph_associations, required_sections, keynotes, header_footer_configs,
  numbering_profiles, revision_nomenclature_profiles (foundation-only tables
  flagged) (ADR-021/022/023/028/031/032; #187, #242)
- new Coordination Report / E&O section (finding vocabulary) and Document
  Concurrency section (locks/optimistic/lifecycle) (#197, #241, #269, #277,
  #282, #283, #284; ADR-018, ADR-033/035/036/037)
- AST meta.articleRole (#273, ADR-033); API-surface note pointing at the
  CI-enforced openapi.yaml + GET /docs; refreshed MCP tool list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant