import { eq, and, asc, desc, ilike, type Db, conversations, messages } from '@mosaicstack/db'; /** Maximum number of conversations returned per list query. */ const MAX_CONVERSATIONS = 200; /** Maximum number of messages returned per conversation history query. */ const MAX_MESSAGES = 500; export type Conversation = typeof conversations.$inferSelect; export type NewConversation = typeof conversations.$inferInsert; export type Message = typeof messages.$inferSelect; export type NewMessage = typeof messages.$inferInsert; export interface MessageSearchResult { messageId: string; conversationId: string; conversationTitle: string | null; role: 'user' | 'assistant' | 'system'; content: string; createdAt: Date; } export function createConversationsRepo(db: Db) { return { async findAll(userId: string): Promise { return db .select() .from(conversations) .where(eq(conversations.userId, userId)) .orderBy(desc(conversations.updatedAt)) .limit(MAX_CONVERSATIONS); }, /** * Find a conversation by ID, scoped to the given user. * Returns undefined if the conversation does not exist or belongs to a different user. */ async findById(id: string, userId: string): Promise { const rows = await db .select() .from(conversations) .where(and(eq(conversations.id, id), eq(conversations.userId, userId))); return rows[0]; }, async create(data: NewConversation): Promise { const rows = await db.insert(conversations).values(data).returning(); return rows[0]!; }, /** * Update a conversation, scoped to the given user. * Returns undefined if the conversation does not exist or belongs to a different user. */ async update( id: string, userId: string, data: Partial, ): Promise { const rows = await db .update(conversations) .set({ ...data, updatedAt: new Date() }) .where(and(eq(conversations.id, id), eq(conversations.userId, userId))) .returning(); return rows[0]; }, /** * Delete a conversation, scoped to the given user. * Returns false if the conversation does not exist or belongs to a different user. */ async remove(id: string, userId: string): Promise { const rows = await db .delete(conversations) .where(and(eq(conversations.id, id), eq(conversations.userId, userId))) .returning(); return rows.length > 0; }, /** * Find messages for a conversation, scoped to the given user. * Returns an empty array if the conversation does not exist or belongs to a different user. */ async findMessages(conversationId: string, userId: string): Promise { // Verify ownership of the parent conversation before returning messages. const conv = await db .select() .from(conversations) .where(and(eq(conversations.id, conversationId), eq(conversations.userId, userId))); if (conv.length === 0) return []; return db .select() .from(messages) .where(eq(messages.conversationId, conversationId)) .orderBy(asc(messages.createdAt)) .limit(MAX_MESSAGES); }, /** * Search messages by content across all conversations belonging to the user. * Uses ILIKE for case-insensitive substring matching. */ async searchMessages( userId: string, query: string, limit: number, offset: number, ): Promise { const rows = await db .select({ messageId: messages.id, conversationId: conversations.id, conversationTitle: conversations.title, role: messages.role, content: messages.content, createdAt: messages.createdAt, }) .from(messages) .innerJoin(conversations, eq(messages.conversationId, conversations.id)) .where(and(eq(conversations.userId, userId), ilike(messages.content, `%${query}%`))) .orderBy(desc(messages.createdAt)) .limit(limit) .offset(offset); return rows; }, /** * Add a message to a conversation, scoped to the given user. * Verifies the parent conversation belongs to the user before inserting. * Returns undefined if the conversation does not exist or belongs to a different user. */ async addMessage(data: NewMessage, userId: string): Promise { // Verify ownership of the parent conversation before inserting the message. const conv = await db .select() .from(conversations) .where(and(eq(conversations.id, data.conversationId), eq(conversations.userId, userId))); if (conv.length === 0) return undefined; const rows = await db.insert(messages).values(data).returning(); return rows[0]!; }, }; } export type ConversationsRepo = ReturnType;