Skip to content

feat(documents): DELETE /api/documents/{id} with graph-cascade cleanup#105

Open
aksOps wants to merge 2 commits intomainfrom
feat/delete-document
Open

feat(documents): DELETE /api/documents/{id} with graph-cascade cleanup#105
aksOps wants to merge 2 commits intomainfrom
feat/delete-document

Conversation

@aksOps
Copy link
Copy Markdown
Contributor

@aksOps aksOps commented May 4, 2026

Summary

Today there is no way to remove a document once it has been uploaded. Users hit the documents.file_hash UNIQUE collision when they try to reindex (the supersede path bumps version but never frees the row), and a bad upload leaves orphan entities, relationships, claims, chunks, and embeddings in the knowledge graph forever.

This PR adds an authenticated DELETE /api/documents/{id} plus a per-row icon button on the documents list and a destructive button on the document detail page. The cascade runs in a single SQLite transaction and is idempotent: redeleting the same id is a 404, not a second 204.

Cascade order (single tx):

  1. relationships WHERE doc_id = ? — drop edges sourced from this doc
  2. claims WHERE doc_id = ? — drop claims sourced from this doc
  3. chunks WHERE doc_id = ? — embeddings cascade via FK
  4. documents WHERE id = ? — the row itself
  5. Orphan-entity sweep — entities with no remaining relationship or claim reference are dropped; community_members rows cascade via FK

Tradeoff: communities are deliberately NOT recomputed inline. Louvain on a meaningfully sized graph is too slow for an interactive DELETE; stale community titles/summaries are tolerable until the next manual community.finalize. Documented in both the handler and the store-layer comments.

The per-project HNSW index is invalidated so the next vector search rebuilds against the post-delete chunk set.

Files changed

Backend (Go)

  • internal/api/router.go — register DELETE /api/documents/{id} in the existing bearer-auth chain
  • internal/api/handlers.go — new deleteDocument handler (204 / 404 / 500)
  • internal/store/store.go — rewrite Store.DeleteDocument with the transactional cascade + orphan sweep
  • internal/api/document_delete_test.go — handler table tests + store-layer cascade test

Frontend (TS/React)

  • ui/src/hooks/api/useDocs.tsuseDeleteDoc mutation, invalidates docs, stats, entityGraph
  • ui/src/routes/documents/DeleteDocumentDialog.tsx — shared confirm dialog with destructive button + sonner toast
  • ui/src/routes/documents/DocumentsList.tsx — per-row Trash2 ghost icon button
  • ui/src/routes/documents/DocumentView.tsx — destructive header button, navigates to /docs on success
  • ui/src/components/layout/Providers.tsx — mount the <Toaster /> portal (was unmounted; no toasts existed in the codebase yet)
  • ui/src/routes/documents/__tests__/DeleteDocumentDialog.test.tsx — Vitest + MSW unit test (render, success, server-error)

Test plan

  • make check clean (Go build + vet + test, UI build + vitest run): 31 UI test files / 105 tests pass; Go suite green
  • New handler tests: 404 on unknown id, 204 on happy path with full graph-state assertions (chunks/embeddings/claims/orphan-entities removed; shared entities preserved), idempotency (second DELETE → 404)
  • New store-layer cascade test confirms transaction touches all five tables
  • New Vitest test asserts: dialog renders label + Cancel/Delete; confirm calls DELETE /api/documents/:id and fires success toast + onDeleted callback; server 500 surfaces error toast and dialog stays open
  • E2E smoke against built ./docsiq binary: seeded a doc with 2 entities + 1 relationship + 1 claim, DELETE /api/documents/{id} returned 204 with empty body, GET /api/documents returned null, GET /api/graph returned {edges:[], nodes:[]}, GET /api/entities returned null (orphan sweep landed). Second DELETE returned 404. Unauthenticated DELETE returned 401.

Out of scope

  • No community recomputation (left stale until next finalize — documented).
  • No soft-delete / restore (hard delete only).
  • No FTS5 or sqlite-vec extension changes.
  • No release tag (parent thread handles that).

🤖 Generated with Claude Code

aksOps and others added 2 commits May 4, 2026 07:32
…ict CSP

The strict CSP shipped from internal/api/security_headers.go uses
script-src 'self' 'wasm-unsafe-eval' — no 'unsafe-inline'. The
theme-flash FOUC guard in index.html was an inline <script>, so every
page load logged a CSP violation:

  Refused to execute inline script because it violates the following
  Content Security Policy directive: script-src 'self' 'wasm-unsafe-eval'

The script never ran, which meant a brief flash-of-wrong-theme on
first paint in dark mode (and vice-versa).

Move the script verbatim into ui/public/theme-flash.js (Vite copies
public/ to /dist root at build time) and reference it from index.html
via <script src="/theme-flash.js">. CSP 'self' allows it without an
inline-script exception. The script body is unchanged so behaviour is
identical when it runs.

Trade-off: one extra HTTP/2 request before paint. The file is 1.2 KiB
so this is well below the perceptible-jank threshold and worth it for
keeping the strict CSP intact.
Adds an authenticated REST DELETE endpoint plus per-row and
header-level UI controls so users can remove a bad upload and have
the knowledge graph clean up entities, relationships, claims, chunks,
and embeddings sourced from that doc — without leaving orphan rows or
hitting the documents.file_hash UNIQUE collision on the next index.

Backend:
- internal/store/store.go: rewrite Store.DeleteDocument as a single
  transaction that deletes relationships/claims by doc_id, chunks
  (embeddings cascade via FK), the document row, then sweeps orphan
  entities. Communities are deliberately left stale for finalize.
- internal/api/handlers.go: new deleteDocument handler returning 204
  on success, 404 on unknown id, 500 on tx failure. Invalidates the
  per-project HNSW index so the next search rebuilds.
- internal/api/router.go: register DELETE /api/documents/{id} under
  the existing bearer-auth + project chain.
- internal/api/document_delete_test.go: handler tests (404, 204 happy
  path with full graph-cleanup assertions, idempotency) plus a store-
  layer cascade test.

Frontend:
- ui/src/hooks/api/useDocs.ts: useDeleteDoc mutation that invalidates
  docs/stats/entityGraph queries on success.
- ui/src/routes/documents/DeleteDocumentDialog.tsx: shared confirm
  dialog with destructive button and sonner toast.
- DocumentsList: per-row Trash2 ghost icon button.
- DocumentView: destructive header button that navigates back on
  success.
- Providers: mount the Toaster portal.
- Vitest unit test for the dialog covering render, success path, and
  server-error path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aksOps aksOps enabled auto-merge (squash) May 4, 2026 07:39
Delete
</Button>
</header>
{data && id && (
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