411 lines
19 KiB
MySQL
411 lines
19 KiB
MySQL
|
|
-- =====================================================================
|
||
|
|
-- TeeCup — initielt databaseskjema (migrasjon 001)
|
||
|
|
-- =====================================================================
|
||
|
|
-- Realiserer arkitektur-beslutningene:
|
||
|
|
-- ADR-001 tenant = organisasjon -> organization_id på alle domenetabeller
|
||
|
|
-- ADR-002 bruker != medlemskap -> app_user / organization_membership
|
||
|
|
-- ADR-003 shared schema + RLS -> policy på hver domenetabell
|
||
|
|
-- ADR-005 allowance = konfig -> session.allowance_override (jsonb)
|
||
|
|
--
|
||
|
|
-- Kjøremiljø: PostgreSQL 15+ (gen_random_uuid(), FORCE ROW LEVEL SECURITY,
|
||
|
|
-- og security_invoker-views for at RLS skal gjelde gjennom viewet).
|
||
|
|
--
|
||
|
|
-- VIKTIG DRIFTSKRAV (ADR-003):
|
||
|
|
-- Applikasjonen MÅ koble til som en rolle UTEN superuser/BYPASSRLS, og MÅ
|
||
|
|
-- sette organisasjonskonteksten per transaksjon:
|
||
|
|
-- SET LOCAL app.current_org = '<organisasjonens uuid>';
|
||
|
|
-- Uten dette returnerer RLS-policyene null rader (trygg standard: se ingenting).
|
||
|
|
-- =====================================================================
|
||
|
|
|
||
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- gen_random_uuid()
|
||
|
|
CREATE EXTENSION IF NOT EXISTS citext; -- case-insensitiv e-post
|
||
|
|
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
-- Enum-typer (stabile, små mengder)
|
||
|
|
-- ---------------------------------------------------------------------
|
||
|
|
CREATE TYPE team_side AS ENUM ('a', 'b');
|
||
|
|
CREATE TYPE hole_scope AS ENUM ('full_18', 'front_9', 'back_9');
|
||
|
|
CREATE TYPE tournament_status AS ENUM ('draft', 'active', 'completed', 'archived');
|
||
|
|
CREATE TYPE course_source AS ENUM ('official', 'custom');
|
||
|
|
CREATE TYPE rating_scope AS ENUM ('full_18', 'front_9', 'back_9');
|
||
|
|
|
||
|
|
-- Format holdes som tekst med CHECK (ikke enum), slik at nye varianter kan
|
||
|
|
-- legges til uten ALTER TYPE. Verdiene speiler Format-enumen i handicap_engine.py.
|
||
|
|
-- Selve prosenttildelingen (allowance) er konfig, ikke låst her (ADR-005).
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 1. Identitet og tenancy (ADR-002)
|
||
|
|
-- =====================================================================
|
||
|
|
|
||
|
|
-- Global identitet. IKKE organisasjonsavgrenset. Ingen RLS på org-nøkkel her.
|
||
|
|
CREATE TABLE app_user (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
email citext, -- ev. NULL for magic-link-only e.l.
|
||
|
|
display_name text NOT NULL,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE organization (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
name text NOT NULL,
|
||
|
|
slug text UNIQUE, -- f.eks. subdomene/URL-vennlig
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Bindeleddet bruker <-> organisasjon. Én bruker kan tilhøre flere org (ADR-002).
|
||
|
|
CREATE TABLE organization_membership (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
|
|
user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
|
||
|
|
role text NOT NULL DEFAULT 'member'
|
||
|
|
CHECK (role IN ('owner', 'admin', 'member')),
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE (organization_id, user_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 2. Personer (spiller-pool på organisasjonsnivå)
|
||
|
|
-- =====================================================================
|
||
|
|
-- En spiller er en person i organisasjonens register. Mange golfere i en
|
||
|
|
-- vennegjeng/klubb har ikke egen konto, derfor er player adskilt fra app_user;
|
||
|
|
-- user_id kobler valgfritt en spiller til en innlogget bruker.
|
||
|
|
CREATE TABLE player (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
|
|
user_id uuid REFERENCES app_user(id) ON DELETE SET NULL,
|
||
|
|
display_name text NOT NULL,
|
||
|
|
-- Gjeldende/standard handicap-indeks. Snapshottes per turnering i team_roster.
|
||
|
|
handicap_index numeric(4,1),
|
||
|
|
gender text CHECK (gender IN ('m', 'f', 'x')), -- for kjønnsspesifikke tee-ratinger
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE (organization_id, id) -- for sammensatte FK-er
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 3. Baner (ADR-004: offisielle hentes fra teeoff via API, custom lages lokalt)
|
||
|
|
-- =====================================================================
|
||
|
|
CREATE TABLE course (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
|
|
name text NOT NULL,
|
||
|
|
source course_source NOT NULL DEFAULT 'custom',
|
||
|
|
-- Referanse til offisiell bane i teeoff_db (kun når source = 'official').
|
||
|
|
-- Ren referanse — ingen fysisk kryss-database-FK (ADR-004).
|
||
|
|
external_course_ref text,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
CHECK (source <> 'official' OR external_course_ref IS NOT NULL)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Hull med par og stroke index (SI). SI er normalt konstant per bane.
|
||
|
|
CREATE TABLE hole (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
course_id uuid NOT NULL,
|
||
|
|
hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18),
|
||
|
|
par smallint NOT NULL CHECK (par BETWEEN 3 AND 6),
|
||
|
|
stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18),
|
||
|
|
FOREIGN KEY (organization_id, course_id)
|
||
|
|
REFERENCES course(organization_id, id) ON DELETE CASCADE,
|
||
|
|
UNIQUE (course_id, hole_number),
|
||
|
|
UNIQUE (course_id, stroke_index) -- SI må være unik per bane
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Tee (utslagssted). Rating/slope varierer per tee.
|
||
|
|
CREATE TABLE tee (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
course_id uuid NOT NULL,
|
||
|
|
name text NOT NULL, -- f.eks. "Gul", "Rød", "Hvit"
|
||
|
|
gender text CHECK (gender IN ('m', 'f', 'x')),
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, course_id)
|
||
|
|
REFERENCES course(organization_id, id) ON DELETE CASCADE,
|
||
|
|
UNIQUE (organization_id, id)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- WHS har separate ratinger for fulle 18, front 9 og back 9. Derav egen tabell
|
||
|
|
-- slik at 9-hulls økter (front/back) bruker riktig rating (viktig for 9-hulls
|
||
|
|
-- course handicap).
|
||
|
|
CREATE TABLE tee_rating (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
tee_id uuid NOT NULL,
|
||
|
|
scope rating_scope NOT NULL,
|
||
|
|
course_rating numeric(4,1) NOT NULL,
|
||
|
|
slope_rating smallint NOT NULL CHECK (slope_rating BETWEEN 55 AND 155),
|
||
|
|
par smallint NOT NULL, -- par for det aktuelle omfanget
|
||
|
|
FOREIGN KEY (organization_id, tee_id)
|
||
|
|
REFERENCES tee(organization_id, id) ON DELETE CASCADE,
|
||
|
|
UNIQUE (tee_id, scope)
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 4. Turneringsstruktur
|
||
|
|
-- =====================================================================
|
||
|
|
CREATE TABLE tournament (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
|
||
|
|
name text NOT NULL,
|
||
|
|
status tournament_status NOT NULL DEFAULT 'draft',
|
||
|
|
start_date date,
|
||
|
|
created_by uuid REFERENCES app_user(id) ON DELETE SET NULL,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE (organization_id, id)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Lag i en turnering. Ryder Cup = 2 lag, men modellen låser ikke antallet.
|
||
|
|
CREATE TABLE team (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
tournament_id uuid NOT NULL,
|
||
|
|
name text NOT NULL, -- f.eks. "Team Europa"
|
||
|
|
color text, -- for UI
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, tournament_id)
|
||
|
|
REFERENCES tournament(organization_id, id) ON DELETE CASCADE,
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
UNIQUE (tournament_id, name)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- SPILLER-POOL: alle spillere som er tatt ut på et lag for turneringen.
|
||
|
|
-- Reserver er ganske enkelt rader her som ikke får en match_participant i en
|
||
|
|
-- gitt økt. handicap_index_snapshot fryser indeksen for reproduserbare resultater.
|
||
|
|
CREATE TABLE team_roster (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
team_id uuid NOT NULL,
|
||
|
|
player_id uuid NOT NULL,
|
||
|
|
handicap_index_snapshot numeric(4,1), -- fryst ved uttak
|
||
|
|
is_captain boolean NOT NULL DEFAULT false,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, team_id)
|
||
|
|
REFERENCES team(organization_id, id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY (organization_id, player_id)
|
||
|
|
REFERENCES player(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
UNIQUE (team_id, player_id) -- en spiller én gang per lag
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ØKT (session): ett format, ett hullomfang, én allowance-konfig. En turnering
|
||
|
|
-- er en ORDNET sekvens av økter (foursome -> fourball -> singles ...).
|
||
|
|
CREATE TABLE session (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
tournament_id uuid NOT NULL,
|
||
|
|
sequence smallint NOT NULL, -- rekkefølge i turneringen (1,2,3...)
|
||
|
|
name text, -- f.eks. "Lørdag formiddag"
|
||
|
|
format text NOT NULL CHECK (format IN
|
||
|
|
('singles','fourball','foursome','greensome','scramble_2','scramble_4')),
|
||
|
|
hole_config hole_scope NOT NULL DEFAULT 'full_18',
|
||
|
|
course_id uuid NOT NULL,
|
||
|
|
points_per_match numeric(3,1) NOT NULL DEFAULT 1.0,
|
||
|
|
-- ADR-005: NULL => bruk motorens standard-allowance for formatet.
|
||
|
|
-- Ellers en overstyring, f.eks. {"type":"per_player","percentage":0.85}.
|
||
|
|
allowance_override jsonb,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, tournament_id)
|
||
|
|
REFERENCES tournament(organization_id, id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY (organization_id, course_id)
|
||
|
|
REFERENCES course(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
UNIQUE (tournament_id, sequence)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- MATCH: én kamp i en økt, side a mot side b. Poeng caches når resultatet er
|
||
|
|
-- avgjort (beregnes av handicap_engine; SQL summerer bare de cachede poengene).
|
||
|
|
CREATE TABLE match (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
session_id uuid NOT NULL,
|
||
|
|
sequence smallint NOT NULL, -- kamp-nr innen økten
|
||
|
|
team_a_id uuid NOT NULL, -- hvilket lag er side a
|
||
|
|
team_b_id uuid NOT NULL, -- hvilket lag er side b
|
||
|
|
-- Cachede resultatfelt (kilde: motoren). NULL = ikke avgjort ennå.
|
||
|
|
status_text text, -- f.eks. "3&2 (A)", "AS"
|
||
|
|
points_side_a numeric(3,1),
|
||
|
|
points_side_b numeric(3,1),
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, session_id)
|
||
|
|
REFERENCES session(organization_id, id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY (organization_id, team_a_id)
|
||
|
|
REFERENCES team(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
FOREIGN KEY (organization_id, team_b_id)
|
||
|
|
REFERENCES team(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
UNIQUE (session_id, sequence),
|
||
|
|
CHECK (team_a_id <> team_b_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Hvilke spillere fra poolen som faktisk spiller matchen, og på hvilken side.
|
||
|
|
-- 1 deltaker per side i singles, 2 i foursome/fourball/greensome, N i scramble.
|
||
|
|
-- (Antallet vs. format valideres i applikasjonslaget.)
|
||
|
|
CREATE TABLE match_participant (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
match_id uuid NOT NULL,
|
||
|
|
team_side team_side NOT NULL,
|
||
|
|
team_roster_id uuid NOT NULL, -- peker inn i poolen
|
||
|
|
tee_id uuid NOT NULL, -- tee spilleren bruker (mixed tees støttes)
|
||
|
|
-- Cachede handicap-felt (kilde: motoren, ut fra indeks-snapshot + tee + allowance).
|
||
|
|
course_handicap smallint,
|
||
|
|
playing_handicap smallint,
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, match_id)
|
||
|
|
REFERENCES match(organization_id, id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY (organization_id, team_roster_id)
|
||
|
|
REFERENCES team_roster(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
FOREIGN KEY (organization_id, tee_id)
|
||
|
|
REFERENCES tee(organization_id, id) ON DELETE RESTRICT,
|
||
|
|
UNIQUE (organization_id, id),
|
||
|
|
UNIQUE (match_id, team_roster_id) -- en spiller kan ikke spille to plasser i samme match
|
||
|
|
);
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 5. Score (kilde-sannhet: brutto slag per hull)
|
||
|
|
-- =====================================================================
|
||
|
|
-- Netto beregnes av motoren (brutto - tildelte slag på hullet). Vi lagrer ikke
|
||
|
|
-- netto; det er avledet. Matchresultat caches på match-raden.
|
||
|
|
--
|
||
|
|
-- To scoringsmoduser i samme tabell:
|
||
|
|
-- * Individuell ball (singles, fourball): match_participant_id satt.
|
||
|
|
-- * Delt ball (foursome, greensome, scramble): match_participant_id = NULL,
|
||
|
|
-- scoren tilhører HELE siden (team_side).
|
||
|
|
-- Hvilken modus som gjelder styres av øktens format (håndheves i app-laget).
|
||
|
|
CREATE TABLE hole_score (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
organization_id uuid NOT NULL,
|
||
|
|
match_id uuid NOT NULL,
|
||
|
|
team_side team_side NOT NULL,
|
||
|
|
match_participant_id uuid, -- NULL for delt-ball-formater
|
||
|
|
hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18),
|
||
|
|
gross_strokes smallint NOT NULL CHECK (gross_strokes BETWEEN 1 AND 20),
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
FOREIGN KEY (organization_id, match_id)
|
||
|
|
REFERENCES match(organization_id, id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY (organization_id, match_participant_id)
|
||
|
|
REFERENCES match_participant(organization_id, id) ON DELETE CASCADE
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Unik score per spiller per hull (individuell ball) ...
|
||
|
|
CREATE UNIQUE INDEX hole_score_player_unique
|
||
|
|
ON hole_score (match_participant_id, hole_number)
|
||
|
|
WHERE match_participant_id IS NOT NULL;
|
||
|
|
|
||
|
|
-- ... og unik score per side per hull (delt ball).
|
||
|
|
CREATE UNIQUE INDEX hole_score_side_unique
|
||
|
|
ON hole_score (match_id, team_side, hole_number)
|
||
|
|
WHERE match_participant_id IS NULL;
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 6. Indekser (RLS-predikat og FK-oppslag; Postgres indekserer ikke FK automatisk)
|
||
|
|
-- =====================================================================
|
||
|
|
CREATE INDEX ON player (organization_id);
|
||
|
|
CREATE INDEX ON course (organization_id);
|
||
|
|
CREATE INDEX ON hole (organization_id, course_id);
|
||
|
|
CREATE INDEX ON tee (organization_id, course_id);
|
||
|
|
CREATE INDEX ON tee_rating (organization_id, tee_id);
|
||
|
|
CREATE INDEX ON tournament (organization_id);
|
||
|
|
CREATE INDEX ON team (organization_id, tournament_id);
|
||
|
|
CREATE INDEX ON team_roster (organization_id, team_id);
|
||
|
|
CREATE INDEX ON team_roster (organization_id, player_id);
|
||
|
|
CREATE INDEX ON session (organization_id, tournament_id);
|
||
|
|
CREATE INDEX ON match (organization_id, session_id);
|
||
|
|
CREATE INDEX ON match_participant (organization_id, match_id);
|
||
|
|
CREATE INDEX ON hole_score (organization_id, match_id);
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 7. Row-Level Security (ADR-003)
|
||
|
|
-- =====================================================================
|
||
|
|
-- Standard org-isolasjonspolicy på hver domenetabell. Konteksten settes av
|
||
|
|
-- applikasjonen via: SET LOCAL app.current_org = '<uuid>';
|
||
|
|
-- current_setting(..., true) gir NULL (ikke feil) når konteksten mangler ->
|
||
|
|
-- policyen slipper da ingen rader gjennom (trygg standard).
|
||
|
|
DO $$
|
||
|
|
DECLARE t text;
|
||
|
|
BEGIN
|
||
|
|
FOREACH t IN ARRAY ARRAY[
|
||
|
|
'player','course','hole','tee','tee_rating',
|
||
|
|
'tournament','team','team_roster','session',
|
||
|
|
'match','match_participant','hole_score'
|
||
|
|
]
|
||
|
|
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 $$;
|
||
|
|
|
||
|
|
-- Organisasjonsraden selv: synlig kun for den aktive konteksten.
|
||
|
|
ALTER TABLE organization ENABLE ROW LEVEL SECURITY;
|
||
|
|
ALTER TABLE organization FORCE ROW LEVEL SECURITY;
|
||
|
|
CREATE POLICY org_self ON organization
|
||
|
|
USING (id = current_setting('app.current_org', true)::uuid);
|
||
|
|
|
||
|
|
-- MERK: app_user og organization_membership håndteres av auth-laget (en bruker
|
||
|
|
-- må kunne se sine EGNE medlemskap for å velge organisasjon). Egne policyer for
|
||
|
|
-- disse defineres sammen med autentiseringsdesignet, ikke her.
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 8. Leaderboard-view (SQL summerer cachede matchpoeng; motoren beregner dem)
|
||
|
|
-- =====================================================================
|
||
|
|
-- security_invoker => viewet kjører med den spørrende rollens RLS, ikke eierens.
|
||
|
|
-- Uten dette kunne standings lekke matcher på tvers av organisasjoner.
|
||
|
|
CREATE VIEW tournament_standings
|
||
|
|
WITH (security_invoker = true) AS
|
||
|
|
SELECT
|
||
|
|
t.organization_id,
|
||
|
|
t.tournament_id,
|
||
|
|
t.id AS team_id,
|
||
|
|
t.name AS team_name,
|
||
|
|
COALESCE(SUM(
|
||
|
|
CASE WHEN m.team_a_id = t.id THEN m.points_side_a
|
||
|
|
WHEN m.team_b_id = t.id THEN m.points_side_b
|
||
|
|
ELSE 0 END
|
||
|
|
), 0) AS points
|
||
|
|
FROM team t
|
||
|
|
LEFT JOIN match m
|
||
|
|
ON m.organization_id = t.organization_id
|
||
|
|
AND (m.team_a_id = t.id OR m.team_b_id = t.id)
|
||
|
|
GROUP BY t.organization_id, t.tournament_id, t.id, t.name;
|
||
|
|
|
||
|
|
|
||
|
|
-- =====================================================================
|
||
|
|
-- 9. updated_at-trigger (valgfritt mønster — vist på to tabeller)
|
||
|
|
-- =====================================================================
|
||
|
|
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
|
||
|
|
BEGIN
|
||
|
|
NEW.updated_at := now();
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$ LANGUAGE plpgsql;
|
||
|
|
|
||
|
|
CREATE TRIGGER trg_tournament_updated
|
||
|
|
BEFORE UPDATE ON tournament
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
|
|
|
||
|
|
CREATE TRIGGER trg_match_updated
|
||
|
|
BEFORE UPDATE ON match
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
|
|
-- (Samme mønster kan legges på øvrige tabeller med updated_at.)
|