Files
neuron-web/migrations/20260502115405_share_votes.sql
Will Anderson e121038382 fix(gallery): proper auth-gated voting with persistence, undo, and change
Replaces the broken counter-bump RPC with a per-user share_votes table
(PK share_id+user_id, RLS-enforced ownership). One vote per user per
card, change direction or undo any time. Auth required for write;
read is public. share_cards.upvotes/downvotes/score stay in sync via
recalc trigger. New endpoints: POST /api/vote (auth-gated), GET
/api/vote-state/:id (auth-aware).
2026-05-02 12:14:31 -05:00

106 lines
4.1 KiB
PL/PgSQL

-- 20260502115405_share_votes.sql
--
-- Per-user vote tracking for the /said share gallery.
--
-- Replaces the broken counter-bump RPC (vote_card) with a proper relational
-- design: one row per (share_card, user) pair, RLS-enforced ownership,
-- aggregate columns on share_cards kept in sync via trigger. Voters can
-- change direction or undo at any time. One vote per user per card.
--
-- This migration is idempotent: re-running it is a no-op.
-- ---------------------------------------------------------------------------
-- Table
-- ---------------------------------------------------------------------------
create table if not exists public.share_votes (
share_id text not null references public.share_cards(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
direction text not null check (direction in ('up','down')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (share_id, user_id)
);
create index if not exists share_votes_share_id_idx on public.share_votes (share_id);
create index if not exists share_votes_user_id_idx on public.share_votes (user_id);
-- ---------------------------------------------------------------------------
-- Row Level Security
-- ---------------------------------------------------------------------------
alter table public.share_votes enable row level security;
drop policy if exists "users can read all votes" on public.share_votes;
drop policy if exists "users can insert their own votes" on public.share_votes;
drop policy if exists "users can update their own votes" on public.share_votes;
drop policy if exists "users can delete their own votes" on public.share_votes;
create policy "users can read all votes" on public.share_votes
for select using (true);
create policy "users can insert their own votes" on public.share_votes
for insert with check (auth.uid() = user_id);
create policy "users can update their own votes" on public.share_votes
for update using (auth.uid() = user_id);
create policy "users can delete their own votes" on public.share_votes
for delete using (auth.uid() = user_id);
-- ---------------------------------------------------------------------------
-- Aggregate sync trigger
--
-- After every insert / update / delete on share_votes, recompute the three
-- aggregate columns on the parent share_cards row. SECURITY DEFINER is
-- required because the calling user only has RLS access to their own vote
-- but the trigger needs to update share_cards.upvotes/downvotes/score.
-- ---------------------------------------------------------------------------
create or replace function public.recalc_share_card_score()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
s_id text;
begin
s_id := coalesce(NEW.share_id, OLD.share_id);
update public.share_cards
set upvotes = (select count(*) from public.share_votes
where share_id = s_id and direction = 'up'),
downvotes = (select count(*) from public.share_votes
where share_id = s_id and direction = 'down'),
score = (select count(*) filter (where direction = 'up')
- count(*) filter (where direction = 'down')
from public.share_votes where share_id = s_id)
where id = s_id;
return null;
end;
$$;
drop trigger if exists share_votes_recalc on public.share_votes;
create trigger share_votes_recalc
after insert or update or delete on public.share_votes
for each row execute function public.recalc_share_card_score();
-- ---------------------------------------------------------------------------
-- updated_at touch trigger (only on UPDATE)
-- ---------------------------------------------------------------------------
create or replace function public.touch_share_votes_updated_at()
returns trigger
language plpgsql
as $$
begin
NEW.updated_at := now();
return NEW;
end;
$$;
drop trigger if exists share_votes_touch on public.share_votes;
create trigger share_votes_touch
before update on public.share_votes
for each row execute function public.touch_share_votes_updated_at();