-- R1 Katalog: games, game_variants, Begriffe (Genre, Mechanik, Kategorie) und Zuordnungen. -- Lesen ist auch ohne Anmeldung möglich (Entscheidung O6), Schreiben nie. create table public.games ( id uuid primary key default gen_random_uuid(), owner_id uuid references public.profiles (id) on delete cascade, title text not null check (char_length(title) between 1 and 200), description text check (char_length(description) <= 5000), year smallint check (year between 1800 and 2100), min_players smallint check (min_players >= 1), max_players smallint check (max_players >= 1), min_minutes smallint check (min_minutes >= 0), max_minutes smallint check (max_minutes >= 0), min_age smallint check (min_age >= 0), image_path text, bgg_id integer, visibility text not null default 'private' check (visibility in ('catalog', 'private')), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), -- Katalogspiele gehören niemandem, private Spiele immer einem Nutzer (O5). constraint games_owner_matches_visibility check ( (visibility = 'catalog' and owner_id is null) or (visibility = 'private' and owner_id is not null) ), constraint games_players_range check (max_players is null or min_players is null or max_players >= min_players), constraint games_minutes_range check (max_minutes is null or min_minutes is null or max_minutes >= min_minutes) ); -- Die BGG-ID ist optional. Im Katalog einmalig; private Einträge mehrerer Nutzer dürfen sie teilen. create unique index games_bgg_id_catalog_key on public.games (bgg_id) where visibility = 'catalog' and bgg_id is not null; create unique index games_bgg_id_private_key on public.games (owner_id, bgg_id) where visibility = 'private' and bgg_id is not null; create index games_owner_id_idx on public.games (owner_id); create index games_title_idx on public.games (lower(title)); create table public.game_variants ( id uuid primary key default gen_random_uuid(), game_id uuid not null references public.games (id) on delete cascade, name text not null check (char_length(name) between 1 and 200), year smallint check (year between 1800 and 2100), language text check (char_length(language) <= 50), bgg_id integer, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), -- Ermöglicht den zusammengesetzten Verweis (variant_id, game_id) in Sammlung und Partien: -- so kann eine Variante nie zu einem anderen Spiel gehören. constraint game_variants_id_game_key unique (id, game_id) ); create index game_variants_game_id_idx on public.game_variants (game_id); create table public.genres ( id uuid primary key default gen_random_uuid(), name_de text not null unique check (char_length(name_de) between 1 and 100), name_en text not null unique check (char_length(name_en) between 1 and 100), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table public.mechanics ( id uuid primary key default gen_random_uuid(), name_de text not null unique check (char_length(name_de) between 1 and 100), name_en text not null unique check (char_length(name_en) between 1 and 100), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table public.categories ( id uuid primary key default gen_random_uuid(), name_de text not null unique check (char_length(name_de) between 1 and 100), name_en text not null unique check (char_length(name_en) between 1 and 100), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table public.game_genres ( id uuid primary key default gen_random_uuid(), game_id uuid not null references public.games (id) on delete cascade, genre_id uuid not null references public.genres (id) on delete cascade, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (game_id, genre_id) ); create index game_genres_genre_id_idx on public.game_genres (genre_id); create table public.game_mechanics ( id uuid primary key default gen_random_uuid(), game_id uuid not null references public.games (id) on delete cascade, mechanic_id uuid not null references public.mechanics (id) on delete cascade, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (game_id, mechanic_id) ); create index game_mechanics_mechanic_id_idx on public.game_mechanics (mechanic_id); create table public.game_categories ( id uuid primary key default gen_random_uuid(), game_id uuid not null references public.games (id) on delete cascade, category_id uuid not null references public.categories (id) on delete cascade, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (game_id, category_id) ); create index game_categories_category_id_idx on public.game_categories (category_id); -- updated_at-Trigger für alle Tabellen dieser Migration create trigger set_updated_at before update on public.games for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.game_variants for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.genres for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.mechanics for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.categories for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.game_genres for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.game_mechanics for each row execute function public.set_updated_at(); create trigger set_updated_at before update on public.game_categories for each row execute function public.set_updated_at(); -- Rechte und RLS alter table public.games enable row level security; alter table public.game_variants enable row level security; alter table public.genres enable row level security; alter table public.mechanics enable row level security; alter table public.categories enable row level security; alter table public.game_genres enable row level security; alter table public.game_mechanics enable row level security; alter table public.game_categories enable row level security; revoke all on public.games, public.game_variants, public.genres, public.mechanics, public.categories, public.game_genres, public.game_mechanics, public.game_categories from anon, authenticated; grant select on public.games, public.game_variants, public.genres, public.mechanics, public.categories, public.game_genres, public.game_mechanics, public.game_categories to anon, authenticated; grant insert, update, delete on public.games, public.game_variants, public.genres, public.mechanics, public.categories, public.game_genres, public.game_mechanics, public.game_categories to authenticated; -- games: Katalog für alle sichtbar, private Spiele nur für den Ersteller. create policy games_select on public.games for select to anon, authenticated using (visibility = 'catalog' or owner_id = (select auth.uid())); create policy games_insert_private on public.games for insert to authenticated with check (visibility = 'private' and owner_id = (select auth.uid())); create policy games_insert_admin on public.games for insert to authenticated with check (public.is_admin()); create policy games_update_private on public.games for update to authenticated using (visibility = 'private' and owner_id = (select auth.uid())) with check (visibility = 'private' and owner_id = (select auth.uid())); create policy games_update_admin on public.games for update to authenticated using (public.is_admin()) with check (public.is_admin()); create policy games_delete_private on public.games for delete to authenticated using (visibility = 'private' and owner_id = (select auth.uid())); create policy games_delete_admin on public.games for delete to authenticated using (public.is_admin()); -- Begriffe: lesbar für alle, pflegbar nur durch Admin. create policy genres_select on public.genres for select to anon, authenticated using (true); create policy genres_write_admin on public.genres for all to authenticated using (public.is_admin()) with check (public.is_admin()); create policy mechanics_select on public.mechanics for select to anon, authenticated using (true); create policy mechanics_write_admin on public.mechanics for all to authenticated using (public.is_admin()) with check (public.is_admin()); create policy categories_select on public.categories for select to anon, authenticated using (true); create policy categories_write_admin on public.categories for all to authenticated using (public.is_admin()) with check (public.is_admin()); -- Varianten und Zuordnungen folgen dem zugehörigen Spiel: Wer das Spiel sehen darf, sieht sie. -- Ändern darf Admin oder der Ersteller eines privaten Spiels. create policy game_variants_select on public.game_variants for select to anon, authenticated using (exists (select 1 from public.games g where g.id = game_id)); create policy game_variants_write on public.game_variants for all to authenticated using ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ) with check ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ); create policy game_genres_select on public.game_genres for select to anon, authenticated using (exists (select 1 from public.games g where g.id = game_id)); create policy game_genres_write on public.game_genres for all to authenticated using ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ) with check ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ); create policy game_mechanics_select on public.game_mechanics for select to anon, authenticated using (exists (select 1 from public.games g where g.id = game_id)); create policy game_mechanics_write on public.game_mechanics for all to authenticated using ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ) with check ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ); create policy game_categories_select on public.game_categories for select to anon, authenticated using (exists (select 1 from public.games g where g.id = game_id)); create policy game_categories_write on public.game_categories for all to authenticated using ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) ) with check ( public.is_admin() or exists (select 1 from public.games g where g.id = game_id and g.owner_id = (select auth.uid())) );