From 0b7cf6c0de71f1d23d24481389688902e9b4f77d Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Mon, 29 Apr 2024 22:59:35 +0200 Subject: [PATCH 01/16] Add healthcheck route --- src/routes/healthcheck/+server.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/routes/healthcheck/+server.ts diff --git a/src/routes/healthcheck/+server.ts b/src/routes/healthcheck/+server.ts new file mode 100644 index 00000000000..edb40a0dd81 --- /dev/null +++ b/src/routes/healthcheck/+server.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response("OK", { status: 200 }); +} From 98c8062c69fc979e5b9bfd283832aee81ad2f07d Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Mon, 29 Apr 2024 22:59:43 +0200 Subject: [PATCH 02/16] Add prom client with basic metrics --- package-lock.json | 38 +++++++++++++++++++++++++++++++++++ package.json | 1 + src/hooks.server.ts | 3 +++ src/lib/server/metrics.ts | 3 +++ src/routes/metrics/+server.ts | 9 +++++++++ 5 files changed, 54 insertions(+) create mode 100644 src/lib/server/metrics.ts create mode 100644 src/routes/metrics/+server.ts diff --git a/package-lock.json b/package-lock.json index 66541d0f92e..3af66fb2cfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "prettier-plugin-tailwindcss": "^0.2.7", + "prom-client": "^15.1.2", "svelte": "^4.2.8", "svelte-check": "^3.6.2", "ts-node": "^10.9.1", @@ -1261,6 +1262,15 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -2717,6 +2727,12 @@ "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==", "optional": true }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "dev": true + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -6313,6 +6329,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prom-client": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.2.tgz", + "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", + "dev": true, + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/protobufjs": { "version": "6.11.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", @@ -7446,6 +7475,15 @@ "streamx": "^2.15.0" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dev": true, + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index fdc02ba1eb7..1ad66db6eee 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.10.1", "prettier-plugin-tailwindcss": "^0.2.7", + "prom-client": "^15.1.2", "svelte": "^4.2.8", "svelte-check": "^3.6.2", "ts-node": "^10.9.1", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index d749c38e531..a2cea18e179 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -21,12 +21,15 @@ import { addWeeks } from "date-fns"; import { checkAndRunMigrations } from "$lib/migrations/migrations"; import { building } from "$app/environment"; import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts"; +import { collectDefaultMetrics } from "prom-client"; +import { register } from "$lib/server/metrics"; if (!building) { await checkAndRunMigrations(); if (ENABLE_ASSISTANTS) { refreshAssistantsCounts(); } + collectDefaultMetrics({ register }); } export const handle: Handle = async ({ event, resolve }) => { diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts new file mode 100644 index 00000000000..caec4861533 --- /dev/null +++ b/src/lib/server/metrics.ts @@ -0,0 +1,3 @@ +import { Registry } from "prom-client"; + +export const register = new Registry(); diff --git a/src/routes/metrics/+server.ts b/src/routes/metrics/+server.ts new file mode 100644 index 00000000000..95b83257e56 --- /dev/null +++ b/src/routes/metrics/+server.ts @@ -0,0 +1,9 @@ +import { register } from "$lib/server/metrics"; + +export async function GET() { + return new Response(await register.metrics(), { + headers: { + "Content-Type": register.contentType, + }, + }); +} From efc0423b2cc38e50fcb269fad4bcbbe1fd578408 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 30 Apr 2024 15:21:56 +0200 Subject: [PATCH 03/16] wip: serve metrics on a different port --- .env | 1 + package-lock.json | 616 +++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- src/hooks.server.ts | 12 +- src/server.ts | 32 +++ 5 files changed, 651 insertions(+), 14 deletions(-) create mode 100644 src/server.ts diff --git a/.env b/.env index 232968a92e0..3caccfbbe95 100644 --- a/.env +++ b/.env @@ -154,3 +154,4 @@ ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to USAGE_LIMITS=`{}` ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls +METRICS_PORT= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 654bd2d6525..8a683fc33b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "browser-image-resizer": "^2.4.1", "date-fns": "^2.29.3", "dotenv": "^16.0.3", + "express": "^4.19.2", "handlebars": "^4.7.8", "highlight.js": "^11.7.0", "image-size": "^1.0.2", @@ -49,6 +50,7 @@ "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.30.4", "@tailwindcss/typography": "^0.5.9", + "@types/express": "^4.17.21", "@types/jsdom": "^21.1.1", "@types/minimist": "^1.2.5", "@types/parquetjs": "^0.10.3", @@ -1997,6 +1999,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "devOptional": true }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", @@ -2012,6 +2024,15 @@ "@types/chai": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -2024,6 +2045,36 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/jsdom": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz", @@ -2051,6 +2102,12 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -2096,6 +2153,18 @@ "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -2108,6 +2177,27 @@ "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", @@ -2470,6 +2560,18 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2600,6 +2702,11 @@ "dequal": "^2.0.3" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2758,6 +2865,67 @@ "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", "dev": true }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2890,6 +3058,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2903,7 +3079,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "optional": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3182,6 +3357,25 @@ "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -3191,6 +3385,11 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3425,7 +3624,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "optional": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3446,6 +3644,14 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3455,6 +3661,15 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -3559,6 +3774,11 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/electron-to-chromium": { "version": "1.4.359", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", @@ -3569,6 +3789,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3592,7 +3820,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "optional": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -3604,7 +3831,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "optional": true, "engines": { "node": ">= 0.4" } @@ -3894,6 +4120,14 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -3941,6 +4175,82 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4056,6 +4366,36 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4128,6 +4468,14 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -4140,6 +4488,14 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4237,7 +4593,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -4368,7 +4723,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "optional": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -4445,7 +4799,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "optional": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -4457,7 +4810,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -4469,7 +4821,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -4528,6 +4879,21 @@ "node": ">=12" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -4685,6 +5051,14 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -5247,11 +5621,24 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5266,6 +5653,14 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -5278,6 +5673,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5484,6 +5890,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -5641,7 +6055,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5670,6 +6083,17 @@ "node": ">=14.0.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5896,6 +6320,14 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5927,6 +6359,11 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6583,6 +7020,18 @@ "pbts": "bin/pbts" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6671,6 +7120,39 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -6973,6 +7455,47 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/serpapi": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz", @@ -6981,6 +7504,20 @@ "undici": "^5.12.0" } }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -6991,7 +7528,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "optional": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7004,6 +7540,11 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/sharp": { "version": "0.33.2", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", @@ -7068,7 +7609,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "optional": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -7261,6 +7801,14 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", @@ -7846,6 +8394,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", @@ -7996,6 +8552,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -8060,6 +8628,14 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unplugin": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz", @@ -8164,6 +8740,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -8187,6 +8771,14 @@ "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", diff --git a/package.json b/package.json index 6dd1e4af90f..61d1255d281 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite preview", + "preview": "vite-node --options.transformMode.ssr='/.*/' src/server.ts", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", @@ -23,6 +23,7 @@ "@sveltejs/adapter-node": "^1.3.1", "@sveltejs/kit": "^1.30.4", "@tailwindcss/typography": "^0.5.9", + "@types/express": "^4.17.21", "@types/jsdom": "^21.1.1", "@types/minimist": "^1.2.5", "@types/parquetjs": "^0.10.3", @@ -58,6 +59,7 @@ "browser-image-resizer": "^2.4.1", "date-fns": "^2.29.3", "dotenv": "^16.0.3", + "express": "^4.19.2", "handlebars": "^4.7.8", "highlight.js": "^11.7.0", "image-size": "^1.0.2", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index d25a61a5385..bff9b3b186e 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -19,7 +19,7 @@ import { ERROR_MESSAGES } from "$lib/stores/errors"; import { sha256 } from "$lib/utils/sha256"; import { addWeeks } from "date-fns"; import { checkAndRunMigrations } from "$lib/migrations/migrations"; -import { building } from "$app/environment"; +import { building, dev } from "$app/environment"; import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts"; import { collectDefaultMetrics } from "prom-client"; import { register } from "$lib/server/metrics"; @@ -59,6 +59,16 @@ export const handleError: HandleServerError = async ({ error, event }) => { }; export const handle: Handle = async ({ event, resolve }) => { + // only allow metrics from localhost if we're not in dev mode + console.log({ host: event.request.headers.get("host") }); + if ( + event.url.pathname.startsWith(`${base}/metrics`) && + !dev && + event.request.headers.get("host") !== "localhost:3000" + ) { + return new Response("Forbidden", { status: 403 }); + } + if (event.url.pathname.startsWith(`${base}/api/`) && EXPOSE_API !== "true") { return new Response("API is disabled", { status: 403 }); } diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000000..bd75abf361f --- /dev/null +++ b/src/server.ts @@ -0,0 +1,32 @@ +import express from "express"; +import { logger } from "$lib/server/logger.js"; +import { handler } from "../build/handler.js"; +import { APP_BASE } from "$env/static/private"; + +const app = express(); +const metricsApp = express(); +const METRICS_PORT = process.env.METRICS_PORT || "3001"; + +// Let SvelteKit handle everything else, including serving prerendered pages and static assets +app.use(handler); + +app.listen(3000, () => { + logger.info("Listening on port 3000"); +}); + +// Set up metrics app to serve something when `/metrics` is accessed +metricsApp.get("/metrics", (req, res) => { + res.set("Content-Type", "text/plain"); + fetch(`http://localhost:3000${APP_BASE}/metrics`) + .then((response) => response.text()) + .then((text) => { + res.send(text); + }) + .catch(() => { + res.send("Error fetching metrics."); + }); +}); + +metricsApp.listen(METRICS_PORT, () => { + logger.info(`Metrics server listening on port ${METRICS_PORT}`); +}); From 979b62df408b6edb06f3321ea25dce44f1968fc0 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 30 Apr 2024 15:25:20 +0200 Subject: [PATCH 04/16] exclude server from tsconfig --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index fbfc0ac0b5a..f84405df539 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "sourceMap": true, "strict": true, "target": "ES2018" - } + }, + "exclude": ["src/server.ts"] // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes From 8c2991dfdede6f03557d630f9aa2b4283eba5325 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Wed, 1 May 2024 10:20:30 +0200 Subject: [PATCH 05/16] latest --- Dockerfile | 4 ++-- entrypoint.sh | 2 +- package.json | 2 +- {src => scripts}/server.ts | 0 src/hooks.server.ts | 5 +++++ tsconfig.json | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) rename {src => scripts}/server.ts (100%) diff --git a/Dockerfile b/Dockerfile index f87b5a64935..5699847566a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \ npm run build FROM node:20-slim -RUN npm install -g pm2 +RUN npm install -g vite-node RUN userdel -r node @@ -39,4 +39,4 @@ COPY --link --chown=1000 package.json /app/package.json COPY --from=builder --chown=1000 /app/build /app/build COPY --chown=1000 gcp-*.json /app/ -CMD pm2 start /app/build/index.js -i $CPU_CORES --no-daemon +CMD vite-node --options.transformMode.ssr='/.*/' /app/scripts/server.ts diff --git a/entrypoint.sh b/entrypoint.sh index c9a9f41fe49..1f67a9a5fa2 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -26,4 +26,4 @@ if [ "$INCLUDE_DB" = "true" ] ; then fi; npm run build -npm run preview -- --host 0.0.0.0 --port 3000 \ No newline at end of file +npm vite-node --options.transformMode.ssr='/.*/' /app/scripts/server.ts -- --host 0.0.0.0 --port 3000 \ No newline at end of file diff --git a/package.json b/package.json index 61d1255d281..cb03e3f1463 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite-node --options.transformMode.ssr='/.*/' src/server.ts", + "preview": "vite-node --options.transformMode.ssr='/.*/' scripts/server.ts", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", diff --git a/src/server.ts b/scripts/server.ts similarity index 100% rename from src/server.ts rename to scripts/server.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index bff9b3b186e..a8f365882fd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -35,6 +35,11 @@ if (!building) { export const handleError: HandleServerError = async ({ error, event }) => { // handle 404 + + if (building) { + throw error; + } + if (event.route.id === null) { return { message: `Page ${event.url.pathname} not found`, diff --git a/tsconfig.json b/tsconfig.json index f84405df539..d8b32db527a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "strict": true, "target": "ES2018" }, - "exclude": ["src/server.ts"] + "exclude": ["scripts/server.ts"] // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes From c3f580e4b60563b6ec3ceee27e1dc80aa50843a2 Mon Sep 17 00:00:00 2001 From: rtrompier Date: Thu, 2 May 2024 10:37:42 +0200 Subject: [PATCH 06/16] refacto(all): use class & singleton --- Dockerfile | 5 +- scripts/populate.ts | 26 +- src/hooks.server.ts | 11 +- .../refresh-assistants-counts.ts | 6 +- src/lib/migrations/lock.ts | 10 +- src/lib/migrations/migrations.spec.ts | 10 +- src/lib/migrations/migrations.ts | 10 +- .../routines/01-update-search-assistants.ts | 6 +- .../routines/02-update-assistants-models.ts | 4 +- src/lib/server/abortedGenerations.ts | 40 ++- src/lib/server/auth.ts | 6 +- src/lib/server/database.ts | 315 ++++++++++-------- src/lib/server/files/downloadFile.ts | 6 +- src/lib/server/files/uploadFile.ts | 4 +- src/lib/utils/tree/addChildren.spec.ts | 16 +- src/lib/utils/tree/addSibling.spec.ts | 10 +- src/lib/utils/tree/buildSubtree.spec.ts | 12 +- .../tree/convertLegacyConversation.spec.ts | 4 +- src/lib/utils/tree/treeHelpers.spec.ts | 8 +- src/routes/+layout.server.ts | 16 +- src/routes/admin/export/+server.ts | 6 +- src/routes/admin/stats/compute/+server.ts | 6 +- src/routes/api/assistant/[id]/+server.ts | 4 +- src/routes/api/assistants/+server.ts | 8 +- src/routes/api/conversation/[id]/+server.ts | 4 +- src/routes/api/conversations/+server.ts | 4 +- src/routes/api/user/assistants/+server.ts | 8 +- .../assistant/[assistantId]/+page.server.ts | 4 +- .../[assistantId]/thumbnail.png/+server.ts | 8 +- src/routes/assistants/+page.server.ts | 8 +- src/routes/conversation/+server.ts | 10 +- src/routes/conversation/[id]/+page.server.ts | 10 +- src/routes/conversation/[id]/+server.ts | 42 +-- .../message/[messageId]/prompt/+server.ts | 6 +- .../[id]/message/[messageId]/vote/+server.ts | 4 +- .../[id]/output/[sha256]/+server.ts | 6 +- src/routes/conversation/[id]/share/+server.ts | 14 +- .../[id]/stop-generating/+server.ts | 6 +- src/routes/conversations/+page.server.ts | 4 +- src/routes/login/callback/updateUser.spec.ts | 28 +- src/routes/login/callback/updateUser.ts | 22 +- src/routes/logout/+page.server.ts | 4 +- src/routes/models/[...model]/+page.server.ts | 4 +- src/routes/settings/(nav)/+server.ts | 4 +- .../assistants/[assistantId]/+page.server.ts | 32 +- .../[assistantId]/avatar.jpg/+server.ts | 8 +- .../[assistantId]/edit/+page.server.ts | 16 +- .../(nav)/assistants/new/+page.server.ts | 10 +- src/routes/settings/+layout.server.ts | 4 +- 49 files changed, 435 insertions(+), 384 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5699847566a..45373a87375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,8 +18,7 @@ RUN --mount=type=cache,target=/app/.npm \ COPY --link --chown=1000 . . -RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local \ - npm run build +RUN npm run build FROM node:20-slim RUN npm install -g vite-node @@ -39,4 +38,4 @@ COPY --link --chown=1000 package.json /app/package.json COPY --from=builder --chown=1000 /app/build /app/build COPY --chown=1000 gcp-*.json /app/ -CMD vite-node --options.transformMode.ssr='/.*/' /app/scripts/server.ts +CMD node /app/build/index.js diff --git a/scripts/populate.ts b/scripts/populate.ts index 2a3e12a541e..d6fa31e08ac 100644 --- a/scripts/populate.ts +++ b/scripts/populate.ts @@ -7,7 +7,7 @@ import { MONGODB_URL } from "$env/static/private"; import { faker } from "@faker-js/faker"; import { ObjectId } from "mongodb"; -import { collections } from "../src/lib/server/database.ts"; +import { Database } from "$lib/server/database"; import { models } from "../src/lib/server/models.ts"; import type { User } from "../src/lib/types/User"; import type { Assistant } from "../src/lib/types/Assistant"; @@ -107,10 +107,10 @@ async function seed() { if (flags.includes("reset")) { console.log("Starting reset of DB"); - await collections.users.deleteMany({}); - await collections.settings.deleteMany({}); - await collections.assistants.deleteMany({}); - await collections.conversations.deleteMany({}); + await Database.getInstance().getCollections().users.deleteMany({}); + await Database.getInstance().getCollections().settings.deleteMany({}); + await Database.getInstance().getCollections().assistants.deleteMany({}); + await Database.getInstance().getCollections().conversations.deleteMany({}); console.log("Reset done"); } @@ -126,11 +126,11 @@ async function seed() { avatarUrl: faker.image.avatar(), })); - await collections.users.insertMany(newUsers); + await Database.getInstance().getCollections().users.insertMany(newUsers); console.log("Done creating users."); } - const users = await collections.users.find().toArray(); + const users = await Database.getInstance().getCollections().users.find().toArray(); if (flags.includes("settings") || flags.includes("all")) { console.log("Updating settings for all users"); users.forEach(async (user) => { @@ -145,7 +145,7 @@ async function seed() { customPrompts: {}, assistants: [], }; - await collections.settings.updateOne( + await Database.getInstance().getCollections().settings.updateOne( { userId: user._id }, { $set: { ...settings } }, { upsert: true } @@ -177,8 +177,8 @@ async function seed() { }), { count: faker.number.int({ min: 3, max: 10 }) } ); - await collections.assistants.insertMany(assistants); - await collections.settings.updateOne( + await Database.getInstance().getCollections().assistants.insertMany(assistants); + await Database.getInstance().getCollections().settings.updateOne( { userId: user._id }, { $set: { assistants: assistants.map((a) => a._id.toString()) } }, { upsert: true } @@ -194,7 +194,7 @@ async function seed() { users.map(async (user) => { const conversations = faker.helpers.multiple( async () => { - const settings = await collections.settings.findOne({ userId: user._id }); + const settings = await Database.getInstance().getCollections().settings.findOne({ userId: user._id }); const assistantId = settings?.assistants && settings.assistants.length > 0 && faker.datatype.boolean(0.1) @@ -203,7 +203,7 @@ async function seed() { const preprompt = (assistantId - ? await collections.assistants + ? await Database.getInstance().getCollections().assistants .findOne({ _id: assistantId }) .then((assistant: Assistant) => assistant?.preprompt ?? "") : faker.helpers.maybe(() => faker.hacker.phrase(), { probability: 0.5 })) ?? ""; @@ -229,7 +229,7 @@ async function seed() { { count: faker.number.int({ min: 10, max: 200 }) } ); - await collections.conversations.insertMany(await Promise.all(conversations)); + await Database.getInstance().getCollections().conversations.insertMany(await Promise.all(conversations)); }) ); console.log("Done creating conversations."); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index a8f365882fd..e67d02ecb22 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -12,7 +12,7 @@ import { PUBLIC_ORIGIN, PUBLIC_APP_DISCLAIMER, } from "$env/static/public"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { base } from "$app/paths"; import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth"; import { ERROR_MESSAGES } from "$lib/stores/errors"; @@ -24,13 +24,18 @@ import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants- import { collectDefaultMetrics } from "prom-client"; import { register } from "$lib/server/metrics"; import { logger } from "$lib/server/logger"; +import { AbortedGenerations } from "$lib/server/abortedGenerations"; +// TODO: move this code on a started server hook, instead of using a "building" flag if (!building) { await checkAndRunMigrations(); if (ENABLE_ASSISTANTS) { refreshAssistantsCounts(); } collectDefaultMetrics({ register }); + + // Init AbortedGenerations refresh process + new AbortedGenerations(); } export const handleError: HandleServerError = async ({ error, event }) => { @@ -162,7 +167,7 @@ export const handle: Handle = async ({ event, resolve }) => { // if the request is a POST request we refresh the cookie refreshSessionCookie(event.cookies, secretSessionId); - await collections.sessions.updateOne( + await Database.getInstance().getCollections().sessions.updateOne( { sessionId }, { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } ); @@ -189,7 +194,7 @@ export const handle: Handle = async ({ event, resolve }) => { !event.url.pathname.startsWith(`${base}/settings`) && !!PUBLIC_APP_DISCLAIMER ) { - const hasAcceptedEthicsModal = await collections.settings.countDocuments({ + const hasAcceptedEthicsModal = await Database.getInstance().getCollections().settings.countDocuments({ sessionId: event.locals.sessionId, ethicsModalAcceptedAt: { $exists: true }, }); diff --git a/src/lib/assistantStats/refresh-assistants-counts.ts b/src/lib/assistantStats/refresh-assistants-counts.ts index 07013e1989a..292f743a25f 100644 --- a/src/lib/assistantStats/refresh-assistants-counts.ts +++ b/src/lib/assistantStats/refresh-assistants-counts.ts @@ -1,4 +1,4 @@ -import { client, collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { acquireLock, refreshLock } from "$lib/migrations/lock"; import type { ObjectId } from "mongodb"; import { subDays } from "date-fns"; @@ -15,9 +15,9 @@ async function refreshAssistantsCountsHelper() { } try { - await client.withSession((session) => + await Database.getInstance().client.withSession((session) => session.withTransaction(async () => { - await collections.assistants + await Database.getInstance().getCollections().assistants .aggregate([ { $project: { _id: 1 } }, { $set: { last24HoursCount: 0 } }, diff --git a/src/lib/migrations/lock.ts b/src/lib/migrations/lock.ts index 6df5d261b41..8ac1f7c1d6e 100644 --- a/src/lib/migrations/lock.ts +++ b/src/lib/migrations/lock.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; /** @@ -8,7 +8,7 @@ export async function acquireLock(key: string): Promise { try { const id = new ObjectId(); - const insert = await collections.semaphores.insertOne({ + const insert = await Database.getInstance().getCollections().semaphores.insertOne({ _id: id, key, createdAt: new Date(), @@ -23,21 +23,21 @@ export async function acquireLock(key: string): Promise { } export async function releaseLock(key: string, lockId: ObjectId) { - await collections.semaphores.deleteOne({ + await Database.getInstance().getCollections().semaphores.deleteOne({ _id: lockId, key, }); } export async function isDBLocked(key: string): Promise { - const res = await collections.semaphores.countDocuments({ + const res = await Database.getInstance().getCollections().semaphores.countDocuments({ key, }); return res > 0; } export async function refreshLock(key: string, lockId: ObjectId): Promise { - const result = await collections.semaphores.updateOne( + const result = await Database.getInstance().getCollections().semaphores.updateOne( { _id: lockId, key, diff --git a/src/lib/migrations/migrations.spec.ts b/src/lib/migrations/migrations.spec.ts index 2df38798ea5..a58936cf455 100644 --- a/src/lib/migrations/migrations.spec.ts +++ b/src/lib/migrations/migrations.spec.ts @@ -1,7 +1,7 @@ import { afterEach, assert, describe, expect, it } from "vitest"; import { migrations } from "./routines"; import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; const LOCK_KEY = "migrations.test"; @@ -40,11 +40,11 @@ describe("migrations", () => { // get the updatedAt time - const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt; + const updatedAtInitially = (await Database.getInstance().getCollections().semaphores.findOne({}))?.updatedAt; await refreshLock(LOCK_KEY, lockId); - const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt; + const updatedAtAfterRefresh = (await Database.getInstance().getCollections().semaphores.findOne({}))?.updatedAt; expect(updatedAtInitially).toBeDefined(); expect(updatedAtAfterRefresh).toBeDefined(); @@ -53,6 +53,6 @@ describe("migrations", () => { }); afterEach(async () => { - await collections.semaphores.deleteMany({}); - await collections.migrationResults.deleteMany({}); + await Database.getInstance().getCollections().semaphores.deleteMany({}); + await Database.getInstance().getCollections().migrationResults.deleteMany({}); }); diff --git a/src/lib/migrations/migrations.ts b/src/lib/migrations/migrations.ts index 6e7da313491..33abd6f969a 100644 --- a/src/lib/migrations/migrations.ts +++ b/src/lib/migrations/migrations.ts @@ -1,4 +1,4 @@ -import { client, collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { migrations } from "./routines"; import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock"; import { isHuggingChat } from "$lib/utils/isHuggingChat"; @@ -13,12 +13,12 @@ export async function checkAndRunMigrations() { } // check if all migrations have already been run - const migrationResults = await collections.migrationResults.find().toArray(); + const migrationResults = await Database.getInstance().getCollections().migrationResults.find().toArray(); logger.info("[MIGRATIONS] Begin check..."); // connect to the database - const connectedClient = await client.connect(); + const connectedClient = await Database.getInstance().getClient().connect(); const lockId = await acquireLock(LOCK_KEY); @@ -71,7 +71,7 @@ export async function checkAndRunMigrations() { }. Applying...` ); - await collections.migrationResults.updateOne( + await Database.getInstance().getCollections().migrationResults.updateOne( { _id: migration._id }, { $set: { @@ -96,7 +96,7 @@ export async function checkAndRunMigrations() { await session.endSession(); } - await collections.migrationResults.updateOne( + await Database.getInstance().getCollections().migrationResults.updateOne( { _id: migration._id }, { $set: { diff --git a/src/lib/migrations/routines/01-update-search-assistants.ts b/src/lib/migrations/routines/01-update-search-assistants.ts index 9f12b27d3fb..70b38d53766 100644 --- a/src/lib/migrations/routines/01-update-search-assistants.ts +++ b/src/lib/migrations/routines/01-update-search-assistants.ts @@ -1,5 +1,5 @@ import type { Migration } from "."; -import { getCollections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId, type AnyBulkWriteOperation } from "mongodb"; import type { Assistant } from "$lib/types/Assistant"; import { generateSearchTokens } from "$lib/utils/searchTokens"; @@ -8,7 +8,7 @@ const migration: Migration = { _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"), name: "Update search assistants", up: async (client) => { - const { assistants } = getCollections(client); + const { assistants } = Database.getInstance().getCollections(); let ops: AnyBulkWriteOperation[] = []; for await (const assistant of assistants @@ -41,7 +41,7 @@ const migration: Migration = { return true; }, down: async (client) => { - const { assistants } = getCollections(client); + const { assistants } = Database.getInstance().getCollections(); await assistants.updateMany({}, { $unset: { searchTokens: "" } }); return true; }, diff --git a/src/lib/migrations/routines/02-update-assistants-models.ts b/src/lib/migrations/routines/02-update-assistants-models.ts index 73655a88f84..c54b9690b6c 100644 --- a/src/lib/migrations/routines/02-update-assistants-models.ts +++ b/src/lib/migrations/routines/02-update-assistants-models.ts @@ -1,5 +1,5 @@ import type { Migration } from "."; -import { getCollections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; const updateAssistantsModels: Migration = { @@ -8,7 +8,7 @@ const updateAssistantsModels: Migration = { up: async (client) => { const models = (await import("$lib/server/models")).models; - const { assistants } = getCollections(client); + const { assistants } = Database.getInstance().getCollections(); const modelIds = models.map((el) => el.id); // string[] const defaultModelId = models[0].id; diff --git a/src/lib/server/abortedGenerations.ts b/src/lib/server/abortedGenerations.ts index 84ad9ec9041..2694025da08 100644 --- a/src/lib/server/abortedGenerations.ts +++ b/src/lib/server/abortedGenerations.ts @@ -1,30 +1,44 @@ // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 import { setTimeout } from "node:timers/promises"; -import { collections } from "./database"; +import { Database } from "$lib/server/database"; import { logger } from "$lib/server/logger"; -let closed = false; -process.on("SIGINT", () => { - closed = true; -}); +export class AbortedGenerations { + private static instance: AbortedGenerations; -export let abortedGenerations: Map = new Map(); + private abortedGenerations: Map = new Map(); -async function maintainAbortedGenerations() { - while (!closed) { - await setTimeout(1000); + private constructor() { + const interval = setInterval(this.updateList, 1000); + process.on("SIGINT", () => { + clearInterval(interval); + }); + } + + public static getInstance(): AbortedGenerations { + if (!AbortedGenerations.instance) { + AbortedGenerations.instance = new AbortedGenerations(); + } + + return AbortedGenerations.instance; + } + + public getList(): Map { + return this.getList(); + } + + private async updateList() { try { - const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); + const aborts = await Database.getInstance().getCollections().abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); - abortedGenerations = new Map( + this.abortedGenerations = new Map( aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt]) ); } catch (err) { logger.error(err); } } -} -maintainAbortedGenerations(); +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 9a76d25fd66..b941dfb7109 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -16,7 +16,7 @@ import { sha256 } from "$lib/utils/sha256"; import { z } from "zod"; import { dev } from "$app/environment"; import type { Cookies } from "@sveltejs/kit"; -import { collections } from "./database"; +import { Database } from "$lib/server/database"; import JSON5 from "json5"; import { logger } from "$lib/server/logger"; @@ -64,13 +64,13 @@ export function refreshSessionCookie(cookies: Cookies, sessionId: string) { } export async function findUser(sessionId: string) { - const session = await collections.sessions.findOne({ sessionId }); + const session = await Database.getInstance().getCollections().sessions.findOne({ sessionId }); if (!session) { return null; } - return await collections.users.findOne({ _id: session.userId }); + return await Database.getInstance().getCollections().users.findOne({ _id: session.userId }); } export const authCondition = (locals: App.Locals) => { return locals.user diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index ba4759ee8a7..7b278eabde9 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,4 +1,4 @@ -import { MONGODB_URL, MONGODB_DB_NAME, MONGODB_DIRECT_CONNECTION } from "$env/static/private"; +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"; @@ -15,148 +15,181 @@ import type { Semaphore } from "$lib/types/Semaphore"; import type { AssistantStats } from "$lib/types/AssistantStats"; import { logger } from "$lib/server/logger"; -if (!MONGODB_URL) { - throw new Error( - "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." - ); -} export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; -const client = new MongoClient(MONGODB_URL, { - directConnection: MONGODB_DIRECT_CONNECTION === "true", -}); - -export const connectPromise = client.connect().catch(logger.error); - -export function getCollections(mongoClient: MongoClient) { - const db = mongoClient.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); - - const conversations = db.collection("conversations"); - const conversationStats = db.collection(CONVERSATION_STATS_COLLECTION); - const assistants = db.collection("assistants"); - const assistantStats = db.collection("assistants.stats"); - const reports = db.collection("reports"); - const sharedConversations = db.collection("sharedConversations"); - const abortedGenerations = db.collection("abortedGenerations"); - const settings = db.collection("settings"); - const users = db.collection("users"); - const sessions = db.collection("sessions"); - const messageEvents = db.collection("messageEvents"); - const bucket = new GridFSBucket(db, { bucketName: "files" }); - const migrationResults = db.collection("migrationResults"); - const semaphores = db.collection("semaphores"); - - return { - conversations, - conversationStats, - assistants, - assistantStats, - reports, - sharedConversations, - abortedGenerations, - settings, - users, - sessions, - messageEvents, - bucket, - migrationResults, - semaphores, - }; -} -const db = client.db(MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); - -const collections = getCollections(client); - -const { - conversations, - conversationStats, - assistants, - assistantStats, - reports, - sharedConversations, - abortedGenerations, - settings, - users, - sessions, - messageEvents, - semaphores, -} = collections; - -export { client, db, collections }; - -client.on("open", () => { - conversations - .createIndex( - { sessionId: 1, updatedAt: -1 }, - { partialFilterExpression: { sessionId: { $exists: true } } } - ) - .catch(logger.error); - conversations - .createIndex( - { userId: 1, updatedAt: -1 }, - { partialFilterExpression: { userId: { $exists: true } } } - ) - .catch(logger.error); - conversations - .createIndex( - { "message.id": 1, "message.ancestors": 1 }, - { partialFilterExpression: { userId: { $exists: true } } } - ) - .catch(logger.error); - // To do stats on conversations - conversations.createIndex({ updatedAt: 1 }).catch(logger.error); - // Not strictly necessary, could use _id, but more convenient. Also for stats - conversations.createIndex({ createdAt: 1 }).catch(logger.error); - // To do stats on conversation messages - conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error); - // Unique index for stats - conversationStats - .createIndex( - { +export class Database { + private client: MongoClient; + + private static instance: Database; + + private constructor() { + if (!env.MONGODB_URL) { + throw new Error( + "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." + ); + } + + this.client = new MongoClient(env.MONGODB_URL, { + directConnection: env.MONGODB_DIRECT_CONNECTION === "true", + }); + + this.client.connect().catch(logger.error); + this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + this.client.on("open", () => this.initDatabase()); + } + + public static getInstance(): Database { + if (!Database.instance) { + Database.instance = new Database(); + } + + return Database.instance; + } + + /** + * Return mongoClient + */ + public getClient(): MongoClient { + return this.client; + } + + /** + * Return map of database's collections + */ + public getCollections() { + const db = this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + + const conversations = db.collection("conversations"); + const conversationStats = db.collection(CONVERSATION_STATS_COLLECTION); + const assistants = db.collection("assistants"); + const assistantStats = db.collection("assistants.stats"); + const reports = db.collection("reports"); + const sharedConversations = db.collection("sharedConversations"); + const abortedGenerations = db.collection("abortedGenerations"); + const settings = db.collection("settings"); + const users = db.collection("users"); + const sessions = db.collection("sessions"); + const messageEvents = db.collection("messageEvents"); + const bucket = new GridFSBucket(db, { bucketName: "files" }); + const migrationResults = db.collection("migrationResults"); + const semaphores = db.collection("semaphores"); + + return { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + bucket, + migrationResults, + semaphores, + }; + } + + + /** + * Init database once connected: Index creation + * @private + */ + private initDatabase() { + const { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + semaphores, + } = this.getCollections(); + + conversations + .createIndex( + { sessionId: 1, updatedAt: -1 }, + { partialFilterExpression: { sessionId: { $exists: true } } } + ) + .catch(logger.error); + + conversations + .createIndex( + { userId: 1, updatedAt: -1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch(logger.error); + + conversations + .createIndex( + { "message.id": 1, "message.ancestors": 1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch(logger.error); + + // To do stats on conversations + conversations.createIndex({ updatedAt: 1 }).catch(logger.error); + // Not strictly necessary, could use _id, but more convenient. Also for stats + conversations.createIndex({ createdAt: 1 }).catch(logger.error); + // To do stats on conversation messages + conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error); + // Unique index for stats + conversationStats + .createIndex( + { + type: 1, + "date.field": 1, + "date.span": 1, + "date.at": 1, + distinct: 1, + }, + { unique: true } + ) + .catch(logger.error); + // Allow easy check of last computed stat for given type/dateField + conversationStats + .createIndex({ type: 1, "date.field": 1, - "date.span": 1, "date.at": 1, - distinct: 1, - }, - { unique: true } - ) - .catch(logger.error); - // Allow easy check of last computed stat for given type/dateField - conversationStats - .createIndex({ - type: 1, - "date.field": 1, - "date.at": 1, - }) - .catch(logger.error); - abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(logger.error); - abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error); - sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error); - settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error); - settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(logger.error); - settings.createIndex({ assistants: 1 }).catch(logger.error); - users.createIndex({ hfUserId: 1 }, { unique: true }).catch(logger.error); - users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error); - // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users - users.createIndex({ username: 1 }).catch(logger.error); - messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error); - sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(logger.error); - sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(logger.error); - assistants.createIndex({ createdById: 1, userCount: -1 }).catch(logger.error); - assistants.createIndex({ userCount: 1 }).catch(logger.error); - assistants.createIndex({ featured: 1, userCount: -1 }).catch(logger.error); - assistants.createIndex({ modelId: 1, userCount: -1 }).catch(logger.error); - assistants.createIndex({ searchTokens: 1 }).catch(logger.error); - assistants.createIndex({ last24HoursCount: 1 }).catch(logger.error); - assistantStats - // Order of keys is important for the queries - .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true }) - .catch(logger.error); - reports.createIndex({ assistantId: 1 }).catch(logger.error); - reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(logger.error); - - // Unique index for semaphore and migration results - semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error); - semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error); -}); + }) + .catch(logger.error); + abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(logger.error); + abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error); + sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error); + settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error); + settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(logger.error); + settings.createIndex({ assistants: 1 }).catch(logger.error); + users.createIndex({ hfUserId: 1 }, { unique: true }).catch(logger.error); + users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error); + // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users + users.createIndex({ username: 1 }).catch(logger.error); + messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error); + sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(logger.error); + sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(logger.error); + assistants.createIndex({ createdById: 1, userCount: -1 }).catch(logger.error); + assistants.createIndex({ userCount: 1 }).catch(logger.error); + assistants.createIndex({ featured: 1, userCount: -1 }).catch(logger.error); + assistants.createIndex({ modelId: 1, userCount: -1 }).catch(logger.error); + assistants.createIndex({ searchTokens: 1 }).catch(logger.error); + assistants.createIndex({ last24HoursCount: 1 }).catch(logger.error); + assistantStats + // Order of keys is important for the queries + .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true }) + .catch(logger.error); + reports.createIndex({ assistantId: 1 }).catch(logger.error); + reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(logger.error); + + // Unique index for semaphore and migration results + semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error); + semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error); + } + +} diff --git a/src/lib/server/files/downloadFile.ts b/src/lib/server/files/downloadFile.ts index 4d2bddb1c30..667a37a3346 100644 --- a/src/lib/server/files/downloadFile.ts +++ b/src/lib/server/files/downloadFile.ts @@ -1,5 +1,5 @@ import { error } from "@sveltejs/kit"; -import { collections } from "../database"; +import { Database } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import type { SharedConversation } from "$lib/types/SharedConversation"; @@ -7,7 +7,7 @@ export async function downloadFile( sha256: string, convId: Conversation["_id"] | SharedConversation["_id"] ) { - const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); + const fileId = Database.getInstance().getCollections().bucket.find({ filename: `${convId.toString()}-${sha256}` }); let mime = ""; const content = await fileId.next().then(async (file) => { @@ -20,7 +20,7 @@ export async function downloadFile( mime = file.metadata?.mime; - const fileStream = collections.bucket.openDownloadStream(file._id); + const fileStream = Database.getInstance().getCollections().bucket.openDownloadStream(file._id); const fileBuffer = await new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; diff --git a/src/lib/server/files/uploadFile.ts b/src/lib/server/files/uploadFile.ts index 1c4a59b6f44..bbb010b0e75 100644 --- a/src/lib/server/files/uploadFile.ts +++ b/src/lib/server/files/uploadFile.ts @@ -1,11 +1,11 @@ import type { Conversation } from "$lib/types/Conversation"; import { sha256 } from "$lib/utils/sha256"; -import { collections } from "../database"; +import { Database } from "$lib/server/database"; export async function uploadFile(file: Blob, conv: Conversation): Promise { const sha = await sha256(await file.text()); - const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, { + const upload = Database.getInstance().getCollections().bucket.openUploadStream(`${conv._id}-${sha}`, { metadata: { conversation: conv._id.toString(), mime: "image/jpeg" }, }); diff --git a/src/lib/utils/tree/addChildren.spec.ts b/src/lib/utils/tree/addChildren.spec.ts index 3c7861f2cbe..96d357db453 100644 --- a/src/lib/utils/tree/addChildren.spec.ts +++ b/src/lib/utils/tree/addChildren.spec.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -16,7 +16,7 @@ Object.freeze(newMessage); describe("addChildren", async () => { it("should let you append on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const convLength = conv.messages.length; @@ -26,14 +26,14 @@ describe("addChildren", async () => { }); it("should not let you create branches on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow(); }); it("should not let you create a message that already exists", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const messageThatAlreadyExists: Message = { @@ -46,7 +46,7 @@ describe("addChildren", async () => { }); it("should let you create branches on conversations with subtrees", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const nChildren = conv.messages[0].children?.length; @@ -57,7 +57,7 @@ describe("addChildren", async () => { it("should let you create a new leaf", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const parentId = conv.messages[conv.messages.length - 1].id; @@ -84,7 +84,7 @@ describe("addChildren", async () => { it("should throw if you don't specify a parentId in a conversation with messages", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addChildren(conv, newMessage)).toThrow(); @@ -92,7 +92,7 @@ describe("addChildren", async () => { it("should return the id of the new message", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual( diff --git a/src/lib/utils/tree/addSibling.spec.ts b/src/lib/utils/tree/addSibling.spec.ts index ac22836653b..e9558d49607 100644 --- a/src/lib/utils/tree/addSibling.spec.ts +++ b/src/lib/utils/tree/addSibling.spec.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -28,7 +28,7 @@ describe("addSibling", async () => { it("should fail on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow( @@ -38,7 +38,7 @@ describe("addSibling", async () => { it("should fail if the sibling message doesn't exist", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( @@ -49,7 +49,7 @@ describe("addSibling", async () => { // TODO: This behaviour should be fixed, we do not need to fail on the root message. it("should fail if the sibling message is the root message", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); if (!conv.rootMessageId) throw new Error("Root message not found"); @@ -60,7 +60,7 @@ describe("addSibling", async () => { it("should add a sibling to a message", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // add sibling and check children count for parnets diff --git a/src/lib/utils/tree/buildSubtree.spec.ts b/src/lib/utils/tree/buildSubtree.spec.ts index 936fb8a2023..733d8069595 100644 --- a/src/lib/utils/tree/buildSubtree.spec.ts +++ b/src/lib/utils/tree/buildSubtree.spec.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -12,7 +12,7 @@ import { buildSubtree } from "./buildSubtree"; describe("buildSubtree", () => { it("a subtree in a legacy conversation should be just a slice", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // check middle @@ -33,7 +33,7 @@ describe("buildSubtree", () => { it("a subtree in a linear branch conversation should be the ancestors and the message", async () => { const convId = await insertLinearBranchConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // check middle @@ -54,7 +54,7 @@ describe("buildSubtree", () => { it("should throw an error if the message is not found", async () => { const convId = await insertLinearBranchConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const id = "not-a-real-id-test"; @@ -64,7 +64,7 @@ describe("buildSubtree", () => { it("should throw an error if the ancestor is not found", async () => { const convId = await insertLinearBranchConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const id = "1-1-1-1-2"; @@ -87,7 +87,7 @@ describe("buildSubtree", () => { it("should work for conversation with subtrees", async () => { const convId = await insertSideBranchesConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const subtree = buildSubtree(conv, "1-1-1-1-2"); diff --git a/src/lib/utils/tree/convertLegacyConversation.spec.ts b/src/lib/utils/tree/convertLegacyConversation.spec.ts index e8adc55abac..456a42b44cd 100644 --- a/src/lib/utils/tree/convertLegacyConversation.spec.ts +++ b/src/lib/utils/tree/convertLegacyConversation.spec.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -8,7 +8,7 @@ import { insertLegacyConversation } from "./treeHelpers.spec"; describe("convertLegacyConversation", () => { it("should convert a legacy conversation", async () => { const convId = await insertLegacyConversation(); - const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const newConv = convertLegacyConversation(conv); diff --git a/src/lib/utils/tree/treeHelpers.spec.ts b/src/lib/utils/tree/treeHelpers.spec.ts index 75ce04927c4..49e0efbff06 100644 --- a/src/lib/utils/tree/treeHelpers.spec.ts +++ b/src/lib/utils/tree/treeHelpers.spec.ts @@ -1,11 +1,11 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; // function used to insert conversations used for testing export const insertLegacyConversation = async () => { - const res = await collections.conversations.insertOne({ + const res = await Database.getInstance().getCollections().conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -39,7 +39,7 @@ export const insertLegacyConversation = async () => { }; export const insertLinearBranchConversation = async () => { - const res = await collections.conversations.insertOne({ + const res = await Database.getInstance().getCollections().conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -83,7 +83,7 @@ export const insertLinearBranchConversation = async () => { }; export const insertSideBranchesConversation = async () => { - const res = await collections.conversations.insertOne({ + const res = await Database.getInstance().getCollections().conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index c353bc4f47c..79acfbd80bf 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,5 +1,5 @@ import type { LayoutServerLoad } from "./$types"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import { UrlDependency } from "$lib/types/UrlDependency"; import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; @@ -22,7 +22,7 @@ import type { ConvSidebar } from "$lib/types/ConvSidebar"; export const load: LayoutServerLoad = async ({ locals, depends }) => { depends(UrlDependency.ConversationList); - const settings = await collections.settings.findOne(authCondition(locals)); + const settings = await Database.getInstance().getCollections().settings.findOne(authCondition(locals)); // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled. if ( @@ -31,7 +31,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) ) { settings.activeModel = defaultModel.id; - await collections.settings.updateOne(authCondition(locals), { + await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { $set: { activeModel: defaultModel.id }, }); } @@ -42,7 +42,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { models.find((m) => m.id === settings?.activeModel)?.unlisted === true ) { settings.activeModel = defaultModel.id; - await collections.settings.updateOne(authCondition(locals), { + await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { $set: { activeModel: defaultModel.id }, }); } @@ -54,14 +54,14 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { const assistant = assistantActive ? JSON.parse( JSON.stringify( - await collections.assistants.findOne({ + await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(settings?.activeModel), }) ) ) : null; - const conversations = await collections.conversations + const conversations = await Database.getInstance().getCollections().conversations .find(authCondition(locals)) .sort({ updatedAt: -1 }) .project< @@ -85,7 +85,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), ]; - const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); + const assistants = await Database.getInstance().getCollections().assistants.find({ _id: { $in: assistantIds } }).toArray(); const messagesBeforeLogin = MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0; @@ -98,7 +98,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { // get the number of messages where `from === "assistant"` across all conversations. const totalMessages = ( - await collections.conversations + await Database.getInstance().getCollections().conversations .aggregate([ { $match: { ...authCondition(locals), "messages.from": "assistant" } }, { $project: { messages: 1 } }, diff --git a/src/routes/admin/export/+server.ts b/src/routes/admin/export/+server.ts index 5c9e791e68d..e7c298892bb 100644 --- a/src/routes/admin/export/+server.ts +++ b/src/routes/admin/export/+server.ts @@ -1,5 +1,5 @@ import { PARQUET_EXPORT_DATASET, PARQUET_EXPORT_HF_TOKEN } from "$env/static/private"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import type { Message } from "$lib/types/Message"; import { error } from "@sveltejs/kit"; import { pathToFileURL } from "node:url"; @@ -44,7 +44,7 @@ export async function POST({ request }) { let count = 0; logger.info("Exporting conversations for model", model); - for await (const conversation of collections.settings.aggregate<{ + for await (const conversation of Database.getInstance().getCollections().settings.aggregate<{ title: string; created_at: Date; updated_at: Date; @@ -95,7 +95,7 @@ export async function POST({ request }) { logger.info("exporting convos with userId"); - for await (const conversation of collections.settings.aggregate<{ + for await (const conversation of Database.getInstance().getCollections().settings.aggregate<{ title: string; created_at: Date; updated_at: Date; diff --git a/src/routes/admin/stats/compute/+server.ts b/src/routes/admin/stats/compute/+server.ts index f814976f505..f892c5b4eaa 100644 --- a/src/routes/admin/stats/compute/+server.ts +++ b/src/routes/admin/stats/compute/+server.ts @@ -1,6 +1,6 @@ import { json } from "@sveltejs/kit"; import type { ConversationStats } from "$lib/types/ConversationStats"; -import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database.js"; +import { CONVERSATION_STATS_COLLECTION, Database } from "$lib/server/database"; import { logger } from "$lib/server/logger"; // Triger like this: @@ -21,7 +21,7 @@ async function computeStats(params: { span: ConversationStats["date"]["span"]; type: ConversationStats["type"]; }) { - const lastComputed = await collections.conversationStats.findOne( + const lastComputed = await Database.getInstance().getCollections().conversationStats.findOne( { "date.field": params.dateField, "date.span": params.span, type: params.type }, { sort: { "date.at": -1 } } ); @@ -212,7 +212,7 @@ async function computeStats(params: { }, ]; - await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next(); + await Database.getInstance().getCollections().conversations.aggregate(pipeline, { allowDiskUse: true }).next(); logger.info("Computed stats for", params.type, params.span, params.dateField); } diff --git a/src/routes/api/assistant/[id]/+server.ts b/src/routes/api/assistant/[id]/+server.ts index 74f7fe6c7e0..997dd7985ea 100644 --- a/src/routes/api/assistant/[id]/+server.ts +++ b/src/routes/api/assistant/[id]/+server.ts @@ -1,11 +1,11 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; export async function GET({ params }) { const id = params.id; const assistantId = new ObjectId(id); - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: assistantId, }); diff --git a/src/routes/api/assistants/+server.ts b/src/routes/api/assistants/+server.ts index 8b99a680bdf..16029fa81db 100644 --- a/src/routes/api/assistants/+server.ts +++ b/src/routes/api/assistants/+server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database.js"; +import { Database } from "$lib/server/database"; import type { Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; import { generateQueryTokens } from "$lib/utils/searchTokens.js"; @@ -16,7 +16,7 @@ export async function GET({ url, locals }) { let user: Pick | null = null; if (username) { - user = await collections.users.findOne>( + user = await Database.getInstance().getCollections().users.findOne>( { username }, { projection: { _id: 1 } } ); @@ -43,14 +43,14 @@ export async function GET({ url, locals }) { ...shouldBeFeatured, ...shouldHaveBeenShared, }; - const assistants = await collections.assistants + const assistants = await Database.getInstance().getCollections().assistants .find(filter) .skip(NUM_PER_PAGE * pageIndex) .sort({ userCount: -1 }) .limit(NUM_PER_PAGE) .toArray(); - const numTotalItems = await collections.assistants.countDocuments(filter); + const numTotalItems = await Database.getInstance().getCollections().assistants.countDocuments(filter); return Response.json({ assistants, diff --git a/src/routes/api/conversation/[id]/+server.ts b/src/routes/api/conversation/[id]/+server.ts index b2b7f1b8a86..b9aee179a51 100644 --- a/src/routes/api/conversation/[id]/+server.ts +++ b/src/routes/api/conversation/[id]/+server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { authCondition } from "$lib/server/auth"; import { z } from "zod"; import { ObjectId } from "mongodb"; @@ -8,7 +8,7 @@ export async function GET({ locals, params }) { const convId = new ObjectId(id); if (locals.user?._id || locals.sessionId) { - const conv = await collections.conversations.findOne({ + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); diff --git a/src/routes/api/conversations/+server.ts b/src/routes/api/conversations/+server.ts index 8b282a1b276..957c2c2a452 100644 --- a/src/routes/api/conversations/+server.ts +++ b/src/routes/api/conversations/+server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { authCondition } from "$lib/server/auth"; import type { Conversation } from "$lib/types/Conversation"; @@ -8,7 +8,7 @@ export async function GET({ locals, url }) { const p = parseInt(url.searchParams.get("p") ?? "0"); if (locals.user?._id || locals.sessionId) { - const convs = await collections.conversations + const convs = await Database.getInstance().getCollections().conversations .find({ ...authCondition(locals), }) diff --git a/src/routes/api/user/assistants/+server.ts b/src/routes/api/user/assistants/+server.ts index 8e8b66bf781..ab89c0ded02 100644 --- a/src/routes/api/user/assistants/+server.ts +++ b/src/routes/api/user/assistants/+server.ts @@ -1,13 +1,13 @@ import { authCondition } from "$lib/server/auth"; import type { Conversation } from "$lib/types/Conversation"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; export async function GET({ locals }) { if (locals.user?._id || locals.sessionId) { - const settings = await collections.settings.findOne(authCondition(locals)); + const settings = await Database.getInstance().getCollections().settings.findOne(authCondition(locals)); - const conversations = await collections.conversations + const conversations = await Database.getInstance().getCollections().conversations .find(authCondition(locals)) .sort({ updatedAt: -1 }) .project>({ @@ -24,7 +24,7 @@ export async function GET({ locals }) { ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), ]; - const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); + const assistants = await Database.getInstance().getCollections().assistants.find({ _id: { $in: assistantIds } }).toArray(); const res = assistants .filter((el) => userAssistantsSet.has(el._id.toString())) diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts index ac14877dbc4..ca0c7d52df0 100644 --- a/src/routes/assistant/[assistantId]/+page.server.ts +++ b/src/routes/assistant/[assistantId]/+page.server.ts @@ -1,11 +1,11 @@ import { base } from "$app/paths"; -import { collections } from "$lib/server/database.js"; +import { Database } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; export const load = async ({ params }) => { try { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts index 48633e4b524..61cfb7bd5c3 100644 --- a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts +++ b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts @@ -1,5 +1,5 @@ import ChatThumbnail from "./ChatThumbnail.svelte"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { error, type RequestHandler } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import type { SvelteComponent } from "svelte"; @@ -13,7 +13,7 @@ import InterBold from "../../../../../static/fonts/Inter-Bold.ttf"; import sharp from "sharp"; export const GET: RequestHandler = (async ({ params }) => { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -22,11 +22,11 @@ export const GET: RequestHandler = (async ({ params }) => { } let avatar = ""; - const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + const fileId = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); const file = await fileId.next(); if (file) { avatar = await (async () => { - const fileStream = collections.bucket.openDownloadStream(file?._id); + const fileStream = Database.getInstance().getCollections().bucket.openDownloadStream(file?._id); const fileBuffer = await new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts index 5be72ff5412..27062001fce 100644 --- a/src/routes/assistants/+page.server.ts +++ b/src/routes/assistants/+page.server.ts @@ -1,6 +1,6 @@ import { base } from "$app/paths"; import { ENABLE_ASSISTANTS, REQUIRE_FEATURED_ASSISTANTS } from "$env/static/private"; -import { collections } from "$lib/server/database.js"; +import { Database } from "$lib/server/database"; import { SortKey, type Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; import { generateQueryTokens } from "$lib/utils/searchTokens.js"; @@ -23,7 +23,7 @@ export const load = async ({ url, locals }) => { let user: Pick | null = null; if (username) { - user = await collections.users.findOne>( + user = await Database.getInstance().getCollections().users.findOne>( { username }, { projection: { _id: 1 } } ); @@ -50,7 +50,7 @@ export const load = async ({ url, locals }) => { ...shouldBeFeatured, ...shouldHaveBeenShared, }; - const assistants = await collections.assistants + const assistants = await Database.getInstance().getCollections().assistants .find(filter) .skip(NUM_PER_PAGE * pageIndex) .sort({ @@ -60,7 +60,7 @@ export const load = async ({ url, locals }) => { .limit(NUM_PER_PAGE) .toArray(); - const numTotalItems = await collections.assistants.countDocuments(filter); + const numTotalItems = await Database.getInstance().getCollections().assistants.countDocuments(filter); return { assistants: JSON.parse(JSON.stringify(assistants)) as Array, diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts index ed48cfaa093..a07151afb3e 100644 --- a/src/routes/conversation/+server.ts +++ b/src/routes/conversation/+server.ts @@ -1,5 +1,5 @@ import type { RequestHandler } from "./$types"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { error, redirect } from "@sveltejs/kit"; import { base } from "$app/paths"; @@ -30,7 +30,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { } const values = parsedBody.data; - const convCount = await collections.conversations.countDocuments(authCondition(locals)); + const convCount = await Database.getInstance().getCollections().conversations.countDocuments(authCondition(locals)); if (usageLimits?.conversations && convCount > usageLimits?.conversations) { throw error( @@ -61,7 +61,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { let embeddingModel: string; if (values.fromShare) { - const conversation = await collections.sharedConversations.findOne({ + const conversation = await Database.getInstance().getCollections().sharedConversations.findOne({ _id: values.fromShare, }); @@ -85,7 +85,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { } // get preprompt from assistant if it exists - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(values.assistantId), }); @@ -99,7 +99,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { messages[0].content = values.preprompt; } - const res = await collections.conversations.insertOne({ + const res = await Database.getInstance().getCollections().conversations.insertOne({ _id: new ObjectId(), title: title || "New Chat", rootMessageId, diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts index e43d52e1b89..771a0a87cc6 100644 --- a/src/routes/conversation/[id]/+page.server.ts +++ b/src/routes/conversation/[id]/+page.server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { error } from "@sveltejs/kit"; import { authCondition } from "$lib/server/auth"; @@ -12,7 +12,7 @@ export const load = async ({ params, depends, locals }) => { // if the conver if (params.id.length === 7) { // shared link of length 7 - conversation = await collections.sharedConversations.findOne({ + conversation = await Database.getInstance().getCollections().sharedConversations.findOne({ _id: params.id, }); shared = true; @@ -22,7 +22,7 @@ export const load = async ({ params, depends, locals }) => { } } else { // todo: add validation on params.id - conversation = await collections.conversations.findOne({ + conversation = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(params.id), ...authCondition(locals), }); @@ -31,7 +31,7 @@ export const load = async ({ params, depends, locals }) => { if (!conversation) { const conversationExists = - (await collections.conversations.countDocuments({ + (await Database.getInstance().getCollections().conversations.countDocuments({ _id: new ObjectId(params.id), })) !== 0; @@ -57,7 +57,7 @@ export const load = async ({ params, depends, locals }) => { assistant: convertedConv.assistantId ? JSON.parse( JSON.stringify( - await collections.assistants.findOne({ + await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(convertedConv.assistantId), }) ) diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts index a057e6e3a43..27bfb1bd7f8 100644 --- a/src/routes/conversation/[id]/+server.ts +++ b/src/routes/conversation/[id]/+server.ts @@ -1,7 +1,7 @@ import { MESSAGES_BEFORE_LOGIN, ENABLE_ASSISTANTS_RAG } from "$env/static/private"; import { startOfHour } from "date-fns"; import { authCondition, requiresUser } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { models } from "$lib/server/models"; import { ERROR_MESSAGES } from "$lib/stores/errors"; import type { Message } from "$lib/types/Message"; @@ -10,7 +10,7 @@ import { ObjectId } from "mongodb"; import { z } from "zod"; import type { MessageUpdate } from "$lib/types/MessageUpdate"; import { runWebSearch } from "$lib/server/websearch/runWebSearch"; -import { abortedGenerations } from "$lib/server/abortedGenerations"; +import { AbortedGenerations } from "$lib/server/abortedGenerations"; import { summarize } from "$lib/server/summarize"; import { uploadFile } from "$lib/server/files/uploadFile"; import sizeof from "image-size"; @@ -38,13 +38,13 @@ export async function POST({ request, locals, params, getClientAddress }) { } // check if the user has access to the conversation - const convBeforeCheck = await collections.conversations.findOne({ + const convBeforeCheck = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); if (convBeforeCheck && !convBeforeCheck.rootMessageId) { - const res = await collections.conversations.updateOne( + const res = await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -61,7 +61,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } } - const conv = await collections.conversations.findOne({ + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); @@ -71,7 +71,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } // register the event for ratelimiting - await collections.messageEvents.insertOne({ + await Database.getInstance().getCollections().messageEvents.insertOne({ userId, createdAt: new Date(), ip: getClientAddress(), @@ -83,7 +83,7 @@ export async function POST({ request, locals, params, getClientAddress }) { if (!locals.user?._id && requiresUser && messagesBeforeLogin) { const totalMessages = ( - await collections.conversations + await Database.getInstance().getCollections().conversations .aggregate([ { $match: { ...authCondition(locals), "messages.from": "assistant" } }, { $project: { messages: 1 } }, @@ -103,8 +103,8 @@ export async function POST({ request, locals, params, getClientAddress }) { if (usageLimits?.messagesPerMinute) { // check if the user is rate limited const nEvents = Math.max( - await collections.messageEvents.countDocuments({ userId }), - await collections.messageEvents.countDocuments({ ip: getClientAddress() }) + await Database.getInstance().getCollections().messageEvents.countDocuments({ userId }), + await Database.getInstance().getCollections().messageEvents.countDocuments({ ip: getClientAddress() }) ); if (nEvents > usageLimits.messagesPerMinute) { throw error(429, ERROR_MESSAGES.rateLimited); @@ -281,7 +281,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } // update the conversation with the new messages - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -323,7 +323,7 @@ export async function POST({ request, locals, params, getClientAddress }) { try { conv.title = (await summarize(conv.messages[1].content)) ?? conv.title; update({ type: "status", status: "title", message: conv.title }); - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -340,7 +340,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } })(); - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -353,7 +353,7 @@ export async function POST({ request, locals, params, getClientAddress }) { ); // check if assistant has a rag - const assistant = await collections.assistants.findOne< + const assistant = await Database.getInstance().getCollections().assistants.findOne< Pick >( { _id: conv.assistantId }, @@ -451,7 +451,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } // abort check - const date = abortedGenerations.get(convId.toString()); + const date = AbortedGenerations.getInstance().getList().get(convId.toString()); if (date && date > promptedAt) { break; } @@ -500,7 +500,7 @@ export async function POST({ request, locals, params, getClientAddress }) { } } - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -527,7 +527,7 @@ export async function POST({ request, locals, params, getClientAddress }) { }, async cancel() { if (!doneStreaming) { - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, @@ -544,7 +544,7 @@ export async function POST({ request, locals, params, getClientAddress }) { }); if (conv.assistantId) { - await collections.assistantStats.updateOne( + await Database.getInstance().getCollections().assistantStats.updateOne( { assistantId: conv.assistantId, "date.at": startOfHour(new Date()), "date.span": "hour" }, { $inc: { count: 1 } }, { upsert: true } @@ -562,7 +562,7 @@ export async function POST({ request, locals, params, getClientAddress }) { export async function DELETE({ locals, params }) { const convId = new ObjectId(params.id); - const conv = await collections.conversations.findOne({ + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); @@ -571,7 +571,7 @@ export async function DELETE({ locals, params }) { throw error(404, "Conversation not found"); } - await collections.conversations.deleteOne({ _id: conv._id }); + await Database.getInstance().getCollections().conversations.deleteOne({ _id: conv._id }); return new Response(); } @@ -583,7 +583,7 @@ export async function PATCH({ request, locals, params }) { const convId = new ObjectId(params.id); - const conv = await collections.conversations.findOne({ + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); @@ -592,7 +592,7 @@ export async function PATCH({ request, locals, params }) { throw error(404, "Conversation not found"); } - await collections.conversations.updateOne( + await Database.getInstance().getCollections().conversations.updateOne( { _id: convId, }, diff --git a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts index 273df6c97b8..543644c225e 100644 --- a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +++ b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts @@ -1,6 +1,6 @@ import { buildPrompt } from "$lib/buildPrompt"; import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { models } from "$lib/server/models"; import { buildSubtree } from "$lib/utils/tree/buildSubtree"; import { isMessageId } from "$lib/utils/tree/isMessageId"; @@ -10,10 +10,10 @@ import { ObjectId } from "mongodb"; export async function GET({ params, locals }) { const conv = params.id.length === 7 - ? await collections.sharedConversations.findOne({ + ? await Database.getInstance().getCollections().sharedConversations.findOne({ _id: params.id, }) - : await collections.conversations.findOne({ + : await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(params.id), ...authCondition(locals), }); diff --git a/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts index 8702a1346ae..0bbb45ae700 100644 --- a/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts +++ b/src/routes/conversation/[id]/message/[messageId]/vote/+server.ts @@ -1,5 +1,5 @@ import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { z } from "zod"; @@ -13,7 +13,7 @@ export async function POST({ params, request, locals }) { const conversationId = new ObjectId(params.id); const messageId = params.messageId; - const document = await collections.conversations.updateOne( + const document = await Database.getInstance().getCollections().conversations.updateOne( { _id: conversationId, ...authCondition(locals), diff --git a/src/routes/conversation/[id]/output/[sha256]/+server.ts b/src/routes/conversation/[id]/output/[sha256]/+server.ts index 79ae37b7585..ef15a75bdc7 100644 --- a/src/routes/conversation/[id]/output/[sha256]/+server.ts +++ b/src/routes/conversation/[id]/output/[sha256]/+server.ts @@ -1,5 +1,5 @@ import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { z } from "zod"; @@ -20,7 +20,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { const convId = new ObjectId(z.string().parse(params.id)); // check if the user has access to the conversation - const conv = await collections.conversations.findOne({ + const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: convId, ...authCondition(locals), }); @@ -30,7 +30,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { } } else { // check if the user has access to the conversation - const conv = await collections.sharedConversations.findOne({ + const conv = await Database.getInstance().getCollections().sharedConversations.findOne({ _id: params.id, }); diff --git a/src/routes/conversation/[id]/share/+server.ts b/src/routes/conversation/[id]/share/+server.ts index a63852f1b65..9eb16c6216a 100644 --- a/src/routes/conversation/[id]/share/+server.ts +++ b/src/routes/conversation/[id]/share/+server.ts @@ -1,5 +1,5 @@ import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import type { SharedConversation } from "$lib/types/SharedConversation"; import { getShareUrl } from "$lib/utils/getShareUrl"; import { hashConv } from "$lib/utils/hashConv"; @@ -8,7 +8,7 @@ import { ObjectId } from "mongodb"; import { nanoid } from "nanoid"; export async function POST({ params, url, locals }) { - const conversation = await collections.conversations.findOne({ + const conversation = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(params.id), ...authCondition(locals), }); @@ -19,7 +19,7 @@ export async function POST({ params, url, locals }) { const hash = await hashConv(conversation); - const existingShare = await collections.sharedConversations.findOne({ hash }); + const existingShare = await Database.getInstance().getCollections().sharedConversations.findOne({ hash }); if (existingShare) { return new Response( @@ -44,10 +44,10 @@ export async function POST({ params, url, locals }) { assistantId: conversation.assistantId, }; - await collections.sharedConversations.insertOne(shared); + await Database.getInstance().getCollections().sharedConversations.insertOne(shared); // copy files from `${conversation._id}-` to `${shared._id}-` - const files = await collections.bucket + const files = await Database.getInstance().getCollections().bucket .find({ filename: { $regex: `${conversation._id}-` } }) .toArray(); @@ -55,8 +55,8 @@ export async function POST({ params, url, locals }) { files.map(async (file) => { const newFilename = file.filename.replace(`${conversation._id}-`, `${shared._id}-`); // copy files from `${conversation._id}-` to `${shared._id}-` by downloading and reuploaidng - const downloadStream = collections.bucket.openDownloadStream(file._id); - const uploadStream = collections.bucket.openUploadStream(newFilename, { + const downloadStream = Database.getInstance().getCollections().bucket.openDownloadStream(file._id); + const uploadStream = Database.getInstance().getCollections().bucket.openUploadStream(newFilename, { metadata: { ...file.metadata, conversation: shared._id.toString() }, }); downloadStream.pipe(uploadStream); diff --git a/src/routes/conversation/[id]/stop-generating/+server.ts b/src/routes/conversation/[id]/stop-generating/+server.ts index d6405434966..81e1da83a2a 100644 --- a/src/routes/conversation/[id]/stop-generating/+server.ts +++ b/src/routes/conversation/[id]/stop-generating/+server.ts @@ -1,5 +1,5 @@ import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; @@ -9,7 +9,7 @@ import { ObjectId } from "mongodb"; export async function POST({ params, locals }) { const conversationId = new ObjectId(params.id); - const conversation = await collections.conversations.findOne({ + const conversation = await Database.getInstance().getCollections().conversations.findOne({ _id: conversationId, ...authCondition(locals), }); @@ -18,7 +18,7 @@ export async function POST({ params, locals }) { throw error(404, "Conversation not found"); } - await collections.abortedGenerations.updateOne( + await Database.getInstance().getCollections().abortedGenerations.updateOne( { conversationId }, { $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, { upsert: true } diff --git a/src/routes/conversations/+page.server.ts b/src/routes/conversations/+page.server.ts index 31865de595c..47ddb083b90 100644 --- a/src/routes/conversations/+page.server.ts +++ b/src/routes/conversations/+page.server.ts @@ -1,13 +1,13 @@ import { base } from "$app/paths"; import { authCondition } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; export const actions = { async delete({ locals }) { // double check we have a user to delete conversations for if (locals.user?._id || locals.sessionId) { - await collections.conversations.deleteMany({ + await Database.getInstance().getCollections().conversations.deleteMany({ ...authCondition(locals), }); } diff --git a/src/routes/login/callback/updateUser.spec.ts b/src/routes/login/callback/updateUser.spec.ts index fefaf8b0f5a..b04383ad730 100644 --- a/src/routes/login/callback/updateUser.spec.ts +++ b/src/routes/login/callback/updateUser.spec.ts @@ -1,6 +1,6 @@ import { assert, it, describe, afterEach, vi, expect } from "vitest"; import type { Cookies } from "@sveltejs/kit"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { updateUser } from "./updateUser"; import { ObjectId } from "mongodb"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; @@ -27,7 +27,7 @@ const cookiesMock: Cookies = { }; const insertRandomUser = async () => { - const res = await collections.users.insertOne({ + const res = await Database.getInstance().getCollections().users.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -41,7 +41,7 @@ const insertRandomUser = async () => { }; const insertRandomConversations = async (count: number) => { - const res = await collections.conversations.insertMany( + const res = await Database.getInstance().getCollections().conversations.insertMany( new Array(count).fill(0).map(() => ({ _id: new ObjectId(), title: "random title", @@ -63,7 +63,7 @@ describe("login", () => { await updateUser({ userData, locals, cookies: cookiesMock }); - const existingUser = await collections.users.findOne({ hfUserId: userData.sub }); + const existingUser = await Database.getInstance().getCollections().users.findOne({ hfUserId: userData.sub }); assert.equal(existingUser?.name, userData.name); @@ -77,14 +77,14 @@ describe("login", () => { await updateUser({ userData, locals, cookies: cookiesMock }); - const conversationCount = await collections.conversations.countDocuments({ + const conversationCount = await Database.getInstance().getCollections().conversations.countDocuments({ userId: insertedId, sessionId: { $exists: false }, }); assert.equal(conversationCount, 2); - await collections.conversations.deleteMany({ userId: insertedId }); + await Database.getInstance().getCollections().conversations.deleteMany({ userId: insertedId }); }); it("should create default settings for new user", async () => { @@ -94,7 +94,7 @@ describe("login", () => { assert.exists(user); - const settings = await collections.settings.findOne({ userId: user?._id }); + const settings = await Database.getInstance().getCollections().settings.findOne({ userId: user?._id }); expect(settings).toMatchObject({ userId: user?._id, @@ -104,11 +104,11 @@ describe("login", () => { ...DEFAULT_SETTINGS, }); - await collections.settings.deleteOne({ userId: user?._id }); + await Database.getInstance().getCollections().settings.deleteOne({ userId: user?._id }); }); it("should migrate pre-existing settings for pre-existing user", async () => { - const { insertedId } = await collections.settings.insertOne({ + const { insertedId } = await Database.getInstance().getCollections().settings.insertOne({ sessionId: locals.sessionId, ethicsModalAcceptedAt: new Date(), updatedAt: new Date(), @@ -119,14 +119,14 @@ describe("login", () => { await updateUser({ userData, locals, cookies: cookiesMock }); - const settings = await collections.settings.findOne({ + const settings = await Database.getInstance().getCollections().settings.findOne({ _id: insertedId, sessionId: { $exists: false }, }); assert.exists(settings); - const user = await collections.users.findOne({ hfUserId: userData.sub }); + const user = await Database.getInstance().getCollections().users.findOne({ hfUserId: userData.sub }); expect(settings).toMatchObject({ userId: user?._id, @@ -137,13 +137,13 @@ describe("login", () => { shareConversationsWithModelAuthors: false, }); - await collections.settings.deleteOne({ userId: user?._id }); + await Database.getInstance().getCollections().settings.deleteOne({ userId: user?._id }); }); }); afterEach(async () => { - await collections.users.deleteMany({ hfUserId: userData.sub }); - await collections.sessions.deleteMany({}); + await Database.getInstance().getCollections().users.deleteMany({ hfUserId: userData.sub }); + await Database.getInstance().getCollections().sessions.deleteMany({}); locals.userId = "1234567890"; locals.sessionId = "1234567890"; diff --git a/src/routes/login/callback/updateUser.ts b/src/routes/login/callback/updateUser.ts index f6fa70bb2ee..02ecc5258f0 100644 --- a/src/routes/login/callback/updateUser.ts +++ b/src/routes/login/callback/updateUser.ts @@ -1,5 +1,5 @@ import { refreshSessionCookie } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; import { z } from "zod"; @@ -59,7 +59,7 @@ export async function updateUser(params: { // This approach allows us to adapt to different OIDC providers flexibly. // check if user already exists - const existingUser = await collections.users.findOne({ hfUserId }); + const existingUser = await Database.getInstance().getCollections().users.findOne({ hfUserId }); let userId = existingUser?._id; // update session cookie on login @@ -67,7 +67,7 @@ export async function updateUser(params: { const secretSessionId = crypto.randomUUID(); const sessionId = await sha256(secretSessionId); - if (await collections.sessions.findOne({ sessionId })) { + if (await Database.getInstance().getCollections().sessions.findOne({ sessionId })) { throw error(500, "Session ID collision"); } @@ -75,14 +75,14 @@ export async function updateUser(params: { if (existingUser) { // update existing user if any - await collections.users.updateOne( + await Database.getInstance().getCollections().users.updateOne( { _id: existingUser._id }, { $set: { username, name, avatarUrl } } ); // remove previous session if it exists and add new one - await collections.sessions.deleteOne({ sessionId: previousSessionId }); - await collections.sessions.insertOne({ + await Database.getInstance().getCollections().sessions.deleteOne({ sessionId: previousSessionId }); + await Database.getInstance().getCollections().sessions.insertOne({ _id: new ObjectId(), sessionId: locals.sessionId, userId: existingUser._id, @@ -94,7 +94,7 @@ export async function updateUser(params: { }); } else { // user doesn't exist yet, create a new one - const { insertedId } = await collections.users.insertOne({ + const { insertedId } = await Database.getInstance().getCollections().users.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -107,7 +107,7 @@ export async function updateUser(params: { userId = insertedId; - await collections.sessions.insertOne({ + await Database.getInstance().getCollections().sessions.insertOne({ _id: new ObjectId(), sessionId: locals.sessionId, userId, @@ -119,7 +119,7 @@ export async function updateUser(params: { }); // move pre-existing settings to new user - const { matchedCount } = await collections.settings.updateOne( + const { matchedCount } = await Database.getInstance().getCollections().settings.updateOne( { sessionId: previousSessionId }, { $set: { userId, updatedAt: new Date() }, @@ -129,7 +129,7 @@ export async function updateUser(params: { if (!matchedCount) { // if no settings found for user, create default settings - await collections.settings.insertOne({ + await Database.getInstance().getCollections().settings.insertOne({ userId, ethicsModalAcceptedAt: new Date(), updatedAt: new Date(), @@ -143,7 +143,7 @@ export async function updateUser(params: { refreshSessionCookie(cookies, secretSessionId); // migrate pre-existing conversations - await collections.conversations.updateMany( + await Database.getInstance().getCollections().conversations.updateMany( { sessionId: previousSessionId }, { $set: { userId }, diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts index dd286eaa51b..a23d388d80b 100644 --- a/src/routes/logout/+page.server.ts +++ b/src/routes/logout/+page.server.ts @@ -1,12 +1,12 @@ import { dev } from "$app/environment"; import { base } from "$app/paths"; import { COOKIE_NAME, ALLOW_INSECURE_COOKIES } from "$env/static/private"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; export const actions = { async default({ cookies, locals }) { - await collections.sessions.deleteOne({ sessionId: locals.sessionId }); + await Database.getInstance().getCollections().sessions.deleteOne({ sessionId: locals.sessionId }); cookies.delete(COOKIE_NAME, { path: "/", diff --git a/src/routes/models/[...model]/+page.server.ts b/src/routes/models/[...model]/+page.server.ts index f7919352f5a..40a8ed7df72 100644 --- a/src/routes/models/[...model]/+page.server.ts +++ b/src/routes/models/[...model]/+page.server.ts @@ -1,6 +1,6 @@ import { base } from "$app/paths"; import { authCondition } from "$lib/server/auth.js"; -import { collections } from "$lib/server/database.js"; +import { Database } from "$lib/server/database"; import { models } from "$lib/server/models"; import { redirect } from "@sveltejs/kit"; @@ -13,7 +13,7 @@ export async function load({ params, locals, parent }) { } if (locals.user?._id ?? locals.sessionId) { - await collections.settings.updateOne( + await Database.getInstance().getCollections().settings.updateOne( authCondition(locals), { $set: { diff --git a/src/routes/settings/(nav)/+server.ts b/src/routes/settings/(nav)/+server.ts index 81289bacba2..e2c56ac096e 100644 --- a/src/routes/settings/(nav)/+server.ts +++ b/src/routes/settings/(nav)/+server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { z } from "zod"; import { authCondition } from "$lib/server/auth"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; @@ -18,7 +18,7 @@ export async function POST({ request, locals }) { }) .parse(body); - await collections.settings.updateOne( + await Database.getInstance().getCollections().settings.updateOne( authCondition(locals), { $set: { diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts b/src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts index 26f09d176f7..57e590b0cb6 100644 --- a/src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts +++ b/src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { type Actions, fail, redirect } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { authCondition } from "$lib/server/auth"; @@ -9,7 +9,7 @@ import { z } from "zod"; import type { Assistant } from "$lib/types/Assistant"; import { logger } from "$lib/server/logger"; async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) { - const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) }); + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(assistantId) }); if (!assistant) { throw Error("Assistant not found"); @@ -31,10 +31,10 @@ export const actions: Actions = { return fail(400, { error: true, message: (e as Error).message }); } - await collections.assistants.deleteOne({ _id: assistant._id }); + await Database.getInstance().getCollections().assistants.deleteOne({ _id: assistant._id }); // and remove it from all users settings - await collections.settings.updateMany( + await Database.getInstance().getCollections().settings.updateMany( { assistants: { $in: [assistant._id] }, }, @@ -44,12 +44,12 @@ export const actions: Actions = { ); // and delete all avatars - const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + const fileCursor = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); // Step 2: Delete the existing file if it exists let fileId = await fileCursor.next(); while (fileId) { - await collections.bucket.delete(fileId._id); + await Database.getInstance().getCollections().bucket.delete(fileId._id); fileId = await fileCursor.next(); } @@ -57,7 +57,7 @@ export const actions: Actions = { }, report: async ({ request, params, locals, url }) => { // is there already a report from this user for this model ? - const report = await collections.reports.findOne({ + const report = await Database.getInstance().getCollections().reports.findOne({ createdBy: locals.user?._id ?? locals.sessionId, assistantId: new ObjectId(params.assistantId), }); @@ -73,7 +73,7 @@ export const actions: Actions = { return fail(400, { error: true, message: "Invalid report reason" }); } - const { acknowledged } = await collections.reports.insertOne({ + const { acknowledged } = await Database.getInstance().getCollections().reports.insertOne({ _id: new ObjectId(), assistantId: new ObjectId(params.assistantId), createdBy: locals.user?._id ?? locals.sessionId, @@ -90,7 +90,7 @@ export const actions: Actions = { const prefixUrl = PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || url.origin}${base}`; const assistantUrl = `${prefixUrl}/assistant/${params.assistantId}`; - const assistant = await collections.assistants.findOne>( + const assistant = await Database.getInstance().getCollections().assistants.findOne>( { _id: new ObjectId(params.assistantId) }, { projection: { name: 1 } } ); @@ -118,7 +118,7 @@ export const actions: Actions = { }, subscribe: async ({ params, locals }) => { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -127,26 +127,26 @@ export const actions: Actions = { } // don't push if it's already there - const settings = await collections.settings.findOne(authCondition(locals)); + const settings = await Database.getInstance().getCollections().settings.findOne(authCondition(locals)); if (settings?.assistants?.includes(assistant._id)) { return fail(400, { error: true, message: "Already subscribed" }); } - const result = await collections.settings.updateOne(authCondition(locals), { + const result = await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { $addToSet: { assistants: assistant._id }, }); // reduce count only if push succeeded if (result.modifiedCount > 0) { - await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: 1 } }); + await Database.getInstance().getCollections().assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: 1 } }); } return { from: "subscribe", ok: true, message: "Assistant added" }; }, unsubscribe: async ({ params, locals }) => { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -154,13 +154,13 @@ export const actions: Actions = { return fail(404, { error: true, message: "Assistant not found" }); } - const result = await collections.settings.updateOne(authCondition(locals), { + const result = await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { $pull: { assistants: assistant._id }, }); // reduce count only if pull succeeded if (result.modifiedCount > 0) { - await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: -1 } }); + await Database.getInstance().getCollections().assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: -1 } }); } throw redirect(302, `${base}/settings`); diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts b/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts index 23f2025610a..4c08621b841 100644 --- a/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts +++ b/src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts @@ -1,9 +1,9 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { error, type RequestHandler } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; export const GET: RequestHandler = async ({ params }) => { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -15,14 +15,14 @@ export const GET: RequestHandler = async ({ params }) => { throw error(404, "No avatar found"); } - const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + const fileId = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); const content = await fileId.next().then(async (file) => { if (!file?._id) { throw error(404, "Avatar not found"); } - const fileStream = collections.bucket.openDownloadStream(file?._id); + const fileStream = Database.getInstance().getCollections().bucket.openDownloadStream(file?._id); const fileBuffer = await new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts index 78c74af50c3..9f099136b85 100644 --- a/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts +++ b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts @@ -1,6 +1,6 @@ import { base } from "$app/paths"; import { requiresUser } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { fail, type Actions, redirect } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; @@ -43,7 +43,7 @@ const newAsssistantSchema = z.object({ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise => { const hash = await sha256(await avatar.text()); - const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, { + const upload = Database.getInstance().getCollections().bucket.openUploadStream(`${assistantId.toString()}`, { metadata: { type: avatar.type, hash }, }); @@ -60,7 +60,7 @@ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise { - const assistant = await collections.assistants.findOne({ + const assistant = await Database.getInstance().getCollections().assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -116,28 +116,28 @@ export const actions: Actions = { return fail(400, { error: true, errors }); } - const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + const fileCursor = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); // Step 2: Delete the existing file if it exists let fileId = await fileCursor.next(); while (fileId) { - await collections.bucket.delete(fileId._id); + await Database.getInstance().getCollections().bucket.delete(fileId._id); fileId = await fileCursor.next(); } hash = await uploadAvatar(new File([image], "avatar.jpg"), assistant._id); } else if (deleteAvatar) { // delete the avatar - const fileCursor = collections.bucket.find({ filename: assistant._id.toString() }); + const fileCursor = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); let fileId = await fileCursor.next(); while (fileId) { - await collections.bucket.delete(fileId._id); + await Database.getInstance().getCollections().bucket.delete(fileId._id); fileId = await fileCursor.next(); } } - const { acknowledged } = await collections.assistants.updateOne( + const { acknowledged } = await Database.getInstance().getCollections().assistants.updateOne( { _id: assistant._id, }, diff --git a/src/routes/settings/(nav)/assistants/new/+page.server.ts b/src/routes/settings/(nav)/assistants/new/+page.server.ts index 50e4b738372..817238a153a 100644 --- a/src/routes/settings/(nav)/assistants/new/+page.server.ts +++ b/src/routes/settings/(nav)/assistants/new/+page.server.ts @@ -1,6 +1,6 @@ import { base } from "$app/paths"; import { authCondition, requiresUser } from "$lib/server/auth"; -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import { fail, type Actions, redirect } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; @@ -43,7 +43,7 @@ const newAsssistantSchema = z.object({ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise => { const hash = await sha256(await avatar.text()); - const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, { + const upload = Database.getInstance().getCollections().bucket.openUploadStream(`${assistantId.toString()}`, { metadata: { type: avatar.type, hash }, }); @@ -84,7 +84,7 @@ export const actions: Actions = { const createdById = locals.user?._id ?? locals.sessionId; - const assistantsCount = await collections.assistants.countDocuments({ createdById }); + const assistantsCount = await Database.getInstance().getCollections().assistants.countDocuments({ createdById }); if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) { const errors = [ @@ -121,7 +121,7 @@ export const actions: Actions = { hash = await uploadAvatar(new File([image], "avatar.jpg"), newAssistantId); } - const { insertedId } = await collections.assistants.insertOne({ + const { insertedId } = await Database.getInstance().getCollections().assistants.insertOne({ _id: newAssistantId, createdById, createdByName: locals.user?.username ?? locals.user?.name, @@ -150,7 +150,7 @@ export const actions: Actions = { // add insertedId to user settings - await collections.settings.updateOne(authCondition(locals), { + await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { $addToSet: { assistants: insertedId }, }); diff --git a/src/routes/settings/+layout.server.ts b/src/routes/settings/+layout.server.ts index 140b1a7e149..14cae01c976 100644 --- a/src/routes/settings/+layout.server.ts +++ b/src/routes/settings/+layout.server.ts @@ -1,4 +1,4 @@ -import { collections } from "$lib/server/database"; +import { Database } from "$lib/server/database"; import type { LayoutServerLoad } from "./$types"; import type { Report } from "$lib/types/Report"; @@ -8,7 +8,7 @@ export const load = (async ({ locals, parent }) => { let reportsByUser: string[] = []; const createdBy = locals.user?._id ?? locals.sessionId; if (createdBy) { - const reports = await collections.reports + const reports = await Database.getInstance().getCollections().reports .find>({ createdBy }, { projection: { _id: 0, assistantId: 1 } }) .toArray(); reportsByUser = reports.map((r) => r.assistantId.toString()); From 4bc96fb21af553057db2948f2214c796b9251b79 Mon Sep 17 00:00:00 2001 From: rtrompier Date: Fri, 3 May 2024 10:24:14 +0200 Subject: [PATCH 07/16] refacto(all): use class & singleton --- Dockerfile | 3 +- scripts/server.ts | 44 ++++++++++-------- src/hooks.server.ts | 12 ++--- .../refresh-assistants-counts.ts | 2 +- src/lib/server/abortedGenerations.ts | 1 - src/lib/server/database.ts | 15 ++++++- src/lib/server/metrics.ts | 45 ++++++++++++++++++- src/routes/metrics/+server.ts | 9 ---- 8 files changed, 92 insertions(+), 39 deletions(-) delete mode 100644 src/routes/metrics/+server.ts diff --git a/Dockerfile b/Dockerfile index 45373a87375..52c48773d64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ RUN --mount=type=cache,target=/app/.npm \ COPY --link --chown=1000 . . -RUN npm run build +RUN APP_BASE="/chat" npm run build +RUN #npm run build FROM node:20-slim RUN npm install -g vite-node diff --git a/scripts/server.ts b/scripts/server.ts index bd75abf361f..ae75a447ccc 100644 --- a/scripts/server.ts +++ b/scripts/server.ts @@ -1,32 +1,40 @@ import express from "express"; import { logger } from "$lib/server/logger.js"; import { handler } from "../build/handler.js"; -import { APP_BASE } from "$env/static/private"; +// import { env } from "$env/dynamic/private"; const app = express(); -const metricsApp = express(); -const METRICS_PORT = process.env.METRICS_PORT || "3001"; +// const metricsApp = express(); +// const METRICS_PORT = process.env.METRICS_PORT || "3001"; // Let SvelteKit handle everything else, including serving prerendered pages and static assets app.use(handler); -app.listen(3000, () => { +const server = app.listen(3000, () => { logger.info("Listening on port 3000"); }); -// Set up metrics app to serve something when `/metrics` is accessed -metricsApp.get("/metrics", (req, res) => { - res.set("Content-Type", "text/plain"); - fetch(`http://localhost:3000${APP_BASE}/metrics`) - .then((response) => response.text()) - .then((text) => { - res.send(text); - }) - .catch(() => { - res.send("Error fetching metrics."); - }); +process.on("SIGINT", async () => { + logger.info("Sigint received, disconnect server ..."); + server.close(() => { + logger.info("Server stopped ..."); + }) + process.exit(); }); -metricsApp.listen(METRICS_PORT, () => { - logger.info(`Metrics server listening on port ${METRICS_PORT}`); -}); +// Set up metrics app to serve something when `/metrics` is accessed +// metricsApp.get("/metrics", (req, res) => { +// res.set("Content-Type", "text/plain"); +// fetch(`http://localhost:3000${env.APP_BASE}/metrics`) +// .then((response) => response.text()) +// .then((text) => { +// res.send(text); +// }) +// .catch(() => { +// res.send("Error fetching metrics."); +// }); +// }); +// +// metricsApp.listen(METRICS_PORT, () => { +// logger.info(`Metrics server listening on port ${METRICS_PORT}`); +// }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e67d02ecb22..408f3d8b8f7 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -21,10 +21,9 @@ import { addWeeks } from "date-fns"; import { checkAndRunMigrations } from "$lib/migrations/migrations"; import { building, dev } from "$app/environment"; import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts"; -import { collectDefaultMetrics } from "prom-client"; -import { register } from "$lib/server/metrics"; import { logger } from "$lib/server/logger"; import { AbortedGenerations } from "$lib/server/abortedGenerations"; +import { MetricsServer } from "$lib/server/metrics"; // TODO: move this code on a started server hook, instead of using a "building" flag if (!building) { @@ -32,10 +31,12 @@ if (!building) { if (ENABLE_ASSISTANTS) { refreshAssistantsCounts(); } - collectDefaultMetrics({ register }); + + // Init metrics server + MetricsServer.getInstance(); // Init AbortedGenerations refresh process - new AbortedGenerations(); + AbortedGenerations.getInstance(); } export const handleError: HandleServerError = async ({ error, event }) => { @@ -70,7 +71,6 @@ export const handleError: HandleServerError = async ({ error, event }) => { export const handle: Handle = async ({ event, resolve }) => { // only allow metrics from localhost if we're not in dev mode - console.log({ host: event.request.headers.get("host") }); if ( event.url.pathname.startsWith(`${base}/metrics`) && !dev && @@ -126,7 +126,7 @@ export const handle: Handle = async ({ event, resolve }) => { secretSessionId = crypto.randomUUID(); sessionId = await sha256(secretSessionId); - if (await collections.sessions.findOne({ sessionId })) { + if (await Database.getInstance().getCollections().sessions.findOne({ sessionId })) { return errorResponse(500, "Session ID collision"); } } diff --git a/src/lib/assistantStats/refresh-assistants-counts.ts b/src/lib/assistantStats/refresh-assistants-counts.ts index 292f743a25f..2b61ba7e112 100644 --- a/src/lib/assistantStats/refresh-assistants-counts.ts +++ b/src/lib/assistantStats/refresh-assistants-counts.ts @@ -15,7 +15,7 @@ async function refreshAssistantsCountsHelper() { } try { - await Database.getInstance().client.withSession((session) => + await Database.getInstance().getClient().withSession((session) => session.withTransaction(async () => { await Database.getInstance().getCollections().assistants .aggregate([ diff --git a/src/lib/server/abortedGenerations.ts b/src/lib/server/abortedGenerations.ts index 2694025da08..c29dd3de224 100644 --- a/src/lib/server/abortedGenerations.ts +++ b/src/lib/server/abortedGenerations.ts @@ -1,6 +1,5 @@ // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 -import { setTimeout } from "node:timers/promises"; import { Database } from "$lib/server/database"; import { logger } from "$lib/server/logger"; diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 7b278eabde9..e542ce74145 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -33,9 +33,22 @@ export class Database { directConnection: env.MONGODB_DIRECT_CONNECTION === "true", }); - this.client.connect().catch(logger.error); + this.client.connect().catch((err) => { + logger.error("Connection error", err); + process.exit(1); + }); this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); this.client.on("open", () => this.initDatabase()); + + // Disconnect DB on process kill + process.on("SIGINT", async () => { + await this.client.close(true); + + // https://github.com/sveltejs/kit/issues/9540 + setTimeout(() => { + process.exit(0); + }, 100); + }); } public static getInstance(): Database { diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts index caec4861533..03b15c29522 100644 --- a/src/lib/server/metrics.ts +++ b/src/lib/server/metrics.ts @@ -1,3 +1,44 @@ -import { Registry } from "prom-client"; +import { collectDefaultMetrics, Registry } from "prom-client"; +import express from "express"; +import { logger } from "$lib/server/logger"; +import { env } from "$env/dynamic/private"; -export const register = new Registry(); +export class MetricsServer { + private static instance: MetricsServer; + + private constructor() { + const app = express(); + const port = env.METRICS_PORT || "3001"; + + const server = app.listen(port, () => { + logger.info(`Metrics server listening on port ${port}`); + }); + + const register = new Registry(); + collectDefaultMetrics({ register }); + + app.get("/metrics", (req, res) => { + register.metrics().then((metrics) => { + res.set("Content-Type", "text/plain"); + res.send(metrics); + }); + }); + + process.on("SIGINT", async () => { + logger.info("Sigint received, disconnect metrics server ..."); + server.close(() => { + logger.info("Server stopped ..."); + }); + process.exit(); + }); + } + + public static getInstance(): MetricsServer { + if (!MetricsServer.instance) { + MetricsServer.instance = new MetricsServer(); + } + + return MetricsServer.instance; + } + +} diff --git a/src/routes/metrics/+server.ts b/src/routes/metrics/+server.ts deleted file mode 100644 index 95b83257e56..00000000000 --- a/src/routes/metrics/+server.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { register } from "$lib/server/metrics"; - -export async function GET() { - return new Response(await register.metrics(), { - headers: { - "Content-Type": register.contentType, - }, - }); -} From 75b283328cdc0e16e3f36bb0dc216536312cbff1 Mon Sep 17 00:00:00 2001 From: rtrompier Date: Fri, 3 May 2024 11:06:16 +0200 Subject: [PATCH 08/16] refacto(all): add request logs --- src/hooks.server.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 408f3d8b8f7..f69b1bca83b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -70,14 +70,12 @@ export const handleError: HandleServerError = async ({ error, event }) => { }; export const handle: Handle = async ({ event, resolve }) => { - // only allow metrics from localhost if we're not in dev mode - if ( - event.url.pathname.startsWith(`${base}/metrics`) && - !dev && - event.request.headers.get("host") !== "localhost:3000" - ) { - return new Response("Forbidden", { status: 403 }); - } + logger.info({ + locals: event.locals, + url: event.url.pathname, + params: event.params, + request: event.request, + }); if (event.url.pathname.startsWith(`${base}/api/`) && EXPOSE_API !== "true") { return new Response("API is disabled", { status: 403 }); From 552abe2dedc37332e0dfa24342cda52015d95b8d Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 3 May 2024 11:21:17 +0200 Subject: [PATCH 09/16] Make all the env vars dynamic (#1092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add truncate to task model (#1090) * retry text area height (#1091) * Make all the env vars dynamic * only check for db at runtime * remove secret from build step * improve dockerfile * Wrap db in singleton * add .env.local to dockerignore * changes to dockerfile * lint * aborted generations * move collections build check * Use a single dockerfile * lint * fixes * lint * don't return db during building * remove dev --------- Co-authored-by: Victor Muštar --- .dockerignore | 2 +- .env.template | 1 + .github/workflows/build-image.yml | 6 +- Dockerfile | 66 ++++++++++++++--- Dockerfile.local | 28 ------- entrypoint.sh | 18 +---- package.json | 2 +- scripts/populate.ts | 37 ++++++---- scripts/server.ts | 40 ---------- src/hooks.server.ts | 51 +++++-------- .../refresh-assistants-counts.ts | 73 ++++++++++--------- src/lib/components/AnnouncementBanner.svelte | 2 +- src/lib/components/DisclaimerModal.svelte | 16 ++-- src/lib/components/LoginModal.svelte | 10 +-- src/lib/components/NavMenu.svelte | 11 ++- .../chat/AssistantIntroduction.svelte | 5 +- .../components/chat/ChatIntroduction.svelte | 14 ++-- src/lib/components/chat/ChatMessage.svelte | 3 +- src/lib/components/chat/ChatWindow.svelte | 2 +- src/lib/components/icons/Logo.svelte | 8 +- src/lib/migrations/lock.ts | 10 +-- src/lib/migrations/migrations.spec.ts | 10 +-- src/lib/migrations/migrations.ts | 47 +++++++----- .../routines/01-update-search-assistants.ts | 10 +-- .../routines/02-update-assistants-models.ts | 6 +- src/lib/migrations/routines/index.ts | 7 +- src/lib/server/abortedGenerations.ts | 5 +- src/lib/server/auth.ts | 41 ++++------- src/lib/server/database.ts | 21 +++--- .../hfApi/embeddingHfApi.ts | 4 +- .../openai/embeddingEndpoints.ts | 4 +- .../tei/embeddingEndpoints.ts | 4 +- src/lib/server/embeddingModels.ts | 4 +- .../endpoints/anthropic/endpointAnthropic.ts | 4 +- .../cloudflare/endpointCloudflare.ts | 6 +- .../server/endpoints/cohere/endpointCohere.ts | 4 +- .../endpoints/llamacpp/endpointLlamacpp.ts | 4 +- .../server/endpoints/openai/endpointOai.ts | 4 +- src/lib/server/endpoints/tgi/endpointTgi.ts | 4 +- src/lib/server/files/downloadFile.ts | 6 +- src/lib/server/files/uploadFile.ts | 4 +- src/lib/server/metrics.ts | 1 - src/lib/server/models.ts | 25 +++---- src/lib/server/summarize.ts | 4 +- src/lib/server/usageLimits.ts | 6 +- src/lib/server/websearch/runWebSearch.ts | 8 +- src/lib/server/websearch/searchSearxng.ts | 4 +- src/lib/server/websearch/searchWeb.ts | 33 ++++----- src/lib/utils/getShareUrl.ts | 6 +- src/lib/utils/isHuggingChat.ts | 4 +- src/lib/utils/tree/addChildren.spec.ts | 16 ++-- src/lib/utils/tree/addSibling.spec.ts | 10 +-- src/lib/utils/tree/buildSubtree.spec.ts | 12 +-- .../tree/convertLegacyConversation.spec.ts | 4 +- src/lib/utils/tree/treeHelpers.spec.ts | 8 +- src/routes/+layout.server.ts | 46 +++++------- src/routes/+layout.svelte | 41 +++++------ src/routes/+page.svelte | 4 +- src/routes/admin/export/+server.ts | 14 ++-- src/routes/admin/stats/compute/+server.ts | 6 +- src/routes/api/assistant/[id]/+server.ts | 4 +- src/routes/api/assistants/+server.ts | 14 ++-- src/routes/api/conversation/[id]/+server.ts | 4 +- src/routes/api/conversations/+server.ts | 4 +- src/routes/api/user/assistants/+server.ts | 8 +- .../assistant/[assistantId]/+page.server.ts | 4 +- .../assistant/[assistantId]/+page.svelte | 9 ++- .../[assistantId]/thumbnail.png/+server.ts | 8 +- src/routes/assistants/+page.server.ts | 21 +++--- src/routes/assistants/+page.svelte | 6 +- src/routes/conversation/+server.ts | 10 +-- src/routes/conversation/[id]/+page.server.ts | 10 +-- src/routes/conversation/[id]/+server.ts | 46 ++++++------ .../message/[messageId]/prompt/+server.ts | 6 +- .../[id]/message/[messageId]/vote/+server.ts | 4 +- .../[id]/output/[sha256]/+server.ts | 6 +- src/routes/conversation/[id]/share/+server.ts | 14 ++-- .../[id]/stop-generating/+server.ts | 6 +- src/routes/conversations/+page.server.ts | 4 +- src/routes/login/+page.server.ts | 4 +- src/routes/login/callback/+page.server.ts | 4 +- src/routes/login/callback/updateUser.spec.ts | 28 +++---- src/routes/login/callback/updateUser.ts | 22 +++--- src/routes/logout/+page.server.ts | 12 +-- src/routes/models/+page.svelte | 4 +- src/routes/models/[...model]/+page.server.ts | 4 +- src/routes/models/[...model]/+page.svelte | 8 +- .../thumbnail.png/ModelThumbnail.svelte | 4 +- src/routes/settings/(nav)/+page.svelte | 4 +- src/routes/settings/(nav)/+server.ts | 4 +- .../settings/(nav)/[...model]/+page.svelte | 4 +- .../assistants/[assistantId]/+page.server.ts | 43 +++++------ .../assistants/[assistantId]/+page.svelte | 5 +- .../[assistantId]/avatar.jpg/+server.ts | 8 +- .../[assistantId]/edit/+page.server.ts | 16 ++-- .../(nav)/assistants/new/+page.server.ts | 10 +-- src/routes/settings/+layout.server.ts | 4 +- tailwind.config.cjs | 4 +- tsconfig.json | 3 +- 99 files changed, 590 insertions(+), 649 deletions(-) delete mode 100644 Dockerfile.local delete mode 100644 scripts/server.ts diff --git a/.dockerignore b/.dockerignore index b501ad1a959..cd4afbe92f2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,4 +8,4 @@ node_modules/ .svelte-kit/ .env* !.env -!.env.local \ No newline at end of file +.env.local \ No newline at end of file diff --git a/.env.template b/.env.template index a4547e53199..bcb0cb20ca6 100644 --- a/.env.template +++ b/.env.template @@ -231,6 +231,7 @@ MODELS=`[ "parameters": { "temperature": 0.1, "stop": ["<|eot_id|>"], + "truncate": 1024, }, "unlisted": true } diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index b284c9f6ec3..0238051484f 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -8,7 +8,7 @@ on: branches: - "*" paths: - - "Dockerfile.local" + - "Dockerfile" - "entrypoint.sh" workflow_dispatch: release: @@ -62,7 +62,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: Dockerfile.local + file: Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -116,7 +116,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: Dockerfile.local + file: Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 52c48773d64..ca205fbbfb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ # syntax=docker/dockerfile:1 # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker # you will also find guides on how best to write your Dockerfile +ARG INCLUDE_DB=false + +# stage that install the dependencies FROM node:20 as builder-production WORKDIR /app @@ -12,31 +15,74 @@ RUN --mount=type=cache,target=/app/.npm \ FROM builder-production as builder +ARG APP_BASE= +ARG PUBLIC_APP_COLOR=blue + RUN --mount=type=cache,target=/app/.npm \ npm set cache /app/.npm && \ npm ci COPY --link --chown=1000 . . -RUN APP_BASE="/chat" npm run build -RUN #npm run build +RUN npm run build -FROM node:20-slim -RUN npm install -g vite-node +# mongo image +FROM mongo:latest as mongo -RUN userdel -r node +# image to be used if INCLUDE_DB is false +FROM node:20-slim as local_db_false + +# image to be used if INCLUDE_DB is true +FROM node:20-slim as local_db_true + +RUN apt-get update +RUN apt-get install gnupg curl -y +# copy mongo from the other stage +COPY --from=mongo /usr/bin/mongo* /usr/bin/ + +ENV MONGODB_URL=mongodb://localhost:27017 +RUN mkdir -p /data/db +RUN chown -R 1000:1000 /data/db + +# final image +FROM local_db_${INCLUDE_DB} as final + +# build arg to determine if the database should be included +ARG INCLUDE_DB=false +ENV INCLUDE_DB=${INCLUDE_DB} + +# svelte requires APP_BASE at build time so it must be passed as a build arg +ARG APP_BASE= +# tailwind requires the primary theme to be known at build time so it must be passed as a build arg +ARG PUBLIC_APP_COLOR=blue -RUN useradd -m -u 1000 user +# install dotenv-cli +RUN npm install -g dotenv-cli + +# switch to a user that works for spaces +RUN userdel -r node +RUN useradd -m -u 1000 user USER user ENV HOME=/home/user \ PATH=/home/user/.local/bin:$PATH +WORKDIR /app -COPY --from=builder-production --chown=1000 /app/node_modules /app/node_modules -COPY --link --chown=1000 package.json /app/package.json -COPY --from=builder --chown=1000 /app/build /app/build +# add a .env.local if the user doesn't bind a volume to it +RUN touch /app/.env.local + +# get the default config, the entrypoint script and the server script +COPY --chown=1000 package.json /app/package.json +COPY --chown=1000 .env /app/.env +COPY --chown=1000 entrypoint.sh /app/entrypoint.sh COPY --chown=1000 gcp-*.json /app/ -CMD node /app/build/index.js +#import the build & dependencies +COPY --from=builder --chown=1000 /app/build /app/build +COPY --from=builder --chown=1000 /app/node_modules /app/node_modules + +RUN chmod +x /app/entrypoint.sh + +CMD ["/bin/bash", "-c", "/app/entrypoint.sh"] diff --git a/Dockerfile.local b/Dockerfile.local deleted file mode 100644 index c6046c3eb92..00000000000 --- a/Dockerfile.local +++ /dev/null @@ -1,28 +0,0 @@ -ARG INCLUDE_DB=false -FROM mongo:latest as mongo - -FROM node:20-slim as local_db_false - -FROM node:20-slim as local_db_true - -RUN apt-get update -RUN apt-get install gnupg curl -y - -COPY --from=mongo /usr/bin/mongo* /usr/bin/ - -FROM local_db_${INCLUDE_DB} as final -ARG INCLUDE_DB=false -ENV INCLUDE_DB=${INCLUDE_DB} - -WORKDIR /app - -COPY --link --chown=1000 package-lock.json package.json ./ -RUN --mount=type=cache,target=/app/.npm \ - npm set cache /app/.npm && \ - npm ci - -# copy the rest of the files, run regardless of -COPY --chown=1000 --link . . -RUN chmod +x /app/entrypoint.sh - -CMD ["/bin/bash", "-c", "/app/entrypoint.sh"] \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 1f67a9a5fa2..a357ed99b77 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,7 +2,7 @@ ENV_LOCAL_PATH=/app/.env.local if test -z "${DOTENV_LOCAL}" ; then if ! test -f "${ENV_LOCAL_PATH}" ; then - echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. We are using the default .env config." + echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. Make sure to set environment variables properly. " fi; else echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file." @@ -10,20 +10,8 @@ else fi; if [ "$INCLUDE_DB" = "true" ] ; then - echo "INCLUDE_DB is set to true." - - MONGODB_CONFIG="MONGODB_URL=mongodb://localhost:27017" - if ! grep -q "^${MONGODB_CONFIG}$" ${ENV_LOCAL_PATH}; then - echo "Appending MONGODB_URL" - touch /app/.env.local - echo -e "\n${MONGODB_CONFIG}" >> ${ENV_LOCAL_PATH} - fi - - mkdir -p /data/db - mongod & echo "Starting local MongoDB instance" - + nohup mongod & fi; -npm run build -npm vite-node --options.transformMode.ssr='/.*/' /app/scripts/server.ts -- --host 0.0.0.0 --port 3000 \ No newline at end of file +dotenv -e /app/.env -c -- node /app/build/index.js -- --host 0.0.0.0 --port 3000 \ No newline at end of file diff --git a/package.json b/package.json index cb03e3f1463..f541740f21a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite-node --options.transformMode.ssr='/.*/' scripts/server.ts", + "preview": "vite-node --options.transformMode.ssr='/.*/' scripts/server.,js", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", diff --git a/scripts/populate.ts b/scripts/populate.ts index d6fa31e08ac..8e06879a634 100644 --- a/scripts/populate.ts +++ b/scripts/populate.ts @@ -2,12 +2,13 @@ import readline from "readline"; import minimist from "minimist"; // @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them -import { MONGODB_URL } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { faker } from "@faker-js/faker"; import { ObjectId } from "mongodb"; -import { Database } from "$lib/server/database"; +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { collections } from "$lib/server/database"; import { models } from "../src/lib/server/models.ts"; import type { User } from "../src/lib/types/User"; import type { Assistant } from "../src/lib/types/Assistant"; @@ -17,6 +18,7 @@ import { defaultEmbeddingModel } from "../src/lib/server/embeddingModels.ts"; import { Message } from "../src/lib/types/Message.ts"; import { addChildren } from "../src/lib/utils/tree/addChildren.ts"; +import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts"; const rl = readline.createInterface({ input: process.stdin, @@ -107,10 +109,10 @@ async function seed() { if (flags.includes("reset")) { console.log("Starting reset of DB"); - await Database.getInstance().getCollections().users.deleteMany({}); - await Database.getInstance().getCollections().settings.deleteMany({}); - await Database.getInstance().getCollections().assistants.deleteMany({}); - await Database.getInstance().getCollections().conversations.deleteMany({}); + await collections.users.deleteMany({}); + await collections.settings.deleteMany({}); + await collections.assistants.deleteMany({}); + await collections.conversations.deleteMany({}); console.log("Reset done"); } @@ -126,11 +128,11 @@ async function seed() { avatarUrl: faker.image.avatar(), })); - await Database.getInstance().getCollections().users.insertMany(newUsers); + await collections.users.insertMany(newUsers); console.log("Done creating users."); } - const users = await Database.getInstance().getCollections().users.find().toArray(); + const users = await collections.users.find().toArray(); if (flags.includes("settings") || flags.includes("all")) { console.log("Updating settings for all users"); users.forEach(async (user) => { @@ -145,7 +147,7 @@ async function seed() { customPrompts: {}, assistants: [], }; - await Database.getInstance().getCollections().settings.updateOne( + await collections.settings.updateOne( { userId: user._id }, { $set: { ...settings } }, { upsert: true } @@ -158,10 +160,11 @@ async function seed() { console.log("Creating assistants for all users"); await Promise.all( users.map(async (user) => { + const name = faker.animal.insect(); const assistants = faker.helpers.multiple( () => ({ _id: new ObjectId(), - name: faker.animal.insect(), + name, createdById: user._id, createdByName: user.username, createdAt: faker.date.recent({ days: 30 }), @@ -174,11 +177,13 @@ async function seed() { exampleInputs: faker.helpers.multiple(() => faker.lorem.sentence(), { count: faker.number.int({ min: 0, max: 4 }), }), + searchTokens: generateSearchTokens(name), + last24HoursCount: faker.number.int({ min: 0, max: 1000 }), }), { count: faker.number.int({ min: 3, max: 10 }) } ); - await Database.getInstance().getCollections().assistants.insertMany(assistants); - await Database.getInstance().getCollections().settings.updateOne( + await collections.assistants.insertMany(assistants); + await collections.settings.updateOne( { userId: user._id }, { $set: { assistants: assistants.map((a) => a._id.toString()) } }, { upsert: true } @@ -194,7 +199,7 @@ async function seed() { users.map(async (user) => { const conversations = faker.helpers.multiple( async () => { - const settings = await Database.getInstance().getCollections().settings.findOne({ userId: user._id }); + const settings = await collections.settings.findOne({ userId: user._id }); const assistantId = settings?.assistants && settings.assistants.length > 0 && faker.datatype.boolean(0.1) @@ -203,7 +208,7 @@ async function seed() { const preprompt = (assistantId - ? await Database.getInstance().getCollections().assistants + ? await collections.assistants .findOne({ _id: assistantId }) .then((assistant: Assistant) => assistant?.preprompt ?? "") : faker.helpers.maybe(() => faker.hacker.phrase(), { probability: 0.5 })) ?? ""; @@ -229,7 +234,7 @@ async function seed() { { count: faker.number.int({ min: 10, max: 200 }) } ); - await Database.getInstance().getCollections().conversations.insertMany(await Promise.all(conversations)); + await collections.conversations.insertMany(await Promise.all(conversations)); }) ); console.log("Done creating conversations."); @@ -241,7 +246,7 @@ async function seed() { try { rl.question( "You're about to run a seeding script on the following MONGODB_URL: \x1b[31m" + - MONGODB_URL + + env.MONGODB_URL + "\x1b[0m\n\n With the following flags: \x1b[31m" + flags.join("\x1b[0m , \x1b[31m") + "\x1b[0m\n \n\n Are you sure you want to continue? (yes/no): ", diff --git a/scripts/server.ts b/scripts/server.ts deleted file mode 100644 index ae75a447ccc..00000000000 --- a/scripts/server.ts +++ /dev/null @@ -1,40 +0,0 @@ -import express from "express"; -import { logger } from "$lib/server/logger.js"; -import { handler } from "../build/handler.js"; -// import { env } from "$env/dynamic/private"; - -const app = express(); -// const metricsApp = express(); -// const METRICS_PORT = process.env.METRICS_PORT || "3001"; - -// Let SvelteKit handle everything else, including serving prerendered pages and static assets -app.use(handler); - -const server = app.listen(3000, () => { - logger.info("Listening on port 3000"); -}); - -process.on("SIGINT", async () => { - logger.info("Sigint received, disconnect server ..."); - server.close(() => { - logger.info("Server stopped ..."); - }) - process.exit(); -}); - -// Set up metrics app to serve something when `/metrics` is accessed -// metricsApp.get("/metrics", (req, res) => { -// res.set("Content-Type", "text/plain"); -// fetch(`http://localhost:3000${env.APP_BASE}/metrics`) -// .then((response) => response.text()) -// .then((text) => { -// res.send(text); -// }) -// .catch(() => { -// res.send("Error fetching metrics."); -// }); -// }); -// -// metricsApp.listen(METRICS_PORT, () => { -// logger.info(`Metrics server listening on port ${METRICS_PORT}`); -// }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index f69b1bca83b..3fb5fc6e058 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,25 +1,14 @@ -import { - ADMIN_API_SECRET, - COOKIE_NAME, - ENABLE_ASSISTANTS, - EXPOSE_API, - MESSAGES_BEFORE_LOGIN, - PARQUET_EXPORT_SECRET, -} from "$env/static/private"; +import { env } from "$env/dynamic/private"; +import { env as envPublic } from "$env/dynamic/public"; import type { Handle, HandleServerError } from "@sveltejs/kit"; -import { - PUBLIC_GOOGLE_ANALYTICS_ID, - PUBLIC_ORIGIN, - PUBLIC_APP_DISCLAIMER, -} from "$env/static/public"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { base } from "$app/paths"; import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth"; import { ERROR_MESSAGES } from "$lib/stores/errors"; import { sha256 } from "$lib/utils/sha256"; import { addWeeks } from "date-fns"; import { checkAndRunMigrations } from "$lib/migrations/migrations"; -import { building, dev } from "$app/environment"; +import { building } from "$app/environment"; import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts"; import { logger } from "$lib/server/logger"; import { AbortedGenerations } from "$lib/server/abortedGenerations"; @@ -28,7 +17,7 @@ import { MetricsServer } from "$lib/server/metrics"; // TODO: move this code on a started server hook, instead of using a "building" flag if (!building) { await checkAndRunMigrations(); - if (ENABLE_ASSISTANTS) { + if (env.ENABLE_ASSISTANTS) { refreshAssistantsCounts(); } @@ -77,7 +66,7 @@ export const handle: Handle = async ({ event, resolve }) => { request: event.request, }); - if (event.url.pathname.startsWith(`${base}/api/`) && EXPOSE_API !== "true") { + if (event.url.pathname.startsWith(`${base}/api/`) && env.EXPOSE_API !== "true") { return new Response("API is disabled", { status: 403 }); } @@ -94,7 +83,7 @@ export const handle: Handle = async ({ event, resolve }) => { } if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) { - const ADMIN_SECRET = ADMIN_API_SECRET || PARQUET_EXPORT_SECRET; + const ADMIN_SECRET = env.ADMIN_API_SECRET || env.PARQUET_EXPORT_SECRET; if (!ADMIN_SECRET) { return errorResponse(500, "Admin API is not configured"); @@ -105,7 +94,7 @@ export const handle: Handle = async ({ event, resolve }) => { } } - const token = event.cookies.get(COOKIE_NAME); + const token = event.cookies.get(env.COOKIE_NAME); let secretSessionId: string; let sessionId: string; @@ -124,7 +113,7 @@ export const handle: Handle = async ({ event, resolve }) => { secretSessionId = crypto.randomUUID(); sessionId = await sha256(secretSessionId); - if (await Database.getInstance().getCollections().sessions.findOne({ sessionId })) { + if (await collections.sessions.findOne({ sessionId })) { return errorResponse(500, "Session ID collision"); } } @@ -144,18 +133,18 @@ export const handle: Handle = async ({ event, resolve }) => { refreshSessionCookie(event.cookies, event.locals.sessionId); if (nativeFormContentTypes.includes(requestContentType)) { - const referer = event.request.headers.get("referer"); + const origin = event.request.headers.get("origin"); - if (!referer) { - return errorResponse(403, "Non-JSON form requests need to have a referer"); + if (!origin) { + return errorResponse(403, "Non-JSON form requests need to have an origin"); } const validOrigins = [ - new URL(event.request.url).origin, - ...(PUBLIC_ORIGIN ? [new URL(PUBLIC_ORIGIN).origin] : []), + new URL(event.request.url).host, + ...(envPublic.PUBLIC_ORIGIN ? [new URL(envPublic.PUBLIC_ORIGIN).host] : []), ]; - if (!validOrigins.includes(new URL(referer).origin)) { + if (!validOrigins.includes(new URL(origin).host)) { return errorResponse(403, "Invalid referer for POST request"); } } @@ -165,7 +154,7 @@ export const handle: Handle = async ({ event, resolve }) => { // if the request is a POST request we refresh the cookie refreshSessionCookie(event.cookies, secretSessionId); - await Database.getInstance().getCollections().sessions.updateOne( + await collections.sessions.updateOne( { sessionId }, { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } ); @@ -179,7 +168,7 @@ export const handle: Handle = async ({ event, resolve }) => { if ( !event.locals.user && requiresUser && - !((MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) > 0) + !((env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0) > 0) ) { return errorResponse(401, ERROR_MESSAGES.authOnly); } @@ -190,9 +179,9 @@ export const handle: Handle = async ({ event, resolve }) => { if ( !requiresUser && !event.url.pathname.startsWith(`${base}/settings`) && - !!PUBLIC_APP_DISCLAIMER + !!envPublic.PUBLIC_APP_DISCLAIMER ) { - const hasAcceptedEthicsModal = await Database.getInstance().getCollections().settings.countDocuments({ + const hasAcceptedEthicsModal = await collections.settings.countDocuments({ sessionId: event.locals.sessionId, ethicsModalAcceptedAt: { $exists: true }, }); @@ -213,7 +202,7 @@ export const handle: Handle = async ({ event, resolve }) => { } replaced = true; - return chunk.html.replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID); + return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID); }, }); diff --git a/src/lib/assistantStats/refresh-assistants-counts.ts b/src/lib/assistantStats/refresh-assistants-counts.ts index 2b61ba7e112..a5bb5f52596 100644 --- a/src/lib/assistantStats/refresh-assistants-counts.ts +++ b/src/lib/assistantStats/refresh-assistants-counts.ts @@ -15,44 +15,49 @@ async function refreshAssistantsCountsHelper() { } try { - await Database.getInstance().getClient().withSession((session) => - session.withTransaction(async () => { - await Database.getInstance().getCollections().assistants - .aggregate([ - { $project: { _id: 1 } }, - { $set: { last24HoursCount: 0 } }, - { - $unionWith: { - coll: "assistants.stats", - pipeline: [ - { $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" } }, - { - $group: { - _id: "$assistantId", - last24HoursCount: { $sum: "$count" }, + await Database.getInstance() + .getClient() + .withSession((session) => + session.withTransaction(async () => { + await Database.getInstance() + .getCollections() + .assistants.aggregate([ + { $project: { _id: 1 } }, + { $set: { last24HoursCount: 0 } }, + { + $unionWith: { + coll: "assistants.stats", + pipeline: [ + { + $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" }, }, - }, - ], + { + $group: { + _id: "$assistantId", + last24HoursCount: { $sum: "$count" }, + }, + }, + ], + }, }, - }, - { - $group: { - _id: "$_id", - last24HoursCount: { $sum: "$last24HoursCount" }, + { + $group: { + _id: "$_id", + last24HoursCount: { $sum: "$last24HoursCount" }, + }, }, - }, - { - $merge: { - into: "assistants", - on: "_id", - whenMatched: "merge", - whenNotMatched: "discard", + { + $merge: { + into: "assistants", + on: "_id", + whenMatched: "merge", + whenNotMatched: "discard", + }, }, - }, - ]) - .next(); - }) - ); + ]) + .next(); + }) + ); } catch (e) { logger.error("Refresh assistants counter failed!"); logger.error(e); diff --git a/src/lib/components/AnnouncementBanner.svelte b/src/lib/components/AnnouncementBanner.svelte index 7d6948a6b38..47ec7b02ef2 100644 --- a/src/lib/components/AnnouncementBanner.svelte +++ b/src/lib/components/AnnouncementBanner.svelte @@ -5,7 +5,7 @@
New {title} diff --git a/src/lib/components/DisclaimerModal.svelte b/src/lib/components/DisclaimerModal.svelte index 590bb088b3c..f0a18abd1dd 100644 --- a/src/lib/components/DisclaimerModal.svelte +++ b/src/lib/components/DisclaimerModal.svelte @@ -1,11 +1,7 @@
- + - {PUBLIC_APP_NAME} + {envPublic.PUBLIC_APP_NAME} Settings - {#if PUBLIC_APP_NAME === "HuggingChat"} + {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"} 0 || assistant?.dynamicPrompt; - const prefix = PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || $page.url.origin}${base}`; + const prefix = + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || $page.url.origin}${base}`; $: shareUrl = `${prefix}/assistant/${assistant?._id}`; diff --git a/src/lib/components/chat/ChatIntroduction.svelte b/src/lib/components/chat/ChatIntroduction.svelte index eb0a11b0537..81fb51b09fa 100644 --- a/src/lib/components/chat/ChatIntroduction.svelte +++ b/src/lib/components/chat/ChatIntroduction.svelte @@ -1,7 +1,5 @@ -{#if PUBLIC_APP_ASSETS === "chatui"} +{#if envPublic.PUBLIC_APP_ASSETS === "chatui"} {/if} diff --git a/src/lib/migrations/lock.ts b/src/lib/migrations/lock.ts index 8ac1f7c1d6e..6df5d261b41 100644 --- a/src/lib/migrations/lock.ts +++ b/src/lib/migrations/lock.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; /** @@ -8,7 +8,7 @@ export async function acquireLock(key: string): Promise { try { const id = new ObjectId(); - const insert = await Database.getInstance().getCollections().semaphores.insertOne({ + const insert = await collections.semaphores.insertOne({ _id: id, key, createdAt: new Date(), @@ -23,21 +23,21 @@ export async function acquireLock(key: string): Promise { } export async function releaseLock(key: string, lockId: ObjectId) { - await Database.getInstance().getCollections().semaphores.deleteOne({ + await collections.semaphores.deleteOne({ _id: lockId, key, }); } export async function isDBLocked(key: string): Promise { - const res = await Database.getInstance().getCollections().semaphores.countDocuments({ + const res = await collections.semaphores.countDocuments({ key, }); return res > 0; } export async function refreshLock(key: string, lockId: ObjectId): Promise { - const result = await Database.getInstance().getCollections().semaphores.updateOne( + const result = await collections.semaphores.updateOne( { _id: lockId, key, diff --git a/src/lib/migrations/migrations.spec.ts b/src/lib/migrations/migrations.spec.ts index a58936cf455..2df38798ea5 100644 --- a/src/lib/migrations/migrations.spec.ts +++ b/src/lib/migrations/migrations.spec.ts @@ -1,7 +1,7 @@ import { afterEach, assert, describe, expect, it } from "vitest"; import { migrations } from "./routines"; import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; const LOCK_KEY = "migrations.test"; @@ -40,11 +40,11 @@ describe("migrations", () => { // get the updatedAt time - const updatedAtInitially = (await Database.getInstance().getCollections().semaphores.findOne({}))?.updatedAt; + const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt; await refreshLock(LOCK_KEY, lockId); - const updatedAtAfterRefresh = (await Database.getInstance().getCollections().semaphores.findOne({}))?.updatedAt; + const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt; expect(updatedAtInitially).toBeDefined(); expect(updatedAtAfterRefresh).toBeDefined(); @@ -53,6 +53,6 @@ describe("migrations", () => { }); afterEach(async () => { - await Database.getInstance().getCollections().semaphores.deleteMany({}); - await Database.getInstance().getCollections().migrationResults.deleteMany({}); + await collections.semaphores.deleteMany({}); + await collections.migrationResults.deleteMany({}); }); diff --git a/src/lib/migrations/migrations.ts b/src/lib/migrations/migrations.ts index 33abd6f969a..2331644d8b8 100644 --- a/src/lib/migrations/migrations.ts +++ b/src/lib/migrations/migrations.ts @@ -13,7 +13,10 @@ export async function checkAndRunMigrations() { } // check if all migrations have already been run - const migrationResults = await Database.getInstance().getCollections().migrationResults.find().toArray(); + const migrationResults = await Database.getInstance() + .getCollections() + .migrationResults.find() + .toArray(); logger.info("[MIGRATIONS] Begin check..."); @@ -71,23 +74,25 @@ export async function checkAndRunMigrations() { }. Applying...` ); - await Database.getInstance().getCollections().migrationResults.updateOne( - { _id: migration._id }, - { - $set: { - name: migration.name, - status: "ongoing", + await Database.getInstance() + .getCollections() + .migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: "ongoing", + }, }, - }, - { upsert: true } - ); + { upsert: true } + ); const session = connectedClient.startSession(); let result = false; try { await session.withTransaction(async () => { - result = await migration.up(connectedClient); + result = await migration.up(Database.getInstance()); }); } catch (e) { logger.info(`[MIGRATIONS] "${migration.name}" failed!`); @@ -96,16 +101,18 @@ export async function checkAndRunMigrations() { await session.endSession(); } - await Database.getInstance().getCollections().migrationResults.updateOne( - { _id: migration._id }, - { - $set: { - name: migration.name, - status: result ? "success" : "failure", + await Database.getInstance() + .getCollections() + .migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: result ? "success" : "failure", + }, }, - }, - { upsert: true } - ); + { upsert: true } + ); } } diff --git a/src/lib/migrations/routines/01-update-search-assistants.ts b/src/lib/migrations/routines/01-update-search-assistants.ts index 70b38d53766..52c8b2f6c99 100644 --- a/src/lib/migrations/routines/01-update-search-assistants.ts +++ b/src/lib/migrations/routines/01-update-search-assistants.ts @@ -1,5 +1,5 @@ import type { Migration } from "."; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId, type AnyBulkWriteOperation } from "mongodb"; import type { Assistant } from "$lib/types/Assistant"; import { generateSearchTokens } from "$lib/utils/searchTokens"; @@ -7,8 +7,8 @@ import { generateSearchTokens } from "$lib/utils/searchTokens"; const migration: Migration = { _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"), name: "Update search assistants", - up: async (client) => { - const { assistants } = Database.getInstance().getCollections(); + up: async () => { + const { assistants } = collections; let ops: AnyBulkWriteOperation[] = []; for await (const assistant of assistants @@ -40,8 +40,8 @@ const migration: Migration = { return true; }, - down: async (client) => { - const { assistants } = Database.getInstance().getCollections(); + down: async () => { + const { assistants } = collections; await assistants.updateMany({}, { $unset: { searchTokens: "" } }); return true; }, diff --git a/src/lib/migrations/routines/02-update-assistants-models.ts b/src/lib/migrations/routines/02-update-assistants-models.ts index c54b9690b6c..f7f0c9dd454 100644 --- a/src/lib/migrations/routines/02-update-assistants-models.ts +++ b/src/lib/migrations/routines/02-update-assistants-models.ts @@ -1,14 +1,14 @@ import type { Migration } from "."; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; const updateAssistantsModels: Migration = { _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"), name: "Update deprecated models in assistants with the default model", - up: async (client) => { + up: async () => { const models = (await import("$lib/server/models")).models; - const { assistants } = Database.getInstance().getCollections(); + const { assistants } = collections; const modelIds = models.map((el) => el.id); // string[] const defaultModelId = models[0].id; diff --git a/src/lib/migrations/routines/index.ts b/src/lib/migrations/routines/index.ts index 96a6a07ab3c..0d6eafa8f04 100644 --- a/src/lib/migrations/routines/index.ts +++ b/src/lib/migrations/routines/index.ts @@ -1,13 +1,14 @@ -import type { MongoClient, ObjectId } from "mongodb"; +import type { ObjectId } from "mongodb"; import updateSearchAssistant from "./01-update-search-assistants"; import updateAssistantsModels from "./02-update-assistants-models"; +import type { Database } from "$lib/server/database"; export interface Migration { _id: ObjectId; name: string; - up: (client: MongoClient) => Promise; - down?: (client: MongoClient) => Promise; + up: (client: Database) => Promise; + down?: (client: Database) => Promise; runForFreshInstall?: "only" | "never"; // leave unspecified to run for both runForHuggingChat?: "only" | "never"; // leave unspecified to run for both runEveryTime?: boolean; diff --git a/src/lib/server/abortedGenerations.ts b/src/lib/server/abortedGenerations.ts index c29dd3de224..b021a069848 100644 --- a/src/lib/server/abortedGenerations.ts +++ b/src/lib/server/abortedGenerations.ts @@ -1,7 +1,7 @@ // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 -import { Database } from "$lib/server/database"; import { logger } from "$lib/server/logger"; +import { collections } from "$lib/server/database"; export class AbortedGenerations { private static instance: AbortedGenerations; @@ -30,7 +30,7 @@ export class AbortedGenerations { private async updateList() { try { - const aborts = await Database.getInstance().getCollections().abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); + const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); this.abortedGenerations = new Map( aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt]) @@ -39,5 +39,4 @@ export class AbortedGenerations { logger.error(err); } } - } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index b941dfb7109..94eacdb476b 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,22 +1,11 @@ import { Issuer, BaseClient, type UserinfoResponse, TokenSet, custom } from "openid-client"; import { addHours, addWeeks } from "date-fns"; -import { - COOKIE_NAME, - OPENID_CLIENT_ID, - OPENID_CLIENT_SECRET, - OPENID_PROVIDER_URL, - OPENID_SCOPES, - OPENID_NAME_CLAIM, - OPENID_TOLERANCE, - OPENID_RESOURCE, - OPENID_CONFIG, - ALLOW_INSECURE_COOKIES, -} from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { sha256 } from "$lib/utils/sha256"; import { z } from "zod"; import { dev } from "$app/environment"; import type { Cookies } from "@sveltejs/kit"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import JSON5 from "json5"; import { logger } from "$lib/server/logger"; @@ -37,40 +26,40 @@ const stringWithDefault = (value: string) => export const OIDConfig = z .object({ - CLIENT_ID: stringWithDefault(OPENID_CLIENT_ID), - CLIENT_SECRET: stringWithDefault(OPENID_CLIENT_SECRET), - PROVIDER_URL: stringWithDefault(OPENID_PROVIDER_URL), - SCOPES: stringWithDefault(OPENID_SCOPES), - NAME_CLAIM: stringWithDefault(OPENID_NAME_CLAIM).refine( + CLIENT_ID: stringWithDefault(env.OPENID_CLIENT_ID), + CLIENT_SECRET: stringWithDefault(env.OPENID_CLIENT_SECRET), + PROVIDER_URL: stringWithDefault(env.OPENID_PROVIDER_URL), + SCOPES: stringWithDefault(env.OPENID_SCOPES), + NAME_CLAIM: stringWithDefault(env.OPENID_NAME_CLAIM).refine( (el) => !["preferred_username", "email", "picture", "sub"].includes(el), { message: "nameClaim cannot be one of the restricted keys." } ), - TOLERANCE: stringWithDefault(OPENID_TOLERANCE), - RESOURCE: stringWithDefault(OPENID_RESOURCE), + TOLERANCE: stringWithDefault(env.OPENID_TOLERANCE), + RESOURCE: stringWithDefault(env.OPENID_RESOURCE), }) - .parse(JSON5.parse(OPENID_CONFIG)); + .parse(JSON5.parse(env.OPENID_CONFIG)); export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET; export function refreshSessionCookie(cookies: Cookies, sessionId: string) { - cookies.set(COOKIE_NAME, sessionId, { + cookies.set(env.COOKIE_NAME, sessionId, { path: "/", // So that it works inside the space's iframe - sameSite: dev || ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", - secure: !dev && !(ALLOW_INSECURE_COOKIES === "true"), + sameSite: dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", + secure: !dev && !(env.ALLOW_INSECURE_COOKIES === "true"), httpOnly: true, expires: addWeeks(new Date(), 2), }); } export async function findUser(sessionId: string) { - const session = await Database.getInstance().getCollections().sessions.findOne({ sessionId }); + const session = await collections.sessions.findOne({ sessionId }); if (!session) { return null; } - return await Database.getInstance().getCollections().users.findOne({ _id: session.userId }); + return await collections.users.findOne({ _id: session.userId }); } export const authCondition = (locals: App.Locals) => { return locals.user diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index e542ce74145..4a8302ce9ca 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -14,6 +14,7 @@ import type { MigrationResult } from "$lib/types/MigrationResult"; import type { Semaphore } from "$lib/types/Semaphore"; import type { AssistantStats } from "$lib/types/AssistantStats"; import { logger } from "$lib/server/logger"; +import { building } from "$app/environment"; export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; @@ -70,7 +71,9 @@ export class Database { * Return map of database's collections */ public getCollections() { - const db = this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + const db = this.client.db( + env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") + ); const conversations = db.collection("conversations"); const conversationStats = db.collection(CONVERSATION_STATS_COLLECTION); @@ -105,7 +108,6 @@ export class Database { }; } - /** * Init database once connected: Index creation * @private @@ -132,25 +134,19 @@ export class Database { { partialFilterExpression: { sessionId: { $exists: true } } } ) .catch(logger.error); - conversations .createIndex( { userId: 1, updatedAt: -1 }, { partialFilterExpression: { userId: { $exists: true } } } ) .catch(logger.error); - conversations .createIndex( { "message.id": 1, "message.ancestors": 1 }, { partialFilterExpression: { userId: { $exists: true } } } ) .catch(logger.error); - - // To do stats on conversations - conversations.createIndex({ updatedAt: 1 }).catch(logger.error); // Not strictly necessary, could use _id, but more convenient. Also for stats - conversations.createIndex({ createdAt: 1 }).catch(logger.error); // To do stats on conversation messages conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error); // Unique index for stats @@ -174,7 +170,9 @@ export class Database { "date.at": 1, }) .catch(logger.error); - abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(logger.error); + abortedGenerations + .createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }) + .catch(logger.error); abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error); sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error); settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error); @@ -204,5 +202,8 @@ export class Database { semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error); semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error); } - } + +export const collections = building + ? ({} as unknown as ReturnType) + : Database.getInstance().getCollections(); diff --git a/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts index 140f223aa52..86f84ac19b8 100644 --- a/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts +++ b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { HF_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { logger } from "$lib/server/logger"; export const embeddingEndpointHfApiSchema = z.object({ @@ -11,7 +11,7 @@ export const embeddingEndpointHfApiSchema = z.object({ authorization: z .string() .optional() - .transform((v) => (!v && HF_TOKEN ? "Bearer " + HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header + .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header }); export async function embeddingEndpointHfApi( diff --git a/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts index 89d7900bb28..527a9732498 100644 --- a/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts +++ b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { OPENAI_API_KEY } from "$env/static/private"; +import { env } from "$env/dynamic/private"; export const embeddingEndpointOpenAIParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("openai"), url: z.string().url().default("https://api.openai.com/v1/embeddings"), - apiKey: z.string().default(OPENAI_API_KEY), + apiKey: z.string().default(env.OPENAI_API_KEY), }); export async function embeddingEndpointOpenAI( diff --git a/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts index 6c2f2f51bb6..c999ceba7da 100644 --- a/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts +++ b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { HF_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { logger } from "$lib/server/logger"; export const embeddingEndpointTeiParametersSchema = z.object({ @@ -12,7 +12,7 @@ export const embeddingEndpointTeiParametersSchema = z.object({ authorization: z .string() .optional() - .transform((v) => (!v && HF_TOKEN ? "Bearer " + HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header + .transform((v) => (!v && env.HF_TOKEN ? "Bearer " + env.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header }); const getModelInfoByUrl = async (url: string, authorization?: string) => { diff --git a/src/lib/server/embeddingModels.ts b/src/lib/server/embeddingModels.ts index 96f3795bd93..67ad8fe5b1e 100644 --- a/src/lib/server/embeddingModels.ts +++ b/src/lib/server/embeddingModels.ts @@ -1,4 +1,4 @@ -import { TEXT_EMBEDDING_MODELS } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { z } from "zod"; import { sum } from "$lib/utils/sum"; @@ -29,7 +29,7 @@ const modelConfig = z.object({ // Default embedding model for backward compatibility const rawEmbeddingModelJSON = - TEXT_EMBEDDING_MODELS || + env.TEXT_EMBEDDING_MODELS || `[ { "name": "Xenova/gte-small", diff --git a/src/lib/server/endpoints/anthropic/endpointAnthropic.ts b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts index e3ef2eb51fa..4353c6b11a5 100644 --- a/src/lib/server/endpoints/anthropic/endpointAnthropic.ts +++ b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { ANTHROPIC_API_KEY } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; @@ -8,7 +8,7 @@ export const endpointAnthropicParametersSchema = z.object({ model: z.any(), type: z.literal("anthropic"), baseURL: z.string().url().default("https://api.anthropic.com"), - apiKey: z.string().default(ANTHROPIC_API_KEY ?? "sk-"), + apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"), defaultHeaders: z.record(z.string()).optional(), defaultQuery: z.record(z.string()).optional(), }); diff --git a/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts index 50f9ac57efa..f09d2723adb 100644 --- a/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts +++ b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts @@ -1,15 +1,15 @@ import { z } from "zod"; import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; -import { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { logger } from "$lib/server/logger"; export const endpointCloudflareParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("cloudflare"), - accountId: z.string().default(CLOUDFLARE_ACCOUNT_ID), - apiToken: z.string().default(CLOUDFLARE_API_TOKEN), + accountId: z.string().default(env.CLOUDFLARE_ACCOUNT_ID), + apiToken: z.string().default(env.CLOUDFLARE_API_TOKEN), }); export async function endpointCloudflare( diff --git a/src/lib/server/endpoints/cohere/endpointCohere.ts b/src/lib/server/endpoints/cohere/endpointCohere.ts index 524152fb991..f1c5562fa02 100644 --- a/src/lib/server/endpoints/cohere/endpointCohere.ts +++ b/src/lib/server/endpoints/cohere/endpointCohere.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { COHERE_API_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Cohere, CohereClient } from "cohere-ai"; @@ -9,7 +9,7 @@ export const endpointCohereParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("cohere"), - apiKey: z.string().default(COHERE_API_TOKEN), + apiKey: z.string().default(env.COHERE_API_TOKEN), raw: z.boolean().default(false), }); diff --git a/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts index 67b5ea954d0..b2b8d1478c2 100644 --- a/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +++ b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts @@ -1,4 +1,4 @@ -import { HF_ACCESS_TOKEN, HF_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { buildPrompt } from "$lib/buildPrompt"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Endpoint } from "../endpoints"; @@ -13,7 +13,7 @@ export const endpointLlamacppParametersSchema = z.object({ accessToken: z .string() .min(1) - .default(HF_TOKEN ?? HF_ACCESS_TOKEN), + .default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), }); export function endpointLlamacpp( diff --git a/src/lib/server/endpoints/openai/endpointOai.ts b/src/lib/server/endpoints/openai/endpointOai.ts index 8bd28540dd4..945921b1b9a 100644 --- a/src/lib/server/endpoints/openai/endpointOai.ts +++ b/src/lib/server/endpoints/openai/endpointOai.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { openAICompletionToTextGenerationStream } from "./openAICompletionToTextGenerationStream"; import { openAIChatToTextGenerationStream } from "./openAIChatToTextGenerationStream"; import { buildPrompt } from "$lib/buildPrompt"; -import { OPENAI_API_KEY } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import type { Endpoint } from "../endpoints"; export const endpointOAIParametersSchema = z.object({ @@ -10,7 +10,7 @@ export const endpointOAIParametersSchema = z.object({ model: z.any(), type: z.literal("openai"), baseURL: z.string().url().default("https://api.openai.com/v1"), - apiKey: z.string().default(OPENAI_API_KEY ?? "sk-"), + apiKey: z.string().default(env.OPENAI_API_KEY ?? "sk-"), completion: z .union([z.literal("completions"), z.literal("chat_completions")]) .default("chat_completions"), diff --git a/src/lib/server/endpoints/tgi/endpointTgi.ts b/src/lib/server/endpoints/tgi/endpointTgi.ts index 131d628ae21..aed06739722 100644 --- a/src/lib/server/endpoints/tgi/endpointTgi.ts +++ b/src/lib/server/endpoints/tgi/endpointTgi.ts @@ -1,4 +1,4 @@ -import { HF_ACCESS_TOKEN, HF_TOKEN } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { buildPrompt } from "$lib/buildPrompt"; import { textGenerationStream } from "@huggingface/inference"; import type { Endpoint } from "../endpoints"; @@ -9,7 +9,7 @@ export const endpointTgiParametersSchema = z.object({ model: z.any(), type: z.literal("tgi"), url: z.string().url(), - accessToken: z.string().default(HF_TOKEN ?? HF_ACCESS_TOKEN), + accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), authorization: z.string().optional(), }); diff --git a/src/lib/server/files/downloadFile.ts b/src/lib/server/files/downloadFile.ts index 667a37a3346..91b430fc5d8 100644 --- a/src/lib/server/files/downloadFile.ts +++ b/src/lib/server/files/downloadFile.ts @@ -1,5 +1,5 @@ import { error } from "@sveltejs/kit"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import type { SharedConversation } from "$lib/types/SharedConversation"; @@ -7,7 +7,7 @@ export async function downloadFile( sha256: string, convId: Conversation["_id"] | SharedConversation["_id"] ) { - const fileId = Database.getInstance().getCollections().bucket.find({ filename: `${convId.toString()}-${sha256}` }); + const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); let mime = ""; const content = await fileId.next().then(async (file) => { @@ -20,7 +20,7 @@ export async function downloadFile( mime = file.metadata?.mime; - const fileStream = Database.getInstance().getCollections().bucket.openDownloadStream(file._id); + const fileStream = collections.bucket.openDownloadStream(file._id); const fileBuffer = await new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; diff --git a/src/lib/server/files/uploadFile.ts b/src/lib/server/files/uploadFile.ts index bbb010b0e75..34452245741 100644 --- a/src/lib/server/files/uploadFile.ts +++ b/src/lib/server/files/uploadFile.ts @@ -1,11 +1,11 @@ import type { Conversation } from "$lib/types/Conversation"; import { sha256 } from "$lib/utils/sha256"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; export async function uploadFile(file: Blob, conv: Conversation): Promise { const sha = await sha256(await file.text()); - const upload = Database.getInstance().getCollections().bucket.openUploadStream(`${conv._id}-${sha}`, { + const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, { metadata: { conversation: conv._id.toString(), mime: "image/jpeg" }, }); diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts index 03b15c29522..58a59ba7336 100644 --- a/src/lib/server/metrics.ts +++ b/src/lib/server/metrics.ts @@ -40,5 +40,4 @@ export class MetricsServer { return MetricsServer.instance; } - } diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index 0ba2fa7c830..7cdbaf5f5bb 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -1,11 +1,4 @@ -import { - HF_TOKEN, - HF_API_ROOT, - MODELS, - OLD_MODELS, - TASK_MODEL, - HF_ACCESS_TOKEN, -} from "$env/static/private"; +import { env } from "$env/dynamic/private"; import type { ChatTemplateInput } from "$lib/types/Template"; import { compileTemplate } from "$lib/utils/template"; import { z } from "zod"; @@ -72,7 +65,7 @@ const modelConfig = z.object({ embeddingModel: validateEmbeddingModelByName(embeddingModels).optional(), }); -const modelsRaw = z.array(modelConfig).parse(JSON5.parse(MODELS)); +const modelsRaw = z.array(modelConfig).parse(JSON5.parse(env.MODELS)); async function getChatPromptRender( m: z.infer @@ -147,8 +140,8 @@ const addEndpoint = (m: Awaited>) => ({ if (!m.endpoints) { return endpointTgi({ type: "tgi", - url: `${HF_API_ROOT}/${m.name}`, - accessToken: HF_TOKEN ?? HF_ACCESS_TOKEN, + url: `${env.HF_API_ROOT}/${m.name}`, + accessToken: env.HF_TOKEN ?? env.HF_ACCESS_TOKEN, weight: 1, model: m, }); @@ -199,7 +192,7 @@ export const models = await Promise.all(modelsRaw.map((e) => processModel(e).the export const defaultModel = models[0]; // Models that have been deprecated -export const oldModels = OLD_MODELS +export const oldModels = env.OLD_MODELS ? z .array( z.object({ @@ -208,7 +201,7 @@ export const oldModels = OLD_MODELS displayName: z.string().min(1).optional(), }) ) - .parse(JSON5.parse(OLD_MODELS)) + .parse(JSON5.parse(env.OLD_MODELS)) .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name })) : []; @@ -219,9 +212,9 @@ export const validateModel = (_models: BackendModel[]) => { // if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself -export const smallModel = TASK_MODEL - ? (models.find((m) => m.name === TASK_MODEL) || - (await processModel(modelConfig.parse(JSON5.parse(TASK_MODEL))).then((m) => +export const smallModel = env.TASK_MODEL + ? (models.find((m) => m.name === env.TASK_MODEL) || + (await processModel(modelConfig.parse(JSON5.parse(env.TASK_MODEL))).then((m) => addEndpoint(m) ))) ?? defaultModel diff --git a/src/lib/server/summarize.ts b/src/lib/server/summarize.ts index f18639637bc..4cef6174dc9 100644 --- a/src/lib/server/summarize.ts +++ b/src/lib/server/summarize.ts @@ -1,10 +1,10 @@ -import { LLM_SUMMERIZATION } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; import type { Message } from "$lib/types/Message"; import { logger } from "$lib/server/logger"; export async function summarize(prompt: string) { - if (!LLM_SUMMERIZATION) { + if (!env.LLM_SUMMERIZATION) { return prompt.split(/\s+/g).slice(0, 5).join(" "); } diff --git a/src/lib/server/usageLimits.ts b/src/lib/server/usageLimits.ts index 0323e83fb50..e1f2390388a 100644 --- a/src/lib/server/usageLimits.ts +++ b/src/lib/server/usageLimits.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { USAGE_LIMITS, RATE_LIMIT } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import JSON5 from "json5"; // RATE_LIMIT is the legacy way to define messages per minute limit @@ -12,7 +12,7 @@ export const usageLimitsSchema = z messagesPerMinute: z .preprocess((val) => { if (val === undefined) { - return RATE_LIMIT; + return env.RATE_LIMIT; } return val; }, z.coerce.number().optional()) @@ -20,4 +20,4 @@ export const usageLimitsSchema = z }) .optional(); -export const usageLimits = usageLimitsSchema.parse(JSON5.parse(USAGE_LIMITS)); +export const usageLimits = usageLimitsSchema.parse(JSON5.parse(env.USAGE_LIMITS)); diff --git a/src/lib/server/websearch/runWebSearch.ts b/src/lib/server/websearch/runWebSearch.ts index 8eba2ddd627..1d06a39c5a8 100644 --- a/src/lib/server/websearch/runWebSearch.ts +++ b/src/lib/server/websearch/runWebSearch.ts @@ -5,7 +5,7 @@ import { chunk } from "$lib/utils/chunk"; import { findSimilarSentences } from "$lib/server/sentenceSimilarity"; import { getWebSearchProvider } from "./searchWeb"; import { defaultEmbeddingModel, embeddingModels } from "$lib/server/embeddingModels"; -import { WEBSEARCH_ALLOWLIST, WEBSEARCH_BLOCKLIST, ENABLE_LOCAL_FETCH } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import type { Conversation } from "$lib/types/Conversation"; import type { MessageUpdate } from "$lib/types/MessageUpdate"; @@ -22,8 +22,8 @@ const MAX_N_PAGES_EMBED = 5 as const; const listSchema = z.array(z.string()).default([]); -const allowList = listSchema.parse(JSON5.parse(WEBSEARCH_ALLOWLIST)); -const blockList = listSchema.parse(JSON5.parse(WEBSEARCH_BLOCKLIST)); +const allowList = listSchema.parse(JSON5.parse(env.WEBSEARCH_ALLOWLIST)); +const blockList = listSchema.parse(JSON5.parse(env.WEBSEARCH_BLOCKLIST)); export async function runWebSearch( conv: Conversation, @@ -52,7 +52,7 @@ export async function runWebSearch( let linksToUse = [...ragSettings.allowedLinks]; - if (ENABLE_LOCAL_FETCH !== "true") { + if (env.ENABLE_LOCAL_FETCH !== "true") { const localLinks = await Promise.all( linksToUse.map(async (link) => { try { diff --git a/src/lib/server/websearch/searchSearxng.ts b/src/lib/server/websearch/searchSearxng.ts index 5378f953b47..9507d536969 100644 --- a/src/lib/server/websearch/searchSearxng.ts +++ b/src/lib/server/websearch/searchSearxng.ts @@ -1,4 +1,4 @@ -import { SEARXNG_QUERY_URL } from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { logger } from "$lib/server/logger"; export async function searchSearxng(query: string) { @@ -6,7 +6,7 @@ export async function searchSearxng(query: string) { setTimeout(() => abortController.abort(), 10000); // Insert the query into the URL template - let url = SEARXNG_QUERY_URL.replace("", query); + let url = env.SEARXNG_QUERY_URL.replace("", query); // Check if "&format=json" already exists in the URL if (!url.includes("&format=json")) { diff --git a/src/lib/server/websearch/searchWeb.ts b/src/lib/server/websearch/searchWeb.ts index 94021e5c014..724be1227a7 100644 --- a/src/lib/server/websearch/searchWeb.ts +++ b/src/lib/server/websearch/searchWeb.ts @@ -1,13 +1,6 @@ import type { YouWebSearch } from "../../types/WebSearch"; import { WebSearchProvider } from "../../types/WebSearch"; -import { - SERPAPI_KEY, - SERPER_API_KEY, - SERPSTACK_API_KEY, - USE_LOCAL_WEBSEARCH, - SEARXNG_QUERY_URL, - YDC_API_KEY, -} from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { getJson } from "serpapi"; import type { GoogleParameters } from "serpapi"; import { searchWebLocal } from "./searchWebLocal"; @@ -15,9 +8,9 @@ import { searchSearxng } from "./searchSearxng"; // get which SERP api is providing web results export function getWebSearchProvider() { - if (YDC_API_KEY) { + if (env.YDC_API_KEY) { return WebSearchProvider.YOU; - } else if (SEARXNG_QUERY_URL) { + } else if (env.SEARXNG_QUERY_URL) { return WebSearchProvider.SEARXNG; } else { return WebSearchProvider.GOOGLE; @@ -26,22 +19,22 @@ export function getWebSearchProvider() { // Show result as JSON export async function searchWeb(query: string) { - if (USE_LOCAL_WEBSEARCH) { + if (env.USE_LOCAL_WEBSEARCH) { return await searchWebLocal(query); } - if (SEARXNG_QUERY_URL) { + if (env.SEARXNG_QUERY_URL) { return await searchSearxng(query); } - if (SERPER_API_KEY) { + if (env.SERPER_API_KEY) { return await searchWebSerper(query); } - if (YDC_API_KEY) { + if (env.YDC_API_KEY) { return await searchWebYouApi(query); } - if (SERPAPI_KEY) { + if (env.SERPAPI_KEY) { return await searchWebSerpApi(query); } - if (SERPSTACK_API_KEY) { + if (env.SERPSTACK_API_KEY) { return await searchSerpStack(query); } throw new Error("No You.com or Serper.dev or SerpAPI key found"); @@ -58,7 +51,7 @@ export async function searchWebSerper(query: string) { method: "POST", body: JSON.stringify(params), headers: { - "x-api-key": SERPER_API_KEY, + "x-api-key": env.SERPER_API_KEY, "Content-type": "application/json; charset=UTF-8", }, }); @@ -84,7 +77,7 @@ export async function searchWebSerpApi(query: string) { hl: "en", gl: "us", google_domain: "google.com", - api_key: SERPAPI_KEY, + api_key: env.SERPAPI_KEY, } satisfies GoogleParameters; // Show result as JSON @@ -97,7 +90,7 @@ export async function searchWebYouApi(query: string) { const response = await fetch(`https://api.ydc-index.io/search?query=${query}`, { method: "GET", headers: { - "X-API-Key": YDC_API_KEY, + "X-API-Key": env.YDC_API_KEY, "Content-type": "application/json; charset=UTF-8", }, }); @@ -123,7 +116,7 @@ export async function searchWebYouApi(query: string) { export async function searchSerpStack(query: string) { const response = await fetch( - `http://api.serpstack.com/search?access_key=${SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, + `http://api.serpstack.com/search?access_key=${env.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, { method: "GET", headers: { diff --git a/src/lib/utils/getShareUrl.ts b/src/lib/utils/getShareUrl.ts index ef4259f6ad3..5278ab6fd6e 100644 --- a/src/lib/utils/getShareUrl.ts +++ b/src/lib/utils/getShareUrl.ts @@ -1,6 +1,8 @@ import { base } from "$app/paths"; -import { PUBLIC_ORIGIN, PUBLIC_SHARE_PREFIX } from "$env/static/public"; +import { env as envPublic } from "$env/dynamic/public"; export function getShareUrl(url: URL, shareId: string): string { - return `${PUBLIC_SHARE_PREFIX || `${PUBLIC_ORIGIN || url.origin}${base}`}/r/${shareId}`; + return `${ + envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}` + }/r/${shareId}`; } diff --git a/src/lib/utils/isHuggingChat.ts b/src/lib/utils/isHuggingChat.ts index fbcbefbc546..df1ad80039e 100644 --- a/src/lib/utils/isHuggingChat.ts +++ b/src/lib/utils/isHuggingChat.ts @@ -1,3 +1,3 @@ -import { PUBLIC_APP_ASSETS } from "$env/static/public"; +import { env as envPublic } from "$env/dynamic/public"; -export const isHuggingChat = PUBLIC_APP_ASSETS === "huggingchat"; +export const isHuggingChat = envPublic.PUBLIC_APP_ASSETS === "huggingchat"; diff --git a/src/lib/utils/tree/addChildren.spec.ts b/src/lib/utils/tree/addChildren.spec.ts index 96d357db453..3c7861f2cbe 100644 --- a/src/lib/utils/tree/addChildren.spec.ts +++ b/src/lib/utils/tree/addChildren.spec.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -16,7 +16,7 @@ Object.freeze(newMessage); describe("addChildren", async () => { it("should let you append on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const convLength = conv.messages.length; @@ -26,14 +26,14 @@ describe("addChildren", async () => { }); it("should not let you create branches on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow(); }); it("should not let you create a message that already exists", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const messageThatAlreadyExists: Message = { @@ -46,7 +46,7 @@ describe("addChildren", async () => { }); it("should let you create branches on conversations with subtrees", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const nChildren = conv.messages[0].children?.length; @@ -57,7 +57,7 @@ describe("addChildren", async () => { it("should let you create a new leaf", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const parentId = conv.messages[conv.messages.length - 1].id; @@ -84,7 +84,7 @@ describe("addChildren", async () => { it("should throw if you don't specify a parentId in a conversation with messages", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addChildren(conv, newMessage)).toThrow(); @@ -92,7 +92,7 @@ describe("addChildren", async () => { it("should return the id of the new message", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual( diff --git a/src/lib/utils/tree/addSibling.spec.ts b/src/lib/utils/tree/addSibling.spec.ts index e9558d49607..ac22836653b 100644 --- a/src/lib/utils/tree/addSibling.spec.ts +++ b/src/lib/utils/tree/addSibling.spec.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -28,7 +28,7 @@ describe("addSibling", async () => { it("should fail on legacy conversations", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow( @@ -38,7 +38,7 @@ describe("addSibling", async () => { it("should fail if the sibling message doesn't exist", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( @@ -49,7 +49,7 @@ describe("addSibling", async () => { // TODO: This behaviour should be fixed, we do not need to fail on the root message. it("should fail if the sibling message is the root message", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); if (!conv.rootMessageId) throw new Error("Root message not found"); @@ -60,7 +60,7 @@ describe("addSibling", async () => { it("should add a sibling to a message", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // add sibling and check children count for parnets diff --git a/src/lib/utils/tree/buildSubtree.spec.ts b/src/lib/utils/tree/buildSubtree.spec.ts index 733d8069595..936fb8a2023 100644 --- a/src/lib/utils/tree/buildSubtree.spec.ts +++ b/src/lib/utils/tree/buildSubtree.spec.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -12,7 +12,7 @@ import { buildSubtree } from "./buildSubtree"; describe("buildSubtree", () => { it("a subtree in a legacy conversation should be just a slice", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // check middle @@ -33,7 +33,7 @@ describe("buildSubtree", () => { it("a subtree in a linear branch conversation should be the ancestors and the message", async () => { const convId = await insertLinearBranchConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); // check middle @@ -54,7 +54,7 @@ describe("buildSubtree", () => { it("should throw an error if the message is not found", async () => { const convId = await insertLinearBranchConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const id = "not-a-real-id-test"; @@ -64,7 +64,7 @@ describe("buildSubtree", () => { it("should throw an error if the ancestor is not found", async () => { const convId = await insertLinearBranchConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const id = "1-1-1-1-2"; @@ -87,7 +87,7 @@ describe("buildSubtree", () => { it("should work for conversation with subtrees", async () => { const convId = await insertSideBranchesConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const subtree = buildSubtree(conv, "1-1-1-1-2"); diff --git a/src/lib/utils/tree/convertLegacyConversation.spec.ts b/src/lib/utils/tree/convertLegacyConversation.spec.ts index 456a42b44cd..e8adc55abac 100644 --- a/src/lib/utils/tree/convertLegacyConversation.spec.ts +++ b/src/lib/utils/tree/convertLegacyConversation.spec.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; @@ -8,7 +8,7 @@ import { insertLegacyConversation } from "./treeHelpers.spec"; describe("convertLegacyConversation", () => { it("should convert a legacy conversation", async () => { const convId = await insertLegacyConversation(); - const conv = await Database.getInstance().getCollections().conversations.findOne({ _id: new ObjectId(convId) }); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); if (!conv) throw new Error("Conversation not found"); const newConv = convertLegacyConversation(conv); diff --git a/src/lib/utils/tree/treeHelpers.spec.ts b/src/lib/utils/tree/treeHelpers.spec.ts index 49e0efbff06..75ce04927c4 100644 --- a/src/lib/utils/tree/treeHelpers.spec.ts +++ b/src/lib/utils/tree/treeHelpers.spec.ts @@ -1,11 +1,11 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { describe, expect, it } from "vitest"; // function used to insert conversations used for testing export const insertLegacyConversation = async () => { - const res = await Database.getInstance().getCollections().conversations.insertOne({ + const res = await collections.conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -39,7 +39,7 @@ export const insertLegacyConversation = async () => { }; export const insertLinearBranchConversation = async () => { - const res = await Database.getInstance().getCollections().conversations.insertOne({ + const res = await collections.conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -83,7 +83,7 @@ export const insertLinearBranchConversation = async () => { }; export const insertSideBranchesConversation = async () => { - const res = await Database.getInstance().getCollections().conversations.insertOne({ + const res = await collections.conversations.insertOne({ _id: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 79acfbd80bf..a79c470f688 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,28 +1,18 @@ import type { LayoutServerLoad } from "./$types"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import { UrlDependency } from "$lib/types/UrlDependency"; import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; import { authCondition, requiresUser } from "$lib/server/auth"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; -import { - SERPAPI_KEY, - SERPER_API_KEY, - SERPSTACK_API_KEY, - MESSAGES_BEFORE_LOGIN, - YDC_API_KEY, - USE_LOCAL_WEBSEARCH, - SEARXNG_QUERY_URL, - ENABLE_ASSISTANTS, - ENABLE_ASSISTANTS_RAG, -} from "$env/static/private"; +import { env } from "$env/dynamic/private"; import { ObjectId } from "mongodb"; import type { ConvSidebar } from "$lib/types/ConvSidebar"; export const load: LayoutServerLoad = async ({ locals, depends }) => { depends(UrlDependency.ConversationList); - const settings = await Database.getInstance().getCollections().settings.findOne(authCondition(locals)); + const settings = await collections.settings.findOne(authCondition(locals)); // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled. if ( @@ -31,7 +21,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) ) { settings.activeModel = defaultModel.id; - await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { + await collections.settings.updateOne(authCondition(locals), { $set: { activeModel: defaultModel.id }, }); } @@ -42,26 +32,26 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { models.find((m) => m.id === settings?.activeModel)?.unlisted === true ) { settings.activeModel = defaultModel.id; - await Database.getInstance().getCollections().settings.updateOne(authCondition(locals), { + await collections.settings.updateOne(authCondition(locals), { $set: { activeModel: defaultModel.id }, }); } - const enableAssistants = ENABLE_ASSISTANTS === "true"; + const enableAssistants = env.ENABLE_ASSISTANTS === "true"; const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); const assistant = assistantActive ? JSON.parse( JSON.stringify( - await Database.getInstance().getCollections().assistants.findOne({ + await collections.assistants.findOne({ _id: new ObjectId(settings?.activeModel), }) ) ) : null; - const conversations = await Database.getInstance().getCollections().conversations + const conversations = await collections.conversations .find(authCondition(locals)) .sort({ updatedAt: -1 }) .project< @@ -85,9 +75,9 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), ]; - const assistants = await Database.getInstance().getCollections().assistants.find({ _id: { $in: assistantIds } }).toArray(); + const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); - const messagesBeforeLogin = MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0; + const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; let loginRequired = false; @@ -98,7 +88,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { // get the number of messages where `from === "assistant"` across all conversations. const totalMessages = ( - await Database.getInstance().getCollections().conversations + await collections.conversations .aggregate([ { $match: { ...authCondition(locals), "messages.from": "assistant" } }, { $project: { messages: 1 } }, @@ -136,12 +126,12 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { }) satisfies ConvSidebar[], settings: { searchEnabled: !!( - SERPAPI_KEY || - SERPER_API_KEY || - SERPSTACK_API_KEY || - YDC_API_KEY || - USE_LOCAL_WEBSEARCH || - SEARXNG_QUERY_URL + env.SERPAPI_KEY || + env.SERPER_API_KEY || + env.SERPSTACK_API_KEY || + env.YDC_API_KEY || + env.USE_LOCAL_WEBSEARCH || + env.SEARXNG_QUERY_URL ), ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt, ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, @@ -188,7 +178,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { }, assistant, enableAssistants, - enableAssistantsRAG: ENABLE_ASSISTANTS_RAG === "true", + enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", loginRequired, loginEnabled: requiresUser, guestMode: requiresUser && messagesBeforeLogin > 0, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index bcfa7c05f37..18f9a884b26 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,13 +7,7 @@ import { page } from "$app/stores"; import { browser } from "$app/environment"; - import { - PUBLIC_APPLE_APP_ID, - PUBLIC_APP_DESCRIPTION, - PUBLIC_ORIGIN, - PUBLIC_PLAUSIBLE_SCRIPT_URL, - } from "$env/static/public"; - import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME } from "$env/static/public"; + import { env as envPublic } from "$env/dynamic/public"; import { error } from "$lib/stores/errors"; import { createSettingsStore } from "$lib/stores/settings"; @@ -134,7 +128,7 @@ - {PUBLIC_APP_NAME} + {envPublic.PUBLIC_APP_NAME} @@ -142,44 +136,49 @@ {#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/")} - + - + - + {/if} - {#if PUBLIC_PLAUSIBLE_SCRIPT_URL && PUBLIC_ORIGIN} + {#if envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL && envPublic.PUBLIC_ORIGIN} {/if} - {#if PUBLIC_APPLE_APP_ID} - + {#if envPublic.PUBLIC_APPLE_APP_ID} + {/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d0410802aa4..abedc1c8862 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@ - {PUBLIC_APP_NAME} + {envPublic.PUBLIC_APP_NAME} " -H "Content-Type: application/json" -d '{"model": "OpenAssistant/oasst-sft-6-llama-30b-xor"}' export async function POST({ request }) { - if (!PARQUET_EXPORT_DATASET || !PARQUET_EXPORT_HF_TOKEN) { + if (!env.PARQUET_EXPORT_DATASET || !env.PARQUET_EXPORT_HF_TOKEN) { throw error(500, "Parquet export is not configured."); } @@ -44,7 +44,7 @@ export async function POST({ request }) { let count = 0; logger.info("Exporting conversations for model", model); - for await (const conversation of Database.getInstance().getCollections().settings.aggregate<{ + for await (const conversation of collections.settings.aggregate<{ title: string; created_at: Date; updated_at: Date; @@ -95,7 +95,7 @@ export async function POST({ request }) { logger.info("exporting convos with userId"); - for await (const conversation of Database.getInstance().getCollections().settings.aggregate<{ + for await (const conversation of collections.settings.aggregate<{ title: string; created_at: Date; updated_at: Date; @@ -144,10 +144,10 @@ export async function POST({ request }) { await uploadFile({ file: pathToFileURL(fileName) as URL, - credentials: { accessToken: PARQUET_EXPORT_HF_TOKEN }, + credentials: { accessToken: env.PARQUET_EXPORT_HF_TOKEN }, repo: { type: "dataset", - name: PARQUET_EXPORT_DATASET, + name: env.PARQUET_EXPORT_DATASET, }, }); diff --git a/src/routes/admin/stats/compute/+server.ts b/src/routes/admin/stats/compute/+server.ts index f892c5b4eaa..d8d7f0ec605 100644 --- a/src/routes/admin/stats/compute/+server.ts +++ b/src/routes/admin/stats/compute/+server.ts @@ -1,6 +1,6 @@ import { json } from "@sveltejs/kit"; import type { ConversationStats } from "$lib/types/ConversationStats"; -import { CONVERSATION_STATS_COLLECTION, Database } from "$lib/server/database"; +import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database"; import { logger } from "$lib/server/logger"; // Triger like this: @@ -21,7 +21,7 @@ async function computeStats(params: { span: ConversationStats["date"]["span"]; type: ConversationStats["type"]; }) { - const lastComputed = await Database.getInstance().getCollections().conversationStats.findOne( + const lastComputed = await collections.conversationStats.findOne( { "date.field": params.dateField, "date.span": params.span, type: params.type }, { sort: { "date.at": -1 } } ); @@ -212,7 +212,7 @@ async function computeStats(params: { }, ]; - await Database.getInstance().getCollections().conversations.aggregate(pipeline, { allowDiskUse: true }).next(); + await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next(); logger.info("Computed stats for", params.type, params.span, params.dateField); } diff --git a/src/routes/api/assistant/[id]/+server.ts b/src/routes/api/assistant/[id]/+server.ts index 997dd7985ea..74f7fe6c7e0 100644 --- a/src/routes/api/assistant/[id]/+server.ts +++ b/src/routes/api/assistant/[id]/+server.ts @@ -1,11 +1,11 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; export async function GET({ params }) { const id = params.id; const assistantId = new ObjectId(id); - const assistant = await Database.getInstance().getCollections().assistants.findOne({ + const assistant = await collections.assistants.findOne({ _id: assistantId, }); diff --git a/src/routes/api/assistants/+server.ts b/src/routes/api/assistants/+server.ts index 16029fa81db..ac588676cbc 100644 --- a/src/routes/api/assistants/+server.ts +++ b/src/routes/api/assistants/+server.ts @@ -1,9 +1,9 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import type { Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; import { generateQueryTokens } from "$lib/utils/searchTokens.js"; import type { Filter } from "mongodb"; -import { REQUIRE_FEATURED_ASSISTANTS } from "$env/static/private"; +import { env } from "$env/dynamic/private"; const NUM_PER_PAGE = 24; @@ -16,7 +16,7 @@ export async function GET({ url, locals }) { let user: Pick | null = null; if (username) { - user = await Database.getInstance().getCollections().users.findOne>( + user = await collections.users.findOne>( { username }, { projection: { _id: 1 } } ); @@ -27,11 +27,11 @@ export async function GET({ url, locals }) { // if there is no user, we show community assistants, so only show featured assistants const shouldBeFeatured = - REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { featured: true } : {}; + env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { featured: true } : {}; // if the user queried is not the current user, only show "public" assistants that have been shared before const shouldHaveBeenShared = - REQUIRE_FEATURED_ASSISTANTS === "true" && !createdByCurrentUser + env.REQUIRE_FEATURED_ASSISTANTS === "true" && !createdByCurrentUser ? { userCount: { $gt: 1 } } : {}; @@ -43,14 +43,14 @@ export async function GET({ url, locals }) { ...shouldBeFeatured, ...shouldHaveBeenShared, }; - const assistants = await Database.getInstance().getCollections().assistants + const assistants = await collections.assistants .find(filter) .skip(NUM_PER_PAGE * pageIndex) .sort({ userCount: -1 }) .limit(NUM_PER_PAGE) .toArray(); - const numTotalItems = await Database.getInstance().getCollections().assistants.countDocuments(filter); + const numTotalItems = await collections.assistants.countDocuments(filter); return Response.json({ assistants, diff --git a/src/routes/api/conversation/[id]/+server.ts b/src/routes/api/conversation/[id]/+server.ts index b9aee179a51..b2b7f1b8a86 100644 --- a/src/routes/api/conversation/[id]/+server.ts +++ b/src/routes/api/conversation/[id]/+server.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { authCondition } from "$lib/server/auth"; import { z } from "zod"; import { ObjectId } from "mongodb"; @@ -8,7 +8,7 @@ export async function GET({ locals, params }) { const convId = new ObjectId(id); if (locals.user?._id || locals.sessionId) { - const conv = await Database.getInstance().getCollections().conversations.findOne({ + const conv = await collections.conversations.findOne({ _id: convId, ...authCondition(locals), }); diff --git a/src/routes/api/conversations/+server.ts b/src/routes/api/conversations/+server.ts index 957c2c2a452..8b282a1b276 100644 --- a/src/routes/api/conversations/+server.ts +++ b/src/routes/api/conversations/+server.ts @@ -1,4 +1,4 @@ -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { authCondition } from "$lib/server/auth"; import type { Conversation } from "$lib/types/Conversation"; @@ -8,7 +8,7 @@ export async function GET({ locals, url }) { const p = parseInt(url.searchParams.get("p") ?? "0"); if (locals.user?._id || locals.sessionId) { - const convs = await Database.getInstance().getCollections().conversations + const convs = await collections.conversations .find({ ...authCondition(locals), }) diff --git a/src/routes/api/user/assistants/+server.ts b/src/routes/api/user/assistants/+server.ts index ab89c0ded02..8e8b66bf781 100644 --- a/src/routes/api/user/assistants/+server.ts +++ b/src/routes/api/user/assistants/+server.ts @@ -1,13 +1,13 @@ import { authCondition } from "$lib/server/auth"; import type { Conversation } from "$lib/types/Conversation"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; export async function GET({ locals }) { if (locals.user?._id || locals.sessionId) { - const settings = await Database.getInstance().getCollections().settings.findOne(authCondition(locals)); + const settings = await collections.settings.findOne(authCondition(locals)); - const conversations = await Database.getInstance().getCollections().conversations + const conversations = await collections.conversations .find(authCondition(locals)) .sort({ updatedAt: -1 }) .project>({ @@ -24,7 +24,7 @@ export async function GET({ locals }) { ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), ]; - const assistants = await Database.getInstance().getCollections().assistants.find({ _id: { $in: assistantIds } }).toArray(); + const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); const res = assistants .filter((el) => userAssistantsSet.has(el._id.toString())) diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts index ca0c7d52df0..fddb181b4b8 100644 --- a/src/routes/assistant/[assistantId]/+page.server.ts +++ b/src/routes/assistant/[assistantId]/+page.server.ts @@ -1,11 +1,11 @@ import { base } from "$app/paths"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; export const load = async ({ params }) => { try { - const assistant = await Database.getInstance().getCollections().assistants.findOne({ + const assistant = await collections.assistants.findOne({ _id: new ObjectId(params.assistantId), }); diff --git a/src/routes/assistant/[assistantId]/+page.svelte b/src/routes/assistant/[assistantId]/+page.svelte index 16608d07ef2..f19b879023b 100644 --- a/src/routes/assistant/[assistantId]/+page.svelte +++ b/src/routes/assistant/[assistantId]/+page.svelte @@ -6,7 +6,7 @@ import { useSettingsStore } from "$lib/stores/settings"; import type { PageData } from "./$types"; import { applyAction, enhance } from "$app/forms"; - import { PUBLIC_APP_NAME, PUBLIC_ORIGIN } from "$env/static/public"; + import { env as envPublic } from "$env/dynamic/public"; import { page } from "$app/stores"; import IconGear from "~icons/bi/gear-fill"; @@ -24,15 +24,16 @@ - + diff --git a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts index 61cfb7bd5c3..48633e4b524 100644 --- a/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts +++ b/src/routes/assistant/[assistantId]/thumbnail.png/+server.ts @@ -1,5 +1,5 @@ import ChatThumbnail from "./ChatThumbnail.svelte"; -import { Database } from "$lib/server/database"; +import { collections } from "$lib/server/database"; import { error, type RequestHandler } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import type { SvelteComponent } from "svelte"; @@ -13,7 +13,7 @@ import InterBold from "../../../../../static/fonts/Inter-Bold.ttf"; import sharp from "sharp"; export const GET: RequestHandler = (async ({ params }) => { - const assistant = await Database.getInstance().getCollections().assistants.findOne({ + const assistant = await collections.assistants.findOne({ _id: new ObjectId(params.assistantId), }); @@ -22,11 +22,11 @@ export const GET: RequestHandler = (async ({ params }) => { } let avatar = ""; - const fileId = Database.getInstance().getCollections().bucket.find({ filename: assistant._id.toString() }); + const fileId = collections.bucket.find({ filename: assistant._id.toString() }); const file = await fileId.next(); if (file) { avatar = await (async () => { - const fileStream = Database.getInstance().getCollections().bucket.openDownloadStream(file?._id); + const fileStream = collections.bucket.openDownloadStream(file?._id); const fileBuffer = await new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts index 27062001fce..0cf3663f704 100644 --- a/src/routes/assistants/+page.server.ts +++ b/src/routes/assistants/+page.server.ts @@ -1,6 +1,6 @@ import { base } from "$app/paths"; -import { ENABLE_ASSISTANTS, REQUIRE_FEATURED_ASSISTANTS } from "$env/static/private"; -import { Database } from "$lib/server/database"; +import { env } from "$env/dynamic/private"; +import { Database, collections } from "$lib/server/database.js"; import { SortKey, type Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; import { generateQueryTokens } from "$lib/utils/searchTokens.js"; @@ -10,7 +10,7 @@ import type { Filter } from "mongodb"; const NUM_PER_PAGE = 24; export const load = async ({ url, locals }) => { - if (!ENABLE_ASSISTANTS) { + if (!env.ENABLE_ASSISTANTS) { throw redirect(302, `${base}/`); } @@ -23,7 +23,7 @@ export const load = async ({ url, locals }) => { let user: Pick | null = null; if (username) { - user = await Database.getInstance().getCollections().users.findOne>( + user = await collections.users.findOne>( { username }, { projection: { _id: 1 } } ); @@ -34,11 +34,11 @@ export const load = async ({ url, locals }) => { // if there is no user, we show community assistants, so only show featured assistants const shouldBeFeatured = - REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { featured: true } : {}; + env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { featured: true } : {}; // if the user queried is not the current user, only show "public" assistants that have been shared before const shouldHaveBeenShared = - REQUIRE_FEATURED_ASSISTANTS === "true" && !createdByCurrentUser + env.REQUIRE_FEATURED_ASSISTANTS === "true" && !createdByCurrentUser ? { userCount: { $gt: 1 } } : {}; @@ -50,8 +50,9 @@ export const load = async ({ url, locals }) => { ...shouldBeFeatured, ...shouldHaveBeenShared, }; - const assistants = await Database.getInstance().getCollections().assistants - .find(filter) + const assistants = await Database.getInstance() + .getCollections() + .assistants.find(filter) .skip(NUM_PER_PAGE * pageIndex) .sort({ ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }), @@ -60,7 +61,9 @@ export const load = async ({ url, locals }) => { .limit(NUM_PER_PAGE) .toArray(); - const numTotalItems = await Database.getInstance().getCollections().assistants.countDocuments(filter); + const numTotalItems = await Database.getInstance() + .getCollections() + .assistants.countDocuments(filter); return { assistants: JSON.parse(JSON.stringify(assistants)) as Array, diff --git a/src/routes/assistants/+page.svelte b/src/routes/assistants/+page.svelte index e8a84bbcfc7..d2f90b6323c 100644 --- a/src/routes/assistants/+page.svelte +++ b/src/routes/assistants/+page.svelte @@ -1,7 +1,7 @@ - + - + diff --git a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte index b76e362af73..86af4085e1f 100644 --- a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +++ b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte @@ -1,5 +1,5 @@