Files
InsanityLab-web/src/lib/db.ts
T

134 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Database from 'better-sqlite3';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { randomUUID } from 'node:crypto';
import { contentSeed } from '../data/content-seed';
const SCHEMA = `
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
category TEXT NOT NULL,
excerpt TEXT NOT NULL DEFAULT '',
body_html TEXT NOT NULL DEFAULT '',
cover TEXT,
draft INTEGER NOT NULL DEFAULT 1,
published_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_posts_pub ON posts (draft, published_at DESC);
CREATE TABLE IF NOT EXISTS content_blocks (
tag TEXT PRIMARY KEY,
type TEXT NOT NULL CHECK (type IN ('text','html','image')),
value TEXT NOT NULL DEFAULT '',
styles TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_by TEXT
);
`;
export function createDb(path?: string): Database.Database {
const p = path ?? process.env.DB_PATH ?? 'data/insanitylab.db';
if (p !== ':memory:') mkdirSync(dirname(p), { recursive: true });
const db = new Database(p);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(SCHEMA);
// Migrazione additiva: DB creati prima dell'introduzione dei ruoli non hanno users.role.
const userCols = db.prepare(`PRAGMA table_info(users)`).all() as { name: string }[];
if (!userCols.some((c) => c.name === 'role')) {
db.exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
}
// 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`);
}
// Migrazione: la sezione "Programmi" è diventata "Servizi" → rinomina dei tag esistenti
// preservando le modifiche fatte dal pannello. Ordine: prima i prefissi più specifici.
// UPDATE OR IGNORE salta le righe in conflitto (tag nuovo già presente); la DELETE
// successiva ripulisce le righe vecchie rimaste. Idempotente: al secondo giro nessun match.
const TAG_RENAMES: [string, string][] = [
['home.programs.', 'home.services.'],
['footer.programs.', 'footer.services.'],
['programs.', 'services.'],
['program.', 'service.'],
];
const renameUpd = db.prepare('UPDATE OR IGNORE content_blocks SET tag = ? || substr(tag, ?) WHERE tag LIKE ?');
const renameDel = db.prepare('DELETE FROM content_blocks WHERE tag LIKE ?');
const renameTx = db.transaction(() => {
for (const [oldP, newP] of TAG_RENAMES) {
renameUpd.run(newP, oldP.length + 1, `${oldP}%`);
renameDel.run(`${oldP}%`);
}
// Tag singolo, non coperto dai prefissi: uguaglianza esatta.
db.prepare("UPDATE OR IGNORE content_blocks SET tag = 'footer.col.services-title' WHERE tag = 'footer.col.programs-title'").run();
db.prepare("DELETE FROM content_blocks WHERE tag = 'footer.col.programs-title'").run();
// I valori di default che nominavano la sezione cambiano con la rinomina. Il seed è
// INSERT OR IGNORE e non tocca le righe esistenti, quindi si aggiornano qui — ma solo
// le righe rimaste identiche al valore originale del seed (mai modificate dal pannello):
// il nuovo valore è quello del seed corrente per lo stesso tag.
const OLD_SECTION_DEFAULTS: [string, string][] = [
['global.nav.3.label', 'Programmi'],
['footer.col.services-title', 'Programmi'],
['home.services.title', 'Programmi'],
['home.services.body', 'Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi unesperienza di allenamento completa e mirata.'],
['services.meta.title', 'Programmi'],
['services.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.'],
['services.hero.title', 'Programmi'],
['services.intro.title', 'Programmi'],
["services.intro.body", "Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."],
['service.side.others-label', 'Altri programmi'],
['service.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'],
];
const seedValue = new Map(contentSeed.map((e) => [e.tag, e.value]));
const updValue = db.prepare('UPDATE content_blocks SET value = ? WHERE tag = ? AND value = ?');
for (const [tag, oldValue] of OLD_SECTION_DEFAULTS) {
const next = seedValue.get(tag);
if (next !== undefined) updValue.run(next, tag, oldValue);
}
});
renameTx();
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)`);
return db;
}
let singleton: Database.Database | null = null;
export function getDb(): Database.Database {
if (!singleton) singleton = createDb();
return singleton;
}