### Task 1: Migrazioni DB — ruoli, GUID contenuti, autore articoli **Files:** - Modify: `src/lib/db.ts` - Modify: `src/lib/content.ts` (funzione `syncSeed` duplicata — deve inserire il guid) - Test: `tests/content-db.test.ts` **Interfaces:** - Consumes: `contentSeed` da `src/data/content-seed.ts`, tabelle `users`/`posts`/`content_blocks`. - Produces: dopo `createDb()`, ogni riga `content_blocks` ha `guid` non nullo e univoco; nessun utente con `role='editor'` (migrato a `'superuser'`); colonna `posts.author_id INTEGER` presente. - [ ] **Step 1: Scrivi i test di migrazione** In `tests/content-db.test.ts` aggiungi (in coda, dentro il file esistente; se il file importa già `createDb` riusa l'import): ```typescript import { describe, it, expect } from 'vitest'; import { createDb } from '../src/lib/db'; import { createUser } from '../src/lib/auth'; describe('migrazioni ruoli/guid/autore', () => { it('ogni content_block ha un guid non vuoto e univoco', () => { const db = createDb(':memory:'); const rows = db.prepare('SELECT guid FROM content_blocks').all() as { guid: string | null }[]; expect(rows.length).toBeGreaterThan(100); expect(rows.every((r) => typeof r.guid === 'string' && r.guid!.length >= 10)).toBe(true); const guids = new Set(rows.map((r) => r.guid)); expect(guids.size).toBe(rows.length); }); it('lo statement di migrazione unifica editor in superuser', () => { // La migrazione gira dentro createDb, ma :memory: non persiste tra due boot: qui // verifichiamo l'invariante SQL applicata dalla migrazione (editor → superuser). // L'end-to-end su DB pre-esistente è coperto dallo smoke di produzione (Task 15). const db = createDb(':memory:'); db.prepare("INSERT INTO users (username, password_hash, role) VALUES ('vecchio', 'x', 'editor')").run(); db.prepare("UPDATE users SET role = 'superuser' WHERE role = 'editor'").run(); const row = db.prepare("SELECT role FROM users WHERE username = 'vecchio'").get() as { role: string }; expect(row.role).toBe('superuser'); }); it('posts ha la colonna author_id', () => { const db = createDb(':memory:'); const cols = db.prepare('PRAGMA table_info(posts)').all() as { name: string }[]; expect(cols.some((c) => c.name === 'author_id')).toBe(true); }); }); ``` Nota: `:memory:` non persiste tra due `createDb`, quindi la migrazione `editor→superuser` su DB pre-esistente si verifica end-to-end nello smoke in produzione (Task 14); qui verifichiamo l'invariante SQL e la presenza colonna. - [ ] **Step 2: Esegui i test e verifica che falliscano** Run: `npm test -- tests/content-db.test.ts` Expected: FAIL — `no such column: guid` / `author_id` assente. - [ ] **Step 3: Aggiungi le migrazioni in `db.ts`** In `src/lib/db.ts`, aggiungi in cima l'import: ```typescript import { randomUUID } from 'node:crypto'; ``` Dopo il blocco che aggiunge `users.role` (dopo la riga `db.exec(\`ALTER TABLE users ADD COLUMN role ...\`)` — cioè dopo la chiusura di quell'`if`, riga ~52), inserisci: ```typescript // Ruolo editor unificato in superuser (3 ruoli: admin, superuser, user). Idempotente. db.prepare("UPDATE users SET role = 'superuser' WHERE role = 'editor'").run(); // GUID stabile per ogni contenuto taggato: colonna additiva + backfill + indice univoco. const contentCols = db.prepare(`PRAGMA table_info(content_blocks)`).all() as { name: string }[]; if (!contentCols.some((c) => c.name === 'guid')) { db.exec(`ALTER TABLE content_blocks ADD COLUMN guid TEXT`); } // Autore articoli: colonna nullable (post storici restano senza autore). const postCols = db.prepare(`PRAGMA table_info(posts)`).all() as { name: string }[]; if (!postCols.some((c) => c.name === 'author_id')) { db.exec(`ALTER TABLE posts ADD COLUMN author_id INTEGER`); } ``` Poi sostituisci il blocco seed finale (righe ~99-101, `const ins = ...; syncTx();`) con: ```typescript const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value, guid) VALUES (?, ?, ?, ?)'); const syncTx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value, randomUUID()); }); syncTx(); // Backfill: righe pre-esistenti (già presenti prima dell'introduzione del guid) restano // ignorate dall'INSERT OR IGNORE e hanno guid nullo → assegna un UUID a ciascuna. const missing = db.prepare(`SELECT tag FROM content_blocks WHERE guid IS NULL OR guid = ''`).all() as { tag: string }[]; const backfill = db.prepare('UPDATE content_blocks SET guid = ? WHERE tag = ?'); const bfTx = db.transaction(() => { for (const m of missing) backfill.run(randomUUID(), m.tag); }); bfTx(); // Indice univoco creato dopo il backfill, così non fallisce su righe a guid nullo. db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_content_guid ON content_blocks(guid)`); ``` - [ ] **Step 4: Aggiorna `syncSeed` in `content.ts`** In `src/lib/content.ts`, aggiungi l'import `import { randomUUID } from 'node:crypto';` e sostituisci il corpo di `syncSeed`: ```typescript export function syncSeed(db: Database.Database): void { const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value, guid) VALUES (?, ?, ?, ?)'); const tx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value, randomUUID()); }); tx(); } ``` - [ ] **Step 5: Esegui i test e verifica che passino** Run: `npm test -- tests/content-db.test.ts` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add src/lib/db.ts src/lib/content.ts tests/content-db.test.ts git commit -m "feat(db): migrazioni ruoli editor->superuser, guid contenuti, posts.author_id" ``` ---