import Database from 'better-sqlite3'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const dbPath = path.join(__dirname, 'data.db'); export const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS articles ( id TEXT PRIMARY KEY, title TEXT NOT NULL, category TEXT, type TEXT, author TEXT, author_role TEXT, author_initial TEXT, publish_date TEXT, pages INTEGER DEFAULT 0, price INTEGER DEFAULT 0, is_free INTEGER DEFAULT 1, summary TEXT, body TEXT, cover_image TEXT, tags TEXT, featured INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_articles_created ON articles(created_at DESC); CREATE INDEX IF NOT EXISTS idx_articles_featured ON articles(featured); CREATE TABLE IF NOT EXISTS risk_signals ( id TEXT PRIMARY KEY, quote TEXT NOT NULL, name TEXT NOT NULL, date TEXT, level TEXT NOT NULL CHECK (level IN ('critical','high','medium','low','opportunity')), sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_risks_sort ON risk_signals(sort_order, created_at); CREATE TABLE IF NOT EXISTS prices ( symbol TEXT PRIMARY KEY, name TEXT NOT NULL, value REAL, unit TEXT, change_value REAL, change_pct REAL, source_url TEXT, fetched_at TEXT NOT NULL DEFAULT (datetime('now')) ); `); export function rowToPrice(row) { if (!row) return null; return { symbol: row.symbol, name: row.name, value: row.value, unit: row.unit, changeValue: row.change_value, changePct: row.change_pct, sourceUrl: row.source_url, fetchedAt: row.fetched_at, }; } export function upsertPrice(p) { db.prepare(` INSERT INTO prices (symbol, name, value, unit, change_value, change_pct, source_url, fetched_at) VALUES (@symbol, @name, @value, @unit, @change_value, @change_pct, @source_url, datetime('now')) ON CONFLICT(symbol) DO UPDATE SET name = excluded.name, value = excluded.value, unit = excluded.unit, change_value = excluded.change_value, change_pct = excluded.change_pct, source_url = excluded.source_url, fetched_at = excluded.fetched_at `).run({ symbol: p.symbol, name: p.name, value: p.value ?? null, unit: p.unit ?? null, change_value: p.changeValue ?? null, change_pct: p.changePct ?? null, source_url: p.sourceUrl ?? null, }); } export function rowToRiskSignal(row) { if (!row) return null; return { id: row.id, quote: row.quote, name: row.name, date: row.date, level: row.level, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToArticle(row) { if (!row) return null; return { id: row.id, title: row.title, category: row.category, type: row.type, author: row.author, authorRole: row.author_role, authorInitial: row.author_initial, publishDate: row.publish_date, pages: row.pages, price: row.price, isFree: !!row.is_free, summary: row.summary, body: row.body, coverImage: row.cover_image, tags: row.tags ? JSON.parse(row.tags) : [], featured: !!row.featured, createdAt: row.created_at, updatedAt: row.updated_at, }; }