diff --git a/src/components/longevity/Questionario.tsx b/src/components/longevity/Questionario.tsx index e60cfb6..5f8ae1e 100644 --- a/src/components/longevity/Questionario.tsx +++ b/src/components/longevity/Questionario.tsx @@ -45,6 +45,11 @@ export default function Questionario({ campi }: Props) { [campi] ); + // Il tipo dichiarato nel registro per ciascun campo: serve sia per scegliere il tipo di + // input (numero o testo) sia, all'invio, per non trattare una risposta testuale come un + // numero da scartare in silenzio. + const tipoDiCampo = useMemo(() => new Map(campi.map((c) => [c.id, c.tipo])), [campi]); + const totalePassi = gruppi.length + 1; // + il blocco note/consenso const ultimo = passo === totalePassi - 1; @@ -54,6 +59,11 @@ export default function Questionario({ campi }: Props) { const risposteNumeriche: Record = {}; for (const [id, v] of Object.entries(risposte)) { + // Le domande testuali (tipo_valore='txt' nel registro) non sono ancora agganciate + // al salvataggio numerico: oggi non esistono nel questionario (i 20 campi attivi + // sono tutti 'num'), ma se un domani ne comparisse una non va convertita in NaN e + // scartata in silenzio — resta fuori esplicitamente. + if (tipoDiCampo.get(id) === 'txt') continue; const n = Number(v); if (v !== '' && Number.isFinite(n)) risposteNumeriche[id] = n; } @@ -97,14 +107,22 @@ export default function Questionario({ campi }: Props) { {gruppi[passo].campi.map((c) => ( ))} diff --git a/src/lib/longevity/anagrafica.ts b/src/lib/longevity/anagrafica.ts index 9bf8eb3..cfea267 100644 --- a/src/lib/longevity/anagrafica.ts +++ b/src/lib/longevity/anagrafica.ts @@ -4,7 +4,18 @@ // risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione // la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica. import type Database from 'better-sqlite3'; -import { getIdentityDb } from './db'; +import { createIdentityDb } from './db'; + +// Connessione singola a identity, privata di questo modulo: non è esportata, quindi non +// è raggiungibile da nessun'altra parte del codice, nemmeno con un re-export su più file +// (un controllo testuale file-per-file non lo vedrebbe, ma qui il problema non si pone: +// non esiste un nome da importare). QUESTO è il modo in cui il vincolo si rende vero, +// non solo sorvegliato. +let identitySingleton: Database.Database | null = null; +function getIdentityDb(): Database.Database { + if (!identitySingleton) identitySingleton = createIdentityDb(); + return identitySingleton; +} /** * Il prossimo codice libero: il massimo fra quelli già usati in identity.clienti E in diff --git a/src/lib/longevity/db.ts b/src/lib/longevity/db.ts index a1cd251..3aa81b7 100644 --- a/src/lib/longevity/db.ts +++ b/src/lib/longevity/db.ts @@ -135,17 +135,16 @@ export function createIdentityDb(path?: string): Database.Database { return apri(path ?? process.env.IDENTITY_DB_PATH ?? 'data/identity.db', SCHEMA_IDENTITY); } -// Connessioni singole per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono +// Connessione singola per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono // una connessione già aperta invece di aprirne una nuova a ogni richiesta. +// ⚠️ Qui c'è SOLO longevity (il database senza nomi: può usarlo chiunque). Un singleton +// analogo per identity non si esporta da qui — vive privato dentro anagrafica.ts, l'unico +// modulo autorizzato a tenerlo: un controllo testuale file-per-file non vede una giunzione +// che passa da un re-export su piu' file, quindi la connessione condivisa a identity non +// deve essere raggiungibile da fuori quel modulo, punto. let longevitySingleton: Database.Database | null = null; -let identitySingleton: Database.Database | null = null; export function getLongevityDb(): Database.Database { if (!longevitySingleton) longevitySingleton = createLongevityDb(); return longevitySingleton; } - -export function getIdentityDb(): Database.Database { - if (!identitySingleton) identitySingleton = createIdentityDb(); - return identitySingleton; -} diff --git a/src/pages/api/longevity/questionario.ts b/src/pages/api/longevity/questionario.ts index 5f0909e..7d8f470 100644 --- a/src/pages/api/longevity/questionario.ts +++ b/src/pages/api/longevity/questionario.ts @@ -46,9 +46,20 @@ export const POST: APIRoute = async ({ request, locals }) => { // La data della compilazione è quella del server, non quella (manomettibile) del client. const oggi = new Date().toISOString().slice(0, 10); - const esito = salvaDalForm(getLongevityDb(), { - client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi, - }); + // salvaDalForm delega a salvaCompilazione, che lancia se una risposta porta un test_id + // sconosciuto al registro (client disallineato, non serve malizia): un'eccezione qui + // non deve diventare un 500 generico, come in src/pages/api/contact.ts per l'invio email. + let esito; + try { + esito = salvaDalForm(getLongevityDb(), { + client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi, + }); + } catch (err) { + console.error('Salvataggio questionario fallito:', err); + return json(400, { + error: 'Impossibile salvare: alcune risposte non sono riconosciute. Ricarica la pagina e riprova.', + }); + } if (!esito.ok) return json(422, { error: esito.errore }); return json(201, { sessioneId: esito.sessioneId }); diff --git a/tests/longevity/anagrafica.test.ts b/tests/longevity/anagrafica.test.ts index 4dfcd0f..27fa181 100644 --- a/tests/longevity/anagrafica.test.ts +++ b/tests/longevity/anagrafica.test.ts @@ -88,15 +88,23 @@ describe('anagrafica pseudonimizzata', () => { expect(id.prepare(`SELECT * FROM clienti WHERE nome = 'Errato'`).get()).toBeUndefined(); }); - it('nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => { - // Non cerchiamo due nomi di funzione precisi (createIdentityDb/createLongevityDb): un - // domani qualcuno aggiunge getIdentityDb, o un terzo accessore con un nome diverso, e - // l'elenco invecchia senza che nessuno se ne accorga (è già successo: getIdentityDb e - // getLongevityDb sono nati DOPO questo test e sono passati indisturbati). Il verso - // giusto è strutturale: qualunque accesso alle due connessioni passa comunque per una - // funzione esportata da db.ts il cui nome contiene "Identity" o "Longevity" — quindi - // basta cercare quei due riferimenti letterali, in QUALUNQUE forma compaiano, e - // segnalare i file che li hanno tutti e due. + it('rete secondaria: nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', () => { + // ⚠️ QUESTO TEST NON È LA GARANZIA. La garanzia vera è che getIdentityDb non è più + // esportata da db.ts (vive privata dentro anagrafica.ts): non esiste, da nessuna + // parte del codice fuori da lì, un nome da importare per ottenere quella connessione. + // Un controllo testuale come questo non può bastare da solo — è stato dimostrato: + // tre file, uno che riesporta la connessione a identity, uno che riesporta quella a + // longevity, un terzo che importa da entrambi e le usa insieme, e NESSUNO dei tre + // contiene singolarmente le due parole. Un controllo file-per-file non vede una + // giunzione che attraversa più file, e non c'è formulazione di questo test che lo + // possa vedere: la risposta era rendere la cosa impossibile, non sorvegliarla meglio. + // Questo test resta come rete secondaria — cattura il caso comune (una giunzione + // dentro un solo file) e protegge contro un domani in cui getIdentityDb tornasse + // esportata per errore — non come prova che la giunzione non possa esistere. + // Cerca i due riferimenti letterali "Identity"/"Longevity" (con l'iniziale maiuscola, + // come compaiono in qualunque accessore camelCase) invece di un elenco chiuso di nomi: + // un elenco di nomi vietati invecchia (è già successo qui: getIdentityDb/getLongevityDb + // sono nati dopo la prima versione di questo test e sono passati indisturbati). const srcDir = join(process.cwd(), 'src'); const colpevoli: string[] = [];