/** * Storage module implementation. * Persists FeedItems to database using Kysely. */ import type { Kysely } from 'kysely'; import type { FeedItem } from '../../interfaces/feed.types.js'; import type { IStorage } from '../../interfaces/storage.interface.js'; import type { Database, FeedItemTable } from '../../infrastructure/db/database.js'; export class SqlStorage implements IStorage { private readonly db: Kysely; constructor(db: Kysely) { this.db = db; } async save(items: FeedItem[]): Promise { if (items.length === 0) { return; } const rows: FeedItemTable[] = items.map((item) => ({ id: item.id, source: item.source, title: item.title, url: item.url, published_at: item.publishedAt.toISOString(), content: item.content ?? null, summary: item.summary ?? null, created_at: new Date().toISOString(), })); // Upsert: insert or update on conflict await this.db .insertInto('feed_items') .values(rows) .onConflict((oc) => oc.column('id').doUpdateSet({ title: (eb) => eb.ref('excluded.title'), content: (eb) => eb.ref('excluded.content'), summary: (eb) => eb.ref('excluded.summary'), }) ) .execute(); } async getRecent(limit: number): Promise { const rows = await this.db .selectFrom('feed_items') .selectAll() .orderBy('published_at', 'desc') .limit(limit) .execute(); return rows.map(this.rowToFeedItem); } async getBySource(source: string, limit: number): Promise { const rows = await this.db .selectFrom('feed_items') .selectAll() .where('source', '=', source) .orderBy('published_at', 'desc') .limit(limit) .execute(); return rows.map(this.rowToFeedItem); } async search(query: string): Promise { const searchTerm = `%${query}%`; const rows = await this.db .selectFrom('feed_items') .selectAll() .where((eb) => eb.or([ eb('title', 'like', searchTerm), eb('summary', 'like', searchTerm), eb('content', 'like', searchTerm), ]) ) .orderBy('published_at', 'desc') .execute(); return rows.map(this.rowToFeedItem); } private rowToFeedItem(row: FeedItemTable): FeedItem { return { id: row.id, source: row.source, title: row.title, url: row.url, publishedAt: new Date(row.published_at), content: row.content ?? undefined, summary: row.summary ?? undefined, }; } }