diff --git a/docs/datenmodell-r1.md b/docs/datenmodell-r1.md index cdfb50b..3da8a51 100644 --- a/docs/datenmodell-r1.md +++ b/docs/datenmodell-r1.md @@ -28,7 +28,7 @@ Die Pfeile zeigen, welche Tabelle auf welche verweist. Jede Partie (`plays`) hä ## Tabellen -R1 braucht 20 Tabellen in vier Bereichen (17 aus der Grundplanung, dazu `catalog_sources`, `publishers` und `game_publishers` aus KAT-05; mit `game_reports` in KAT-05 PR 2c werden es 21). Jede Tabelle hat einen eigenen Primärschlüssel `id` (uuid, PK) sowie `created_at` und `updated_at`; diese drei Spalten stehen in den Tabellen unten nicht mehr einzeln. `updated_at` setzt ein Datenbank-Trigger bei jeder Änderung, nie der Client. Ein Fremdschlüssel (FK) steht nur dort, wo zwei Tabellen in Beziehung stehen, und heißt `_id`; er speichert die `id` der Zielzeile. Spaltennamen sind Englisch (Konvention aus CLAUDE.md), die Namen der Statuswerte sind mein Vorschlag. +R1 braucht 21 Tabellen in vier Bereichen (17 aus der Grundplanung, dazu `catalog_sources`, `publishers`, `game_publishers` und `game_reports` aus KAT-05). Jede Tabelle hat einen eigenen Primärschlüssel `id` (uuid, PK) sowie `created_at` und `updated_at`; diese drei Spalten stehen in den Tabellen unten nicht mehr einzeln. `updated_at` setzt ein Datenbank-Trigger bei jeder Änderung, nie der Client. Ein Fremdschlüssel (FK) steht nur dort, wo zwei Tabellen in Beziehung stehen, und heißt `_id`; er speichert die `id` der Zielzeile. Spaltennamen sind Englisch (Konvention aus CLAUDE.md), die Namen der Statuswerte sind mein Vorschlag. ### Konto und Profil @@ -43,6 +43,7 @@ R1 braucht 20 Tabellen in vier Bereichen (17 aus der Grundplanung, dazu `catalog | Tabelle | Spalten | Zweck | | --- | --- | --- | | `games` | `owner_id` (FK → `profiles`, nur bei privaten Spielen), `title`, `description`, `description_locale`, `year`, `min_players`, `max_players`, `min_minutes`, `max_minutes`, `min_age`, `image_path`, `bgg_id` (optional, eindeutig), `visibility` (`catalog` oder `private`), `hidden_at`, `merged_into_id` (FK → `games`), `manual_fields`, `catalog_source_id` (FK → `catalog_sources`), `source_ref`, `source_url`, `source_synced_at` | Ein Eintrag je Spiel; private Einträge sind vorläufig und nur für den Ersteller sichtbar (O5). Die letzten sieben Spalten gehören der Katalogpflege (KAT-05), siehe unten | +| `game_reports` | `owner_id` (FK → `profiles`), `origin` (`user`/`import`), `title`, `publisher_name`, `year`, `link`, `bgg_id`, `provisional_game_id` (FK → `games`), `status` (`open`/`approved`/`rejected`/`merged`), `target_game_id` (FK → `games`), `resolved_at` | Meldungen fehlender Spiele (KAT-05 PR 2c, Anlegen durch Nutzer kommt mit KAT-04); siehe Katalogpflege | | `catalog_sources` | `key`, `name`, `attribution_text`, `attribution_url`, `images_allowed`, `texts_allowed`, `terms_url`, `terms_checked_at` | Datenquellen des Katalogs mit Anzeige-Erlaubnis für Bilder und Texte je Quelle (KAT-05); die Migration legt `bgg` ohne Erlaubnis an | | `publishers`, `game_publishers` | `name` (eindeutig ohne Beachtung der Schreibweise); `game_id`, `publisher_id` | Verlage und ihre Zuordnung zu Spielen (Klärung 4) | | `game_variants` | `game_id` (FK → `games`), `name`, `year`, `language`, `bgg_id` (optional) | Editionen und Varianten eines Spiels (O4) | @@ -76,6 +77,7 @@ Jede Tabelle hat Row Level Security (RLS) und eine Policy-Test-Suite (pgTAP). De | `profiles` | Eigenes Profil; fremde nur `display_name`, `avatar_path`, `city` | Nur eigenes | | `favorite_games`, `bgg_links` | Nur eigene | Nur eigene | | `games`, `game_variants`, Begriffe | Katalog für alle (O6); private Spiele nur der Ersteller | Katalog nur Admin (in R1 nur über Studio/SQL, E10); private Spiele nur der Ersteller, und nur die Spalten laut Spaltenrechten | +| `game_reports` | nur der Meldende liest seine Zeilen (Admin-Policy `is_admin()` für R4); Schreiben kein Nutzerrecht | Anlegen ergänzt KAT-04 (#13); Bearbeiten nur über `admin_resolve_report` (Studio) | | `catalog_sources`, `publishers`, `game_publishers`, View `catalog_games` | für alle, auch ohne Anmeldung (`game_publishers` folgt dem Spiel) | Quellen und Verlage nur Admin; `game_publishers` Admin oder Ersteller des privaten Spiels; die View ist nicht beschreibbar | | `collection_items`, `score_sheets`, `score_categories` | Nur eigene | Nur eigene | | `plays` | Ersteller und eingeladene Mitspieler (offen oder bestätigt) | Nur Ersteller | @@ -233,6 +235,26 @@ Diese Struktur ist aus den fertigen Migrationen in `supabase/migrations/` ausgel | `created_at` | timestamptz | ja | `now()` | | | `updated_at` | timestamptz | ja | `now()` | | +**`game_reports`** (KAT-05, Migration `game_reports`): + +| Spalte | Typ | Pflicht | Standard | Schlüssel | +| --- | --- | --- | --- | --- | +| `id` | uuid | ja | `gen_random_uuid()` | PK | +| `owner_id` | uuid | ja | | FK → profiles (`on delete cascade`) | +| `origin` | text | ja | `'user'` | `user` oder `import` | +| `title` | text | ja | | 1 bis 200 Zeichen | +| `publisher_name` | text | nein | | höchstens 200 | +| `year` | smallint | nein | | 1800 bis 2100 | +| `link` | text | nein | | `http(s)://`, höchstens 500 | +| `bgg_id` | integer | nein | | | +| `provisional_game_id` | uuid | nein | | FK → games (`on delete set null`), eindeutig (1:1) | +| `status` | text | ja | `'open'` | `open`, `approved`, `rejected`, `merged` | +| `target_game_id` | uuid | nein | | FK → games (`on delete restrict`) | +| `resolved_at` | timestamptz | nein | | gesetzt genau dann, wenn `status <> 'open'` | +| `created_at`, `updated_at` | timestamptz | ja | `now()` | | + +Checks: offen hat kein Ziel; `approved`/`merged` brauchen ein Ziel. Indizes: `(owner_id, created_at)`, eindeutig `(owner_id, bgg_id)` für offene Import-Meldungen (N12a), `(created_at)` für offene Meldungen, eindeutig `provisional_game_id`, `target_game_id`. + **`genres`**, **`mechanics`** und **`categories`** sind gleich aufgebaut: | Spalte | Typ | Pflicht | Standard | Schlüssel | @@ -343,9 +365,10 @@ Die Pflege läuft in R1 nur über Supabase Studio bzw. `psql` als Datenbankrolle - **Ausblenden:** `games.hidden_at` gesetzt = ausgeblendet. Die View `catalog_games` enthält nur Katalogspiele (`visibility = 'catalog'`) ohne `hidden_at` und ohne `merged_into_id`. **Suche und Entdecken lesen nur diese View.** Ein ausgeblendetes Spiel bleibt per ID lesbar (Sammlungen, Partien und Detailseite verweisen darauf). - **Zusammenführen** (Migration `catalog_merge`, PR 2b): `games.merged_into_id` zeigt auf das Zielspiel; ein Trigger lässt nur Katalogspiele als Ziel zu (Fehler `merged_into_not_catalog`). **Regel: `merged_into_id` wird nur über `public.admin_merge_games(p_source, p_target) returns jsonb` gesetzt, nie von Hand.** (Ablauf und Ergebnis-JSON: `docs/katalog-pflege.md`, Abschnitt 5). Sie ist `security invoker`, prüft `current_user in ('postgres', 'supabase_admin')` (sonst `admin_only`) und ist für `anon`, `authenticated` und `service_role` nicht ausführbar; einen API-Aufruf gibt es nicht. Beide Spiele müssen Katalogspiele sein, das Ziel sichtbar. Die Funktion hängt über die interne Funktion `public.repoint_game_references(p_from, p_to)` Sammlung, Favoriten, Partien (mit Edition) und Wertungsbögen um, kopiert die Varianten der Quelle ans Ziel (gleichnamige Zielvarianten werden abgebildet: `lower(btrim(name))`, bei mehreren die älteste, dann die kleinste `id`) und löscht danach die alten Varianten. Bei Doppelbesitz bleibt der Zieleintrag, leere Felder füllt die Quelle, die Liste folgt `owned` vor `wishlist` vor `shopping`, der Quell-Eintrag wird gelöscht; ein Favorit der Quelle entfällt, wenn der Nutzer das Ziel schon als Favorit hat. `bgg_id` und `catalog_source_id`/`source_*` gehen aufs Ziel über, wenn es keine hat; sonst werden sie an der Quelle trotzdem geleert und der Konflikt mit den alten Werten im Ergebnis gemeldet. Die Quelle wird nie gelöscht, sondern ausgeblendet und verwiesen (`hidden_at`, `merged_into_id`); Ketten werden geglättet. Zuordnungen (Genre, Mechanik, Kategorie, Verlag) bleiben am Quell-Eintrag. Ein pgTAP-Wächter (`07`) listet alle Fremdschlüssel auf `games` und lässt neue Verweise erst nach einer Entscheidung zu. +- **Meldungen** (Migration `game_reports`, PR 2c): Tabelle `game_reports` (siehe Tabellenstruktur). Lesen darf nur der Meldende (Policy auf `owner_id`; die Policy für `is_admin()` ist für R4 vorbereitet); Schreibrechte haben `anon` und `authenticated` nicht, #13 ergänzt Insert-Recht und -Policy. Bearbeitet wird in R1 nur über `public.admin_resolve_report(p_report, p_status, p_target) returns jsonb` (Ablauf, Ergebnis und Fehler: `docs/katalog-pflege.md`, Abschnitt 6): `security invoker`, prüft `current_user in ('postgres', 'supabase_admin')` (sonst `admin_only`), für `anon`, `authenticated` und `service_role` nicht ausführbar. `approved` und `merged` hängen den vorläufigen privaten Eintrag über `repoint_game_references` auf das Ziel um (Varianten werden nicht kopiert, gleichnamige abgebildet, sonst geleert, `variants_cleared`) und blenden ihn aus (`hidden_at`, `merged_into_id`); `rejected` speichert höchstens den Verweis. `admin_merge_games` hängt zusätzlich `game_reports.target_game_id` von der Quelle auf das Ziel (`reports_repointed`). Der Trigger `games_visibility_immutable` lehnt jede Änderung von `visibility` und `owner_id` an bestehenden Spielen ab (Fehler `visibility_immutable`, für alle Rollen auch `postgres`; vor der RLS-Prüfung, ein Nutzer bekommt also P0001 statt 42501). Beim Konto-Löschen werden Meldungen und vorläufige Einträge (auch nach der Freigabe) hart gelöscht, Katalogspiele und Daten anderer Nutzer bleiben. Fremdschlüssel-Wächter: `07` und `08`. - **Manuell gepflegte Felder:** Der Trigger `games_mark_manual_fields` trägt jedes Feld eines Katalogspiels, das ein Mensch setzt oder ändert, in `manual_fields` ein (beim Anlegen alle gesetzten Felder). Der Trigger `game_links_mark_manual` ergänzt `genres`, `mechanics`, `categories` und `publishers`, wenn Zuordnungen angelegt, geändert oder gelöscht werden. Ausgenommen ist nur ein späterer Import: Rolle `catalog_importer` **und** Einstellung `shuffle.catalog_import = 'on'` (der Import setzt sie per `set local`; das ist Konvention, die Rolle ist die Sperre). Private Spiele werden nicht gekennzeichnet. Eine bewusst entfernte Markierung entfernt der Trigger nicht wieder. - **Anzeige-Erlaubnis:** `catalog_games.description` und `image_path` sind leer, wenn die Quelle (`catalog_sources`) Texte bzw. Bilder nicht erlaubt und das Feld nicht in `manual_fields` steht. Das ist nur die zweite Sicherung: Daten ohne Erlaubnis dürfen gar nicht erst gespeichert werden (Pflicht für den Import). - **Spaltenrechte auf `games`:** Nutzer dürfen nur `owner_id` (nur beim Anlegen), `title`, `description`, `description_locale`, `year`, `min_players`, `max_players`, `min_minutes`, `max_minutes`, `min_age`, `image_path`, `bgg_id` und `visibility` schreiben, nicht `id`, `hidden_at`, `merged_into_id`, `manual_fields`, `catalog_source_id` und `source_*`. - **Startbestand:** `supabase/catalog/` (CSV, Werkzeug, erzeugte SQL-Dateien); `supabase db reset` spielt Vokabular und Startbestand ein. Der Startbestand enthält nur Fakten und einen eigenen deutschen Kurztext, kein Cover, keine Quelle, keine BGG-Kennung. - **Konto löschen:** Neue Nutzerdaten entstehen nicht; die Zuordnungen privater Spiele (`game_publishers` usw.) werden mit dem Spiel gelöscht. -- **API-Änderungen für das Frontend:** neu lesbar für alle: View `catalog_games`, Tabellen `catalog_sources`, `publishers`, `game_publishers`. Beim Anlegen und Ändern eines privaten Spiels dürfen Clients keine `id`, `created_at` und `updated_at` mehr mitsenden (42501; die ID kommt per `returning`); ein `toJson()` eines Modells darf nie ungefiltert an `insert`/`update` gehen (Merkpunkt für #3 und #13). Der `is_admin()`-Zweig der Zuordnungs-Policies (`game_*_write`) greift für Katalogspiele faktisch nicht mehr, weil der Trigger `game_links_mark_manual` `manual_fields` schreiben muss; ein späterer Admin-Pfad (QUE-08) braucht eigene RPCs. Es gibt keine generierten Datenbanktypen im Repo; das Domänenmodell `CatalogGame` legt KAT-01 (#3) auf Basis der Spalten von `catalog_games` an. +- **API-Änderungen für das Frontend:** neu lesbar für alle: View `catalog_games`, Tabellen `catalog_sources`, `publishers`, `game_publishers`; neu für den Meldenden lesbar: `game_reports`. Das Ändern von `visibility` an einem Spiel ist nicht mehr möglich (P0001 `visibility_immutable`); wer eine Sichtbarkeit mitsendet, muss den alten Wert senden oder die Spalte weglassen. Beim Anlegen und Ändern eines privaten Spiels dürfen Clients keine `id`, `created_at` und `updated_at` mehr mitsenden (42501; die ID kommt per `returning`); ein `toJson()` eines Modells darf nie ungefiltert an `insert`/`update` gehen (Merkpunkt für #3 und #13). Der `is_admin()`-Zweig der Zuordnungs-Policies (`game_*_write`) greift für Katalogspiele faktisch nicht mehr, weil der Trigger `game_links_mark_manual` `manual_fields` schreiben muss; ein späterer Admin-Pfad (QUE-08) braucht eigene RPCs. Es gibt keine generierten Datenbanktypen im Repo; das Domänenmodell `CatalogGame` legt KAT-01 (#3) auf Basis der Spalten von `catalog_games` an. diff --git a/docs/specs/2-katalog-daten.md b/docs/specs/2-katalog-daten.md index 6fb2d58..5c4fd50 100644 --- a/docs/specs/2-katalog-daten.md +++ b/docs/specs/2-katalog-daten.md @@ -720,3 +720,22 @@ Umgesetzt: B5 (Migration `20261103193928_catalog_merge.sql`), B6 (pgTAP `07_cata - **Runbook:** Alle SQL-Bausteine aus Abschnitt 5 laufen wörtlich in `07` (einmalige Prüfung per Skript; keine CI-Prüfung). Abschnitt 5 enthält die Duplikatsuche, eine Vorschau (Verweise, Doppelbesitz), den Aufruf, die Nachprüfung, die Regeln, die Schlüssel des Ergebnis-JSON und die Fehlertexte. - **Nachtrag Review 2b:** Tests ergänzt (Spec-F1-Edition am Ziel, `variants_cleared` im Kollisionszweig, gleichnamige Quellvarianten, zweiter Fremdschlüssel-Wächter auf `game_variants`, unbeteiligte Spiele, Ziel `null`); Runbook um Trockenlauf (`begin; …; rollback;` in einem Lauf, nur dieser Baustein läuft nicht wörtlich in pgTAP), Ketten-Nachprüfung und Nachschlagen von `merged_into_id` ergänzt. Der Null-Check in `admin_merge_games` ist ein äquivalenter Mutant (das spätere `not found` liefert denselben Fehler) und bleibt als ausdrückliche Absicherung. Offen für Chris: Verweise, die nach dem Merge noch auf die Quelle entstehen (W3). + +### 2026-11-03 – Umsetzung PR 2c (backend-dev) + +Umgesetzt: B8 (Migration `20261103195151_game_reports.sql`), B9 (pgTAP `08_game_reports.test.sql`, 153 Tests; `07` und `00` angepasst), B10 (Runbook Abschnitt 6). Abweichungen und Ergänzungen, jeweils zur Prüfung im PR: + +- **Branch:** `feature/2c-katalog-meldungen` statt `feature/2-katalog-meldungen` (Vorgabe des Auftrags), gestapelt auf `feature/2b-katalog-zusammenfuehren`. +- **Zusätzliche Indizes** auf beide Fremdschlüssel nach `games` (Regel: Index auf jeden Fremdschlüssel). `provisional_game_id` ist **eindeutig** (`where provisional_game_id is not null`, 1:1: ein vorläufiger Eintrag gehört zu höchstens einer Meldung), `target_game_id` ein einfacher Index. +- **Zusatzfehler `report_provisional_not_private`** in `admin_resolve_report`: Ist der vorläufige Eintrag kein privates Spiel (Datenfehler per Studio), wird nichts umgehängt; sonst würde `repoint_game_references` ein Katalogspiel wie beim Zusammenführen behandeln (Varianten kopieren und löschen). +- **Ergebnis-JSON `admin_resolve_report`:** `report_id`, `status`, `target_id`, `provisional_game_id`, `repointed` (bool). Nur bei `repointed = true` zusätzlich die Zähler aus `repoint_game_references`. `rejected` und Meldungen ohne vorläufigen Eintrag liefern `repointed = false` und keine Zähler. +- **Ergebnis-JSON `admin_merge_games`:** neuer Schlüssel `reports_repointed` (Anzahl umgehängter Meldungen, immer vorhanden). Dadurch mussten zwei Assertions in `07` (exaktes JSON) und das Beispiel im Runbook Abschnitt 5 angepasst werden. Rumpf von `admin_merge_games` sonst unverändert (per `diff` gegen 2b geprüft); `revoke execute … from public, anon, authenticated, service_role` nach dem `create or replace` ausdrücklich wiederholt. +- **Sperren:** `admin_resolve_report` sperrt die Meldung und danach Ziel und vorläufigen Eintrag in `id`-Reihenfolge (`for update`), wie `admin_merge_games`. Dass der vorläufige Eintrag existiert, sichert der Fremdschlüssel (`on delete set null`); `repoint_game_references` selbst prüft die Existenz von `p_from` weiterhin nicht. +- **Ziel wird auch bei `rejected` geprüft** (`report_target_not_listed`), wenn eines angegeben ist. Reihenfolge der Prüfungen wie in 3.4; `p_report = null` ist `report_not_found`, `p_status = null` ist `report_status_invalid`. +- **Trigger `games_visibility_immutable`** läuft vor der RLS-Prüfung: Ein Nutzer, der `visibility` ändert, bekommt `P0001 visibility_immutable` statt `42501` (API-Änderung für Clients; bestehende Tests in `02` bleiben grün, weil dort `owner_id` ohne Spaltenrecht vorher mit 42501 scheitert). Es gibt weiterhin keine generierten Datenbanktypen im Repo. +- **RLS:** zwei Lese-Policies (eigene Zeilen; `is_admin()` für R4). Bewusst kein Test mit `is_admin()` als Zugang in R1, aber `08` dokumentiert, dass `app_metadata.role = 'admin'` lesen, nicht schreiben kann, und `user_metadata` nichts öffnet. +- **Tests stabil gegenüber #13:** `08` prüft Schreibrechte je Spalte (`has_column_privilege` auf `status`, `target_game_id`, `resolved_at`, `origin`, `owner_id`, `bgg_id`, `created_at`), „Delete verboten“ und „fremde Meldung nicht sichtbar/änderbar“, nicht „kein Insert/Update auf der Tabelle“. +- **Bewusst nicht gebaut (offene Entscheidung Chris, Spec #13):** `game_reports.result_seen_at` und `editions_cleared`. #13 ergänzt bei Freigabe das Spaltenrecht `update (result_seen_at)` und ggf. `editions_cleared` sowie Insert-Recht und -Policy; die Tests in `08` sind darauf vorbereitet. +- **Fremdschlüssel-Wächter:** erweiterte Liste in `07` und in `08` (`game_reports.target_game_id` behandelt, `game_reports.provisional_game_id` bewusst ausgelassen). `00_schema.test.sql`: 21 Tabellen. +- **Runbook Abschnitt 6:** Alle SQL-Bausteine laufen wörtlich in `08` (einmalige Prüfung per Skript, keine CI-Prüfung). Der Trockenlauf-Baustein aus 2b gilt entsprechend (`begin; …; rollback;` in einem Lauf). +- **Mutationsprüfung:** 25 Mutanten (u. a. Trigger entfernt/ohne `owner_id`, Grants, Policies, RLS aus, Fremdschlüssel-Aktionen, N12a-Index, Funktionsrumpf-Änderungen) gegen `08`; alle werden erkannt. Bei der Prüfung fiel ein Scheintest auf (Löschen eines Ziels scheiterte an anderen Verweisen), jetzt mit eigenem Ziel. Nicht erkennbar: das Weglassen des wiederholten `revoke` allein (Postgres behält die Rechte bei `create or replace`); erkannt wird stattdessen ein ausdrückliches `grant execute`. diff --git a/supabase/README.md b/supabase/README.md index 6f67b9d..6dc0427 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -3,7 +3,7 @@ ## Inhalt | Pfad | Zweck | |---|---| -| `migrations/` | Schema, Trigger und RLS-Policies für R1 (20 Tabellen, 3 Views). Nur additiv ändern, nie bestehende Dateien editieren. | +| `migrations/` | Schema, Trigger und RLS-Policies für R1 (21 Tabellen, 3 Views). Nur additiv ändern, nie bestehende Dateien editieren. | | `tests/database/` | pgTAP-Tests: Struktur-Regeln und Zugriffsrechte je Bereich. | | `templates/` | Auth-Mailvorlagen (HTML, Go-Template) für Bestätigung und Passwort-Reset. | | `seed.sql` | Beispieldaten nur für die lokale Entwicklung. | @@ -32,7 +32,8 @@ supabase test db # alle pgTAP-Tests ausführen ## Katalog (KAT-05) - Pflege in R1 nur über Studio bzw. `psql` (Rolle `postgres`), Ablauf und Regeln: [`docs/katalog-pflege.md`](../docs/katalog-pflege.md). Spec: `docs/specs/2-katalog-daten.md`. - Suche und Entdecken lesen nur die View `catalog_games` (ohne ausgeblendete und zusammengeführte Spiele). Geänderte Fakten trägt ein Trigger in `games.manual_fields` ein. -- Zusammenführen von Duplikaten nur über die Funktion `public.admin_merge_games(quelle, ziel)` (nur Datenbankrolle `postgres`/`supabase_admin`, nicht über die API aufrufbar); Ablauf in `docs/katalog-pflege.md`, Abschnitt 5. Jede neue Tabelle mit Verweis auf `games` muss dort behandelt werden (Wächter in `tests/database/07_catalog_merge.test.sql`). +- Zusammenführen von Duplikaten nur über die Funktion `public.admin_merge_games(quelle, ziel)` (nur Datenbankrolle `postgres`/`supabase_admin`, nicht über die API aufrufbar); Ablauf in `docs/katalog-pflege.md`, Abschnitt 5. Jede neue Tabelle mit Verweis auf `games` muss dort behandelt werden (Wächter in `tests/database/07_catalog_merge.test.sql` und `08_game_reports.test.sql`). +- Meldungen fehlender Spiele (`game_reports`) bearbeitet der Admin nur über `public.admin_resolve_report(meldung, status, ziel)` (nur Datenbankrolle `postgres`/`supabase_admin`); Ablauf in `docs/katalog-pflege.md`, Abschnitt 6. `visibility` und `owner_id` eines Spiels sind unveränderlich (Trigger `games_visibility_immutable`). - `db reset` spielt `seed.sql`, `catalog/vocabulary.sql` und `catalog/startbestand.sql` ein (`[db.seed] sql_paths` in `config.toml`). Die SQL-Dateien in `catalog/` sind **erzeugt**; Quellen sind die CSV-Dateien. Neu erzeugen und testen (Deno, `brew install deno`): ```bash deno run --allow-read=supabase --allow-write=supabase supabase/catalog/build_startbestand.ts diff --git a/supabase/tests/database/08_game_reports.test.sql b/supabase/tests/database/08_game_reports.test.sql index ab4629b..8b6013a 100644 --- a/supabase/tests/database/08_game_reports.test.sql +++ b/supabase/tests/database/08_game_reports.test.sql @@ -5,7 +5,7 @@ -- Läuft auch mit eingespieltem Startbestand: eigene Fixtures mit festen IDs (Präfix 7c1d/f8), gezählt wird nur darüber. -- Regel dieser Datei: Jede Verweigerung steht neben einem Positivfall, sonst würde "alles verweigern" bestehen. begin; -select plan(152); +select plan(153); insert into auth.users (id, email, raw_user_meta_data) values ('11111111-1111-1111-1111-111111111111', 'anna@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Anna"}'), @@ -241,8 +241,14 @@ insert into public.game_reports (id, owner_id, title, provisional_game_id) value delete from public.games where id = 'f8000000-0000-0000-0000-000000000c01'; select is((select count(*) || ':' || count(provisional_game_id) from public.game_reports where id = 'f8000000-0000-0000-0000-000000000c02'), '1:0', 'Fremdschlüssel: wird der vorläufige Eintrag gelöscht, bleibt die Meldung und der Verweis wird leer (on delete set null)'); -select throws_ok($$delete from public.games where id = '7c1d2c3d-0000-4000-8000-000000000004'$$, '23503', null, +-- eigenes Ziel, auf das nur eine Meldung verweist (sonst würde ein anderer Verweis die Prüfung vortäuschen) +insert into public.games (id, title, visibility) values ('f8000000-0000-0000-0000-000000000c03', 'Ziel nur für eine Meldung', 'catalog'); +insert into public.game_reports (id, owner_id, title, status, resolved_at, target_game_id) values + ('f8000000-0000-0000-0000-000000000c04', '22222222-2222-2222-2222-222222222222', 'Verweist auf Ziel', 'rejected', now(), 'f8000000-0000-0000-0000-000000000c03'); +select throws_ok($$delete from public.games where id = 'f8000000-0000-0000-0000-000000000c03'$$, '23503', null, 'Fremdschlüssel: ein Ziel mit Verweis aus einer Meldung lässt sich nicht löschen (on delete restrict)'); +select is((select target_game_id from public.game_reports where id = 'f8000000-0000-0000-0000-000000000c04'), 'f8000000-0000-0000-0000-000000000c03'::uuid, + 'Der Verweis der Meldung auf das Ziel bleibt bestehen'); select throws_ok($$insert into public.game_reports (owner_id, title) values (gen_random_uuid(), 'Ohne Konto')$$, '23503', null, 'Fremdschlüssel: Meldung braucht ein vorhandenes Profil');