import { DatabaseConnection } from '../shared/db/index'; import { QueryBuilder } from '../shared/utils/QueryBuilder'; import type { NewsArticleRow } from '../shared/types'; /** * Persistence for the free-tier news pipeline (FREE-DATA-STACK §3). * Pure data access — all filtering/dedupe decisions live in NewsPipeline. */ export class NewsRepository { constructor(private readonly db: DatabaseConnection) {} /** Returns true if the row was inserted (false = duplicate url_hash). */ insertArticle(a: { urlHash: string; titleHash: string; tickers: string[]; headline: string; body: string | null; source: string; catalyst: string | null; url: string; publishedAt: string; }): boolean { const qb = new QueryBuilder('NEWS_QUERIES.INSERT_ARTICLE', [ a.urlHash, a.titleHash, JSON.stringify(a.tickers), a.headline, a.body, a.source, a.catalyst, a.url, a.publishedAt, new Date().toISOString(), ]); return this.db.run(qb) > 0; } titleSeenSince(titleHash: string, sinceIso: string): boolean { const qb = new QueryBuilder('NEWS_QUERIES.TITLE_SEEN_SINCE', [titleHash, sinceIso]); return this.db.get(qb) != null; } linkTicker(ticker: string, day: string, urlHash: string): void { const qb = new QueryBuilder('NEWS_QUERIES.INSERT_CATALYST_LINK', [ticker, day, urlHash]); this.db.run(qb); } countTickerDay(ticker: string, day: string): number { const qb = new QueryBuilder('NEWS_QUERIES.COUNT_TICKER_DAY', [ticker, day]); return this.db.get<{ n: number }>(qb)?.n ?? 0; } newsForTicker(ticker: string, sinceDay: string): NewsArticleRow[] { const qb = new QueryBuilder('NEWS_QUERIES.SELECT_TICKER_NEWS', [ ticker.toUpperCase(), sinceDay, ]); return this.db.all(qb); } recent(limit: number): NewsArticleRow[] { const qb = new QueryBuilder('NEWS_QUERIES.SELECT_RECENT', [limit]); return this.db.all(qb); } /** Retention: null out bodies older than cutoff. Returns rows changed. */ purgeBodiesBefore(cutoffIso: string): number { return this.db.run(new QueryBuilder('NEWS_QUERIES.PURGE_BODIES_BEFORE', [cutoffIso])); } /** Retention: delete old rows no ticker references. Returns rows deleted. */ deleteUnreferencedBefore(cutoffIso: string): number { return this.db.run(new QueryBuilder('NEWS_QUERIES.DELETE_UNREFERENCED_BEFORE', [cutoffIso])); } }