import 'server-only'; import Database from 'better-sqlite3'; import { existsSync, mkdirSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; /** * Persistent content store for the CMS. * * A single `sections` table holds JSON overrides keyed by section name * (e.g. "hero", "services", "portfolio"). The stored JSON is a bilingual * payload — `{ fa: , en: }` — that mirrors the * shape of the matching key inside `dict`. At request time the content loader * merges these overrides on top of the in-code `dict` defaults, so editing a * section in the admin panel transparently rewrites what every public section * renders without touching any component. * * The database file lives under DATA_DIR (default ./data) which on the * self-hosted deployment is a mounted Docker volume, so content survives * container rebuilds. */ export const DATA_DIR = resolve(process.env.DATA_DIR ?? join(process.cwd(), 'data')); export const UPLOADS_DIR = join(DATA_DIR, 'uploads'); const DB_PATH = join(DATA_DIR, 'cms.db'); let _db: Database.Database | null = null; function db(): Database.Database { if (_db) return _db; if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); if (!existsSync(UPLOADS_DIR)) mkdirSync(UPLOADS_DIR, { recursive: true }); const handle = new Database(DB_PATH); handle.pragma('journal_mode = WAL'); handle.exec(` CREATE TABLE IF NOT EXISTS sections ( key TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); _db = handle; return handle; } export type SectionRow = { key: string; /** JSON-encoded `{ fa, en }` payload. */ data: string; updated_at: number; }; export type SectionOverride = { key: string; data: unknown; updatedAt: number; }; /** Every stored override, used by the content loader to merge onto defaults. */ export function getAllSections(): SectionRow[] { try { return db() .prepare('SELECT key, data, updated_at FROM sections') .all() as SectionRow[]; } catch { // A missing/locked DB must never crash a public render — fall back to dict. return []; } } /** A single override, or null when the section has never been edited. */ export function getSection(key: string): SectionOverride | null { const row = db() .prepare('SELECT key, data, updated_at FROM sections WHERE key = ?') .get(key) as SectionRow | undefined; if (!row) return null; return { key: row.key, data: JSON.parse(row.data), updatedAt: row.updated_at }; } /** Insert or replace a section override (admin only). */ export function setSection(key: string, data: unknown): void { db() .prepare( `INSERT INTO sections (key, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`, ) .run(key, JSON.stringify(data), Date.now()); } /** Drop an override so the section reverts to its in-code default. */ export function resetSection(key: string): void { db().prepare('DELETE FROM sections WHERE key = ?').run(key); } /** Map of key → updatedAt for showing edit status in the dashboard. */ export function sectionStatus(): Record { const out: Record = {}; for (const row of getAllSections()) out[row.key] = row.updated_at; return out; } export { dirname };