From 2024c329659107677739ed956782537d16c2fb98 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 21 Aug 2026 20:05:19 +0200 Subject: [PATCH] longevity: export pseudonimizzato per costruzione --- src/lib/longevity/export.ts | 36 +++++++++++++++++++++++++ tests/longevity/export.test.ts | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/lib/longevity/export.ts create mode 100644 tests/longevity/export.test.ts diff --git a/src/lib/longevity/export.ts b/src/lib/longevity/export.ts new file mode 100644 index 0000000..34b0919 --- /dev/null +++ b/src/lib/longevity/export.ts @@ -0,0 +1,36 @@ +// +// L'export per le statistiche legge SOLO longevity.db, che per costruzione non contiene +// nomi: non serve nessuna funzione di anonimizzazione, e non c'รจ niente da ricordarsi. +// I campi liberi (farmaci, patologie) restano fuori: sono per il coach, non per l'analisi. +import type Database from 'better-sqlite3'; + +const INTESTAZIONI = [ + 'client_code', 'sesso', 'eta', 'data', 'tipo_sessione', + 'quest_version', 'test_id', 'valore', 'unita', 'fonte', 'fuori_range', +]; + +export function esportaMisure(db: Database.Database): { + intestazioni: string[]; + righe: (string | number | null)[][]; +} { + const righe = db.prepare( + `SELECT sg.client_code, sg.sesso, s.eta_alla_data, s.data, s.tipo, s.quest_version, + m.test_id, COALESCE(m.valore_num, m.valore_txt) AS valore, m.unita, m.fonte, m.fuori_range + FROM misure m + JOIN sessioni s ON s.id = m.sessione_id + JOIN soggetti sg ON sg.client_code = m.client_code + ORDER BY s.data, m.id` + ).raw().all() as (string | number | null)[][]; + + return { intestazioni: INTESTAZIONI, righe }; +} + +export function esportaCsv(db: Database.Database): string { + const { intestazioni, righe } = esportaMisure(db); + const cella = (v: string | number | null) => { + if (v === null) return ''; + const s = String(v); + return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + return [intestazioni.join(','), ...righe.map((r) => r.map(cella).join(','))].join('\n') + '\n'; +} diff --git a/tests/longevity/export.test.ts b/tests/longevity/export.test.ts new file mode 100644 index 0000000..577c76d --- /dev/null +++ b/tests/longevity/export.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { createLongevityDb } from '../../src/lib/longevity/db'; +import { seedRegistro } from '../../src/lib/longevity/registro'; +import { salvaCompilazione } from '../../src/lib/longevity/questionario'; +import { esportaMisure, esportaCsv } from '../../src/lib/longevity/export'; + +function dbConDati() { + const db = createLongevityDb(':memory:'); + seedRegistro(db); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run(); + salvaCompilazione(db, { + client_code: 'ISL-0001', data: '2026-08-21', eta: 35, + risposte: { q_ore_sonno: 7, q_riposato: 8 }, + liberi: { farmaci: 'nessuno' }, + }); + return db; +} + +describe('export per le statistiche', () => { + it('esporta una riga per misura con codice, non con nome', () => { + const db = dbConDati(); + const out = esportaMisure(db); + expect(out.intestazioni).toEqual( + ['client_code', 'sesso', 'eta', 'data', 'tipo_sessione', 'quest_version', 'test_id', 'valore', 'unita', 'fonte', 'fuori_range'] + ); + expect(out.righe.length).toBe(2); + expect(out.righe[0][0]).toBe('ISL-0001'); + }); + + it('l export non contiene campi identificativi', () => { + const db = dbConDati(); + const testo = JSON.stringify(esportaMisure(db)); + for (const vietato of ['nome', 'cognome', 'email', 'telefono', 'data_nascita']) { + expect(testo).not.toContain(vietato); + } + }); + + it('i campi liberi restano fuori dall export statistico', () => { + const db = dbConDati(); + expect(JSON.stringify(esportaMisure(db))).not.toContain('nessuno'); + }); + + it('produce un CSV con intestazione e una riga per misura', () => { + const db = dbConDati(); + const righe = esportaCsv(db).trim().split('\n'); + expect(righe.length).toBe(3); // intestazione + 2 misure + expect(righe[0]).toBe('client_code,sesso,eta,data,tipo_sessione,quest_version,test_id,valore,unita,fonte,fuori_range'); + }); +});