Files
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

5.6 KiB

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):

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:

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:

  // 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:

  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:

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
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"