-- Migration 002: collectives, collective_members, collective_invitations -- ── Enums ───────────────────────────────────────────────────────────────────── CREATE TYPE public.member_role AS ENUM ('admin', 'member', 'guest'); -- ── collectives ─────────────────────────────────────────────────────────────── CREATE TABLE public.collectives ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, emoji text NOT NULL DEFAULT '🏠', created_by uuid NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT, created_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE public.collectives ENABLE ROW LEVEL SECURITY; -- ── collective_members ──────────────────────────────────────────────────────── CREATE TABLE public.collective_members ( collective_id uuid NOT NULL REFERENCES public.collectives(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, role public.member_role NOT NULL DEFAULT 'member', joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (collective_id, user_id) ); ALTER TABLE public.collective_members ENABLE ROW LEVEL SECURITY; CREATE INDEX collective_members_user_idx ON public.collective_members (user_id); -- ── collective_invitations ──────────────────────────────────────────────────── CREATE TABLE public.collective_invitations ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), collective_id uuid NOT NULL REFERENCES public.collectives(id) ON DELETE CASCADE, token uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(), role public.member_role NOT NULL DEFAULT 'member', created_by uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL DEFAULT (now() + INTERVAL '7 days'), accepted_at timestamptz NULL, accepted_by uuid NULL REFERENCES public.users(id) ON DELETE SET NULL ); ALTER TABLE public.collective_invitations ENABLE ROW LEVEL SECURITY; CREATE INDEX collective_invitations_token_idx ON public.collective_invitations (token); CREATE INDEX collective_invitations_collective_idx ON public.collective_invitations (collective_id); -- ── Trigger: auto-promote oldest member when last admin deletes account ──────── -- Fires BEFORE DELETE on public.users. If the departing user is the sole admin -- of any collective, the longest-standing member (by joined_at) is promoted to -- admin before the delete proceeds, satisfying business rule RN-10. CREATE OR REPLACE FUNCTION public.promote_oldest_member_on_admin_leave() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE r RECORD; BEGIN -- For each collective where the departing user is an admin: FOR r IN SELECT cm.collective_id FROM public.collective_members cm WHERE cm.user_id = OLD.id AND cm.role = 'admin' -- Only act when this is the SOLE remaining admin AND ( SELECT count(*) FROM public.collective_members WHERE collective_id = cm.collective_id AND role = 'admin' ) = 1 LOOP -- Promote the earliest-joined non-admin member in that collective UPDATE public.collective_members SET role = 'admin' WHERE (collective_id, user_id) = ( SELECT collective_id, user_id FROM public.collective_members WHERE collective_id = r.collective_id AND user_id <> OLD.id ORDER BY joined_at ASC LIMIT 1 ); END LOOP; RETURN OLD; END; $$; CREATE TRIGGER promote_on_admin_leave BEFORE DELETE ON public.users FOR EACH ROW EXECUTE FUNCTION public.promote_oldest_member_on_admin_leave();