import { DatabaseSync } from 'node:sqlite'; /** A stored RSS source; tags are a comma-separated string in the DB, split here. */ export interface RssSource { name: string; url: string; tags: string[]; createdAt: number; } /** Idempotent — safe to call on every open (the table lives in the shared MESH_DB_PATH file). */ export function initRssTable(db: DatabaseSync): void { db.exec(`CREATE TABLE IF NOT EXISTS rss_sources ( name TEXT PRIMARY KEY, url TEXT NOT NULL, tags TEXT NOT NULL DEFAULT '', created_at INTEGER )`); } /** CRUD for the rss_sources table. Names are matched case-insensitively everywhere. */ export class RssStore { constructor(private readonly db: DatabaseSync) { initRssTable(db); } list(): RssSource[] { const rows = this.db .prepare( 'SELECT name, url, tags, created_at FROM rss_sources ORDER BY name', ) .all() as { name: string; url: string; tags: string; created_at: number; }[]; return rows.map((r) => ({ name: r.name, url: r.url, tags: splitTags(r.tags), createdAt: r.created_at, })); } /** Case-insensitive lookup by name. */ get(name: string): RssSource | undefined { const row = this.db .prepare( 'SELECT name, url, tags, created_at FROM rss_sources WHERE name = ? COLLATE NOCASE', ) .get(name) as | { name: string; url: string; tags: string; created_at: number } | undefined; return ( row && { name: row.name, url: row.url, tags: splitTags(row.tags), createdAt: row.created_at, } ); } /** Case-insensitive duplicate check. */ exists(name: string): boolean { const row = this.db .prepare('SELECT 1 FROM rss_sources WHERE name = ? COLLATE NOCASE') .get(name); return row !== undefined; } add(source: { name: string; url: string; tags: string[] }): void { this.db .prepare( 'INSERT INTO rss_sources (name, url, tags, created_at) VALUES (?, ?, ?, ?)', ) .run(source.name, source.url, joinTags(source.tags), Date.now()); } /** Case-insensitive delete; true when a row was removed. */ remove(name: string): boolean { const result = this.db .prepare('DELETE FROM rss_sources WHERE name = ? COLLATE NOCASE') .run(name); return Number(result.changes) > 0; } /** All distinct tags across sources, sorted. */ allTags(): string[] { const set = new Set(); for (const source of this.list()) { for (const tag of source.tags) set.add(tag); } return [...set].sort(); } } export function splitTags(raw: string): string[] { return raw .split(',') .map((t) => t.trim()) .filter(Boolean); } export function joinTags(tags: string[]): string { return tags.join(','); }