Initial commit: EGBE chatbot template
Stripped from vercel/chatbot (Apache 2.0): - Dropped @vercel/* packages and AI Gateway - Removed artifacts feature (code/text/sheet/image side panel) - Switched AI provider to @ai-sdk/openai-compatible -> EGBE LiteLLM - Replaced Vercel Blob upload with data URLs - Dropped Redis resumable streams and rate limiter (in-memory now) - Added Dockerfile (Next.js standalone) + entrypoint that runs migrations - Wired DATABASE_URL, EGBE_AI_API_URL/KEY, NEXT_PUBLIC_BASE_URL for app-deploy.sh
This commit is contained in:
commit
3e21c2334c
129 changed files with 21913 additions and 0 deletions
33
lib/db/migrate.ts
Normal file
33
lib/db/migrate.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { config } from "dotenv";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import postgres from "postgres";
|
||||
|
||||
config({
|
||||
path: ".env.local",
|
||||
});
|
||||
|
||||
const runMigrate = async () => {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.log("DATABASE_URL not defined, skipping migrations");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const connection = postgres(process.env.DATABASE_URL, { max: 1 });
|
||||
const db = drizzle(connection);
|
||||
|
||||
console.log("Running migrations...");
|
||||
|
||||
const start = Date.now();
|
||||
await migrate(db, { migrationsFolder: "./lib/db/migrations" });
|
||||
const end = Date.now();
|
||||
|
||||
console.log("Migrations completed in", end - start, "ms");
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
runMigrate().catch((err) => {
|
||||
console.error("Migration failed");
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
59
lib/db/migrations/0000_bouncy_payback.sql
Normal file
59
lib/db/migrations/0000_bouncy_payback.sql
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
CREATE TABLE IF NOT EXISTS "Chat" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"createdAt" timestamp NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"visibility" varchar DEFAULT 'private' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Message_v2" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"chatId" uuid NOT NULL,
|
||||
"role" varchar NOT NULL,
|
||||
"parts" json NOT NULL,
|
||||
"attachments" json NOT NULL,
|
||||
"createdAt" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "User" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" varchar(64) NOT NULL,
|
||||
"password" varchar(64),
|
||||
"name" text,
|
||||
"emailVerified" boolean DEFAULT false NOT NULL,
|
||||
"image" text,
|
||||
"isAnonymous" boolean DEFAULT false NOT NULL,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Vote_v2" (
|
||||
"chatId" uuid NOT NULL,
|
||||
"messageId" uuid NOT NULL,
|
||||
"isUpvoted" boolean NOT NULL,
|
||||
CONSTRAINT "Vote_v2_chatId_messageId_pk" PRIMARY KEY("chatId","messageId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Message_v2" ADD CONSTRAINT "Message_v2_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Vote_v2" ADD CONSTRAINT "Vote_v2_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Vote_v2" ADD CONSTRAINT "Vote_v2_messageId_Message_v2_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message_v2"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
265
lib/db/migrations/meta/0000_snapshot.json
Normal file
265
lib/db/migrations/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
{
|
||||
"id": "e4157317-da3b-4af3-b3e6-dbddfc2e1ced",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.Chat": {
|
||||
"name": "Chat",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"visibility": {
|
||||
"name": "visibility",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'private'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Chat_userId_User_id_fk": {
|
||||
"name": "Chat_userId_User_id_fk",
|
||||
"tableFrom": "Chat",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Message_v2": {
|
||||
"name": "Message_v2",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"parts": {
|
||||
"name": "parts",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"attachments": {
|
||||
"name": "attachments",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Message_v2_chatId_Chat_id_fk": {
|
||||
"name": "Message_v2_chatId_Chat_id_fk",
|
||||
"tableFrom": "Message_v2",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.User": {
|
||||
"name": "User",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"isAnonymous": {
|
||||
"name": "isAnonymous",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updatedAt": {
|
||||
"name": "updatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Vote_v2": {
|
||||
"name": "Vote_v2",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messageId": {
|
||||
"name": "messageId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"isUpvoted": {
|
||||
"name": "isUpvoted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Vote_v2_chatId_Chat_id_fk": {
|
||||
"name": "Vote_v2_chatId_Chat_id_fk",
|
||||
"tableFrom": "Vote_v2",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"Vote_v2_messageId_Message_v2_id_fk": {
|
||||
"name": "Vote_v2_messageId_Message_v2_id_fk",
|
||||
"tableFrom": "Vote_v2",
|
||||
"tableTo": "Message_v2",
|
||||
"columnsFrom": [
|
||||
"messageId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Vote_v2_chatId_messageId_pk": {
|
||||
"name": "Vote_v2_chatId_messageId_pk",
|
||||
"columns": [
|
||||
"chatId",
|
||||
"messageId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
lib/db/migrations/meta/_journal.json
Normal file
13
lib/db/migrations/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1779705963902,
|
||||
"tag": "0000_bouncy_payback",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
426
lib/db/queries.ts
Normal file
426
lib/db/queries.ts
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
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 { VisibilityType } from "@/components/chat/visibility-selector";
|
||||
import { ChatbotError } from "../errors";
|
||||
import { generateUUID } from "../utils";
|
||||
import {
|
||||
type Chat,
|
||||
chat,
|
||||
type DBMessage,
|
||||
message,
|
||||
type User,
|
||||
user,
|
||||
vote,
|
||||
} from "./schema";
|
||||
import { generateHashedPassword } from "./utils";
|
||||
|
||||
const client = postgres(process.env.DATABASE_URL ?? "");
|
||||
const db = drizzle(client);
|
||||
|
||||
export async function getUser(email: string): Promise<User[]> {
|
||||
try {
|
||||
return await db.select().from(user).where(eq(user.email, email));
|
||||
} catch (_error) {
|
||||
throw new ChatbotError(
|
||||
"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 ChatbotError("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 ChatbotError(
|
||||
"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 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));
|
||||
|
||||
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));
|
||||
|
||||
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<unknown>) =>
|
||||
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 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) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMessageCountByUserId({
|
||||
id,
|
||||
differenceInHours,
|
||||
}: {
|
||||
id: string;
|
||||
differenceInHours: number;
|
||||
}) {
|
||||
try {
|
||||
const cutoffTime = 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, cutoffTime),
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/db/schema.ts
Normal file
70
lib/db/schema.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
json,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("User", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
email: varchar("email", { length: 64 }).notNull(),
|
||||
password: varchar("password", { length: 64 }),
|
||||
name: text("name"),
|
||||
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||
image: text("image"),
|
||||
isAnonymous: boolean("isAnonymous").notNull().default(false),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type User = InferSelectModel<typeof user>;
|
||||
|
||||
export const chat = pgTable("Chat", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
title: text("title").notNull(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
visibility: varchar("visibility", { enum: ["public", "private"] })
|
||||
.notNull()
|
||||
.default("private"),
|
||||
});
|
||||
|
||||
export type Chat = InferSelectModel<typeof chat>;
|
||||
|
||||
export const message = pgTable("Message_v2", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
chatId: uuid("chatId")
|
||||
.notNull()
|
||||
.references(() => chat.id),
|
||||
role: varchar("role").notNull(),
|
||||
parts: json("parts").notNull(),
|
||||
attachments: json("attachments").notNull(),
|
||||
createdAt: timestamp("createdAt").notNull(),
|
||||
});
|
||||
|
||||
export type DBMessage = InferSelectModel<typeof message>;
|
||||
|
||||
export const vote = pgTable(
|
||||
"Vote_v2",
|
||||
{
|
||||
chatId: uuid("chatId")
|
||||
.notNull()
|
||||
.references(() => chat.id),
|
||||
messageId: uuid("messageId")
|
||||
.notNull()
|
||||
.references(() => message.id),
|
||||
isUpvoted: boolean("isUpvoted").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.chatId, table.messageId] }),
|
||||
})
|
||||
);
|
||||
|
||||
export type Vote = InferSelectModel<typeof vote>;
|
||||
16
lib/db/utils.ts
Normal file
16
lib/db/utils.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { generateId } from "ai";
|
||||
import { genSaltSync, hashSync } from "bcrypt-ts";
|
||||
|
||||
export function generateHashedPassword(password: string) {
|
||||
const salt = genSaltSync(10);
|
||||
const hash = hashSync(password, salt);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function generateDummyPassword() {
|
||||
const password = generateId();
|
||||
const hashedPassword = generateHashedPassword(password);
|
||||
|
||||
return hashedPassword;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue