import "server-only"; import { and, asc, count, desc, eq, gt, gte, inArray, lt, type SQL, } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import type { ArtifactKind } from "@/components/artifact"; import type { VisibilityType } from "@/components/visibility-selector"; import { ChatbotError } from "../errors"; import { type Chat, chat, type DBMessage, document, message, type Suggestion, stream, suggestion, vote, } from "./schema"; // biome-ignore lint: Forbidden non-null assertion. const client = postgres(process.env.POSTGRES_URL!); const db = drizzle(client); export async function saveChat({ id, userId, title, visibility, }: { id: string; userId: string; title: string; visibility: VisibilityType; }) { try { return await db.insert(chat).values({ id, createdAt: new Date(), userId, title, visibility, }); } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to save chat"); } } export async function deleteChatById({ id }: { id: string }) { try { await db.delete(vote).where(eq(vote.chatId, id)); await db.delete(message).where(eq(message.chatId, id)); await db.delete(stream).where(eq(stream.chatId, id)); const [chatsDeleted] = await db .delete(chat) .where(eq(chat.id, id)) .returning(); return chatsDeleted; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to delete chat by id" ); } } export async function deleteAllChatsByUserId({ userId }: { userId: string }) { try { const userChats = await db .select({ id: chat.id }) .from(chat) .where(eq(chat.userId, userId)); if (userChats.length === 0) { return { deletedCount: 0 }; } const chatIds = userChats.map((c) => c.id); await db.delete(vote).where(inArray(vote.chatId, chatIds)); await db.delete(message).where(inArray(message.chatId, chatIds)); await db.delete(stream).where(inArray(stream.chatId, chatIds)); const deletedChats = await db .delete(chat) .where(eq(chat.userId, userId)) .returning(); return { deletedCount: deletedChats.length }; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to delete all chats by user id" ); } } export async function getChatsByUserId({ id, limit, startingAfter, endingBefore, }: { id: string; limit: number; startingAfter: string | null; endingBefore: string | null; }) { try { const extendedLimit = limit + 1; const query = (whereCondition?: SQL) => db .select() .from(chat) .where( whereCondition ? and(whereCondition, eq(chat.userId, id)) : eq(chat.userId, id) ) .orderBy(desc(chat.createdAt)) .limit(extendedLimit); let filteredChats: Chat[] = []; if (startingAfter) { const [selectedChat] = await db .select() .from(chat) .where(eq(chat.id, startingAfter)) .limit(1); if (!selectedChat) { throw new ChatbotError( "not_found:database", `Chat with id ${startingAfter} not found` ); } filteredChats = await query(gt(chat.createdAt, selectedChat.createdAt)); } else if (endingBefore) { const [selectedChat] = await db .select() .from(chat) .where(eq(chat.id, endingBefore)) .limit(1); if (!selectedChat) { throw new ChatbotError( "not_found:database", `Chat with id ${endingBefore} not found` ); } filteredChats = await query(lt(chat.createdAt, selectedChat.createdAt)); } else { filteredChats = await query(); } const hasMore = filteredChats.length > limit; return { chats: hasMore ? filteredChats.slice(0, limit) : filteredChats, hasMore, }; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get chats by user id" ); } } export async function getChatById({ id }: { id: string }) { try { const [selectedChat] = await db.select().from(chat).where(eq(chat.id, id)); if (!selectedChat) { return null; } return selectedChat; } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to get chat by id"); } } export async function saveMessages({ messages }: { messages: DBMessage[] }) { try { return await db.insert(message).values(messages); } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to save messages"); } } export async function updateMessage({ id, parts, }: { id: string; parts: DBMessage["parts"]; }) { try { return await db.update(message).set({ parts }).where(eq(message.id, id)); } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to update message"); } } export async function getMessagesByChatId({ id }: { id: string }) { try { return await db .select() .from(message) .where(eq(message.chatId, id)) .orderBy(asc(message.createdAt)); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get messages by chat id" ); } } export async function voteMessage({ chatId, messageId, type, }: { chatId: string; messageId: string; type: "up" | "down"; }) { try { const [existingVote] = await db .select() .from(vote) .where(and(eq(vote.messageId, messageId))); if (existingVote) { return await db .update(vote) .set({ isUpvoted: type === "up" }) .where(and(eq(vote.messageId, messageId), eq(vote.chatId, chatId))); } return await db.insert(vote).values({ chatId, messageId, isUpvoted: type === "up", }); } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to vote message"); } } export async function getVotesByChatId({ id }: { id: string }) { try { return await db.select().from(vote).where(eq(vote.chatId, id)); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get votes by chat id" ); } } export async function saveDocument({ id, title, kind, content, userId, }: { id: string; title: string; kind: ArtifactKind; content: string; userId: string; }) { try { return await db .insert(document) .values({ id, title, kind, content, userId, createdAt: new Date(), }) .returning(); } catch (_error) { throw new ChatbotError("bad_request:database", "Failed to save document"); } } export async function getDocumentsById({ id }: { id: string }) { try { const documents = await db .select() .from(document) .where(eq(document.id, id)) .orderBy(asc(document.createdAt)); return documents; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get documents by id" ); } } export async function getDocumentById({ id }: { id: string }) { try { const [selectedDocument] = await db .select() .from(document) .where(eq(document.id, id)) .orderBy(desc(document.createdAt)); return selectedDocument; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get document by id" ); } } export async function deleteDocumentsByIdAfterTimestamp({ id, timestamp, }: { id: string; timestamp: Date; }) { try { await db .delete(suggestion) .where( and( eq(suggestion.documentId, id), gt(suggestion.documentCreatedAt, timestamp) ) ); return await db .delete(document) .where(and(eq(document.id, id), gt(document.createdAt, timestamp))) .returning(); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to delete documents by id after timestamp" ); } } export async function saveSuggestions({ suggestions, }: { suggestions: Suggestion[]; }) { try { return await db.insert(suggestion).values(suggestions); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to save suggestions" ); } } export async function getSuggestionsByDocumentId({ documentId, }: { documentId: string; }) { try { return await db .select() .from(suggestion) .where(eq(suggestion.documentId, documentId)); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get suggestions by document id" ); } } export async function getMessageById({ id }: { id: string }) { try { return await db.select().from(message).where(eq(message.id, id)); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get message by id" ); } } export async function deleteMessagesByChatIdAfterTimestamp({ chatId, timestamp, }: { chatId: string; timestamp: Date; }) { try { const messagesToDelete = await db .select({ id: message.id }) .from(message) .where( and(eq(message.chatId, chatId), gte(message.createdAt, timestamp)) ); const messageIds = messagesToDelete.map( (currentMessage) => currentMessage.id ); if (messageIds.length > 0) { await db .delete(vote) .where( and(eq(vote.chatId, chatId), inArray(vote.messageId, messageIds)) ); return await db .delete(message) .where( and(eq(message.chatId, chatId), inArray(message.id, messageIds)) ); } } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to delete messages by chat id after timestamp" ); } } export async function updateChatVisibilityById({ chatId, visibility, }: { chatId: string; visibility: "private" | "public"; }) { try { return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId)); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to update chat visibility by id" ); } } export async function updateChatTitleById({ chatId, title, }: { chatId: string; title: string; }) { try { return await db.update(chat).set({ title }).where(eq(chat.id, chatId)); } catch (error) { console.warn("Failed to update title for chat", chatId, error); return; } } export async function getMessageCountByUserId({ id, differenceInHours, }: { id: string; differenceInHours: number; }) { try { const twentyFourHoursAgo = new Date( Date.now() - differenceInHours * 60 * 60 * 1000 ); const [stats] = await db .select({ count: count(message.id) }) .from(message) .innerJoin(chat, eq(message.chatId, chat.id)) .where( and( eq(chat.userId, id), gte(message.createdAt, twentyFourHoursAgo), eq(message.role, "user") ) ) .execute(); return stats?.count ?? 0; } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get message count by user id" ); } } export async function createStreamId({ streamId, chatId, }: { streamId: string; chatId: string; }) { try { await db .insert(stream) .values({ id: streamId, chatId, createdAt: new Date() }); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to create stream id" ); } } export async function getStreamIdsByChatId({ chatId }: { chatId: string }) { try { const streamIds = await db .select({ id: stream.id }) .from(stream) .where(eq(stream.chatId, chatId)) .orderBy(asc(stream.createdAt)) .execute(); return streamIds.map(({ id }) => id); } catch (_error) { throw new ChatbotError( "bad_request:database", "Failed to get stream ids by chat id" ); } }