- Spezza il registro delle decisioni in docs/decisions/DD-XXX.md, con indice - Ripulisce DATABASE.md dalla storia delle migration e aggiunge la sezione Lettura - Riduce PROJECT_STATE.md a una pagina; la procedura di allestimento passa in OPERATIONS.md - Aggiunge OPERATIONS.md, GLOSSARIO.md, trappole note e checklist in ARCHITECTURE.md - Sintetizza la copertura test di badge.md e documenta docs/archive - Allinea AGENTS.md, README.md e la regola Cursor (tolto l'obbligo di develop, in contrasto con DD-019) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5.0 KiB
DD-029 — Cancellare un evento pulisce a cascata i dati collegati
Data: 9 settembre 2026
Stato: Accettata
Contesto
Un audit del modulo Obiettivi ha verificato che gli obiettivi in sé non hanno bisogno di
nessuna pulizia quando un evento viene cancellato: sono ricalcolati a runtime sull'elenco
eventi corrente (obiettivi.ts), quindi un evento sparito da eventi_app semplicemente
smette di contare. Il problema è un livello sotto: useEliminaEvento()
(src/lib/eventi.ts) cancella solo la riga in eventi_app. Nessuna delle otto tabelle
collegate (risposte_presenze, cacche_partita, mvp_voti, pagelle_voti,
badge_social_voti, turni_palloni, scout_sessioni, scout_live, scout_partite) ha mai
avuto una foreign key verso eventi_app(id): le loro righe restavano orfane a database.
Oggi è innocuo per le statistiche, perché nessun calcolo legge quelle tabelle se non partendo
dall'elenco eventi corrente. Ma è un rischio latente: un id evento futuro identico a uno
passato (generato come "e" + timestamp, collisione improbabile ma non impossibile)
erediterebbe dati vecchi non suoi; e un calcolo futuro che iterasse direttamente una di quelle
tabelle, invece di partire da eventi_app, conterebbe anche le righe orfane.
Decisione
Migration m14_pulizia_dati_evento_cancellato: un trigger AFTER DELETE ON eventi_app
cancella a cascata le righe corrispondenti (evento_id/match_id = id evento cancellato)
nelle otto tabelle collegate, tramite una funzione SECURITY DEFINER. Agisce a database, non
in useEliminaEvento(): protegge anche chi cancella un evento scrivendo direttamente su
PostgREST, non solo chi passa dal bottone dell'app.
Alternative scartate
- Foreign key con
ON DELETE CASCADEversoeventi_app(id)→ non applicabile subito:mvp.mddocumenta che storicamentematch_idinmvp_voti/pagelle_voti/badge_social_votia volte conteneva l'id di una sessione Scout o di una partita CSI, non l'id evento CrAPP. Un vincolo FK avrebbe rifiutato la migration alla prima riga storica disallineata; il trigger non valida i dati esistenti, solo le cancellazioni da qui in avanti. - Cancellazione manuale nelle otto tabelle dentro
useEliminaEvento()→ fragile: va tenuta aggiornata a mano ogni volta che un nuovo modulo aggiunge una tabella conevento_id, e non protegge chi scrive/cancella direttamente su PostgREST. - Bonificare anche le righe orfane già esistenti nella stessa migration → rimandato: tocca dati reali già scritti, è un intervento più delicato che merita una migration a sé, non urgente perché quelle righe sono già invisibili a ogni calcolo attuale.
Conseguenze
- Da questa migration in poi, cancellare un evento (da qualunque punto, app o REST diretto) ripulisce automaticamente tutte le tabelle collegate.
- Le righe orfane generate da cancellazioni precedenti a questa migration restano nel database: il trigger previene il problema da qui in avanti, non ripulisce lo storico.
test/integration/pulizia-evento.test.tsè la definizione eseguibile del comportamento: scrive una riga in ciascuna delle otto tabelle, cancella l'evento e verifica che spariscano tutte.
Aggiornamento (9 settembre 2026, stesso giorno) — la bonifica dello storico prevista sopra
come "riesame" è stata fatta subito dopo, migration m15_bonifica_dati_evento_orfani: righe
orfane in risposte_presenze, cacche_partita, turni_palloni, scout_sessioni,
scout_live, scout_partite identificate confrontando evento_id con eventi_app. Per
mvp_voti/pagelle_voti/badge_social_voti il confronto si applica solo ai match_id
nel formato id evento CrAPP (^e[0-9a-z]+$, quello di nuovoIdEvento()): i vecchi voti su id
Scout (prefisso s + timestamp decimale) o CSI (numerico o data-squadra-squadra) non sono
orfani, sono dati storici legittimi mai collegati a un evento CrAPP (vedi contesto sopra), e la
migration non li tocca. Verificato manualmente sul database locale prima di applicarla: un voto
di test su id Scout è sopravvissuto alla bonifica, un voto di test su id evento CrAPP orfano è
stato rimosso.
Aggiornamento (9 settembre 2026) — la verifica manuale di M15 non lasciava nessuna rete di
sicurezza automatica per il futuro, a differenza del resto del progetto (DD-020). Migration
m16_funzione_bonifica_dati_evento_orfani rende lo stesso corpo una funzione
bonifica_dati_evento_orfani() (riservata al service_role, non richiamabile dall'app),
coperta da test/integration/bonifica-evento.test.ts: inserisce una riga orfana e una storica
su id Scout/CSI, richiama la funzione via RPC e verifica che tocchi solo la prima. Non serve
richiamarla di nuovo ora (M15 ha già ripulito lo storico): resta pronta come intervento di
manutenzione se in futuro il trigger di M14 smettesse di funzionare o emergesse un altro batch
di orfani.
Riesame
Se una nuova tabella collegata a un evento non viene aggiunta al trigger quando creata (va
aggiornata a mano, non c'è un meccanismo che lo forzi).