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>
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
/**
|
|
* 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");
|
|
}
|