Commit e46b1a95 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add auto-migration on startup with idempotent schema

- Migrator creates _migrations table, tracks executed files by name
- Skips already-run migrations on subsequent deploys
- Schema SQL uses IF NOT EXISTS, DO/EXCEPTION blocks, DROP POLICY IF EXISTS
- Runs before server starts, exits on failure
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5a159882
Pipeline #37 canceled with stages
-- Swiss System Tournament API - Initial Schema -- Swiss System Tournament API - Initial Schema
-- Run on Supabase PostgreSQL 15 -- Idempotent: safe to re-run
-- ============================================================ -- ============================================================
-- ENUM TYPES -- ENUM TYPES
-- ============================================================ -- ============================================================
CREATE TYPE public.user_role AS ENUM ('super_admin','org_admin','arbiter','player','spectator'); DO $$ BEGIN
CREATE TYPE public.tournament_type AS ENUM ('swiss','round_robin','double_round_robin'); CREATE TYPE public.user_role AS ENUM ('super_admin','org_admin','arbiter','player','spectator');
CREATE TYPE public.tournament_status AS ENUM ('draft','registration','in_progress','completed','cancelled'); EXCEPTION WHEN duplicate_object THEN NULL;
CREATE TYPE public.round_status AS ENUM ('pending','paired','in_progress','completed'); END $$;
CREATE TYPE public.game_result AS ENUM ('white_wins','black_wins','draw','white_forfeit','black_forfeit','double_forfeit','bye_full','bye_half','bye_zero','not_played');
CREATE TYPE public.color AS ENUM ('white','black'); DO $$ BEGIN
CREATE TYPE public.tiebreak_type AS ENUM ('buchholz','buchholz_cut_1','buchholz_median','sonneborn_berger','direct_encounter','number_of_wins','number_of_blacks','koya','progressive_score','average_rating_opponents','performance_rating'); CREATE TYPE public.tournament_type AS ENUM ('swiss','round_robin','double_round_robin');
CREATE TYPE public.time_control_type AS ENUM ('standard','rapid','blitz','bullet'); EXCEPTION WHEN duplicate_object THEN NULL;
CREATE TYPE public.audit_action AS ENUM ('create','update','delete','pair_round','unpair_round','enter_result','modify_result','generate_standings','export_trf'); END $$;
CREATE TYPE public.org_membership_status AS ENUM ('active','suspended','invited');
DO $$ BEGIN
CREATE TYPE public.tournament_status AS ENUM ('draft','registration','in_progress','completed','cancelled');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.round_status AS ENUM ('pending','paired','in_progress','completed');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.game_result AS ENUM ('white_wins','black_wins','draw','white_forfeit','black_forfeit','double_forfeit','bye_full','bye_half','bye_zero','not_played');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.color AS ENUM ('white','black');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.tiebreak_type AS ENUM ('buchholz','buchholz_cut_1','buchholz_median','sonneborn_berger','direct_encounter','number_of_wins','number_of_blacks','koya','progressive_score','average_rating_opponents','performance_rating');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.time_control_type AS ENUM ('standard','rapid','blitz','bullet');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.audit_action AS ENUM ('create','update','delete','pair_round','unpair_round','enter_result','modify_result','generate_standings','export_trf');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE public.org_membership_status AS ENUM ('active','suspended','invited');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
-- ============================================================ -- ============================================================
-- ORGANIZATIONS -- ORGANIZATIONS
-- ============================================================ -- ============================================================
CREATE TABLE public.organizations ( CREATE TABLE IF NOT EXISTS public.organizations (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text NOT NULL, name text NOT NULL,
slug text NOT NULL UNIQUE, slug text NOT NULL UNIQUE,
...@@ -38,7 +77,7 @@ CREATE TABLE public.organizations ( ...@@ -38,7 +77,7 @@ CREATE TABLE public.organizations (
-- ============================================================ -- ============================================================
-- USER PROFILES (extends auth.users) -- USER PROFILES (extends auth.users)
-- ============================================================ -- ============================================================
CREATE TABLE public.user_profiles ( CREATE TABLE IF NOT EXISTS public.user_profiles (
id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
full_name text NOT NULL, full_name text NOT NULL,
display_name text, display_name text,
...@@ -59,7 +98,7 @@ CREATE TABLE public.user_profiles ( ...@@ -59,7 +98,7 @@ CREATE TABLE public.user_profiles (
-- ============================================================ -- ============================================================
-- ORG MEMBERSHIPS -- ORG MEMBERSHIPS
-- ============================================================ -- ============================================================
CREATE TABLE public.org_memberships ( CREATE TABLE IF NOT EXISTS public.org_memberships (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
...@@ -75,7 +114,7 @@ CREATE TABLE public.org_memberships ( ...@@ -75,7 +114,7 @@ CREATE TABLE public.org_memberships (
-- ============================================================ -- ============================================================
-- EVENTS -- EVENTS
-- ============================================================ -- ============================================================
CREATE TABLE public.events ( CREATE TABLE IF NOT EXISTS public.events (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
name text NOT NULL, name text NOT NULL,
...@@ -100,7 +139,7 @@ CREATE TABLE public.events ( ...@@ -100,7 +139,7 @@ CREATE TABLE public.events (
-- ============================================================ -- ============================================================
-- TOURNAMENTS -- TOURNAMENTS
-- ============================================================ -- ============================================================
CREATE TABLE public.tournaments ( CREATE TABLE IF NOT EXISTS public.tournaments (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
event_id uuid NOT NULL REFERENCES public.events(id) ON DELETE CASCADE, event_id uuid NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
...@@ -133,7 +172,7 @@ CREATE TABLE public.tournaments ( ...@@ -133,7 +172,7 @@ CREATE TABLE public.tournaments (
-- ============================================================ -- ============================================================
-- CATEGORIES -- CATEGORIES
-- ============================================================ -- ============================================================
CREATE TABLE public.categories ( CREATE TABLE IF NOT EXISTS public.categories (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE, tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
...@@ -153,7 +192,7 @@ CREATE TABLE public.categories ( ...@@ -153,7 +192,7 @@ CREATE TABLE public.categories (
-- ============================================================ -- ============================================================
-- TOURNAMENT PLAYERS -- TOURNAMENT PLAYERS
-- ============================================================ -- ============================================================
CREATE TABLE public.tournament_players ( CREATE TABLE IF NOT EXISTS public.tournament_players (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE, tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
...@@ -187,7 +226,7 @@ CREATE TABLE public.tournament_players ( ...@@ -187,7 +226,7 @@ CREATE TABLE public.tournament_players (
-- ============================================================ -- ============================================================
-- ROUNDS -- ROUNDS
-- ============================================================ -- ============================================================
CREATE TABLE public.rounds ( CREATE TABLE IF NOT EXISTS public.rounds (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE, tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
...@@ -206,7 +245,7 @@ CREATE TABLE public.rounds ( ...@@ -206,7 +245,7 @@ CREATE TABLE public.rounds (
-- ============================================================ -- ============================================================
-- PAIRINGS -- PAIRINGS
-- ============================================================ -- ============================================================
CREATE TABLE public.pairings ( CREATE TABLE IF NOT EXISTS public.pairings (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
round_id uuid NOT NULL REFERENCES public.rounds(id) ON DELETE CASCADE, round_id uuid NOT NULL REFERENCES public.rounds(id) ON DELETE CASCADE,
tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE, tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE,
...@@ -230,7 +269,7 @@ CREATE TABLE public.pairings ( ...@@ -230,7 +269,7 @@ CREATE TABLE public.pairings (
-- ============================================================ -- ============================================================
-- STANDINGS -- STANDINGS
-- ============================================================ -- ============================================================
CREATE TABLE public.standings ( CREATE TABLE IF NOT EXISTS public.standings (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE, tournament_id uuid NOT NULL REFERENCES public.tournaments(id) ON DELETE CASCADE,
round_id uuid NOT NULL REFERENCES public.rounds(id) ON DELETE CASCADE, round_id uuid NOT NULL REFERENCES public.rounds(id) ON DELETE CASCADE,
...@@ -257,7 +296,7 @@ CREATE TABLE public.standings ( ...@@ -257,7 +296,7 @@ CREATE TABLE public.standings (
-- ============================================================ -- ============================================================
-- AUDIT LOGS -- AUDIT LOGS
-- ============================================================ -- ============================================================
CREATE TABLE public.audit_logs ( CREATE TABLE IF NOT EXISTS public.audit_logs (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY, id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE, organization_id uuid NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
user_id uuid REFERENCES auth.users(id), user_id uuid REFERENCES auth.users(id),
...@@ -272,24 +311,24 @@ CREATE TABLE public.audit_logs ( ...@@ -272,24 +311,24 @@ CREATE TABLE public.audit_logs (
-- ============================================================ -- ============================================================
-- INDEXES -- INDEXES
-- ============================================================ -- ============================================================
CREATE INDEX idx_org_memberships_user ON public.org_memberships(user_id); CREATE INDEX IF NOT EXISTS idx_org_memberships_user ON public.org_memberships(user_id);
CREATE INDEX idx_org_memberships_org ON public.org_memberships(organization_id); CREATE INDEX IF NOT EXISTS idx_org_memberships_org ON public.org_memberships(organization_id);
CREATE INDEX idx_events_org ON public.events(organization_id); CREATE INDEX IF NOT EXISTS idx_events_org ON public.events(organization_id);
CREATE INDEX idx_tournaments_event ON public.tournaments(event_id); CREATE INDEX IF NOT EXISTS idx_tournaments_event ON public.tournaments(event_id);
CREATE INDEX idx_tournaments_org ON public.tournaments(organization_id); CREATE INDEX IF NOT EXISTS idx_tournaments_org ON public.tournaments(organization_id);
CREATE INDEX idx_tournament_players_tournament ON public.tournament_players(tournament_id); CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament ON public.tournament_players(tournament_id);
CREATE INDEX idx_tournament_players_user ON public.tournament_players(user_id); CREATE INDEX IF NOT EXISTS idx_tournament_players_user ON public.tournament_players(user_id);
CREATE INDEX idx_tournament_players_fide ON public.tournament_players(fide_id); CREATE INDEX IF NOT EXISTS idx_tournament_players_fide ON public.tournament_players(fide_id);
CREATE INDEX idx_rounds_tournament ON public.rounds(tournament_id); CREATE INDEX IF NOT EXISTS idx_rounds_tournament ON public.rounds(tournament_id);
CREATE INDEX idx_pairings_round ON public.pairings(round_id); CREATE INDEX IF NOT EXISTS idx_pairings_round ON public.pairings(round_id);
CREATE INDEX idx_pairings_white ON public.pairings(white_player_id); CREATE INDEX IF NOT EXISTS idx_pairings_white ON public.pairings(white_player_id);
CREATE INDEX idx_pairings_black ON public.pairings(black_player_id); CREATE INDEX IF NOT EXISTS idx_pairings_black ON public.pairings(black_player_id);
CREATE INDEX idx_pairings_tournament ON public.pairings(tournament_id); CREATE INDEX IF NOT EXISTS idx_pairings_tournament ON public.pairings(tournament_id);
CREATE INDEX idx_standings_tournament_round ON public.standings(tournament_id, round_id); CREATE INDEX IF NOT EXISTS idx_standings_tournament_round ON public.standings(tournament_id, round_id);
CREATE INDEX idx_standings_player ON public.standings(player_id); CREATE INDEX IF NOT EXISTS idx_standings_player ON public.standings(player_id);
CREATE INDEX idx_audit_logs_org ON public.audit_logs(organization_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_org ON public.audit_logs(organization_id);
CREATE INDEX idx_audit_logs_resource ON public.audit_logs(resource_type, resource_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON public.audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_created ON public.audit_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON public.audit_logs(created_at DESC);
-- ============================================================ -- ============================================================
-- ROW LEVEL SECURITY -- ROW LEVEL SECURITY
...@@ -326,74 +365,110 @@ RETURNS boolean AS $$ ...@@ -326,74 +365,110 @@ RETURNS boolean AS $$
); );
$$ LANGUAGE sql STABLE SECURITY DEFINER; $$ LANGUAGE sql STABLE SECURITY DEFINER;
-- RLS Policies -- RLS Policies (DROP IF EXISTS + CREATE for idempotency)
DROP POLICY IF EXISTS "Users see their orgs" ON public.organizations;
CREATE POLICY "Users see their orgs" ON public.organizations FOR SELECT CREATE POLICY "Users see their orgs" ON public.organizations FOR SELECT
USING (id = ANY(public.get_user_org_ids())); USING (id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Authenticated can create orgs" ON public.organizations;
CREATE POLICY "Authenticated can create orgs" ON public.organizations FOR INSERT CREATE POLICY "Authenticated can create orgs" ON public.organizations FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL); WITH CHECK (auth.uid() IS NOT NULL);
DROP POLICY IF EXISTS "Admins update orgs" ON public.organizations;
CREATE POLICY "Admins update orgs" ON public.organizations FOR UPDATE CREATE POLICY "Admins update orgs" ON public.organizations FOR UPDATE
USING (public.user_has_role_in_org(id, ARRAY['org_admin','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(id, ARRAY['org_admin','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see memberships" ON public.org_memberships;
CREATE POLICY "Users see memberships" ON public.org_memberships FOR SELECT CREATE POLICY "Users see memberships" ON public.org_memberships FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage memberships" ON public.org_memberships;
CREATE POLICY "Admins manage memberships" ON public.org_memberships FOR ALL CREATE POLICY "Admins manage memberships" ON public.org_memberships FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see own profile" ON public.user_profiles;
CREATE POLICY "Users see own profile" ON public.user_profiles FOR SELECT CREATE POLICY "Users see own profile" ON public.user_profiles FOR SELECT
USING (id = auth.uid()); USING (id = auth.uid());
DROP POLICY IF EXISTS "Users update own profile" ON public.user_profiles;
CREATE POLICY "Users update own profile" ON public.user_profiles FOR UPDATE CREATE POLICY "Users update own profile" ON public.user_profiles FOR UPDATE
USING (id = auth.uid()); USING (id = auth.uid());
DROP POLICY IF EXISTS "Users see events in their orgs" ON public.events;
CREATE POLICY "Users see events in their orgs" ON public.events FOR SELECT CREATE POLICY "Users see events in their orgs" ON public.events FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage events" ON public.events;
CREATE POLICY "Admins manage events" ON public.events FOR ALL CREATE POLICY "Admins manage events" ON public.events FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see tournaments" ON public.tournaments;
CREATE POLICY "Users see tournaments" ON public.tournaments FOR SELECT CREATE POLICY "Users see tournaments" ON public.tournaments FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage tournaments" ON public.tournaments;
CREATE POLICY "Admins manage tournaments" ON public.tournaments FOR ALL CREATE POLICY "Admins manage tournaments" ON public.tournaments FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see players" ON public.tournament_players;
CREATE POLICY "Users see players" ON public.tournament_players FOR SELECT CREATE POLICY "Users see players" ON public.tournament_players FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage players" ON public.tournament_players;
CREATE POLICY "Admins manage players" ON public.tournament_players FOR ALL CREATE POLICY "Admins manage players" ON public.tournament_players FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see rounds" ON public.rounds;
CREATE POLICY "Users see rounds" ON public.rounds FOR SELECT CREATE POLICY "Users see rounds" ON public.rounds FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage rounds" ON public.rounds;
CREATE POLICY "Admins manage rounds" ON public.rounds FOR ALL CREATE POLICY "Admins manage rounds" ON public.rounds FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see pairings" ON public.pairings;
CREATE POLICY "Users see pairings" ON public.pairings FOR SELECT CREATE POLICY "Users see pairings" ON public.pairings FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage pairings" ON public.pairings;
CREATE POLICY "Admins manage pairings" ON public.pairings FOR ALL CREATE POLICY "Admins manage pairings" ON public.pairings FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Users see standings" ON public.standings;
CREATE POLICY "Users see standings" ON public.standings FOR SELECT CREATE POLICY "Users see standings" ON public.standings FOR SELECT
USING (organization_id = ANY(public.get_user_org_ids())); USING (organization_id = ANY(public.get_user_org_ids()));
DROP POLICY IF EXISTS "Admins manage standings" ON public.standings;
CREATE POLICY "Admins manage standings" ON public.standings FOR ALL CREATE POLICY "Admins manage standings" ON public.standings FOR ALL
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','arbiter','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "Admins see audit logs" ON public.audit_logs;
CREATE POLICY "Admins see audit logs" ON public.audit_logs FOR SELECT CREATE POLICY "Admins see audit logs" ON public.audit_logs FOR SELECT
USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','super_admin']::public.user_role[])); USING (public.user_has_role_in_org(organization_id, ARRAY['org_admin','super_admin']::public.user_role[]));
DROP POLICY IF EXISTS "System inserts audit logs" ON public.audit_logs;
CREATE POLICY "System inserts audit logs" ON public.audit_logs FOR INSERT CREATE POLICY "System inserts audit logs" ON public.audit_logs FOR INSERT
WITH CHECK (true); WITH CHECK (true);
-- ============================================================ -- ============================================================
-- REALTIME PUBLICATIONS -- REALTIME PUBLICATIONS
-- ============================================================ -- ============================================================
ALTER PUBLICATION supabase_realtime ADD TABLE public.pairings; DO $$ BEGIN
ALTER PUBLICATION supabase_realtime ADD TABLE public.standings; ALTER PUBLICATION supabase_realtime ADD TABLE public.pairings;
ALTER PUBLICATION supabase_realtime ADD TABLE public.rounds; EXCEPTION WHEN duplicate_object THEN NULL;
ALTER PUBLICATION supabase_realtime ADD TABLE public.tournament_players; END $$;
DO $$ BEGIN
ALTER PUBLICATION supabase_realtime ADD TABLE public.standings;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
ALTER PUBLICATION supabase_realtime ADD TABLE public.rounds;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
ALTER PUBLICATION supabase_realtime ADD TABLE public.tournament_players;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
import { buildApp } from './app.js'; import { buildApp } from './app.js';
import { closeDatabase } from './config/database.js'; import { closeDatabase } from './config/database.js';
import { runMigrations } from './migrator.js';
async function main() { async function main() {
const databaseUrl = process.env.DATABASE_URL;
if (databaseUrl) {
try {
await runMigrations(databaseUrl);
} catch (err) {
console.error('[migrate] FAILED:', err);
process.exit(1);
}
}
const app = await buildApp(); const app = await buildApp();
const shutdown = async (signal: string) => { const shutdown = async (signal: string) => {
......
import postgres from 'postgres';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { createHash } from 'crypto';
export async function runMigrations(databaseUrl: string): Promise<void> {
const sql = postgres(databaseUrl, { max: 1 });
try {
await sql`
CREATE TABLE IF NOT EXISTS public._migrations (
id serial PRIMARY KEY,
name text NOT NULL UNIQUE,
hash text NOT NULL,
executed_at timestamptz DEFAULT now() NOT NULL
)
`;
const executed = await sql<{ name: string }[]>`SELECT name FROM public._migrations ORDER BY id`;
const executedNames = new Set(executed.map(r => r.name));
const migrationsDir = join(import.meta.dirname, '..', 'migrations');
const files = readdirSync(migrationsDir)
.filter(f => f.endsWith('.sql'))
.sort();
for (const file of files) {
if (executedNames.has(file)) {
console.log(`[migrate] skip: ${file} (already executed)`);
continue;
}
const filePath = join(migrationsDir, file);
const content = readFileSync(filePath, 'utf-8');
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
console.log(`[migrate] running: ${file}...`);
await sql.unsafe(content);
await sql`INSERT INTO public._migrations (name, hash) VALUES (${file}, ${hash})`;
console.log(`[migrate] done: ${file}`);
}
console.log('[migrate] all migrations up to date');
} finally {
await sql.end();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment