nsarrazin HF Staff commited on
Commit
d4471e3
·
unverified ·
1 Parent(s): a99bd3b

feat(db): use in memory db when MONGODB_URL not set (#1773)

Browse files

* feat(db): allow use of in memory db when MONGODB_URL not set

* fix(db): enhance database disconnection handling with server stop

* feat(db): persist db to disk

* fix: use proper root folder in both build & dev

.dockerignore CHANGED
@@ -8,4 +8,5 @@ node_modules/
8
  .svelte-kit/
9
  .env*
10
  !.env
11
- .env.local
 
 
8
  .svelte-kit/
9
  .env*
10
  !.env
11
+ .env.local
12
+ db
.gitignore CHANGED
@@ -11,4 +11,5 @@ SECRET_CONFIG
11
  .idea
12
  !.env.ci
13
  !.env
14
- gcp-*.json
 
 
11
  .idea
12
  !.env.ci
13
  !.env
14
+ gcp-*.json
15
+ db
src/lib/jobs/refresh-assistants-counts.ts CHANGED
@@ -26,49 +26,49 @@ async function refreshAssistantsCountsHelper() {
26
  }
27
 
28
  try {
29
- await Database.getInstance()
30
- .getClient()
31
- .withSession((session) =>
32
- session.withTransaction(async () => {
33
  await Database.getInstance()
34
- .getCollections()
35
- .assistants.aggregate([
36
- { $project: { _id: 1 } },
37
- { $set: { last24HoursCount: 0 } },
38
- {
39
- $unionWith: {
40
- coll: "assistants.stats",
41
- pipeline: [
42
- {
43
- $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" },
 
 
 
 
 
 
44
  },
45
- {
46
- $group: {
47
- _id: "$assistantId",
48
- last24HoursCount: { $sum: "$count" },
49
- },
50
- },
51
- ],
52
- },
53
  },
54
- {
55
- $group: {
56
- _id: "$_id",
57
- last24HoursCount: { $sum: "$last24HoursCount" },
58
- },
59
  },
60
- {
61
- $merge: {
62
- into: "assistants",
63
- on: "_id",
64
- whenMatched: "merge",
65
- whenNotMatched: "discard",
66
- },
67
  },
68
- ])
69
- .next();
70
- })
71
- );
 
72
  } catch (e) {
73
  logger.error(e, "Refresh assistants counter failed!");
74
  }
 
26
  }
27
 
28
  try {
29
+ await (await Database.getInstance()).getClient().withSession((session) =>
30
+ session.withTransaction(async () => {
31
+ await (
 
32
  await Database.getInstance()
33
+ )
34
+ .getCollections()
35
+ .assistants.aggregate([
36
+ { $project: { _id: 1 } },
37
+ { $set: { last24HoursCount: 0 } },
38
+ {
39
+ $unionWith: {
40
+ coll: "assistants.stats",
41
+ pipeline: [
42
+ {
43
+ $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" },
44
+ },
45
+ {
46
+ $group: {
47
+ _id: "$assistantId",
48
+ last24HoursCount: { $sum: "$count" },
49
  },
50
+ },
51
+ ],
 
 
 
 
 
 
52
  },
53
+ },
54
+ {
55
+ $group: {
56
+ _id: "$_id",
57
+ last24HoursCount: { $sum: "$last24HoursCount" },
58
  },
59
+ },
60
+ {
61
+ $merge: {
62
+ into: "assistants",
63
+ on: "_id",
64
+ whenMatched: "merge",
65
+ whenNotMatched: "discard",
66
  },
67
+ },
68
+ ])
69
+ .next();
70
+ })
71
+ );
72
  } catch (e) {
73
  logger.error(e, "Refresh assistants counter failed!");
74
  }
src/lib/migrations/migrations.ts CHANGED
@@ -13,7 +13,7 @@ export async function checkAndRunMigrations() {
13
  }
14
 
15
  // check if all migrations have already been run
16
- const migrationResults = await Database.getInstance()
17
  .getCollections()
18
  .migrationResults.find()
19
  .toArray();
