teecup/090_tournament_round_message.sql
Erol Haagenrud d8d1cf39ae Rundespesifikk kommentartråd for org-turneringer (ADR-102)
Ekte parallell til frittstående runders RoundMessages (ADR-044), ikke
en utvidelse av Banter Board (ADR-025) -- ny org-scopet (RLS)
tournament_round_message-tabell, én tråd per turneringsrunde. Reaksjoner
og trådede kommentarer med via eksisterende PostEngagement-komponent
uendret; @-tagging og sanntid bevisst utenfor v1 (se ADR-102).

Migrasjon 090 IKKE kjørt mot ekte teecup_db ennå -- venter på
brukerbekreftelse før utrulling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 06:39:44 +02:00

109 lines
5.5 KiB
SQL

-- =====================================================================
-- TeeCup — Rundespesifikk kommentartråd for org-turneringer, migrasjon
-- 090 (ADR-102). Speiler `058_round_messages.sql` +
-- `059_post_reactions_and_comments.sql` sin round_message/
-- round_message_reaction/round_message_comment, MEN med organization_id
-- + full RLS -- i motsetning til den frittstående sidens bevisst
-- RLS-frie plain_connection()-mønster (ADR-033 Beslutning A), er
-- turneringer ER org-scopet (CLAUDE.md-invarianten).
--
-- Én tråd PER TURNERINGSRUNDE (hele feltet på tvers av flighter deler
-- tråd, IKKE per flight) -- bevisst valgt av bruker, se ADR-102.
--
-- Skriverett = leserett = org-medlemskap (get_authorized_org), IKKE en
-- ny synlighetsmodell -- samme presedens som individual_tournaments.py
-- sin egen modul-docstring allerede fastslår for runde-/deltaker-
-- OPPSETT ("vanlig org-medlemsnivå", ikke det strengere self-only-kravet
-- update_hole har for selve SCORING-en). Moderering: forfatter ELLER
-- org-admin (is_org_admin), samme regel som Banter Board (ADR-025).
--
-- Bevisst UTENFOR omfang i v1 (se ADR-102): ingen @-tagging (round_
-- message_tag-motparten er utelatt), ingen WebSocket-kringkasting
-- (individual-tournament-detail.tsx har ingen sanntids-tilkobling i
-- det hele tatt ennå -- eget, uprioritert gap).
-- =====================================================================
\set ON_ERROR_STOP on
CREATE TABLE tournament_round_message (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL,
tournament_round_id uuid NOT NULL,
author_user_id uuid NOT NULL REFERENCES app_user(id),
-- Frosset ved skrivetidspunkt, samme prinsipp som round_message.author_display_name
-- -- fullt navn (roster-/forfatter-kontekst, ikke direkte adressering,
-- CLAUDE.md navneformat-regel).
author_display_name text NOT NULL,
body text,
image_key text,
-- Offline-kø-idempotens (samme mønster som round_message.client_message_id,
-- migrasjon 073) -- et gjentatt POST med samme id (avbrutt synk prøvd på
-- nytt) returnerer den allerede opprettede meldingen fremfor en duplikat.
client_message_id uuid,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (organization_id, tournament_round_id)
REFERENCES tournament_round(organization_id, id) ON DELETE CASCADE,
CHECK (body IS NOT NULL OR image_key IS NOT NULL)
);
CREATE INDEX ON tournament_round_message (organization_id, tournament_round_id, created_at);
CREATE INDEX ON tournament_round_message (organization_id, author_user_id);
CREATE UNIQUE INDEX ON tournament_round_message (tournament_round_id, client_message_id)
WHERE client_message_id IS NOT NULL;
CREATE TABLE tournament_round_message_reaction (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL,
tournament_round_message_id uuid NOT NULL REFERENCES tournament_round_message(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES app_user(id),
emoji text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (tournament_round_message_id, user_id)
);
CREATE INDEX ON tournament_round_message_reaction (organization_id, tournament_round_message_id);
CREATE TABLE tournament_round_message_comment (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL,
tournament_round_message_id uuid NOT NULL REFERENCES tournament_round_message(id) ON DELETE CASCADE,
parent_comment_id uuid REFERENCES tournament_round_message_comment(id) ON DELETE CASCADE,
author_user_id uuid NOT NULL REFERENCES app_user(id),
author_display_name text NOT NULL,
body text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (length(btrim(body)) > 0)
);
CREATE INDEX ON tournament_round_message_comment (organization_id, tournament_round_message_id, created_at);
CREATE INDEX ON tournament_round_message_comment (organization_id, parent_comment_id);
-- ---------------------------------------------------------------------
-- Row-Level Security (samme org_isolation-policy-mønster som 040)
-- ---------------------------------------------------------------------
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'tournament_round_message', 'tournament_round_message_reaction',
'tournament_round_message_comment'
]
LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', t);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY;', t);
EXECUTE format($p$
CREATE POLICY org_isolation ON %I
USING (organization_id = current_setting('app.current_org', true)::uuid)
WITH CHECK (organization_id = current_setting('app.current_org', true)::uuid);
$p$, t);
END LOOP;
END $$;
-- Samme "slett og opprett på nytt, aldri rediger"-prinsipp som
-- round_message/message -- ingen UPDATE-grant på selve meldingen/
-- kommentaren. Reaksjoner får UPDATE (emoji-bytte er en UPSERT).
GRANT SELECT, INSERT, DELETE ON tournament_round_message TO teecup_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON tournament_round_message_reaction TO teecup_app;
GRANT SELECT, INSERT, DELETE ON tournament_round_message_comment TO teecup_app;