server: add DELETE /api/projects route; UI delete-project action#522
Merged
Conversation
Hosted projects could be created and edited but never removed (issue #49), which makes the web app awkward for repeated use -- e.g. a class that builds many throwaway models over a semester. Add a delete capability end to end: - server: a DELETE route (route-handlers.ts `createDeleteProjectHandler`) that removes the project document and, best-effort, its cached preview PNG. Ownership is checked against the URL username before any DB lookup -- so a logged-in user can't probe other users' private projects via 404-vs-401 -- and again against the stored record's ownerId as defense in depth. The superseded `File` documents are intentionally left orphaned: every save already orphans the prior one (tracked separately as #521). - diagram: a low-emphasis destructive "Delete project" button in the model-properties drawer (next to "Download model"), behind a modal confirmation. It lives in a small `DeleteProjectButton`; a rejected delete keeps the dialog open with the error message so the user can retry. `Editor` gains an optional `onDeleteProject` prop, forwarded to the drawer only when the editor is not read-only. `Button` gains a `color="error"` variant. - HostedWebEditor wires `onDeleteProject` to a `handleDelete` that calls the new endpoint and, on success, full-navigates back to the project list so it refetches without the deleted project. It is only passed when the viewer owns the project; the local file-backed viewer and embeds leave it undefined. Hard delete, no undo: a deleted project's SSR route returns the existing 404, the same as any nonexistent project.
|
Reviewed the server No blocking issues found. A few notes (none require action):
Overall correctness: correct. Existing code and tests should not break, and the patch is free of blocking bugs. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #522 +/- ##
=======================================
Coverage 82.44% 82.45%
=======================================
Files 242 242
Lines 63290 63290
=======================================
+ Hits 52182 52184 +2
+ Misses 11108 11106 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the ability to delete a hosted project. Closes #49.
Hosted projects could be created and edited but never removed, which makes the web app awkward for repeated use -- e.g. a class that builds many throwaway models over a semester (#49).
How
End to end:
DELETE /api/projects/:username/:projectNameroute, implemented ascreateDeleteProjectHandlerinroute-handlers.ts(following the existingcreateProjectRouteHandlerpattern: a factory over a narrow DB interface, unit-tested with mocks). It deletes the project document and, best-effort, the cached preview PNG. Ownership is checked against the URL username before any DB lookup (so a logged-in user can't probe whether someone else's private project exists via 404-vs-401) and again against the stored record'sownerIdas defense in depth.authzalready 401s unauthenticated non-GET/projects/...requests, and the handler re-checks defensively.DeleteProjectButton). A rejected delete keeps the dialog open with the error message so the user can retry; a resolved one means the host has navigated away.Editorgains an optionalonDeleteProjectprop, forwarded to the drawer only when the editor is not read-only.Buttongains acolor="error"variant (uses the existing--color-errortoken) and an SVGDeleteIcon.src/app's editor wrapper) wiresonDeleteProjecttohandleDelete, which calls the new endpoint and on success full-navigates back to the project list (so it refetches without the deleted project). It's only passed when the viewer owns the project; the local file-backed viewer (simlin-serve) and embeds leave it undefined and never see the affordance.Decisions
Filedocuments are left behind. Every save already orphans the priorFiledoc (no GC,prev_idis dead schema); cleaning that up generally is out of scope and tracked separately as server: hosted-project saves leak orphaned File docs in Firestore (no version chain, no GC) #521.Tests
TDD throughout:
src/server/tests/delete-project.test.ts-- owner deletes (project + preview removed, 200); 404 for missing project; 401 unauthenticated; 401 (without a DB lookup) for non-URL-owner; 401 for stored-owner mismatch; preview-delete failure is non-fatal.src/diagram/tests/delete-project-button.test.tsx-- renders/opens/cancels the dialog; confirm callsonDelete; a rejectedonDeletekeeps the dialog open and shows the error; action buttons disabled while in flight.src/diagram/tests/model-properties-drawer.test.tsx-- delete action present only whenonDeletesupplied; confirming invokes it.src/diagram/tests/editor-drawer-delete.test.ts--getDrawer()forwardsonDeleteProjectonly when editable; omits it when read-only / unset / embedded.src/diagram/tests/hosted-web-editor-delete.test.ts-- DELETEs the right URL (relative and base-prefixed), navigates home on success, throws the server error and doesn't navigate on failure, no-op in read-only mode.src/diagram/tests/button.test.tsx--color="error"class application for all three variants.Full pre-commit gate (Rust fmt/clippy/test, TS lint/build/tsc/test, pysimlin) is green.
🤖 Generated with Claude Code