@@ -21,7 +21,7 @@ export async function checkAndRunMigrations() {
21
  logger.info("[MIGRATIONS] Begin check...");
22
 
23
  // connect to the database
24
- const connectedClient = await Database.getInstance().getClient().connect();
25
 
26
  const lockId = await acquireLock(LOCK_KEY);
27
 
@@ -74,25 +74,23 @@ export async function checkAndRunMigrations() {
74
  }. Applying...`
75
  );
76
 
77
- await Database.getInstance()
78
- .getCollections()
79
- .migrationResults.updateOne(
80
- { _id: migration._id },
81
- {
82
- $set: {
83
- name: migration.name,
84
- status: "ongoing",
85
- },
86
  },
87
- { upsert: true }
88
- );
 
89
 
90
  const session = connectedClient.startSession();
91
  let result = false;
92
 
93
  try {
94
  await session.withTransaction(async () => {
95
- result = await migration.up(Database.getInstance());
96
  });
97
  } catch (e) {
98
  logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
@@ -101,18 +99,16 @@ export async function checkAndRunMigrations() {
101
  await session.endSession();
102
  }
103
 
104
- await Database.getInstance()
105
- .getCollections()
106
- .migrationResults.updateOne(
107
- { _id: migration._id },
108
- {
109
- $set: {
110
- name: migration.name,
111
- status: result ? "success" : "failure",
112
- },
113
  },
114
- { upsert: true }
115
- );
 
116
  }
117
  }
118
 
 
13
  }
14
 
15
  // check if all migrations have already been run
16
+ const migrationResults = await (await Database.getInstance())
17
  .getCollections()
18
  .migrationResults.find()
19
  .toArray();
 
21
  logger.info("[MIGRATIONS] Begin check...");
22
 
23
  // connect to the database
24
+ const connectedClient = await (await Database.getInstance()).getClient().connect();
25
 
26
  const lockId = await acquireLock(LOCK_KEY);
27
 
 
74
  }. Applying...`
75
  );
76
 
77
+ await (await Database.getInstance()).getCollections().migrationResults.updateOne(
78
+ { _id: migration._id },
79
+ {
80
+ $set: {
81
+ name: migration.name,
82
+ status: "ongoing",
 
 
 
83
  },
84
+ },
85
+ { upsert: true }
86
+ );
87
 
88
  const session = connectedClient.startSession();
89
  let result = false;
90
 
91
  try {
92
  await session.withTransaction(async () => {
93
+ result = await migration.up(await Database.getInstance());
94
  });
95
  } catch (e) {
96
  logger.info(`[MIGRATIONS] "${migration.name}" failed!`);
 
99
  await session.endSession();
100
  }
101
 
102
+ await (await Database.getInstance()).getCollections().migrationResults.updateOne(
103
+ { _id: migration._id },
104
+ {
105
+ $set: {
106
+ name: migration.name,
107
+ status: result ? "success" : "failure",
 
 
 
108
  },
109
+ },
110
+ { upsert: true }
111
+ );
112
  }
113
  }
114
 
src/lib/server/database.ts CHANGED
@@ -14,29 +14,69 @@ import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
16
  import type { CommunityToolDB } from "$lib/types/Tool";
17
-
18
  import { logger } from "$lib/server/logger";
19
  import { building } from "$app/environment";
20
  import type { TokenCache } from "$lib/types/TokenCache";
21
  import { onExit } from "./exitHandler";
 
 
 
22
 
23
  export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
