-- Migration 020: restore_list() + hard_delete_list() RPCs — Fase 10.4. -- (Migration number 019 intentionally reserved; see plan/fase-10-spec-completion.md.) -- -- Both functions are SECURITY DEFINER and guard explicitly on caller -- membership (admin or member; guests cannot mutate). They give the -- trash drawer a single round-trip that does what it says, instead of -- chained PostgREST UPDATEs (which evaluate SELECT RLS on the returned -- row — see CLAUDE.md "Backend / RLS / Kong" gotcha). -- ── restore_list ───────────────────────────────────────────────────────────── -- Clear deleted_at on a soft-deleted list. Rejects if the list is not -- currently soft-deleted (defensive — restore on an active list is a no-op -- but tests can rely on the throw to detect logic errors). CREATE OR REPLACE FUNCTION public.restore_list(p_list_id uuid) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, auth AS $$ DECLARE v_user uuid := auth.uid(); v_collective uuid; v_role public.member_role; v_deleted timestamptz; BEGIN IF v_user IS NULL THEN RAISE EXCEPTION 'not authenticated' USING ERRCODE = '28000'; END IF; SELECT collective_id, deleted_at INTO v_collective, v_deleted FROM public.shopping_lists WHERE id = p_list_id; IF v_collective IS NULL THEN RAISE EXCEPTION 'list not found' USING ERRCODE = 'P0404'; END IF; SELECT role INTO v_role FROM public.collective_members WHERE collective_id = v_collective AND user_id = v_user; IF v_role IS NULL OR v_role = 'guest' THEN RAISE EXCEPTION 'not allowed' USING ERRCODE = '42501'; END IF; IF v_deleted IS NULL THEN RAISE EXCEPTION 'list is not in trash' USING ERRCODE = 'P0001'; END IF; UPDATE public.shopping_lists SET deleted_at = NULL WHERE id = p_list_id; END; $$; REVOKE ALL ON FUNCTION public.restore_list(uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.restore_list(uuid) TO authenticated; -- ── hard_delete_list ───────────────────────────────────────────────────────── -- Permanently delete a list that is already in the trash. Rejects if the -- list is still active (i.e. not soft-deleted yet) — the user must trash -- it first. shopping_items cascade on the FK. CREATE OR REPLACE FUNCTION public.hard_delete_list(p_list_id uuid) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, auth AS $$ DECLARE v_user uuid := auth.uid(); v_collective uuid; v_role public.member_role; v_deleted timestamptz; BEGIN IF v_user IS NULL THEN RAISE EXCEPTION 'not authenticated' USING ERRCODE = '28000'; END IF; SELECT collective_id, deleted_at INTO v_collective, v_deleted FROM public.shopping_lists WHERE id = p_list_id; IF v_collective IS NULL THEN RAISE EXCEPTION 'list not found' USING ERRCODE = 'P0404'; END IF; SELECT role INTO v_role FROM public.collective_members WHERE collective_id = v_collective AND user_id = v_user; IF v_role IS NULL OR v_role = 'guest' THEN RAISE EXCEPTION 'not allowed' USING ERRCODE = '42501'; END IF; IF v_deleted IS NULL THEN RAISE EXCEPTION 'list is not in trash' USING ERRCODE = 'P0001'; END IF; DELETE FROM public.shopping_lists WHERE id = p_list_id; END; $$; REVOKE ALL ON FUNCTION public.hard_delete_list(uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.hard_delete_list(uuid) TO authenticated;