| | 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 { |
| | user, |
| | chat, |
| | type User, |
| | document, |
| | type Suggestion, |
| | suggestion, |
| | message, |
| | vote, |
| | type DBMessage, |
| | type Chat, |
| | stream, |
| | } from './schema'; |
| | import type { ArtifactKind } from '@/components/artifact'; |
| | import { generateUUID } from '../utils'; |
| | import { generateHashedPassword } from './utils'; |
| | import type { VisibilityType } from '@/components/visibility-selector'; |
| | import { ChatSDKError } from '../errors'; |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | const client = postgres(process.env.POSTGRES_URL!); |
| | const db = drizzle(client); |
| |
|
| | export async function getUser(email: string): Promise<Array<User>> { |
| | try { |
| | return await db.select().from(user).where(eq(user.email, email)); |
| | } catch (error) { |
| | throw new ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to get user by email', |
| | ); |
| | } |
| | } |
| |
|
| | export async function createUser(email: string, password: string) { |
| | const hashedPassword = generateHashedPassword(password); |
| |
|
| | try { |
| | return await db.insert(user).values({ email, password: hashedPassword }); |
| | } catch (error) { |
| | throw new ChatSDKError('bad_request:database', 'Failed to create user'); |
| | } |
| | } |
| |
|
| | export async function createGuestUser() { |
| | const email = `guest-${Date.now()}`; |
| | const password = generateHashedPassword(generateUUID()); |
| |
|
| | try { |
| | return await db.insert(user).values({ email, password }).returning({ |
| | id: user.id, |
| | email: user.email, |
| | }); |
| | } catch (error) { |
| | throw new ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to create guest user', |
| | ); |
| | } |
| | } |
| |
|
| | 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 ChatSDKError('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 ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to delete chat by 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<any>) => |
| | db |
| | .select() |
| | .from(chat) |
| | .where( |
| | whereCondition |
| | ? and(whereCondition, eq(chat.userId, id)) |
| | : eq(chat.userId, id), |
| | ) |
| | .orderBy(desc(chat.createdAt)) |
| | .limit(extendedLimit); |
| |
|
| | let filteredChats: Array<Chat> = []; |
| |
|
| | if (startingAfter) { |
| | const [selectedChat] = await db |
| | .select() |
| | .from(chat) |
| | .where(eq(chat.id, startingAfter)) |
| | .limit(1); |
| |
|
| | if (!selectedChat) { |
| | throw new ChatSDKError( |
| | '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 ChatSDKError( |
| | '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 ChatSDKError( |
| | '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)); |
| | return selectedChat; |
| | } catch (error) { |
| | throw new ChatSDKError('bad_request:database', 'Failed to get chat by id'); |
| | } |
| | } |
| |
|
| | export async function saveMessages({ |
| | messages, |
| | }: { |
| | messages: Array<DBMessage>; |
| | }) { |
| | try { |
| | return await db.insert(message).values(messages); |
| | } catch (error) { |
| | throw new ChatSDKError('bad_request:database', 'Failed to save messages'); |
| | } |
| | } |
| |
|
| | 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 ChatSDKError( |
| | '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 ChatSDKError('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 ChatSDKError( |
| | '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 ChatSDKError('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 ChatSDKError( |
| | '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 ChatSDKError( |
| | '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 ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to delete documents by id after timestamp', |
| | ); |
| | } |
| | } |
| |
|
| | export async function saveSuggestions({ |
| | suggestions, |
| | }: { |
| | suggestions: Array<Suggestion>; |
| | }) { |
| | try { |
| | return await db.insert(suggestion).values(suggestions); |
| | } catch (error) { |
| | throw new ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to save suggestions', |
| | ); |
| | } |
| | } |
| |
|
| | export async function getSuggestionsByDocumentId({ |
| | documentId, |
| | }: { |
| | documentId: string; |
| | }) { |
| | try { |
| | return await db |
| | .select() |
| | .from(suggestion) |
| | .where(and(eq(suggestion.documentId, documentId))); |
| | } catch (error) { |
| | throw new ChatSDKError( |
| | '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 ChatSDKError( |
| | '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((message) => message.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 ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to delete messages by chat id after timestamp', |
| | ); |
| | } |
| | } |
| |
|
| | export async function updateChatVisiblityById({ |
| | chatId, |
| | visibility, |
| | }: { |
| | chatId: string; |
| | visibility: 'private' | 'public'; |
| | }) { |
| | try { |
| | return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId)); |
| | } catch (error) { |
| | throw new ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to update chat visibility by id', |
| | ); |
| | } |
| | } |
| |
|
| | 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 ChatSDKError( |
| | '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 ChatSDKError( |
| | '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 ChatSDKError( |
| | 'bad_request:database', |
| | 'Failed to get stream ids by chat id', |
| | ); |
| | } |
| | } |
| |
|