Pulisce a cascata i dati collegati quando un evento viene cancellato (DD-029)

Un audit del modulo Obiettivi ha verificato che gli obiettivi non hanno
bisogno di pulizia: sono ricalcolati a runtime sull'elenco eventi
corrente. Il problema reale era un livello sotto: cancellare un evento
lasciava orfane le righe collegate in 8 tabelle (risposte_presenze,
cacche_partita, mvp_voti, pagelle_voti, badge_social_voti,
turni_palloni, scout_sessioni, scout_live, scout_partite), nessuna con
una foreign key verso eventi_app.

Aggiunge un trigger AFTER DELETE su eventi_app (migration
m14_pulizia_dati_evento_cancellato) invece di una FK con CASCADE, non
applicabile per dati storici disallineati (match_id che a volte punta
a id Scout/CSI). Verificato con un nuovo test di integrazione contro
il database locale; documentato in DESIGN_DECISIONS.md, DATABASE.md,
obiettivi-squadra.md, test/README.md e CHANGELOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 10:16:29 +02:00
co-authored by Claude Sonnet 5
parent 00235d1594
commit c7cd527045
7 changed files with 268 additions and 2 deletions
+151
View File
@@ -0,0 +1,151 @@
/**
* Pulizia a cascata dei dati collegati alla cancellazione di un evento (M14):
* `bun test/integration/pulizia-evento.test.ts`.
*
* Scrive una riga per ciascuna delle 9 tabelle collegate a un evento (`risposte_presenze`,
* `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`,
* `scout_sessioni`, `scout_live`, `scout_partite`), cancella l'evento e verifica che il
* trigger `eventi_app_pulisci_dati_collegati` le abbia rimosse tutte. Prima di M14 queste
* righe restavano orfane a database (`docs/DESIGN_DECISIONS.md`, DD-029).
*
* Gira solo sullo stack locale (`npx supabase start`): usa id con il prefisso
* `test-pulizia-evento`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("pulizia dati evento cancellato", "stack locale non attivo (npx supabase start)");
riepilogo("pulizia-evento");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`pulizia dati evento cancellato su ${URL_BASE}`);
const ID_EVENTO = "test-pulizia-evento-e1";
const rest = (percorso: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: SERVIZIO,
Authorization: `Bearer ${SERVIZIO}`,
"content-type": "application/json",
Prefer: "return=minimal",
...(init?.headers ?? {}),
},
});
async function inserisci(tabella: string, riga: Record<string, unknown>) {
const res = await rest(tabella, { method: "POST", body: JSON.stringify(riga) });
if (!res.ok) throw new Error(`insert su ${tabella}: ${res.status} ${await res.text()}`);
}
async function conta(tabella: string, filtro: string): Promise<number> {
const res = await rest(`${tabella}?${filtro}&select=*`, {
headers: { Prefer: "count=exact" },
});
return Number(res.headers.get("content-range")?.split("/")[1] ?? 0);
}
const TABELLE_EVENTO_ID = [
"risposte_presenze",
"cacche_partita",
"turni_palloni",
"scout_sessioni",
"scout_live",
"scout_partite",
];
const TABELLE_MATCH_ID = ["mvp_voti", "pagelle_voti", "badge_social_voti"];
async function pulisciTutto() {
await rest(`eventi_app?id=eq.${ID_EVENTO}`, { method: "DELETE" });
for (const t of TABELLE_EVENTO_ID) {
await rest(`${t}?evento_id=eq.${ID_EVENTO}`, { method: "DELETE" });
}
for (const t of TABELLE_MATCH_ID) {
await rest(`${t}?match_id=eq.${ID_EVENTO}`, { method: "DELETE" });
}
}
try {
await prova("cancellare un evento pulisce a cascata tutte le tabelle collegate", async () => {
await inserisci("eventi_app", {
id: ID_EVENTO,
tipo: "allenamento",
titolo: "Test pulizia",
data: "2026-09-01",
});
await inserisci("risposte_presenze", {
evento_id: ID_EVENTO,
giocatore_id: "test-g1",
stato: "presente",
});
await inserisci("cacche_partita", {
evento_id: ID_EVENTO,
giocatore_id: "test-g1",
quantita: 2,
});
await inserisci("turni_palloni", { evento_id: ID_EVENTO, giocatore_id: "test-g1" });
await inserisci("scout_sessioni", {
evento_id: ID_EVENTO,
giocatore_id: "test-g1",
giocatore_nome: "Uno",
});
await inserisci("scout_live", { evento_id: ID_EVENTO, stato: {} });
await inserisci("scout_partite", {
id: `${ID_EVENTO}-scout`,
evento_id: ID_EVENTO,
data: "2026-09-01",
avversario: "Test",
set_nostri: 3,
set_loro: 0,
});
await inserisci("mvp_voti", {
match_id: ID_EVENTO,
votante_id: "test-g1",
votato_id: "test-g2",
votato_nome: "Due",
});
await inserisci("pagelle_voti", {
match_id: ID_EVENTO,
votante_id: "test-g1",
votato_id: "test-g2",
voto: 7,
});
await inserisci("badge_social_voti", {
match_id: ID_EVENTO,
categoria: "top",
votante_id: "test-g1",
votato_id: "test-g2",
votato_nome: "Due",
});
// Tutte le righe esistono prima della cancellazione.
for (const t of TABELLE_EVENTO_ID) {
assert.equal(await conta(t, `evento_id=eq.${ID_EVENTO}`), 1, `${t}: riga presente`);
}
for (const t of TABELLE_MATCH_ID) {
assert.equal(await conta(t, `match_id=eq.${ID_EVENTO}`), 1, `${t}: riga presente`);
}
const res = await rest(`eventi_app?id=eq.${ID_EVENTO}`, { method: "DELETE" });
if (!res.ok) throw new Error(`delete evento: ${res.status} ${await res.text()}`);
// Il trigger deve aver ripulito tutte le righe collegate.
for (const t of TABELLE_EVENTO_ID) {
assert.equal(await conta(t, `evento_id=eq.${ID_EVENTO}`), 0, `${t}: pulita a cascata`);
}
for (const t of TABELLE_MATCH_ID) {
assert.equal(await conta(t, `match_id=eq.${ID_EVENTO}`), 0, `${t}: pulita a cascata`);
}
});
} finally {
await pulisciTutto();
}
riepilogo("pulizia-evento");
}