Rende testabile la bonifica di M15 come funzione RPC (M16)
M15 ha ripulito una tantum le righe orfane con un blocco di DELETE verificato solo a mano, senza lasciare nessuna rete di sicurezza automatica per il futuro. Questa migration rende lo stesso corpo la funzione bonifica_dati_evento_orfani(), riservata al service role, così resta richiamabile se il trigger di M14 smettesse mai di funzionare. Aggiunge 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. Documenta l'aggiornamento in DESIGN_DECISIONS.md (DD-029), DATABASE.md, test/README.md e CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,9 @@ Prima versione, pre-release.
|
||||
- Bonificate una tantum le righe orfane lasciate da cancellazioni precedenti a M14 (migration
|
||||
`m15_bonifica_dati_evento_orfani`), senza toccare i vecchi voti MVP/pagelle/badge social
|
||||
legati a id Scout o CSI, che restano dati storici legittimi (DD-029).
|
||||
- La bonifica sopra è ora anche una funzione richiamabile, `bonifica_dati_evento_orfani()`
|
||||
(migration `m16_funzione_bonifica_dati_evento_orfani`, riservata al service role), coperta
|
||||
da test di integrazione invece che verificata solo a mano (DD-029).
|
||||
|
||||
### Sicurezza
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `eventi_app` | Eventi gestionali utilizzati dall'app. | Modello in uso dal codice attuale. Cancellare un evento pulisce a cascata, tramite trigger, tutte le tabelle collegate elencate in questa pagina (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) — migration `m14_pulizia_dati_evento_cancellato`, DD-029. Le righe orfane da cancellazioni precedenti sono state bonificate una tantum da `m15_bonifica_dati_evento_orfani`, senza toccare i vecchi voti MVP/pagelle/badge social legati a id Scout o CSI. |
|
||||
| `eventi_app` | Eventi gestionali utilizzati dall'app. | Modello in uso dal codice attuale. Cancellare un evento pulisce a cascata, tramite trigger, tutte le tabelle collegate elencate in questa pagina (`risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite`) — migration `m14_pulizia_dati_evento_cancellato`, DD-029. Le righe orfane da cancellazioni precedenti sono state bonificate una tantum da `m15_bonifica_dati_evento_orfani`, senza toccare i vecchi voti MVP/pagelle/badge social legati a id Scout o CSI; la stessa logica resta richiamabile come funzione `bonifica_dati_evento_orfani()` (`m16_funzione_bonifica_dati_evento_orfani`, riservata al service role) se mai servisse di nuovo. |
|
||||
| `risposte_presenze` | Risposte dei giocatori agli eventi. | Modello in uso dal codice attuale. `risposto_il` è l'istante della **prima** risposta (migration `m9_risposte_presenze_risposto_il`): confrontato con `eventi_app.creato_il` dà la serie "Conferme 24h". Un trigger lo rende immutabile, così un ripensamento non fa risultare rapida una risposta lenta — `aggiornato_il` resta l'ultima modifica. |
|
||||
| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello "nuovo" con autenticazione e vincoli, non ancora adottato (DD-014). |
|
||||
| `presenze` | Presenze agli eventi. | Come sopra (DD-014). |
|
||||
|
||||
@@ -1142,6 +1142,16 @@ migration non li tocca. Verificato manualmente sul database locale prima di appl
|
||||
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).
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
-- M16 — La bonifica di M15 diventa una funzione richiamabile e testabile
|
||||
--
|
||||
-- M15 ha ripulito una tantum le righe orfane lasciate da eventi cancellati prima di M14
|
||||
-- (DD-029), con un blocco di DELETE eseguito una sola volta: non restava nulla da richiamare
|
||||
-- né da testare in automatico, solo una verifica manuale fatta prima di applicarla.
|
||||
--
|
||||
-- Qui lo stesso corpo diventa la funzione `bonifica_dati_evento_orfani()`: se in futuro il
|
||||
-- trigger di M14 venisse per errore rimosso o disattivato, o emergesse un altro batch di
|
||||
-- orfani per un motivo imprevisto, si può rilanciare `select bonifica_dati_evento_orfani();`
|
||||
-- invece di riscrivere da capo la stessa query delicata — e la sua logica resta coperta da
|
||||
-- un test di integrazione (`test/integration/bonifica-evento.test.ts`) invece che verificata
|
||||
-- a mano una tantum.
|
||||
--
|
||||
-- Riservata al service role: non è un'azione che un giocatore o un admin devono poter
|
||||
-- richiamare dall'app, solo un intervento di manutenzione database.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.bonifica_dati_evento_orfani()
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.risposte_presenze
|
||||
WHERE evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.cacche_partita
|
||||
WHERE evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.turni_palloni
|
||||
WHERE evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.scout_sessioni
|
||||
WHERE evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.scout_live
|
||||
WHERE evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.scout_partite
|
||||
WHERE evento_id IS NOT NULL
|
||||
AND evento_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
-- Solo i match_id nel formato di nuovoIdEvento() ("e" + timestamp base36): i vecchi voti
|
||||
-- storici su id Scout ("s" + timestamp) o CSI (numerico o "data-squadra-squadra") non
|
||||
-- rientrano nel filtro e restano intatti (vedi M15 per il dettaglio).
|
||||
DELETE FROM public.mvp_voti
|
||||
WHERE match_id ~ '^e[0-9a-z]+$'
|
||||
AND match_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.pagelle_voti
|
||||
WHERE match_id ~ '^e[0-9a-z]+$'
|
||||
AND match_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
|
||||
DELETE FROM public.badge_social_voti
|
||||
WHERE match_id ~ '^e[0-9a-z]+$'
|
||||
AND match_id NOT IN (SELECT id FROM public.eventi_app);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.bonifica_dati_evento_orfani() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.bonifica_dati_evento_orfani() TO service_role;
|
||||
+3
-2
@@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
|
||||
| Cartella | Cosa verifica | Serve rete? |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route) | No |
|
||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) sul database locale | Sì |
|
||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) e la funzione di bonifica delle righe orfane (`bonifica-evento`, M16) sul database locale | Sì |
|
||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||
|
||||
@@ -46,7 +46,8 @@ bun test/integration/permessi.test.ts # permessi per ruolo sulle tabelle
|
||||
bun test/integration/permessi-route.test.ts # chi può far partire le notifiche
|
||||
bun test/integration/scritture.test.ts # semantica degli upsert e vincoli
|
||||
bun test/integration/lettori-server.test.ts # le letture server delle route push
|
||||
bun test/integration/pulizia-evento.test.ts # cascata alla cancellazione di un evento (M14)
|
||||
bun test/integration/pulizia-evento.test.ts # cascata alla cancellazione di un evento (M14)
|
||||
bun test/integration/bonifica-evento.test.ts # funzione di bonifica righe orfane (M16)
|
||||
```
|
||||
|
||||
`permessi-route` avvia il server di sviluppo **puntato al database locale** invece che al
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Funzione `bonifica_dati_evento_orfani()` (M16): `bun test/integration/bonifica-evento.test.ts`.
|
||||
*
|
||||
* M15 ha ripulito una tantum le righe orfane lasciate da eventi cancellati prima del trigger
|
||||
* di M14 (DD-029); M16 ha reso permanente la stessa logica come funzione RPC, così resta
|
||||
* richiamabile e testabile invece che verificata a mano una volta sola.
|
||||
*
|
||||
* Il punto delicato: per `mvp_voti`/`pagelle_voti`/`badge_social_voti` la funzione deve
|
||||
* cancellare solo i `match_id` nel formato id evento CrAPP ("e" + timestamp base36) senza
|
||||
* corrispondenza in `eventi_app` — mai i vecchi voti storici su id Scout ("s" + timestamp) o
|
||||
* CSI (numerico), che sono dati legittimi mai collegati a un evento CrAPP (`docs/modules/mvp.md`).
|
||||
* Questo test copre esattamente quella distinzione.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`): usa id con il prefisso
|
||||
* `test-bonifica-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("bonifica dati evento orfani", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("bonifica-evento");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`bonifica dati evento orfani su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-bonifica-evento";
|
||||
// Deve rispettare ^e[0-9a-z]+$ (formato di nuovoIdEvento()): un id con trattini non
|
||||
// verrebbe mai filtrato dalla funzione, quindi non testerebbe la regola che conta.
|
||||
const ORFANO = `etestbonificaevento${Date.now().toString(36)}`;
|
||||
const STORICO_SCOUT = `s${Date.now()}`; // formato id Scout storico: va preservato
|
||||
const STORICO_CSI = "42"; // formato id CSI storico (numerico): va preservato
|
||||
|
||||
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 esiste(tabella: string, filtro: string): Promise<boolean> {
|
||||
const res = await rest(`${tabella}?${filtro}&select=*`, {
|
||||
headers: { Prefer: "count=exact" },
|
||||
});
|
||||
return Number(res.headers.get("content-range")?.split("/")[1] ?? 0) > 0;
|
||||
}
|
||||
|
||||
async function pulisci() {
|
||||
await rest(`risposte_presenze?evento_id=eq.${ORFANO}`, { method: "DELETE" });
|
||||
await rest(`mvp_voti?match_id=eq.${ORFANO}`, { method: "DELETE" });
|
||||
await rest(`mvp_voti?match_id=eq.${STORICO_SCOUT}`, { method: "DELETE" });
|
||||
await rest(`pagelle_voti?match_id=eq.${ORFANO}`, { method: "DELETE" });
|
||||
await rest(`pagelle_voti?match_id=eq.${STORICO_CSI}&votante_id=eq.${PREFISSO}-va`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
"bonifica_dati_evento_orfani() rimuove solo gli orfani veri, non lo storico Scout/CSI",
|
||||
async () => {
|
||||
// Riga orfana: id in formato evento CrAPP, nessun evento corrispondente.
|
||||
await inserisci("risposte_presenze", {
|
||||
evento_id: ORFANO,
|
||||
giocatore_id: `${PREFISSO}-g1`,
|
||||
stato: "presente",
|
||||
});
|
||||
await inserisci("mvp_voti", {
|
||||
match_id: ORFANO,
|
||||
votante_id: `${PREFISSO}-va`,
|
||||
votato_id: `${PREFISSO}-vb`,
|
||||
votato_nome: "Orfano",
|
||||
});
|
||||
await inserisci("pagelle_voti", {
|
||||
match_id: ORFANO,
|
||||
votante_id: `${PREFISSO}-va`,
|
||||
votato_id: `${PREFISSO}-vb`,
|
||||
voto: 6,
|
||||
});
|
||||
|
||||
// Voti storici legittimi su id Scout/CSI: nessun evento CrAPP li ha mai referenziati.
|
||||
await inserisci("mvp_voti", {
|
||||
match_id: STORICO_SCOUT,
|
||||
votante_id: `${PREFISSO}-va`,
|
||||
votato_id: `${PREFISSO}-vb`,
|
||||
votato_nome: "Storico",
|
||||
});
|
||||
await inserisci("pagelle_voti", {
|
||||
match_id: STORICO_CSI,
|
||||
votante_id: `${PREFISSO}-va`,
|
||||
votato_id: `${PREFISSO}-vb`,
|
||||
voto: 8,
|
||||
});
|
||||
|
||||
const res = await fetch(`${URL_BASE}/rest/v1/rpc/bonifica_dati_evento_orfani`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`rpc bonifica_dati_evento_orfani: ${res.status} ${await res.text()}`);
|
||||
|
||||
assert.equal(
|
||||
await esiste("risposte_presenze", `evento_id=eq.${ORFANO}`),
|
||||
false,
|
||||
"riga orfana rimossa",
|
||||
);
|
||||
assert.equal(
|
||||
await esiste("mvp_voti", `match_id=eq.${ORFANO}`),
|
||||
false,
|
||||
"voto MVP orfano rimosso",
|
||||
);
|
||||
assert.equal(
|
||||
await esiste("pagelle_voti", `match_id=eq.${ORFANO}`),
|
||||
false,
|
||||
"voto pagella orfano rimosso",
|
||||
);
|
||||
assert.equal(
|
||||
await esiste("mvp_voti", `match_id=eq.${STORICO_SCOUT}`),
|
||||
true,
|
||||
"voto MVP storico su id Scout preservato",
|
||||
);
|
||||
assert.equal(
|
||||
await esiste("pagelle_voti", `match_id=eq.${STORICO_CSI}`),
|
||||
true,
|
||||
"voto pagella storico su id CSI preservato",
|
||||
);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await pulisci();
|
||||
}
|
||||
|
||||
riepilogo("bonifica-evento");
|
||||
}
|
||||
Reference in New Issue
Block a user