import Database from 'better-sqlite3'; import fs from 'node:fs'; import path from 'node:path'; import { config } from './config.ts'; const SCHEMA = ` CREATE TABLE IF NOT EXISTS mints ( url TEXT PRIMARY KEY, host TEXT NOT NULL, name TEXT, description TEXT, icon_url TEXT, icon_file TEXT, pubkey TEXT, info_json TEXT, nuts_json TEXT, version TEXT, status TEXT NOT NULL DEFAULT 'unknown', consecutive_fails INTEGER NOT NULL DEFAULT 0, last_online INTEGER, last_probe INTEGER, first_seen INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_mints_host ON mints(host); CREATE INDEX IF NOT EXISTS idx_mints_pubkey ON mints(pubkey); CREATE TABLE IF NOT EXISTS reviews ( event_id TEXT PRIMARY KEY, mint_url TEXT NOT NULL, pubkey TEXT NOT NULL, rating INTEGER, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_reviews_mint ON reviews(mint_url, created_at); CREATE INDEX IF NOT EXISTS idx_reviews_author ON reviews(mint_url, pubkey, created_at); CREATE TABLE IF NOT EXISTS probes ( mint_url TEXT NOT NULL, ts INTEGER NOT NULL, ok INTEGER NOT NULL, latency_ms INTEGER ); CREATE INDEX IF NOT EXISTS idx_probes_mint ON probes(mint_url, ts); CREATE TABLE IF NOT EXISTS state ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); `; export type DB = Database.Database; let db: DB | null = null; export function getDb(): DB { if (db) return db; fs.mkdirSync(path.dirname(config.dbPath), { recursive: true }); fs.mkdirSync(config.iconDir, { recursive: true }); const handle = new Database(config.dbPath); handle.pragma('journal_mode = WAL'); handle.pragma('synchronous = NORMAL'); handle.pragma('busy_timeout = 5000'); handle.exec(SCHEMA); db = handle; return handle; } export function closeDb(): void { db?.close(); db = null; } export function getState(key: string): string | null { const row = getDb().prepare('SELECT value FROM state WHERE key = ?').get(key) as | { value: string } | undefined; return row?.value ?? null; } export function setState(key: string, value: string): void { getDb() .prepare('INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value') .run(key, value); } export function getStateNumber(key: string): number | null { const raw = getState(key); if (raw === null) return null; const n = Number.parseInt(raw, 10); return Number.isFinite(n) ? n : null; } /** Delete probe rows older than 90 days. Called once a day. */ export function pruneProbes(): number { const cutoff = Math.floor(Date.now() / 1000) - 90 * 24 * 60 * 60; return getDb().prepare('DELETE FROM probes WHERE ts < ?').run(cutoff).changes; }