Forked from altra/research
db.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.openDb = openDb;
exports.upsertEntity = upsertEntity;
exports.insertClaim = insertClaim;
exports.upsertSource = upsertSource;
exports.insertTimelineEvent = insertTimelineEvent;
exports.insertReport = insertReport;
exports.listReports = listReports;
exports.getEntityByName = getEntityByName;
exports.getEntityIdsByNames = getEntityIdsByNames;
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const path_1 = require("path");
const os_1 = require("os");
const fs_1 = require("fs");
function openDb(dataPath) {
const raw = dataPath.trim() || (0, path_1.join)((0, os_1.homedir)(), "research-data");
const dir = raw.startsWith("~/") ? (0, path_1.join)((0, os_1.homedir)(), raw.slice(2)) : raw;
(0, fs_1.mkdirSync)(dir, { recursive: true });
const db = new better_sqlite3_1.default((0, path_1.join)(dir, "research.db"));
db.pragma("journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
tags TEXT DEFAULT '[]',
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS claims (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entityId INTEGER REFERENCES entities(id),
claim TEXT NOT NULL,
type TEXT DEFAULT 'factual',
confidence REAL DEFAULT 0.5,
sourceUrl TEXT DEFAULT '',
verbatimQuote TEXT DEFAULT '',
corroborated INTEGER DEFAULT 0,
contradicted INTEGER DEFAULT 0,
createdAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT UNIQUE NOT NULL,
domain TEXT DEFAULT '',
score INTEGER DEFAULT 0,
verdict TEXT DEFAULT 'unknown',
signals TEXT DEFAULT '{}',
fetchedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS timeline_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entityId INTEGER NOT NULL REFERENCES entities(id),
event TEXT NOT NULL,
date TEXT DEFAULT '',
sourceUrl TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
format TEXT DEFAULT 'briefing',
entityIds TEXT DEFAULT '[]',
sourceUrls TEXT DEFAULT '[]',
createdAt TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
CREATE INDEX IF NOT EXISTS idx_claims_entityId ON claims(entityId);
CREATE INDEX IF NOT EXISTS idx_claims_sourceUrl ON claims(sourceUrl);
CREATE INDEX IF NOT EXISTS idx_reports_topic ON reports(topic);
CREATE INDEX IF NOT EXISTS idx_timeline_entityId ON timeline_events(entityId);
`);
return db;
}
function upsertEntity(db, name, type, tags) {
const now = new Date().toISOString();
const existing = db.prepare("SELECT id FROM entities WHERE name = ?").get(name);
if (existing) {
db.prepare("UPDATE entities SET type = ?, tags = ?, updatedAt = ? WHERE id = ?")
.run(type, JSON.stringify(tags), now, existing.id);
return existing.id;
}
const r = db.prepare("INSERT INTO entities (name, type, tags, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)").run(name, type, JSON.stringify(tags), now, now);
return r.lastInsertRowid;
}
function insertClaim(db, entityId, claim, type, confidence, sourceUrl, verbatimQuote) {
db.prepare("INSERT INTO claims (entityId, claim, type, confidence, sourceUrl, verbatimQuote, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?)").run(entityId, claim, type, confidence, sourceUrl, verbatimQuote, new Date().toISOString());
}
function upsertSource(db, url, domain, score, verdict, signals) {
db.prepare(`
INSERT INTO sources (url, domain, score, verdict, signals, fetchedAt)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(url) DO UPDATE SET
score = excluded.score,
verdict = excluded.verdict,
signals = excluded.signals,
fetchedAt = excluded.fetchedAt
`).run(url, domain, score, verdict, JSON.stringify(signals), new Date().toISOString());
}
function insertTimelineEvent(db, entityId, event, date, sourceUrl) {
db.prepare("INSERT INTO timeline_events (entityId, event, date, sourceUrl) VALUES (?, ?, ?, ?)").run(entityId, event, date, sourceUrl);
}
function insertReport(db, topic, format, entityIds, sourceUrls) {
const r = db.prepare("INSERT INTO reports (topic, format, entityIds, sourceUrls, createdAt) VALUES (?, ?, ?, ?, ?)").run(topic, format, JSON.stringify(entityIds), JSON.stringify(sourceUrls), new Date().toISOString());
return r.lastInsertRowid;
}
function listReports(db, topicFilter, limit = 20) {
const sql = topicFilter
? "SELECT id, topic, format, createdAt, json_array_length(entityIds) as entityCount, json_array_length(sourceUrls) as sourceCount FROM reports WHERE topic LIKE ? ORDER BY createdAt DESC LIMIT ?"
: "SELECT id, topic, format, createdAt, json_array_length(entityIds) as entityCount, json_array_length(sourceUrls) as sourceCount FROM reports ORDER BY createdAt DESC LIMIT ?";
const params = topicFilter ? [`%${topicFilter}%`, limit] : [limit];
return db.prepare(sql).all(...params);
}
function getEntityByName(db, name) {
return db.prepare("SELECT * FROM entities WHERE name = ?").get(name);
}
function getEntityIdsByNames(db, names) {
if (names.length === 0)
return [];
const placeholders = names.map(() => "?").join(", ");
const rows = db.prepare(`SELECT id FROM entities WHERE name IN (${placeholders})`).all(...names);
return rows.map(r => r.id);
}