steelforesight/panel/db.js

143 lines
4.0 KiB
JavaScript

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,
};
}