import { eq, and, desc, sql, lt, type Db, insights } from '@mosaicstack/db'; export type Insight = typeof insights.$inferSelect; export type NewInsight = typeof insights.$inferInsert; export interface SearchResult { insight: Insight; distance: number; } export function createInsightsRepo(db: Db) { return { async findByUser(userId: string, limit = 50): Promise { return db .select() .from(insights) .where(eq(insights.userId, userId)) .orderBy(desc(insights.createdAt)) .limit(limit); }, async findById(id: string, userId: string): Promise { const rows = await db .select() .from(insights) .where(and(eq(insights.id, id), eq(insights.userId, userId))); return rows[0]; }, async create(data: NewInsight): Promise { const rows = await db.insert(insights).values(data).returning(); return rows[0]!; }, async update( id: string, userId: string, data: Partial, ): Promise { const rows = await db .update(insights) .set({ ...data, updatedAt: new Date() }) .where(and(eq(insights.id, id), eq(insights.userId, userId))) .returning(); return rows[0]; }, async remove(id: string, userId: string): Promise { const rows = await db .delete(insights) .where(and(eq(insights.id, id), eq(insights.userId, userId))) .returning(); return rows.length > 0; }, /** * Semantic search using pgvector cosine distance. * Requires the vector extension and an embedding for the query. */ async searchByEmbedding( userId: string, queryEmbedding: number[], limit = 10, maxDistance = 0.8, ): Promise { const embeddingStr = `[${queryEmbedding.join(',')}]`; const rows = await db.execute(sql` SELECT *, (embedding <=> ${embeddingStr}::vector) AS distance FROM insights WHERE user_id = ${userId} AND embedding IS NOT NULL AND (embedding <=> ${embeddingStr}::vector) < ${maxDistance} ORDER BY distance ASC LIMIT ${limit} `); return rows as unknown as SearchResult[]; }, /** * Decay relevance scores for old insights that haven't been accessed recently. * Scoped to a specific user to prevent cross-user data mutation. */ async decayOldInsights(userId: string, olderThan: Date, decayFactor = 0.95): Promise { const result = await db .update(insights) .set({ relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`, decayedAt: new Date(), updatedAt: new Date(), }) .where( and( eq(insights.userId, userId), lt(insights.updatedAt, olderThan), sql`${insights.relevanceScore} > 0.1`, ), ) .returning(); return result.length; }, /** * Decay relevance scores for all users' old insights. * This is a system-level maintenance operation intended for scheduled cron jobs only. * Do NOT expose this through user-facing API endpoints. */ async decayAllInsights(olderThan: Date, decayFactor = 0.95): Promise { const result = await db .update(insights) .set({ relevanceScore: sql`${insights.relevanceScore} * ${decayFactor}`, decayedAt: new Date(), updatedAt: new Date(), }) .where(and(lt(insights.updatedAt, olderThan), sql`${insights.relevanceScore} > 0.1`)) .returning(); return result.length; }, }; } export type InsightsRepo = ReturnType;