24
 
 
 
 
 
 
 
 
 
 
 
 
25
  export class Database {
26
- private client: MongoClient;
 
27
 
28
  private static instance: Database;
29
 
30
- private constructor() {
31
  if (!env.MONGODB_URL) {
32
- throw new Error(
33
- "Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example."
34
- );
35
- }
36
 
37
- this.client = new MongoClient(env.MONGODB_URL, {
38
- directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
39
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  this.client.connect().catch((err) => {
42
  logger.error(err, "Connection error");
@@ -46,12 +86,17 @@ export class Database {
46
  this.client.on("open", () => this.initDatabase());
47
 
48
  // Disconnect DB on exit
49
- onExit(() => this.client.close(true));
 
 
 
 
50
  }
51
 
52
- public static getInstance(): Database {
53
  if (!Database.instance) {
54
  Database.instance = new Database();
 
55
  }
56
 
57
  return Database.instance;
@@ -61,6 +106,10 @@ export class Database {
61
  * Return mongoClient
62
  */
63
  public getClient(): MongoClient {
 
 
 
 
64
  return this.client;
65
  }
66
 
@@ -68,6 +117,10 @@ export class Database {
68
  * Return map of database's collections
69
  */
70
  public getCollections() {
 
 
 
 
71
  const db = this.client.db(
72
  env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
73
  );
@@ -247,4 +300,4 @@ export class Database {
247
 
248
  export const collections = building
249
  ? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
250
- : Database.getInstance().getCollections();
 
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
  import type { AssistantStats } from "$lib/types/AssistantStats";
16
  import type { CommunityToolDB } from "$lib/types/Tool";
17
+ import { MongoMemoryServer } from "mongodb-memory-server";
18
  import { logger } from "$lib/server/logger";
19
  import { building } from "$app/environment";
20
  import type { TokenCache } from "$lib/types/TokenCache";
21
  import { onExit } from "./exitHandler";
22
+ import { fileURLToPath } from "url";
23
+ import { dirname, join } from "path";
24
+ import { existsSync, mkdirSync } from "fs";
25
 
26
  export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
27
 
28
+ function findRepoRoot(startPath: string): string {
29
+ let currentPath = startPath;
30
+ while (currentPath !== "/") {
31
+ if (existsSync(join(currentPath, "package.json"))) {
32
+ return currentPath;
33
+ }
34
+ currentPath = dirname(currentPath);
35
+ }
36
+ throw new Error("Could not find repository root (no package.json found)");
37
+ }
38
+
39
  export class Database {
40
+ private client?: MongoClient;
41
+ private mongoServer?: MongoMemoryServer;
42
 
43
  private static instance: Database;
44
 
45
+ private async init() {
46
  if (!env.MONGODB_URL) {
47
+ logger.warn("No MongoDB URL found, using in-memory server");
 
 
 
48
 
49
+ // Find repo root by looking for package.json
50
+ const currentFilePath = fileURLToPath(import.meta.url);
51
+ const repoRoot = findRepoRoot(dirname(currentFilePath));
52
+
53
+ // Use MONGO_STORAGE_PATH from env if set, otherwise use db/ in repo root
54
+ const dbPath = env.MONGO_STORAGE_PATH || join(repoRoot, "db");
55
+
56
+ logger.info(`Using database path: ${dbPath}`);
57
+ // Create db directory if it doesn't exist
58
+ if (!existsSync(dbPath)) {
59
+ logger.info(`Creating database directory at ${dbPath}`);
60
+ mkdirSync(dbPath, { recursive: true });
61
+ }
62
+
63
+ this.mongoServer = await MongoMemoryServer.create({
64
+ instance: {
65
+ dbName: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""),
66
+ dbPath,
67
+ },
68
+ binary: {
69
+ version: "7.0.18",
70
+ },
71
+ });
72
+ this.client = new MongoClient(this.mongoServer.getUri(), {
73
+ directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
74
+ });
75
+ } else {
76
+ this.client = new MongoClient(env.MONGODB_URL, {
77
+ directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
78
+ });
79
+ }
80
 
81
  this.client.connect().catch((err) => {
82
  logger.error(err, "Connection error");
 
86
  this.client.on("open", () => this.initDatabase());
87
 
88
  // Disconnect DB on exit
89
+ onExit(async () => {
90
+ logger.info("Closing database connection");
91
+ await this.client?.close(true);
92
+ await this.mongoServer?.stop();
93
+ });
94
  }
95
 
96
+ public static async getInstance(): Promise<Database> {
97
  if (!Database.instance) {
98
  Database.instance = new Database();
99
+ await Database.instance.init();
100
  }
101
 
102
  return Database.instance;
 
106
  * Return mongoClient
107
  */
108
  public getClient(): MongoClient {
109
+ if (!this.client) {
110
+ throw new Error("Database not initialized");
111
+ }
112
+
113
  return this.client;
114
  }
115
 
 
117
  * Return map of database's collections
118
  */
119
  public getCollections() {
120
+ if (!this.client) {
121
+ throw new Error("Database not initialized");
122
+ }
123
+
124
  const db = this.client.db(
125
  env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
126
  );
 
300
 
301
  export const collections = building
302
  ? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
303
+ : await Database.getInstance().then((db) => db.getCollections());
src/routes/assistants/+page.server.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { base } from "$app/paths";
2
  import { env } from "$env/dynamic/private";
3
- import { Database, collections } from "$lib/server/database.js";
4
  import { SortKey, type Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
@@ -58,9 +58,8 @@ export const load = async ({ url, locals }) => {
58
  ...shouldBeFeatured,
59
  };
60
 
61
- const assistants = await Database.getInstance()
62
- .getCollections()
63
- .assistants.find(filter)
64
  .sort({
65
  ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
66
  userCount: -1,
@@ -70,9 +69,7 @@ export const load = async ({ url, locals }) => {
70
  .limit(NUM_PER_PAGE)
71
  .toArray();
72
 
73
- const numTotalItems = await Database.getInstance()
74
- .getCollections()
75
- .assistants.countDocuments(filter);
76
 
77
  return {
78
  assistants: JSON.parse(JSON.stringify(assistants)) as Array<Assistant>,
 
1
  import { base } from "$app/paths";
2
  import { env } from "$env/dynamic/private";
3
+ import { collections } from "$lib/server/database.js";
4
  import { SortKey, type Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
 
58
  ...shouldBeFeatured,
59
  };
60
 
61
+ const assistants = await collections.assistants
62
+ .find(filter)
 
63
  .sort({
64
  ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
65
  userCount: -1,
 
69
  .limit(NUM_PER_PAGE)
70
  .toArray();
71
 
72
+ const numTotalItems = await collections.assistants.countDocuments(filter);
 
 
73
 
74
  return {
75
  assistants: JSON.parse(JSON.stringify(assistants)) as Array<Assistant>,
src/routes/tools/+page.server.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { env } from "$env/dynamic/private";
2
  import { authCondition } from "$lib/server/auth.js";
3
- import { Database, collections } from "$lib/server/database.js";
4
  import { toolFromConfigs } from "$lib/server/tools/index.js";
5
  import { SortKey } from "$lib/types/Assistant.js";
6
  import { ReviewStatus } from "$lib/types/Review";
@@ -60,9 +60,8 @@ export const load = async ({ url, locals }) => {
60
  }),
61
  };
62
 
63
- const communityTools = await Database.getInstance()
64
- .getCollections()
65
- .tools.find(filter)
66
  .skip(NUM_PER_PAGE * pageIndex)
67
  .sort({
68
  ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
@@ -84,9 +83,7 @@ export const load = async ({ url, locals }) => {
84
 
85
  const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
86
 
87
- const numTotalItems =
88
- (await Database.getInstance().getCollections().tools.countDocuments(filter)) +
89
- toolFromConfigs.length;
90
 
91
  return {
92
  tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],
 
1
  import { env } from "$env/dynamic/private";
2
  import { authCondition } from "$lib/server/auth.js";
3
+ import { collections } from "$lib/server/database.js";
4
  import { toolFromConfigs } from "$lib/server/tools/index.js";
5
  import { SortKey } from "$lib/types/Assistant.js";
6
  import { ReviewStatus } from "$lib/types/Review";
 
60
  }),
61
  };
62
 
63
+ const communityTools = await collections.tools
64
+ .find(filter)
 
65
  .skip(NUM_PER_PAGE * pageIndex)
66
  .sort({
67
  ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
 
83
 
84
  const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
85
 
86
+ const numTotalItems = (await collections.tools.countDocuments(filter)) + toolFromConfigs.length;
 
 
87
 
88
  return {
89
  tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],