-- Migration 008: task_lists and tasks -- -- No trash for tasks (per analysis §4.4): deletion is immediate. -- Completed tasks remain visible at the bottom of the list. -- ── task_lists ─────────────────────────────────────────────────────────────── CREATE TABLE public.task_lists ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), collective_id uuid NOT NULL REFERENCES public.collectives(id) ON DELETE CASCADE, name text NOT NULL, created_by uuid NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE public.task_lists ENABLE ROW LEVEL SECURITY; CREATE INDEX task_lists_collective_idx ON public.task_lists (collective_id); -- ── tasks ──────────────────────────────────────────────────────────────────── CREATE TABLE public.tasks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), list_id uuid NOT NULL REFERENCES public.task_lists(id) ON DELETE CASCADE, title text NOT NULL, is_completed boolean NOT NULL DEFAULT false, completed_by uuid NULL REFERENCES public.users(id) ON DELETE SET NULL, completed_at timestamptz NULL, sort_order integer NOT NULL DEFAULT 0, created_by uuid NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT, created_at timestamptz NOT NULL DEFAULT now(), -- Invariant: completed_at NOT NULL ⇔ completed_by NOT NULL ⇔ is_completed = true CONSTRAINT tasks_completion_consistent CHECK ( (is_completed = true AND completed_at IS NOT NULL AND completed_by IS NOT NULL) OR (is_completed = false AND completed_at IS NULL AND completed_by IS NULL) ) ); ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY; CREATE INDEX tasks_list_order_idx ON public.tasks (list_id, sort_order); -- ── Helper: resolve collective for a task list ─────────────────────────────── CREATE OR REPLACE FUNCTION public.task_list_collective_id(p_list_id uuid) RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT collective_id FROM public.task_lists WHERE id = p_list_id; $$; -- ── RLS: task_lists ────────────────────────────────────────────────────────── CREATE POLICY task_lists_select ON public.task_lists FOR SELECT USING (public.is_member(collective_id)); CREATE POLICY task_lists_insert ON public.task_lists FOR INSERT WITH CHECK ( public.is_active_member(collective_id) AND created_by = auth.uid() ); CREATE POLICY task_lists_update ON public.task_lists FOR UPDATE USING (public.is_active_member(collective_id)) WITH CHECK (public.is_active_member(collective_id)); CREATE POLICY task_lists_delete ON public.task_lists FOR DELETE USING (public.is_active_member(collective_id)); -- ── RLS: tasks ─────────────────────────────────────────────────────────────── CREATE POLICY tasks_select ON public.tasks FOR SELECT USING (public.is_member(public.task_list_collective_id(list_id))); CREATE POLICY tasks_insert ON public.tasks FOR INSERT WITH CHECK ( public.is_active_member(public.task_list_collective_id(list_id)) AND created_by = auth.uid() ); CREATE POLICY tasks_update ON public.tasks FOR UPDATE USING (public.is_active_member(public.task_list_collective_id(list_id))) WITH CHECK (public.is_active_member(public.task_list_collective_id(list_id))); CREATE POLICY tasks_delete ON public.tasks FOR DELETE USING (public.is_active_member(public.task_list_collective_id(list_id)));