Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 +2 -1
- .gitignore +2 -1
- src/lib/jobs/refresh-assistants-counts.ts +38 -38
- src/lib/migrations/migrations.ts +21 -25
- src/lib/server/database.ts +66 -13
- src/routes/assistants/+page.server.ts +4 -7
- src/routes/tools/+page.server.ts +4 -7
.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 |
-
.
|
| 31 |
-
|
| 32 |
-
session.withTransaction(async () => {
|
| 33 |
await Database.getInstance()
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
},
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
_id: "$assistantId",
|
| 48 |
-
last24HoursCount: { $sum: "$count" },
|
| 49 |
-
},
|
| 50 |
-
},
|
| 51 |
-
],
|
| 52 |
-
},
|
| 53 |
},
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
},
|
| 59 |
},
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
},
|
| 68 |
-
|
| 69 |
-
|
| 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 |
-
.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
name: migration.name,
|
| 84 |
-
status: "ongoing",
|
| 85 |
-
},
|
| 86 |
},
|
| 87 |
-
|
| 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 |
-
.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
name: migration.name,
|
| 111 |
-
status: result ? "success" : "failure",
|
| 112 |
-
},
|
| 113 |
},
|
| 114 |
-
|
| 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
|
|
|
|
| 27 |
|
| 28 |
private static instance: Database;
|
| 29 |
|
| 30 |
-
private
|
| 31 |
if (!env.MONGODB_URL) {
|
| 32 |
-
|
| 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 |
-
|
| 38 |
-
|
| 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(() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
| 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
|
| 62 |
-
.
|
| 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
|
| 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 {
|
| 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
|
| 64 |
-
.
|
| 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[],
|