feat(fase-10): trash drawer — restore + hard-delete RPCs (10.4)
Migration 020 (019 reserved per plan) introduces two SECURITY DEFINER RPCs that back the trash drawer's per-row actions: - restore_list(uuid): clears deleted_at; requires caller to be admin or member of the owning collective; rejects if the list is not currently in trash (defensive) - hard_delete_list(uuid): permanently deletes a soft-deleted list; same role guard; rejects if the list is still active Both close the existing UI gap where guest could in principle issue the equivalent UPDATE (the FROM-UPDATE policy is is_active_member, which excludes guest, but the surface area is broader than these two intents — the RPCs make the contract explicit). The lists drawer already exposed the Restore + Delete-for-ever buttons; only the store calls swap from chained PostgREST writes to .rpc() — the UI itself is unchanged. pgTAP 012 (8 assertions) covers existence, restore happy path, non-member rejection, hard-delete happy path, active-list guard. Integration T-10..T-14 (5 vitest) verifies the wire format the store relies on (rpc errors, row state, active-listing visibility). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -85,12 +85,19 @@ export async function softDeleteList(id: string) {
|
||||
}
|
||||
|
||||
export async function restoreList(id: string, collectiveId: string) {
|
||||
await getSupabase().from('shopping_lists').update({ deleted_at: null }).eq('id', id);
|
||||
// Fase 10.4: route through SECURITY DEFINER RPC so the role check is
|
||||
// explicit and not implicit-via-RLS (guest is allowed by UPDATE policy
|
||||
// today because we never bothered to scope `restoreList`-style writes
|
||||
// distinctly from "any UPDATE" in 005_shopping.sql — the RPC closes that).
|
||||
await getSupabase().rpc('restore_list', { p_list_id: id });
|
||||
await loadLists(collectiveId);
|
||||
}
|
||||
|
||||
export async function permanentDeleteList(id: string) {
|
||||
await getSupabase().from('shopping_lists').delete().eq('id', id);
|
||||
// Fase 10.4: matching RPC for hard delete, which also requires the row
|
||||
// to be soft-deleted first (defensive guard against accidental hard
|
||||
// delete from the active view).
|
||||
await getSupabase().rpc('hard_delete_list', { p_list_id: id });
|
||||
}
|
||||
|
||||
export async function archiveList(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user