|
|
import { env } from "$env/dynamic/private"; |
|
|
import { GridFSBucket, MongoClient } from "mongodb"; |
|
|
import type { Conversation } from "$lib/types/Conversation"; |
|
|
import type { SharedConversation } from "$lib/types/SharedConversation"; |
|
|
import type { AbortedGeneration } from "$lib/types/AbortedGeneration"; |
|
|
import type { Settings } from "$lib/types/Settings"; |
|
|
import type { User } from "$lib/types/User"; |
|
|
import type { MessageEvent } from "$lib/types/MessageEvent"; |
|
|
import type { Session } from "$lib/types/Session"; |
|
|
import type { Assistant } from "$lib/types/Assistant"; |
|
|
import type { Report } from "$lib/types/Report"; |
|
|
import type { ConversationStats } from "$lib/types/ConversationStats"; |
|
|
import type { MigrationResult } from "$lib/types/MigrationResult"; |
|
|
import type { Semaphore } from "$lib/types/Semaphore"; |
|
|
import type { AssistantStats } from "$lib/types/AssistantStats"; |
|
|
import type { CommunityToolDB } from "$lib/types/Tool"; |
|
|
import { MongoMemoryServer } from "mongodb-memory-server"; |
|
|
import { logger } from "$lib/server/logger"; |
|
|
import { building } from "$app/environment"; |
|
|
import type { TokenCache } from "$lib/types/TokenCache"; |
|
|
import { onExit } from "./exitHandler"; |
|
|
import { fileURLToPath } from "url"; |
|
|
import { dirname, join } from "path"; |
|
|
import { existsSync, mkdirSync } from "fs"; |
|
|
|
|
|
export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; |
|
|
|
|
|
function findRepoRoot(startPath: string): string { |
|
|
let currentPath = startPath; |
|
|
while (currentPath !== "/") { |
|
|
if (existsSync(join(currentPath, "package.json"))) { |
|
|
return currentPath; |
|
|
} |
|
|
currentPath = dirname(currentPath); |
|
|
} |
|
|
throw new Error("Could not find repository root (no package.json found)"); |
|
|
} |
|
|
|
|
|
export class Database { |
|
|
private client?: MongoClient; |
|
|
private mongoServer?: MongoMemoryServer; |
|
|
|
|
|
private static instance: Database; |
|
|
|
|
|
private async init() { |
|
|
if (!env.MONGODB_URL) { |
|
|
logger.warn("No MongoDB URL found, using in-memory server"); |
|
|
|
|
|
|
|
|
const currentFilePath = fileURLToPath(import.meta.url); |
|
|
const repoRoot = findRepoRoot(dirname(currentFilePath)); |
|
|
|
|
|
|
|
|
const dbPath = env.MONGO_STORAGE_PATH || join(repoRoot, "db"); |
|
|
|
|
|
logger.info(`Using database path: ${dbPath}`); |
|
|
|
|
|
if (!existsSync(dbPath)) { |
|
|
logger.info(`Creating database directory at ${dbPath}`); |
|
|
mkdirSync(dbPath, { recursive: true }); |
|
|
} |
|
|
|
|
|
this.mongoServer = await MongoMemoryServer.create({ |
|
|
instance: { |
|
|
dbName: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""), |
|
|
dbPath, |
|
|
}, |
|
|
binary: { |
|
|
version: "7.0.18", |
|
|
}, |
|
|
}); |
|
|
this.client = new MongoClient(this.mongoServer.getUri(), { |
|
|
directConnection: env.MONGODB_DIRECT_CONNECTION === "true", |
|
|
}); |
|
|
} else { |
|
|
this.client = new MongoClient(env.MONGODB_URL, { |
|
|
directConnection: env.MONGODB_DIRECT_CONNECTION === "true", |
|
|
}); |
|
|
} |
|
|
|
|
|
this.client.connect().catch((err) => { |
|
|
logger.error(err, "Connection error"); |
|
|
process.exit(1); |
|
|
}); |
|
|
this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); |
|
|
this.client.on("open", () => this.initDatabase()); |
|
|
|
|
|
|
|
|
onExit(async () => { |
|
|
logger.info("Closing database connection"); |
|
|
await this.client?.close(true); |
|
|
await this.mongoServer?.stop(); |
|
|
}); |
|
|
} |
|
|
|
|
|
public static async getInstance(): Promise<Database> { |
|
|
if (!Database.instance) { |
|
|
Database.instance = new Database(); |
|
|
await Database.instance.init(); |
|
|
} |
|
|
|
|
|
return Database.instance; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getClient(): MongoClient { |
|
|
if (!this.client) { |
|
|
throw new Error("Database not initialized"); |
|
|
} |
|
|
|
|
|
return this.client; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getCollections() { |
|
|
if (!this.client) { |
|
|
throw new Error("Database not initialized"); |
|
|
} |
|
|
|
|
|
const db = this.client.db( |
|
|
env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") |
|
|
); |
|
|
|
|
|
const conversations = db.collection<Conversation>("conversations"); |
|
|
const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION); |
|
|
const assistants = db.collection<Assistant>("assistants"); |
|
|
const assistantStats = db.collection<AssistantStats>("assistants.stats"); |
|
|
const reports = db.collection<Report>("reports"); |
|
|
const sharedConversations = db.collection<SharedConversation>("sharedConversations"); |
|
|
const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations"); |
|
|
const settings = db.collection<Settings>("settings"); |
|
|
const users = db.collection<User>("users"); |
|
|
const sessions = db.collection<Session>("sessions"); |
|
|
const messageEvents = db.collection<MessageEvent>("messageEvents"); |
|
|
const bucket = new GridFSBucket(db, { bucketName: "files" }); |
|
|
const migrationResults = db.collection<MigrationResult>("migrationResults"); |
|
|
const semaphores = db.collection<Semaphore>("semaphores"); |
|
|
const tokenCaches = db.collection<TokenCache>("tokens"); |
|
|
const tools = db.collection<CommunityToolDB>("tools"); |
|
|
|
|
|
return { |
|
|
conversations, |
|
|
conversationStats, |
|
|
assistants, |
|
|
assistantStats, |
|
|
reports, |
|
|
sharedConversations, |
|
|
abortedGenerations, |
|
|
settings, |
|
|
users, |
|
|
sessions, |
|
|
messageEvents, |
|
|
bucket, |
|
|
migrationResults, |
|
|
semaphores, |
|
|
tokenCaches, |
|
|
tools, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private initDatabase() { |
|
|
const { |
|
|
conversations, |
|
|
conversationStats, |
|
|
assistants, |
|
|
assistantStats, |
|
|
reports, |
|
|
sharedConversations, |
|
|
abortedGenerations, |
|
|
settings, |
|
|
users, |
|
|
sessions, |
|
|
messageEvents, |
|
|
semaphores, |
|
|
tokenCaches, |
|
|
tools, |
|
|
} = this.getCollections(); |
|
|
|
|
|
conversations |
|
|
.createIndex( |
|
|
{ sessionId: 1, updatedAt: -1 }, |
|
|
{ partialFilterExpression: { sessionId: { $exists: true } } } |
|
|
) |
|
|
.catch((e) => logger.error(e)); |
|
|
conversations |
|
|
.createIndex( |
|
|
{ userId: 1, updatedAt: -1 }, |
|
|
{ partialFilterExpression: { userId: { $exists: true } } } |
|
|
) |
|
|
.catch((e) => logger.error(e)); |
|
|
conversations |
|
|
.createIndex( |
|
|
{ "message.id": 1, "message.ancestors": 1 }, |
|
|
{ partialFilterExpression: { userId: { $exists: true } } } |
|
|
) |
|
|
.catch((e) => logger.error(e)); |
|
|
|
|
|
|
|
|
conversations |
|
|
.createIndex({ "messages.createdAt": 1 }, { sparse: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
|
|
|
conversationStats |
|
|
.createIndex( |
|
|
{ |
|
|
type: 1, |
|
|
"date.field": 1, |
|
|
"date.span": 1, |
|
|
"date.at": 1, |
|
|
distinct: 1, |
|
|
}, |
|
|
{ unique: true } |
|
|
) |
|
|
.catch((e) => logger.error(e)); |
|
|
|
|
|
conversationStats |
|
|
.createIndex({ |
|
|
type: 1, |
|
|
"date.field": 1, |
|
|
"date.at": 1, |
|
|
}) |
|
|
.catch((e) => logger.error(e)); |
|
|
abortedGenerations |
|
|
.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }) |
|
|
.catch((e) => logger.error(e)); |
|
|
abortedGenerations |
|
|
.createIndex({ conversationId: 1 }, { unique: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch((e) => logger.error(e)); |
|
|
settings |
|
|
.createIndex({ sessionId: 1 }, { unique: true, sparse: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
settings |
|
|
.createIndex({ userId: 1 }, { unique: true, sparse: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
settings.createIndex({ assistants: 1 }).catch((e) => logger.error(e)); |
|
|
users.createIndex({ hfUserId: 1 }, { unique: true }).catch((e) => logger.error(e)); |
|
|
users |
|
|
.createIndex({ sessionId: 1 }, { unique: true, sparse: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
|
|
|
users.createIndex({ username: 1 }).catch((e) => logger.error(e)); |
|
|
messageEvents |
|
|
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) |
|
|
.catch((e) => logger.error(e)); |
|
|
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch((e) => logger.error(e)); |
|
|
sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ userCount: 1 }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ review: 1, userCount: -1 }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ modelId: 1, userCount: -1 }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ searchTokens: 1 }).catch((e) => logger.error(e)); |
|
|
assistants.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e)); |
|
|
assistants |
|
|
.createIndex({ last24HoursUseCount: -1, useCount: -1, _id: 1 }) |
|
|
.catch((e) => logger.error(e)); |
|
|
assistantStats |
|
|
|
|
|
.createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true }) |
|
|
.catch((e) => logger.error(e)); |
|
|
reports.createIndex({ assistantId: 1 }).catch((e) => logger.error(e)); |
|
|
reports.createIndex({ createdBy: 1, assistantId: 1 }).catch((e) => logger.error(e)); |
|
|
|
|
|
|
|
|
semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); |
|
|
semaphores |
|
|
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) |
|
|
.catch((e) => logger.error(e)); |
|
|
tokenCaches |
|
|
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 }) |
|
|
.catch((e) => logger.error(e)); |
|
|
tokenCaches.createIndex({ tokenHash: 1 }).catch((e) => logger.error(e)); |
|
|
tools.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e)); |
|
|
tools.createIndex({ userCount: 1 }).catch((e) => logger.error(e)); |
|
|
tools.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e)); |
|
|
|
|
|
conversations |
|
|
.createIndex({ |
|
|
"messages.from": 1, |
|
|
createdAt: 1, |
|
|
}) |
|
|
.catch((e) => logger.error(e)); |
|
|
|
|
|
conversations |
|
|
.createIndex({ |
|
|
userId: 1, |
|
|
sessionId: 1, |
|
|
}) |
|
|
.catch((e) => logger.error(e)); |
|
|
} |
|
|
} |
|
|
|
|
|
export const collections = building |
|
|
? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>) |
|
|
: await Database.getInstance().then((db) => db.getCollections()); |
|
|
|