-- Diagram sharing/collaborators UI support, per organized-ideas.md §8. The -- diagrams/diagram_collaborators/diagram_snapshots tables and their RLS -- already exist (init schema) — this migration adds the two small pieces -- of backend those needed to actually be usable from a UI. -- ---------------------------------------------------------------------- -- Username -> id lookup, for "share with @username". profiles_select -- deliberately keeps a regular user from browsing other users' profiles -- (organized-ideas.md §2/§6), but a username is meant to be a shareable -- handle — that's the whole point of having one — so resolving it to an id -- (and nothing else: no email, no role) is safe to expose broadly, unlike -- the Admin-gated lookups elsewhere in this schema. Any authenticated user -- can call this; there's no privilege check because none is needed. -- ---------------------------------------------------------------------- create or replace function public.find_user_id_by_username(p_username text) returns uuid language sql stable security definer set search_path = public as $$ select id from public.profiles where username = p_username; $$; -- ---------------------------------------------------------------------- -- Collaborator usernames for one diagram's sharing UI. Same problem as -- above in reverse: diagram_collaborators only stores user ids, and -- profiles_select_self_or_super_admin blocks a regular owner from reading -- some *other* user's profile row directly to get their username. Gated -- to "can you see this diagram at all" — the same condition as -- diagrams_select's USING clause, reusing its own helper functions so the -- two can't drift apart. -- ---------------------------------------------------------------------- create or replace function public.diagram_collaborator_usernames(p_diagram_id uuid) returns table(user_id uuid, username text) language plpgsql stable security definer set search_path = public as $$ begin if not ( public.is_super_admin() or public.diagram_owner_id(p_diagram_id) = auth.uid() or public.diagram_collaborator_permission(p_diagram_id, auth.uid()) is not null ) then raise exception 'insufficient_privilege' using errcode = '42501'; end if; return query select dc.user_id, p.username from public.diagram_collaborators dc join public.profiles p on p.id = dc.user_id where dc.diagram_id = p_diagram_id; end; $$; -- ---------------------------------------------------------------------- -- Rolling snapshot retention (organized-ideas.md §8: "keep a rolling -- window of recent diagram snapshots... exact policy TBD" — settled on -- count-based, 50 per diagram). Enforced at write time via a trigger -- rather than a scheduled job: diagram_snapshots has no update/delete -- policy for regular users at all (it's meant to be immutable from their -- side), so pruning has to run as this SECURITY DEFINER function -- regardless of whether it's trigger- or cron-driven — a trigger is -- simpler than also standing up pg_cron for this app's scale. -- ---------------------------------------------------------------------- create or replace function public.prune_diagram_snapshots() returns trigger language plpgsql security definer set search_path = public as $$ begin delete from public.diagram_snapshots where diagram_id = new.diagram_id and id not in ( select id from public.diagram_snapshots where diagram_id = new.diagram_id order by created_at desc limit 50 ); return new; end; $$; create trigger trg_prune_diagram_snapshots after insert on public.diagram_snapshots for each row execute function public.prune_diagram_snapshots(); -- ---------------------------------------------------------------------- -- Who saved each snapshot, for the version-history UI. Same shape as -- diagram_collaborator_usernames above (gated to "can you see this -- diagram", batched per diagram rather than per snapshot). -- ---------------------------------------------------------------------- create or replace function public.diagram_snapshot_saved_by_usernames(p_diagram_id uuid) returns table(user_id uuid, username text) language plpgsql stable security definer set search_path = public as $$ begin if not ( public.is_super_admin() or public.diagram_owner_id(p_diagram_id) = auth.uid() or public.diagram_collaborator_permission(p_diagram_id, auth.uid()) is not null ) then raise exception 'insufficient_privilege' using errcode = '42501'; end if; return query select distinct p.id, p.username from public.diagram_snapshots s join public.profiles p on p.id = s.saved_by where s.diagram_id = p_diagram_id; end; $$;