longevity: atomicità compilazione questionario con transazione unica

This commit is contained in:
2026-08-21 20:02:23 +02:00
parent f21b64fcc5
commit 91e6487da3
2 changed files with 46 additions and 23 deletions
+26 -23
View File
@@ -19,37 +19,40 @@ export function salvaCompilazione(
liberi?: Record<string, string>; liberi?: Record<string, string>;
} }
): number { ): number {
// Validare PRIMA di aprire la sessione: altrimenti un id sbagliato lascia // Validare PRIMA della transazione: se un test_id è sconosciuto, non apriamo
// in giro una sessione vuota. // nemmeno una sessione. La validazione non è nel corpo della transazione.
for (const id of Object.keys(input.risposte)) { for (const id of Object.keys(input.risposte)) {
if (!esisteTest(db, id)) throw new Error(`test_id non nel registro: ${id}`); if (!esisteTest(db, id)) throw new Error(`test_id non nel registro: ${id}`);
} }
const sessioneId = apriSessione(db, { // Avvolgi le tre scritture (apertura sessione, registra misure, inserisci note)
client_code: input.client_code, // in un'unica transazione: tutto o niente, senza tracce parziali su dati sanitari.
data: input.data, const tx = db.transaction(() => {
tipo: 'questionario', const sessioneId = apriSessione(db, {
eta_alla_data: input.eta, client_code: input.client_code,
quest_version: QUEST_VERSION, data: input.data,
}); tipo: 'questionario',
eta_alla_data: input.eta,
quest_version: QUEST_VERSION,
});
registraMisure( registraMisure(
db, sessioneId, 'questionario', db, sessioneId, 'questionario',
Object.entries(input.risposte).map(([test_id, valore_num]) => ({ test_id, valore_num })) Object.entries(input.risposte).map(([test_id, valore_num]) => ({ test_id, valore_num }))
);
if (input.liberi) {
const ins = db.prepare(
`INSERT INTO profilo_note (client_code, sessione_id, campo_id, testo) VALUES (?, ?, ?, ?)`
); );
const tx = db.transaction(() => {
for (const [campo, testo] of Object.entries(input.liberi!)) { if (input.liberi) {
const ins = db.prepare(
`INSERT INTO profilo_note (client_code, sessione_id, campo_id, testo) VALUES (?, ?, ?, ?)`
);
for (const [campo, testo] of Object.entries(input.liberi)) {
if (testo.trim() === '') continue; if (testo.trim() === '') continue;
ins.run(input.client_code, sessioneId, campo, testo); ins.run(input.client_code, sessioneId, campo, testo);
} }
}); }
tx();
}
return sessioneId; return sessioneId;
});
return tx();
} }
+20
View File
@@ -64,4 +64,24 @@ describe('compilazione del questionario', () => {
const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }; const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number };
expect(n.n).toBe(0); expect(n.n).toBe(0);
}); });
it('fallimento a metà non lascia traccia: sessione, misure e note sono atomiche', () => {
const db = dbPronto();
// Passa null nel campo libero per provocare un errore NOT NULL nella tabella profilo_note.
// Questo forza il fallimento DOPO aver aperto la sessione e registrato le misure.
expect(() =>
salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-21',
risposte: { q_riposato: 6 },
liberi: { farmaci: null as any }, // Tipo violato: NOT NULL
})
).toThrow();
// Verifica che la transazione sia stata rollback: niente sessione, niente misure, niente note.
const sessioni = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number };
const misure = db.prepare(`SELECT COUNT(*) n FROM misure`).get() as { n: number };
const note = db.prepare(`SELECT COUNT(*) n FROM profilo_note`).get() as { n: number };
expect(sessioni.n).toBe(0);
expect(misure.n).toBe(0);
expect(note.n).toBe(0);
});
}); });