docs: deleteForm cascade + deleteSubmission/relinkAll; HANDOFF pass-33

API_REFERENCE + ARCHITECTURE updated (deleteForm now permanently
cascades incl. signed waivers; immutability caveat; new deleteSubmission
+ relinkAllSubmissions). Corrected pass-32 header to DEPLOYED.
This commit is contained in:
Moheeb Zara
2026-05-17 00:12:43 -07:00
parent 1e6e5d523b
commit cec2d69097
3 changed files with 27 additions and 6 deletions
+1 -1
View File
@@ -892,7 +892,7 @@ validate input with Zod via `parseInput`, authorize via `requireMember` /
### Forms & waivers: `forms.ts` (migrations 026-029)
Management actions are gated by `forms.manage` (checked via the `user_has_permission` RPC; the `forms` RLS independently enforces it):
- `getPublicForm({ space, slug })` — service-client read for the public `/f/[space]/[slug]` page; returns only a published, non-members form. `createForm(input)` — friendly error on slug collision (slug is unique per space, migration 028). `updateForm(input)` — slug is immutable; for a published waiver, a legal-text or schema change bumps `version` (existing submissions stay valid against their own snapshot, non-blocking re-sign). `setFormStatus(input)` — draft/published/closed. `deleteForm(input)` — refused if the form has any submissions (close instead; submissions are immutable records). `listForms()`, `getFormResults(input)`, `exportFormResultsCsv(input)` (audited via `activity_log`).
- `getPublicForm({ space, slug })` — service-client read for the public `/f/[space]/[slug]` page; returns only a published, non-members form. `createForm(input)` — friendly error on slug collision (slug is unique per space, migration 028). `updateForm(input)` — slug is immutable; for a published waiver, a legal-text or schema change bumps `version` (existing submissions stay valid against their own snapshot, non-blocking re-sign). `setFormStatus(input)` — draft/published/closed. `deleteForm({formId, confirm:true})` — **permanently** deletes the form and FK-cascades every submission (including signed waivers); requires explicit `confirm:true` (a stale/accidental call cannot destroy records) and audits the destroyed count; the UI gates it behind a destructive confirm. `deleteSubmission({submissionId})` — `forms.manage`; permanently removes one submission via the service client (form_submissions has no client write policy), space-scoped, audited. `relinkAllSubmissions()` — `forms.manage`; re-runs email→member association across the whole space (members earliest-joined first for deterministic shared-email claim; reuses `linkSubmissionsByEmail`; NULL-only). `listForms()`, `getFormResults(input)`, `exportFormResultsCsv(input)` (audited via `activity_log`).
- `submitForm(input)` — public entry point (anonymous, signed-in non-member, or member by form `visibility`). Reads the form with the service client (anon has no `forms` grant), enforces visibility + waiver consent, validates `answers` against the stored field schema (`lib/forms-schema.ts`, unknown keys discarded), snapshots schema/legal-text/version, captures IP + user-agent, and writes the row with the service client (`form_submissions` has no write policy, so this is the only path; rows are immutable).
- `linkSubmissionsForMember(input)` — `forms.manage` admin manual re-link of prior submissions by email (kept for explicit re-linking).
- `listMemberSubmissions({ memberId })` — `forms.manage`; the forms a member has submitted (title, kind, version, date), for the per-member "Forms" panel on `/members`. Honors the `form_submissions`/`forms` RLS (no service-client bypass); metadata only — answers stay in the audited results surface.
+9 -4
View File
@@ -459,10 +459,15 @@ all submissions (anonymous, public-authenticated, or members) are written by a
single validated server action using the service client AFTER server-side
schema validation and snapshotting (same pattern as `finishOnboarding`); the
`form_submissions` table has no INSERT/UPDATE/DELETE policy, so RLS hard-denies
every non-service client and submissions are immutable. The public fill page is
served by a service-client server action, so the `anon` Postgres role gets no
grant on `forms`. Form management is gated by the new additive `forms.manage`
permission via `user_has_permission`.
every non-service client; submissions are immutable to clients but a
`forms.manage` holder can permanently delete an individual submission
(`deleteSubmission`) or a whole form (`deleteForm`, which FK-cascades all its
submissions including signed waivers) through validated service-client actions
behind destructive UI confirms (`deleteForm` also requires an explicit
`confirm:true`). The public fill page is served by a service-client server
action, so the `anon` Postgres role gets no grant on `forms`. Form management
is gated by the new additive `forms.manage` permission via
`user_has_permission`.
Email-match association (migration 039 + app, owner-chosen 2026-05): a
submission is linked to a member whenever `submitter_email` matches a member
+17 -1
View File
@@ -4,7 +4,23 @@ Append-only. Newest entries on top. Keep each entry to one screen.
---
## 2026-05-17 (pass 32) — Forms↔member email association + landing mobile; LOCAL, awaiting deploy
## 2026-05-17 (pass 33) — Form/submission deletion + space-wide re-link; LOCAL, awaiting deploy
Branch `main`. 4 commits `c4ae5d2..` (this entry). No migration, no browser verification here.
- **DF1 `c4ae5d2`** actions: `deleteForm` now PERMANENTLY deletes the form + FK-cascades all submissions (the old "refuse if submissions" guard removed); requires explicit `confirm:true` (`deleteFormSchema`) and audits the destroyed count. `deleteSubmission({submissionId})` — forms.manage, service client (form_submissions has no client write policy), space-scoped, audited. `relinkAllSubmissions()` — forms.manage, re-runs email→member linking space-wide (members earliest-joined first; reuses `linkSubmissionsByEmail`; NULL-only).
- **DF2 `1e6e5d5`** UI: forms list per-form Delete (destructive confirm spelling out permanent loss incl. signed waivers) + header "Re-link submissions"; results page per-response Delete (confirm) + "Delete form" header action. Optimistic updates.
- **DF3 (this)**: API_REFERENCE + ARCHITECTURE updated (deleteForm now cascades; immutability caveat); HANDOFF. Suite 464, build clean each phase.
### Decision note
User explicitly wanted forms + results + individual entries deletable with a confirmation; this overrides the prior "submissions are immutable, close instead" stance. Hard delete (no archive); CSV export remains available and the confirm copy points to it. Documented so an audit doesn't revert it.
### Open
- LOCAL & undeployed: pass-33 (4 commits). **Awaiting deploy approval** (no migration; app + docs only). After push: smoke /forms (delete a draft form, delete a response, Re-link button), confirm cascade removed submissions. Default remains ASK-before-deploy.
---
## 2026-05-17 (pass 32) — Forms↔member email association + landing mobile (DEPLOYED, run 25984078567)
Branch `main`. 6 commits `ea521de..` (this entry). No browser verification here.