From 91e6487da37493c2e79893c4960c1d815f6c5559 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 21 Aug 2026 20:02:23 +0200 Subject: [PATCH] =?UTF-8?q?longevity:=20atomicit=C3=A0=20compilazione=20qu?= =?UTF-8?q?estionario=20con=20transazione=20unica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/longevity/questionario.ts | 49 +++++++++++++++------------- tests/longevity/questionario.test.ts | 20 ++++++++++++ 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/lib/longevity/questionario.ts b/src/lib/longevity/questionario.ts index 5f1032a..3bbfb06 100644 --- a/src/lib/longevity/questionario.ts +++ b/src/lib/longevity/questionario.ts @@ -19,37 +19,40 @@ export function salvaCompilazione( liberi?: Record; } ): number { - // Validare PRIMA di aprire la sessione: altrimenti un id sbagliato lascia - // in giro una sessione vuota. + // Validare PRIMA della transazione: se un test_id è sconosciuto, non apriamo + // nemmeno una sessione. La validazione non è nel corpo della transazione. for (const id of Object.keys(input.risposte)) { if (!esisteTest(db, id)) throw new Error(`test_id non nel registro: ${id}`); } - const sessioneId = apriSessione(db, { - client_code: input.client_code, - data: input.data, - tipo: 'questionario', - eta_alla_data: input.eta, - quest_version: QUEST_VERSION, - }); + // Avvolgi le tre scritture (apertura sessione, registra misure, inserisci note) + // in un'unica transazione: tutto o niente, senza tracce parziali su dati sanitari. + const tx = db.transaction(() => { + const sessioneId = apriSessione(db, { + client_code: input.client_code, + data: input.data, + tipo: 'questionario', + eta_alla_data: input.eta, + quest_version: QUEST_VERSION, + }); - registraMisure( - db, sessioneId, 'questionario', - 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 (?, ?, ?, ?)` + registraMisure( + db, sessioneId, 'questionario', + Object.entries(input.risposte).map(([test_id, valore_num]) => ({ test_id, valore_num })) ); - 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; ins.run(input.client_code, sessioneId, campo, testo); } - }); - tx(); - } + } - return sessioneId; + return sessioneId; + }); + + return tx(); } diff --git a/tests/longevity/questionario.test.ts b/tests/longevity/questionario.test.ts index dcf65ad..a7a590c 100644 --- a/tests/longevity/questionario.test.ts +++ b/tests/longevity/questionario.test.ts @@ -64,4 +64,24 @@ describe('compilazione del questionario', () => { const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number }; 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); + }); });