102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
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;
|
|
}
|