diff --git a/Dockerfile b/Dockerfile index 9562cdf5dad7fe1406a65d9dc73f1dc81378e6bf..8805e9642b765f9f45cf65ef59ed735739f1d2a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,12 +31,11 @@ LABEL name="absd" \ # Copy only the relevant files COPY --from=builder /usr/build/src/server ./src/server +COPY --from=builder /usr/build/src/scripts ./src/scripts COPY --from=builder /usr/build/dist/ ./dist/ COPY --from=builder /usr/build/package.json ./package.json COPY --from=builder /usr/build/package-lock.json ./package-lock.json COPY ./data/ ./data/ -# Scripts are needed to build the database if there is none -COPY ./scripts/ ./scripts/ # Install production dependencies and remove the cache for extra space saving RUN npm ci && npm cache clean --force diff --git a/biome.json b/biome.json index 18bef1a36cfcc583c7d450793864abd1d69658bd..b724216311f220e92778bda923ac76e7ed274393 100644 --- a/biome.json +++ b/biome.json @@ -21,7 +21,8 @@ "rules": { "recommended": true, "complexity": { - "noForEach": "off" + "noForEach": "off", + "useArrowFunction": "off" } } } diff --git a/data/2024-09-21-stats.json b/data/2024-09-21-stats.json index 3a4842ccc1323111f5e151b1afc55e570d97e2fc..4eee69e6bf05cd23c61f8ad279b55acfcd4ad53e 100644 --- a/data/2024-09-21-stats.json +++ b/data/2024-09-21-stats.json @@ -40,7 +40,7 @@ "CoV-AbDab-PDB": 331, "EBOLA": 294 }, - "antibodiesPerHeavySegments": { + "antibodiesPerHeavySegment": { "IGHV1": 114950, "IGHV3": 405509, "IGHV4": 173337, @@ -57,7 +57,7 @@ "IGHV11": 10, "IGHV15": 4 }, - "antibodiesPerLightSegments": { + "antibodiesPerLightSegment": { "IGLV8": 6932, "IGLV6": 7105, "IGKV3": 165916, diff --git a/data/2024-12-03-stats.json b/data/2024-12-03-stats.json index 626fc71b43c0ab189a7e4f989afbade01d1daffa..5180df8b03a124c8ee87293e3eba7c8d2a531dcd 100644 --- a/data/2024-12-03-stats.json +++ b/data/2024-12-03-stats.json @@ -50,7 +50,7 @@ "OAS": 708308, "UNIPROT": 1822 }, - "antibodiesPerHeavySegments": { + "antibodiesPerHeavySegment": { "IGHV1": 231581, "IGHV2": 50537, "IGHV3": 679756, @@ -67,7 +67,7 @@ "IGHV12": 32, "IGHV15": 14 }, - "antibodiesPerLightSegments": { + "antibodiesPerLightSegment": { "IGKV4": 96494, "IGLV11": 31, "IGLV8": 10559, diff --git a/data/2024-12-23-stats.json b/data/2024-12-23-stats.json index 3d978b72c49948bc9257bd9dfa3bccd3c8fb2d17..ec864cf830db68b068c7e0b799c65642326aef6a 100644 --- a/data/2024-12-23-stats.json +++ b/data/2024-12-23-stats.json @@ -50,7 +50,7 @@ "UNIPROT": 1822, "EBOLA": 282 }, - "antibodiesPerHeavySegments": { + "antibodiesPerHeavySegment": { "IGHV7": 10884, "IGHV4": 296316, "IGHV1": 231576, @@ -67,7 +67,7 @@ "IGHV15": 14, "IGHV12": 32 }, - "antibodiesPerLightSegments": { + "antibodiesPerLightSegment": { "IGLV7": 18156, "IGLV1": 152339, "IGKV2": 93389, diff --git a/k8s/absd.yaml b/k8s/absd.yaml index ab68a7ef6d2283895a8947ca5c9101b796792c02..0886fd57178dc80e9627331a4b59d54c1d2ed795 100644 --- a/k8s/absd.yaml +++ b/k8s/absd.yaml @@ -9,7 +9,7 @@ spec: - ReadWriteMany resources: requests: - storage: 10Gi + storage: 20Gi storageClassName: ceph-fs --- apiVersion: apps/v1 diff --git a/package-lock.json b/package-lock.json index 44a3bf77b430055e8e614a3d1223c28ffa2ffe57..16d08d9f8734d7c892174fd60609bea89aa34372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,35 +9,35 @@ "version": "1.0.0", "license": "GPL-3.0", "dependencies": { - "@fastify/mongodb": "^9.0.1", - "@fastify/static": "^8.0.3", + "@fastify/mongodb": "^9.0.2", + "@fastify/static": "^8.0.4", "axios": "^1.7.9", - "env-schema": "^6.0.0", - "fastify": "^5.1.0", + "env-schema": "^6.0.1", + "fastify": "^5.2.1", "modern-normalize": "^3.0.1", - "mongodb": "^6.11.0", - "pinia": "^2.3.0", - "plotly.js-dist-min": "^2.35.2", - "qs": "^6.13.1", + "mongodb": "^6.12.0", + "pinia": "^2.3.1", + "plotly.js-dist-min": "^2.35.3", + "qs": "^6.14.0", "vue": "^3.5.13", "vue-router": "^4.5.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/node": "^22.10.1", + "@types/node": "^22.10.9", "@types/plotly.js-dist-min": "^2.3.4", - "@types/qs": "^6.9.17", + "@types/qs": "^6.9.18", "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", - "concurrently": "^9.1.0", + "concurrently": "^9.1.2", "cssnano": "^7.0.6", - "nodemon": "^3.1.7", + "nodemon": "^3.1.9", "pino-pretty": "^13.0.0", "rollup-plugin-brotli": "^3.1.0", "rollup-plugin-gzip": "^4.0.1", - "typescript": "^5.6.3", - "vite": "^6.0.3", - "vue-tsc": "^2.1.10" + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vue-tsc": "^2.2.0" } }, "node_modules/@babel/helper-string-parser": { @@ -251,9 +251,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], @@ -268,9 +268,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], @@ -285,9 +285,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], @@ -302,9 +302,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], @@ -319,9 +319,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], @@ -353,9 +353,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], @@ -370,9 +370,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], @@ -404,9 +404,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], @@ -421,9 +421,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], @@ -438,9 +438,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], @@ -455,9 +455,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], @@ -472,9 +472,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], @@ -489,9 +489,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], @@ -506,9 +506,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], @@ -523,9 +523,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], @@ -539,10 +539,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], @@ -557,9 +574,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", "cpu": [ "arm64" ], @@ -574,9 +591,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], @@ -591,9 +608,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], @@ -608,9 +625,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], @@ -625,9 +642,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], @@ -642,9 +659,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], @@ -690,6 +707,12 @@ "fast-json-stringify": "^6.0.0" } }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -700,15 +723,35 @@ } }, "node_modules/@fastify/mongodb": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@fastify/mongodb/-/mongodb-9.0.1.tgz", - "integrity": "sha512-ysl0jvD76dCFKLtoPSGbsZwyH/iM8qcplg9/PRWoZ0CZy8GYb2f929h4bSoyU1MurrkALBTCCV7x3N02+RNS7A==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@fastify/mongodb/-/mongodb-9.0.2.tgz", + "integrity": "sha512-h04HpQ7nVeB2eR4YPJiFWaeFot+E6K6DHP5ymby3WEhExnVMaxd6FUVszDoU+bM3MmK9wtIFgJLUfOKcYU+nKQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "fastify-plugin": "^5.0.0", "mongodb": "^6.5.0" } }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@fastify/send": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-3.3.0.tgz", @@ -723,9 +766,19 @@ } }, "node_modules/@fastify/static": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.0.3.tgz", - "integrity": "sha512-GHSoOVDIxEYEeVR5l044bRCuAKDErD/+9VE+Z9fnaTRr+DDz0Avrm4kKai1mHbPx6C0U7BVNthjd/gcMquZZUA==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.0.4.tgz", + "integrity": "sha512-JdJIlXDYXZxbTFQazWOEfHxyD5uRXqRsLnp4rV9MwJnxadA0rrWBI8ZelPF2TPk/xDi5wunY/6ZmfwHXld13bA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "@fastify/accept-negotiator": "^2.0.0", @@ -1139,9 +1192,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.9.tgz", + "integrity": "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==", "dev": true, "license": "MIT", "dependencies": { @@ -1166,9 +1219,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "dev": true, "license": "MIT" }, @@ -1200,30 +1253,30 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.8.tgz", - "integrity": "sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.8" + "@volar/source-map": "2.4.11" } }, "node_modules/@volar/source-map": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.8.tgz", - "integrity": "sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.8.tgz", - "integrity": "sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.8", + "@volar/language-core": "2.4.11", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -1295,17 +1348,17 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, "node_modules/@vue/language-core": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", - "integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", + "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.8", + "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", - "alien-signals": "^0.2.0", + "alien-signals": "^0.4.9", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" @@ -1450,9 +1503,9 @@ } }, "node_modules/alien-signals": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.0.tgz", - "integrity": "sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.14.tgz", + "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", "dev": true, "license": "MIT" }, @@ -1687,17 +1740,27 @@ "ieee754": "^1.2.1" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -1874,9 +1937,9 @@ "dev": true }, "node_modules/concurrently": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", - "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2145,23 +2208,6 @@ } } }, - "node_modules/define-data-property": { - "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==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2253,6 +2299,20 @@ "node": ">=12" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2291,9 +2351,20 @@ } }, "node_modules/env-schema": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/env-schema/-/env-schema-6.0.0.tgz", - "integrity": "sha512-/IHp1EmrfubUOfF1wfe8koDWM5/dxUDylHANPNrPyrsYWJ7KRiB8gXbjtqQBujmOhpSpXXOhhnaL+meb+MaGtA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/env-schema/-/env-schema-6.0.1.tgz", + "integrity": "sha512-WRD40Q25pP4NUbI3g3CNU5PPzcaiX7YYcPwiCZlfR4qGsKmTlckRixgHww0/fOXiXSNKA87pwshzq0ULTK/48A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", "dependencies": { "ajv": "^8.12.0", "dotenv": "^16.4.5", @@ -2301,13 +2372,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -2321,10 +2389,22 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2335,30 +2415,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/escalade": { @@ -2465,9 +2546,9 @@ "license": "MIT" }, "node_modules/fastify": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.1.0.tgz", - "integrity": "sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.1.tgz", + "integrity": "sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==", "funding": [ { "type": "github", @@ -2483,6 +2564,7 @@ "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", @@ -2490,9 +2572,8 @@ "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^4.0.0", - "proxy-addr": "^2.0.7", "rfdc": "^1.3.1", - "secure-json-parse": "^2.7.0", + "secure-json-parse": "^3.0.1", "semver": "^7.6.0", "toad-cache": "^3.7.0" } @@ -2503,6 +2584,22 @@ "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", "license": "MIT" }, + "node_modules/fastify/node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2585,14 +2682,6 @@ "node": ">= 6" } }, - "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.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2639,16 +2728,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2657,6 +2751,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", @@ -2725,33 +2832,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", - "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2840,11 +2920,12 @@ "license": "ISC" }, "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==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/is-binary-path": { @@ -2999,6 +3080,15 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -3084,13 +3174,13 @@ } }, "node_modules/mongodb": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.11.0.tgz", - "integrity": "sha512-yVbPw0qT268YKhG241vAMLaDQAPbRyTgo++odSgGc9kXnzOujQI60Iyj23B9sQQFPSvmNPvMZ3dsFz0aN55KgA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", + "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.0", + "bson": "^6.10.1", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -3098,7 +3188,7 @@ }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", @@ -3175,9 +3265,9 @@ "dev": true }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", "dev": true, "license": "MIT", "dependencies": { @@ -3354,9 +3444,9 @@ } }, "node_modules/pinia": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz", - "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.3", @@ -3476,9 +3566,9 @@ "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" }, "node_modules/plotly.js-dist-min": { - "version": "2.35.2", - "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.35.2.tgz", - "integrity": "sha512-oWDTf2kYOmTtEw3epeeSBdfH/H3OSktF0suST9oI6fIgKfbyd4MT7TPh8+CVzdHYllYon24Q0HI1hZjOnLqk6g==", + "version": "2.35.3", + "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.35.3.tgz", + "integrity": "sha512-sz2HLP8gkysLx/BanM2PtJTtZ1PLPwdHwMWNri2YxLBy3IOeuDsVQtlmWa4hoK3j/fi4naaD3uZJqH5ozM3zGg==", "license": "MIT" }, "node_modules/postcss": { @@ -3953,18 +4043,6 @@ "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", "license": "MIT" }, - "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/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3995,12 +4073,12 @@ } }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -4198,7 +4276,8 @@ "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true }, "node_modules/semver": { "version": "7.6.3", @@ -4217,23 +4296,6 @@ "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==", "license": "MIT" }, - "node_modules/set-function-length": { - "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==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4269,15 +4331,69 @@ } }, "node_modules/side-channel": { - "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -4558,9 +4674,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -4629,13 +4745,13 @@ "dev": true }, "node_modules/vite": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", - "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.0", + "esbuild": "^0.24.2", "postcss": "^8.4.49", "rollup": "^4.23.0" }, @@ -4744,15 +4860,14 @@ } }, "node_modules/vue-tsc": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", - "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.0.tgz", + "integrity": "sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "~2.4.8", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/package.json b/package.json index 031254e5a22d3d22ff7943bf357c8c0ded36c5cd..ca2a2f5190353ddd842c05438477a5d435857898 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "dev:client": "vite", "dev:server": "nodemon --use-strict src/server/app.js | pino-pretty -t 'yyyy-mm-dd HH:MM:ss'", "build": "vue-tsc && vite build", - "build:db": "node scripts/buildDatabase.js", - "stats": "node scripts/buildStatistics", - "validate": "node scripts/fastaValidator.js", + "build:db": "node src/scripts/buildDatabase.js", + "stats": "node src/scripts/displayStatistics", + "validate": "node src/scripts/fastaValidator.js", "lint": "npm run lint:client; npm run lint:server", "lint:fix": "npm run lint:client:fix; npm run lint:server:fix", "lint:client": "biome check src/client", @@ -28,34 +28,34 @@ "author": "Simon Malesys", "license": "GPL-3.0", "dependencies": { - "@fastify/mongodb": "^9.0.1", - "@fastify/static": "^8.0.3", + "@fastify/mongodb": "^9.0.2", + "@fastify/static": "^8.0.4", "axios": "^1.7.9", - "env-schema": "^6.0.0", - "fastify": "^5.1.0", + "env-schema": "^6.0.1", + "fastify": "^5.2.1", "modern-normalize": "^3.0.1", - "mongodb": "^6.11.0", - "pinia": "^2.3.0", - "plotly.js-dist-min": "^2.35.2", - "qs": "^6.13.1", + "mongodb": "^6.12.0", + "pinia": "^2.3.1", + "plotly.js-dist-min": "^2.35.3", + "qs": "^6.14.0", "vue": "^3.5.13", "vue-router": "^4.5.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/node": "^22.10.1", + "@types/node": "^22.10.9", "@types/plotly.js-dist-min": "^2.3.4", - "@types/qs": "^6.9.17", + "@types/qs": "^6.9.18", "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", - "concurrently": "^9.1.0", + "concurrently": "^9.1.2", "cssnano": "^7.0.6", - "nodemon": "^3.1.7", + "nodemon": "^3.1.9", "pino-pretty": "^13.0.0", "rollup-plugin-brotli": "^3.1.0", "rollup-plugin-gzip": "^4.0.1", - "typescript": "^5.6.3", - "vite": "^6.0.3", - "vue-tsc": "^2.1.10" + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vue-tsc": "^2.2.0" } } diff --git a/scripts/antibodySchema.json b/scripts/antibodySchema.json deleted file mode 100644 index 2402bc28624d9501c7e9627647615d9caaf04dfd..0000000000000000000000000000000000000000 --- a/scripts/antibodySchema.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "$jsonSchema": { - "title": "Antibody validation schema", - "required": [ - "_id", - "id", - "species", - "heavyChain", - "lightChain" - ], - "additionalProperties": false, - "properties": { - "_id": { - "bsonType": "objectId" - }, - "id": { - "bsonType": "string" - }, - "hashId": { - "bsonType": "string" - }, - "species": { - "bsonType": "string" - }, - "heavyChain": { - "bsonType": "object", - "properties": { - "headers": { - "bsonType": "array", - "items": { - "bsonType": "object", - "properties": { - "id": { - "bsonType": "string" - }, - "source": { - "bsonType": "string" - }, - "header": { - "bsonType": "string" - } - }, - "required": [ - "id", - "source", - "header" - ], - "additionalProperties": false - } - }, - "sequence": { - "bsonType": "string" - }, - "rawFasta": { - "bsonType": "string" - } - } - }, - "lightChain": { - "bsonType": "object", - "properties": { - "headers": { - "bsonType": "array", - "items": { - "bsonType": "object", - "properties": { - "id": { - "bsonType": "string" - }, - "source": { - "bsonType": "string" - }, - "header": { - "bsonType": "string" - } - }, - "required": [ - "id", - "source", - "header" - ], - "additionalProperties": false - } - }, - "sequence": { - "bsonType": "string" - }, - "rawFasta": { - "bsonType": "string" - } - } - } - } - } -} diff --git a/scripts/buildStatistics.js b/scripts/buildStatistics.js deleted file mode 100644 index 3b63f8d06d8b71876b1512029e66ef25c4abc9c9..0000000000000000000000000000000000000000 --- a/scripts/buildStatistics.js +++ /dev/null @@ -1,20 +0,0 @@ -import buildStatistics from './fastaStatsBuilder.js' - -/** - * Wrapper file of the fastaStatsBuilder to compute the - * statistics from the command line, with the - * npm script 'npm run stats'. - */ -buildStatistics().then(result => { - // Stringify the stats object with a replacer function - // to handle the sets. - const stringifiedStats = JSON.stringify(result, (key, value) => { - return value instanceof Set ? [...value] : value - }, 2) - - console.log(stringifiedStats) - process.exit(0) -}).catch(err => { - console.log(err) - process.exit(1) -}) diff --git a/scripts/fastaStatsBuilder.js b/scripts/fastaStatsBuilder.js deleted file mode 100644 index b1903c741e6df4052f7de49019b8c47ef56ef093..0000000000000000000000000000000000000000 --- a/scripts/fastaStatsBuilder.js +++ /dev/null @@ -1,152 +0,0 @@ -import { createReadStream } from 'node:fs' -import { readdir } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { Writable } from 'node:stream' -import { pipeline } from 'node:stream/promises' -import { fileURLToPath } from 'node:url' -import { SplitFastaStream } from './streams/SplitFastaStream.js' -import { ParseFastaStream } from './streams/ParseFastaStream.js' -import { SanitizeFastaStream } from './streams/SanitizeFastaStream.js' -import { BuildAntibodiesStream } from './streams/BuildAntibodiesStream.js' - -/** - * Statistics of the antibodies. - */ -const stats = { - antibodies: 0, - species: new Set(), - sources: new Set(), - antibodiesPerSpecies: {}, - antibodiesPerSource: {}, - antibodiesPerHeavySegments: {}, - antibodiesPerLightSegments: {}, - antibodiesOnlyInSource: {}, - antibodiesInMultipleSources: {} -} - -/** - * Make a unique list of the sources for an antibody. - * @param {object} antibody - The given antibody - * @return {Set<string>} The list of sources present in this antibody - */ -function listSources(antibody) { - const sources = new Set() - antibody.heavyChain.headers.forEach(header => { sources.add(header.source) }) - antibody.lightChain.headers.forEach(header => { sources.add(header.source) }) - return sources -} - -/** - * Make a unique list of the v gene segments for an antibody. - * @param {object} antibody - The given antibody - * @return {object} An object with the two lists of heavy and light chain segments. - */ -function listSegments(antibody) { - const segments = { - heavyChain: new Set(), - lightChain: new Set() - } - antibody.heavyChain.headers.forEach(header => { segments.heavyChain.add(header.vGeneSegment) }) - antibody.lightChain.headers.forEach(header => { segments.lightChain.add(header.vGeneSegment) }) - return segments -} - -/** - * Build the statistics - */ -class StatisticsStream extends Writable { - constructor(options) { - if (!options) options = {}; - options.objectMode = true; - super(options); - } - - _write(antibody, encoding, done) { - const species = antibody.species; - const sources = listSources(antibody); - const segments = listSegments(antibody); - - // Total antibodies - stats.antibodies += 1; - - // Total species - stats.species.add(antibody.species); - - // Total sources - stats.sources = stats.sources.union(sources); - - // Antibodies per species - stats.antibodiesPerSpecies[species] = stats.antibodiesPerSpecies[species] - ? stats.antibodiesPerSpecies[species] + 1 - : 1; - - // Antibodies per source - sources.forEach(source => { - stats.antibodiesPerSource[source] = stats.antibodiesPerSource[source] - ? stats.antibodiesPerSource[source] + 1 - : 1; - }); - - // Antibodies per heavy segments - segments.heavyChain.forEach(segment => { - stats.antibodiesPerHeavySegments[segment] = stats.antibodiesPerHeavySegments[segment] - ? stats.antibodiesPerHeavySegments[segment] + 1 - : 1; - }); - - // Antibodies per light segments - segments.lightChain.forEach(segment => { - stats.antibodiesPerLightSegments[segment] = stats.antibodiesPerLightSegments[segment] - ? stats.antibodiesPerLightSegments[segment] + 1 - : 1; - }); - - // Antibodies only in one source - if (sources.size === 1) { - const source = Array.from(sources)[0]; - stats.antibodiesOnlyInSource[source] = stats.antibodiesOnlyInSource[source] - ? stats.antibodiesOnlyInSource[source] + 1 - : 1; - } - - // Antibodies in multiple sources - if (sources.size >= 1) { - stats.antibodiesInMultipleSources[sources.size] = stats.antibodiesInMultipleSources[sources.size] - ? stats.antibodiesInMultipleSources[sources.size] + 1 - : 1; - } - - return done(); - } -} - -// ========================================================================= - -export default async function () { - const currentPath = dirname(fileURLToPath(import.meta.url)) - const dataFolderPath = resolve(currentPath, '../data') - const dataFolder = await readdir(dataFolderPath) - const fastaList = dataFolder.filter(fileName => { - return fileName.endsWith('.fasta') - }) - - for (const fastaFileName of fastaList) { - const fastaFile = resolve(dataFolderPath, fastaFileName) - const species = fastaFileName.replace('.fasta', '').replace('_', ' ') - - await pipeline( - createReadStream(fastaFile), - new SplitFastaStream(), - new ParseFastaStream(), - new SanitizeFastaStream(), - new BuildAntibodiesStream({ species }), - new StatisticsStream() - ).catch(err => { - console.log(`${fastaFile}: \x1B[1;31merror\x1B[0m`) - console.log(err.message) - process.exit(1) - }) - } - - return stats -} diff --git a/scripts/fastaValidator.js b/scripts/fastaValidator.js deleted file mode 100644 index 74593f62c60614dbeedb3b4d60f046df7cf2c37b..0000000000000000000000000000000000000000 --- a/scripts/fastaValidator.js +++ /dev/null @@ -1,89 +0,0 @@ -import { createReadStream } from 'node:fs' -import { readdir } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { createHash } from 'node:crypto' -import { Writable } from 'node:stream' -import { pipeline } from 'node:stream/promises' -import { fileURLToPath } from 'node:url' -import { SplitFastaStream } from './streams/SplitFastaStream.js' -import { ParseFastaStream } from './streams/ParseFastaStream.js' - -const fileNameFormat = /^[A-Z][a-z]+_[a-z]+\.fasta$/ -const idFormat = /^[^\s^]+$/ -const subHeaderFormat = /.+;[^\s]+;[^\s]+;[^\s]+$/ -const sequenceFormat = /^[ABCDEFGHIKLMNPQRSTUVWXYZ]+$/ - -/** - * Stream for validating fasta entries. - */ -export class ValidateFastaStream extends Writable { - constructor(options) { - if (!options) options = {}; - options.objectMode = true; - super(options); - - this.sequenceHashes = new Set(); - } - - _write(fastaObject, encoding, done) { - const [id, ...headers] = fastaObject.header.split('|||'); - const sequence = fastaObject.sequence; - - if (!idFormat.test(id)) { - return done(new Error(`\x1B[1;31mError with header: ${header} \nID must not contain any space.\x1B[0m`)); - } - - for (const subheader of headers) { - if (!subHeaderFormat.test(subheader)) { - return done(new Error(`\x1B[1;31mError with subheader: ${subheader} \nThe subheaders must have an id, V gene segment and source at their end, separated by semicolons: header;id;vGeneSegment;source. ID, V gene segment and source must not contain any space.\x1B[0m`)); - } - } - - if (!sequenceFormat.test(sequence)) { - return done(new Error(`\x1B[1;31mError with sequence: ${sequence} \nSequences must only contain characters among "ABCDEFGHIKLMNPQRSTUVWXYZ".\x1B[0m`)); - } - - const hash = createHash('sha256').update(sequence).digest('hex'); - - // Check the uniqueness of each sequence trough the whole fasta file - if (this.sequenceHashes.has(hash)) { - return done(new Error(`\x1B[1;31mDuplication error with sequence: ${sequence} \nThis one already exist in the file.`)); - } else { - this.sequenceHashes.add(hash); - } - - return done(); - } -} - -// ========================================================================= - -const currentPath = dirname(fileURLToPath(import.meta.url)) -const dataFolderPath = resolve(currentPath, '../data') -const dataFolder = await readdir(dataFolderPath) -const fastaList = dataFolder.filter(fileName => { - return fileName.endsWith('.fasta') -}) - -for (const fastaFileName of fastaList) { - // Validate that the fasta file name follows the format "Genus_species.fasta". - if (!fileNameFormat.test(fastaFileName)) { - console.log(`\x1B[1;31mError with file name: ${fastaFileName} \nFasta file names must follow the format "Genus_species.fasta".\x1B[0m`) - process.exit(1) - } - - const fastaFile = resolve(dataFolderPath, fastaFileName) - - await pipeline( - createReadStream(fastaFile, { encoding: 'utf-8' }), - new SplitFastaStream(), - new ParseFastaStream(), - new ValidateFastaStream(), - ).then(async () => { - console.log(`${fastaFile}: \x1B[1;32mvalidation complete\x1B[0m`) - }).catch(err => { - console.log(`${fastaFile}: \x1B[1;31merror\x1B[0m`) - console.log(err.message) - process.exit(1) - }) -} diff --git a/src/client/components/TheStatisticsPage.vue b/src/client/components/TheStatisticsPage.vue index 403386fed20b0207b9e68b53116ba3a4e1f3d7af..6aeeed698d40a8f4736d3bce36184ae8bfaeca37 100644 --- a/src/client/components/TheStatisticsPage.vue +++ b/src/client/components/TheStatisticsPage.vue @@ -119,8 +119,8 @@ onMounted(() => { watch(() => statistics.value?.antibodiesPerSpecies, () => buildSpeciesPlot()) watch(() => statistics.value?.antibodiesPerSource, () => buildSourcesPlot()) -watch(() => statistics.value?.antibodiesPerHeavySegments, () => buildHeavySegmentsPlot()) -watch(() => statistics.value?.antibodiesPerLightSegments, () => buildLightSegmentsPlot()) +watch(() => statistics.value?.antibodiesPerHeavySegment, () => buildHeavySegmentsPlot()) +watch(() => statistics.value?.antibodiesPerLightSegment, () => buildLightSegmentsPlot()) watch(() => statistics.value, () => buildSankeyPlot()) @@ -168,9 +168,9 @@ function buildHeavySegmentsPlot() { data[0].values = [] data[0].labels = [] - const antibodiesPerHeavySegments = Object.entries(statistics.value?.antibodiesPerHeavySegments) + const antibodiesPerHeavySegment = Object.entries(statistics.value?.antibodiesPerHeavySegment) - for (const [source, number] of antibodiesPerHeavySegments) { + for (const [source, number] of antibodiesPerHeavySegment) { data[0].labels.push(`${source} - <i>${number.toLocaleString()}<\i>`) data[0].values.push(number) } @@ -186,9 +186,9 @@ function buildLightSegmentsPlot() { data[0].values = [] data[0].labels = [] - const antibodiesPerLightSegments = Object.entries(statistics.value?.antibodiesPerLightSegments) + const antibodiesPerLightSegment = Object.entries(statistics.value?.antibodiesPerLightSegment) - for (const [source, number] of antibodiesPerLightSegments) { + for (const [source, number] of antibodiesPerLightSegment) { data[0].labels.push(`${source} - <i>${number.toLocaleString()}<\i>`) data[0].values.push(number) } diff --git a/scripts/buildDatabase.js b/src/scripts/buildDatabase.js similarity index 90% rename from scripts/buildDatabase.js rename to src/scripts/buildDatabase.js index 8ff878cc3eb02a952d54113d165cdc49e25548ec..c18da80b4e88e3dcfae7f8f163566e362ec5e3bd 100644 --- a/scripts/buildDatabase.js +++ b/src/scripts/buildDatabase.js @@ -1,5 +1,5 @@ import buildDatabase from './databaseBuilder.js' -import config from '../src/server/config/env.js' +import config from '../server/config/env.js' console.log(`--- Start building the \x1B[1m${config.ABSD_DB_NAME}\x1B[0m database`) diff --git a/scripts/databaseBuilder.js b/src/scripts/databaseBuilder.js similarity index 64% rename from scripts/databaseBuilder.js rename to src/scripts/databaseBuilder.js index e63c2d6342b3c34139b447c847b5962d96042bd6..a13774cb5e7f1244c3dc11fb3195835d0c5ad52f 100644 --- a/scripts/databaseBuilder.js +++ b/src/scripts/databaseBuilder.js @@ -1,5 +1,5 @@ import { createReadStream } from 'node:fs' -import { readdir } from 'node:fs/promises' +import { readdir, readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { pipeline } from 'node:stream/promises' import { fileURLToPath } from 'node:url' @@ -11,9 +11,9 @@ import { BuildAntibodiesStream } from './streams/BuildAntibodiesStream.js' import { GenerateHashIdStream } from './streams/GenerateHashIdStream.js' import { GroupAntibodiesStream } from './streams/GroupAntibodiesStream.js' import { InsertAntibodiesStream } from './streams/InsertAntibodiesStream.js' -import AntibodySchema from '../src/server/schemas/Antibodies.json' with { type: "json" } -import StatisticsSchema from '../src/server/schemas/Statistics.json' with { type: "json" } -import config from '../src/server/config/env.js' +import AntibodySchema from '../server/schemas/Antibodies.json' with { type: "json" } +import StatisticsSchema from '../server/schemas/Statistics.json' with { type: "json" } +import config from '../server/config/env.js' /** * Create or cleanup existing database to repopulate it @@ -53,17 +53,63 @@ async function createDatabase() { console.log(`Database \x1B[1;32m${config.ABSD_DB_NAME}\x1B[0m created`) } +/** + * Insert the last statistics file data into the database. + * This process is simple enough to not need streams. + * @param {object} stats - The statistics object + * @param {object} dbConfig - An object with the database configuration + * @returns {Promise<object>} - A promise with the results of the insertion. + */ +async function insertStatistics(stats, dbConfig) { + const uri = `mongodb://${dbConfig.host}:${dbConfig.port}` + const mongoClient = new MongoClient(uri) + const db = mongoClient.db(dbConfig.name) + const collection = db.collection('statistics') + + return collection.insertOne(stats) +} + +/** + * Get the last statistics from the stats files. + * @returns {Promise<object>} A promise with the statistics object. + */ +async function getStatistics() { + const currentPath = dirname(fileURLToPath(import.meta.url)) + const dataFolderPath = resolve(currentPath, '../../data') + const dataFolder = await readdir(dataFolderPath) + + const statsFilePath = dataFolder + .filter((file) => { return file.endsWith('-stats.json') }) + .sort() + .reverse() + .map(file => { return resolve(dataFolderPath, file) }) + .shift() + + const statsDate = new Date(statsFilePath.match(/(\d{4}-\d{2}-\d{2})-stats/)[1]) + const statsFile = await readFile(statsFilePath, 'utf8') + const stats = JSON.parse(statsFile) + stats.versionDate = statsDate + + return stats +} + // ========================================================================= export default async function buildDatabase() { const currentPath = dirname(fileURLToPath(import.meta.url)) - const dataFolderPath = resolve(currentPath, '../data') + const dataFolderPath = resolve(currentPath, '../../data') const dataFolder = await readdir(dataFolderPath) const fastaList = dataFolder.filter(fileName => { return fileName.endsWith('.fasta') }) + const stats = await getStatistics() await createDatabase() + await insertStatistics(stats, { + host: config.ABSD_DB_HOST, + port: config.ABSD_DB_PORT, + name: config.ABSD_DB_NAME + }) for (const fastaFileName of fastaList) { const fastaFile = resolve(dataFolderPath, fastaFileName) diff --git a/src/scripts/displayStatistics.js b/src/scripts/displayStatistics.js new file mode 100644 index 0000000000000000000000000000000000000000..b851c867782f07d4324a7d34a247c512a428d5e4 --- /dev/null +++ b/src/scripts/displayStatistics.js @@ -0,0 +1,13 @@ +import buildStatistics from './fastaStatsBuilder.js' + +/** + * Wrapper file of the fastaStatsBuilder to display the + * statistics from the command line, using a npm script. + */ +buildStatistics().then(stats => { + console.log(stats.toString()) + process.exit(0) +}).catch(err => { + console.log(err) + process.exit(1) +}) diff --git a/src/scripts/fastaStatsBuilder.js b/src/scripts/fastaStatsBuilder.js new file mode 100644 index 0000000000000000000000000000000000000000..873be1644d25bda0de326b6739488f1fc8d98a8f --- /dev/null +++ b/src/scripts/fastaStatsBuilder.js @@ -0,0 +1,44 @@ +import { createReadStream } from 'node:fs' +import { readdir } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { pipeline } from 'node:stream/promises' +import { fileURLToPath } from 'node:url' +import { SplitFastaStream } from './streams/SplitFastaStream.js' +import { ParseFastaStream } from './streams/ParseFastaStream.js' +import { SanitizeFastaStream } from './streams/SanitizeFastaStream.js' +import { BuildAntibodiesStream } from './streams/BuildAntibodiesStream.js' +import { StatisticsStream } from './streams/StatisticsStream.js' +import { Statistics } from '../server/models/Statistics.js' + +// ========================================================================= + +export default async function () { + const currentPath = dirname(fileURLToPath(import.meta.url)) + const dataFolderPath = resolve(currentPath, '../../data') + const dataFolder = await readdir(dataFolderPath) + const fastaList = dataFolder.filter(fileName => { + return fileName.endsWith('.fasta') + }) + + const stats = new Statistics() + + for (const fastaFileName of fastaList) { + const fastaFile = resolve(dataFolderPath, fastaFileName) + const species = fastaFileName.replace('.fasta', '').replace('_', ' ') + + await pipeline( + createReadStream(fastaFile), + new SplitFastaStream(), + new ParseFastaStream(), + new SanitizeFastaStream(), + new BuildAntibodiesStream({ species }), + new StatisticsStream({ stats }) + ).catch(err => { + console.log(`${fastaFile}: \x1B[1;31merror\x1B[0m`) + console.log(err.message) + process.exit(1) + }) + } + + return stats +} diff --git a/src/scripts/fastaValidator.js b/src/scripts/fastaValidator.js new file mode 100644 index 0000000000000000000000000000000000000000..0dbf374e836df3e9060aeb9f364eeeaf33e474b9 --- /dev/null +++ b/src/scripts/fastaValidator.js @@ -0,0 +1,40 @@ +import { createReadStream } from 'node:fs' +import { readdir } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { pipeline } from 'node:stream/promises' +import { fileURLToPath } from 'node:url' +import { SplitFastaStream } from './streams/SplitFastaStream.js' +import { ParseFastaStream } from './streams/ParseFastaStream.js' +import { ValidateFastaStream } from './streams/ValidateFastaStream.js' + +// ========================================================================= + +const currentPath = dirname(fileURLToPath(import.meta.url)) +const dataFolderPath = resolve(currentPath, '../../data') +const dataFolder = await readdir(dataFolderPath) +const fastaList = dataFolder.filter(fileName => { + return fileName.endsWith('.fasta') +}) + +for (const fastaFileName of fastaList) { + // Validate that the fasta file name follows the format "Genus_species.fasta". + if (!ValidateFastaStream.fileNameFormat.test(fastaFileName)) { + console.log(`\x1B[1;31mError with file name: ${fastaFileName} \nFasta file names must follow the format "Genus_species.fasta".\x1B[0m`) + process.exit(1) + } + + const fastaFile = resolve(dataFolderPath, fastaFileName) + + await pipeline( + createReadStream(fastaFile, { encoding: 'utf-8' }), + new SplitFastaStream(), + new ParseFastaStream(), + new ValidateFastaStream(), + ).then(async () => { + console.log(`${fastaFile}: \x1B[1;32mvalidation complete\x1B[0m`) + }).catch(err => { + console.log(`${fastaFile}: \x1B[1;31merror\x1B[0m`) + console.log(err.message) + process.exit(1) + }) +} diff --git a/scripts/streams/BuildAntibodiesStream.js b/src/scripts/streams/BuildAntibodiesStream.js similarity index 92% rename from scripts/streams/BuildAntibodiesStream.js rename to src/scripts/streams/BuildAntibodiesStream.js index a52392e47e699028af639adecb2dd1f68d70effc..5461dbf9a173f83cffeca33fa36faae333d54906 100644 --- a/scripts/streams/BuildAntibodiesStream.js +++ b/src/scripts/streams/BuildAntibodiesStream.js @@ -54,11 +54,15 @@ export class BuildAntibodiesStream extends Transform { } _transform(fastaEntry, encoding, done) { + // Light chains always come first, so store it + // for the next entry. if (!this.lightChain) { this.lightChain = fastaEntry; return done(); } + // If a light chain is stored, then we can build + // the antibody. this.push({ id: this.lightChain.header.split('|||')[0], species: this.species, diff --git a/scripts/streams/GenerateHashIdStream.js b/src/scripts/streams/GenerateHashIdStream.js similarity index 62% rename from scripts/streams/GenerateHashIdStream.js rename to src/scripts/streams/GenerateHashIdStream.js index 5221b835f956b37ea9be278d7dfe364ef198b718..2a70434f0d95a81fcd094f93811f5565524f4b51 100644 --- a/scripts/streams/GenerateHashIdStream.js +++ b/src/scripts/streams/GenerateHashIdStream.js @@ -1,5 +1,5 @@ -import { createHash } from "crypto"; import { Transform } from "stream"; +import { Antibody } from "../../server/models/Antibody.js"; /** * Add the hashId field to antibodies object. @@ -13,14 +13,7 @@ export class GenerateHashIdStream extends Transform { } _transform(antibody, encoding, done) { - const hashInput = antibody.species + - antibody.heavyChain.sequence + - antibody.lightChain.sequence; - - antibody.hashId = createHash('sha256') - .update(hashInput, 'ascii') - .digest('hex'); - + antibody.hashId = Antibody.generateHashId(antibody) this.push(antibody); return done(); } diff --git a/scripts/streams/GroupAntibodiesStream.js b/src/scripts/streams/GroupAntibodiesStream.js similarity index 100% rename from scripts/streams/GroupAntibodiesStream.js rename to src/scripts/streams/GroupAntibodiesStream.js diff --git a/scripts/streams/InsertAntibodiesStream.js b/src/scripts/streams/InsertAntibodiesStream.js similarity index 96% rename from scripts/streams/InsertAntibodiesStream.js rename to src/scripts/streams/InsertAntibodiesStream.js index f92129b726812852f51bb1593955c7df6044ca5c..9ed83b21810bc5362bcea3a6b34187d5b8136a76 100644 --- a/scripts/streams/InsertAntibodiesStream.js +++ b/src/scripts/streams/InsertAntibodiesStream.js @@ -1,6 +1,6 @@ import { MongoClient } from "mongodb"; import { Writable } from "stream"; -import config from "../../src/server/config/env.js"; +import config from "../../server/config/env.js"; /** * Insert groups of antibodies into the MongoDB database. diff --git a/scripts/streams/ParseFastaStream.js b/src/scripts/streams/ParseFastaStream.js similarity index 100% rename from scripts/streams/ParseFastaStream.js rename to src/scripts/streams/ParseFastaStream.js diff --git a/scripts/streams/SanitizeFastaStream.js b/src/scripts/streams/SanitizeFastaStream.js similarity index 100% rename from scripts/streams/SanitizeFastaStream.js rename to src/scripts/streams/SanitizeFastaStream.js diff --git a/scripts/streams/SplitFastaStream.js b/src/scripts/streams/SplitFastaStream.js similarity index 100% rename from scripts/streams/SplitFastaStream.js rename to src/scripts/streams/SplitFastaStream.js diff --git a/src/scripts/streams/StatisticsStream.js b/src/scripts/streams/StatisticsStream.js new file mode 100644 index 0000000000000000000000000000000000000000..1a68970db2470509a79d804b33f19f7d6e7dcda0 --- /dev/null +++ b/src/scripts/streams/StatisticsStream.js @@ -0,0 +1,75 @@ +import { Writable } from 'node:stream' +import { Antibody } from '../../server/models/Antibody.js' + +// ========================================================================= + +/** + * Build the statistics by incrementing a given stats object. + */ +export class StatisticsStream extends Writable { + constructor(options) { + if (!options) options = {}; + options.objectMode = true; + super(options); + + this.stats = options.stats + } + + _write(antibody, encoding, done) { + const species = antibody.species; + const sources = Antibody.listSources(antibody); + const segments = Antibody.listSegments(antibody); + + // Total antibodies + this.stats.antibodies += 1; + + // Total species + this.stats.species.add(antibody.species); + + // Total sources + this.stats.sources = this.stats.sources.union(sources); + + // Antibodies per species + this.stats.antibodiesPerSpecies[species] = this.stats.antibodiesPerSpecies[species] + ? this.stats.antibodiesPerSpecies[species] + 1 + : 1; + + // Antibodies per source + sources.forEach(source => { + this.stats.antibodiesPerSource[source] = this.stats.antibodiesPerSource[source] + ? this.stats.antibodiesPerSource[source] + 1 + : 1; + }); + + // Antibodies per heavy segment + segments.heavyChain.forEach(segment => { + this.stats.antibodiesPerHeavySegment[segment] = this.stats.antibodiesPerHeavySegment[segment] + ? this.stats.antibodiesPerHeavySegment[segment] + 1 + : 1; + }); + + // Antibodies per light segment + segments.lightChain.forEach(segment => { + this.stats.antibodiesPerLightSegment[segment] = this.stats.antibodiesPerLightSegment[segment] + ? this.stats.antibodiesPerLightSegment[segment] + 1 + : 1; + }); + + // Antibodies only in one source + if (sources.size === 1) { + const source = Array.from(sources)[0]; + this.stats.antibodiesOnlyInSource[source] = this.stats.antibodiesOnlyInSource[source] + ? this.stats.antibodiesOnlyInSource[source] + 1 + : 1; + } + + // Antibodies in multiple sources + if (sources.size >= 1) { + this.stats.antibodiesInMultipleSources[sources.size] = this.stats.antibodiesInMultipleSources[sources.size] + ? this.stats.antibodiesInMultipleSources[sources.size] + 1 + : 1; + } + + return done(); + } +} diff --git a/src/scripts/streams/ValidateFastaStream.js b/src/scripts/streams/ValidateFastaStream.js new file mode 100644 index 0000000000000000000000000000000000000000..246364da6dc5e4c1be6443ccd73e9509d48c83bb --- /dev/null +++ b/src/scripts/streams/ValidateFastaStream.js @@ -0,0 +1,50 @@ +import { createHash } from 'crypto'; +import { Writable } from 'stream'; + +/** + * Stream for validating fasta entries. + */ +export class ValidateFastaStream extends Writable { + static fileNameFormat = /^[A-Z][a-z]+_[a-z]+\.fasta$/ + static idFormat = /^[^\s^]+$/ + static subHeaderFormat = /.+;[^\s]+;[^\s]+;[^\s]+$/ + static sequenceFormat = /^[ABCDEFGHIKLMNPQRSTUVWXYZ]+$/ + + constructor(options) { + if (!options) options = {}; + options.objectMode = true; + super(options); + + this.sequenceHashes = new Set(); + } + + _write(fastaObject, encoding, done) { + const [id, ...headers] = fastaObject.header.split('|||'); + const sequence = fastaObject.sequence; + + if (!ValidateFastaStream.idFormat.test(id)) { + return done(new Error(`\x1B[1;31mError with header: ${header} \nID must not contain any space.\x1B[0m`)); + } + + for (const subheader of headers) { + if (!ValidateFastaStream.subHeaderFormat.test(subheader)) { + return done(new Error(`\x1B[1;31mError with subheader: ${subheader} \nThe subheaders must have an id, V gene segment and source at their end, separated by semicolons: header;id;vGeneSegment;source. ID, V gene segment and source must not contain any space.\x1B[0m`)); + } + } + + if (!ValidateFastaStream.sequenceFormat.test(sequence)) { + return done(new Error(`\x1B[1;31mError with sequence: ${sequence} \nSequences must only contain characters among "ABCDEFGHIKLMNPQRSTUVWXYZ".\x1B[0m`)); + } + + const hash = createHash('sha256').update(sequence).digest('hex'); + + // Check the uniqueness of each sequence trough the whole fasta file + if (this.sequenceHashes.has(hash)) { + return done(new Error(`\x1B[1;31mDuplication error with sequence: ${sequence} \nThis one already exist in the file.`)); + } else { + this.sequenceHashes.add(hash); + } + + return done(); + } +} diff --git a/src/server/app.js b/src/server/app.js index 9891fbbb8af75b16ca42aea91758beafcf6e2383..a635b5ab67400fc56d785b0c4643c49d5cbc0056 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -20,7 +20,7 @@ import { fileURLToPath } from 'node:url' import fastifyMongo from '@fastify/mongodb' import fastifyStatic from '@fastify/static' import Fastify from 'fastify' -import buildDatabase from "../../scripts/databaseBuilder.js" +import buildDatabase from "../../src/scripts/databaseBuilder.js" import envConfig from './config/env.js' import { ArchiveJobs } from './models/ArchiveJobs.js' import antibodiesCountRoute from './routes/antibodiesCountRoute.js' @@ -37,6 +37,7 @@ import downloadsRoute from './routes/downloadsRoute.js' import healthCheckRoute from './routes/healthCheckRoute.js' import readyCheckRoute from './routes/readyCheckRoute.js' import statisticsRoute from './routes/statisticsRoute.js' +import databaseIsSync from './utils/databaseIsSync.js' /** * Setup the fastify instance with custom @@ -164,7 +165,9 @@ fastify.listen({ host: envConfig.ABSD_SERVER_HOST, port: envConfig.ABSD_SERVER_PORT }).then(async () => { - if (envConfig.NODE_ENV === 'production') { + const dbShouldBeRebuilt = !await databaseIsSync(fastify) + + if (envConfig.NODE_ENV === 'production' && dbShouldBeRebuilt) { return await buildDatabase() } }).then((dbResults) => { diff --git a/src/server/models/Antibody.js b/src/server/models/Antibody.js new file mode 100644 index 0000000000000000000000000000000000000000..8c8e67b869f63ed729d58197335f813ca7f9e581 --- /dev/null +++ b/src/server/models/Antibody.js @@ -0,0 +1,90 @@ +// ABSD +// Copyright (C) 2023 Institut Pasteur +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +import { createHash } from "node:crypto"; + +/** + * The main Antibody model. + * Purely static class to factorize the Antibody related functions. + */ +export class Antibody { + /** + * Make a unique list of the sources for an antibody. + * @param {object} antibody - The given antibody + * @return {Set<string>} The list of sources present in this antibody + */ + static listSources(antibody) { + const sources = new Set() + antibody.heavyChain.headers.forEach(header => { sources.add(header.source) }) + antibody.lightChain.headers.forEach(header => { sources.add(header.source) }) + return sources + } + + /** + * Make a unique list of the v gene segments for an antibody. + * @param {object} antibody - The given antibody + * @return {object} An object with the two lists of heavy and light chain segments. + */ + static listSegments(antibody) { + const segments = { + heavyChain: new Set(), + lightChain: new Set() + } + antibody.heavyChain.headers.forEach(header => { segments.heavyChain.add(header.vGeneSegment) }) + antibody.lightChain.headers.forEach(header => { segments.lightChain.add(header.vGeneSegment) }) + return segments + } + + /** + * Generate a sha256 hash for a given antibody, + * using the species, light and heavy sequences. + * @param {object} antibody - The given antibody + * @returns {string} The sha256 hash + */ + static generateHashId(antibody) { + const hashInput = antibody.species + + antibody.heavyChain.sequence + + antibody.lightChain.sequence; + + return createHash('sha256') + .update(hashInput, 'ascii') + .digest('hex'); + } + + /** + * Turn an antibody object into a FASTA entry, + * using the rawFasta property. + * @param {object} antibody - The given antibody + * @returns {string} The antibody as a FASTA data string + */ + static toFasta(antibody) { + return [ + `>${antibody.lightChain.rawFasta}`, + antibody.lightChain.sequence, + `>${antibody.heavyChain.rawFasta}`, + antibody.heavyChain.sequence + ].join('\n') + } + + /** + * Turn an antibody object into string. + * @param {object} antibody - The given antibody + * @returns {string} the stringified antibody + */ + static toString(antibody) { + return JSON.stringify(antibody) + } +} diff --git a/src/server/models/AntibodyFastaStream.js b/src/server/models/AntibodyFastaStream.js index bc68171921d1402e4786a90e5a16710dd8f88784..2e1f61a707868267b91df05e2f84252a6cb289b7 100644 --- a/src/server/models/AntibodyFastaStream.js +++ b/src/server/models/AntibodyFastaStream.js @@ -15,7 +15,7 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. import { Transform } from "node:stream" -import antibodyToFasta from "../utils/antibodyToFasta.js" +import { Antibody } from "./Antibody.js" /** * Transform stream for turning antibodies entries from the database @@ -31,10 +31,10 @@ export default class AntibodyFastaStream extends Transform { super(options) } - // Use the antibodyToFasta function to simply turn an entry into + // Use the toFasta function to simply turn an entry into // a FASTA string. _transform(antibody, encoding, done) { - this.push(`${antibodyToFasta(antibody)}\n`) + this.push(`${Antibody.toFasta(antibody)}\n`) return done() } } diff --git a/src/server/models/AntibodyJsonStream.js b/src/server/models/AntibodyJsonStream.js index 424b579b0a44158270ea77034bb2efa3a8f7b164..72f55865b26dd744dc1c8e31c451c2f8df38eef7 100644 --- a/src/server/models/AntibodyJsonStream.js +++ b/src/server/models/AntibodyJsonStream.js @@ -15,6 +15,7 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. import { Transform } from "node:stream" +import { Antibody } from "./Antibody.js" /** * Transform stream for turning antibodies entries from the database @@ -51,12 +52,12 @@ export default class AntibodyJsonStream extends Transform { // will be the last one. _transform(antibody, encoding, done) { if (this.firstEntry) { - this.push(JSON.stringify(antibody)) + this.push(Antibody.toString(antibody)) this.firstEntry = false return done() } - this.push(`,\n${JSON.stringify(antibody)}`) + this.push(`,\n${Antibody.toString(antibody)}`) return done() } diff --git a/src/server/utils/antibodyToFasta.js b/src/server/models/Statistics.js similarity index 54% rename from src/server/utils/antibodyToFasta.js rename to src/server/models/Statistics.js index 56812e21d1e49e32e0d29b88ff0a26d04c228ff6..d38b5122266020350fe70af4b80ce3b7699c3376 100644 --- a/src/server/utils/antibodyToFasta.js +++ b/src/server/models/Statistics.js @@ -15,16 +15,27 @@ // along with this program. If not, see <http://www.gnu.org/licenses/>. /** - * Turn an antibody object into a FASTA entry, - * using the rawFasta property. - * @param {object} antibody - The antibody from the database - * @returns {string} The antibody as a FASTA data string + * Statistics about the antibodies. */ -export default function antibodyToFasta(antibody) { - return [ - `>${antibody.lightChain.rawFasta}`, - antibody.lightChain.sequence, - `>${antibody.heavyChain.rawFasta}`, - antibody.heavyChain.sequence - ].join('\n') +export class Statistics { + antibodies = 0 + species = new Set() + sources = new Set() + antibodiesPerSpecies = {} + antibodiesPerSource = {} + antibodiesPerHeavySegment = {} + antibodiesPerLightSegment = {} + antibodiesOnlyInSource = {} + antibodiesInMultipleSources = {} + + /** + * Stringify the stats object with a replacer function + * to handle the sets. + * @returns {string} The stringified stats object + */ + toString() { + return JSON.stringify(this, (key, value) => { + return value instanceof Set ? [...value] : value + }, 2) + } } diff --git a/src/server/routes/antibodiesDownloadRoute.js b/src/server/routes/antibodiesDownloadRoute.js index 8fbf2587337ba9ff1e92bba17376b003c57631d3..5b2b933695b02b4acbc0b931987faa593b2842dc 100644 --- a/src/server/routes/antibodiesDownloadRoute.js +++ b/src/server/routes/antibodiesDownloadRoute.js @@ -58,12 +58,15 @@ export default { ], handler: function (request, reply) { const archiveId = `absd-${randomUUID()}` + const requestFields = request.query.format === 'fasta' + ? fieldsProjection.fasta + : fieldsProjection.json this.archiveJobs.add(new ArchiveBuilder({ archiveId, archiveFormat: request.query.format, requestWhere: request.where, - requestFields: fieldsProjection + requestFields })) reply.code(202).send({ diff --git a/src/server/routes/readyCheckRoute.js b/src/server/routes/readyCheckRoute.js index 5f45c42b12621bd4f0a706b8a526cfe5a54d9663..2ec8df6c46b393ef58917fe78b819191a7955d28 100644 --- a/src/server/routes/readyCheckRoute.js +++ b/src/server/routes/readyCheckRoute.js @@ -22,6 +22,7 @@ export default { method: 'GET', url: '/ready', + logLevel: 'warn', handler: function (request, reply) { if (this.serverIsReady) { return reply diff --git a/src/server/routes/statisticsRoute.js b/src/server/routes/statisticsRoute.js index b175351cebf056b8e9b9bcfdb8db1554b1d7d215..eb033bcdf0944c0bd496d6f75df5e54b1989d0a1 100644 --- a/src/server/routes/statisticsRoute.js +++ b/src/server/routes/statisticsRoute.js @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. -import { readFileSync, readdirSync } from 'node:fs' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' - -/** - * The absolute path of this file. - */ -const currentPath = dirname(fileURLToPath(import.meta.url)) - /** * Route for getting the statistics data. */ @@ -30,23 +21,16 @@ export default { method: 'GET', url: '/api/statistics', handler: function (request, reply) { - const dirPath = resolve(currentPath, '../../../data') - const statisticsList = readdirSync(dirPath) - .filter((file) => { - return file.endsWith('-stats.json') + this.mongo.db + .collection('statistics') + .findOne() + .then(stats => { + return reply + .code(200) + .header('content-type', 'application/json') + .send(stats) + }).catch(err => { + reply.code(500).send(err) }) - .sort() - .reverse() - .map(file => { - return resolve(dirPath, file) - }) - .map(path => { - return readFileSync(path, 'utf-8') - }) - .map(fileContent => { - return JSON.parse(fileContent) - }) - - reply.send(statisticsList[0]) } } diff --git a/src/server/schemas/Statistics.json b/src/server/schemas/Statistics.json index d9e220a9965640df974b3be65fc30da4ce21b1a0..2c53ea243ee4b4055ce81365dde750859c0d1873 100644 --- a/src/server/schemas/Statistics.json +++ b/src/server/schemas/Statistics.json @@ -6,17 +6,29 @@ "_id": { "bsonType": "objectId" }, + "versionDate": { + "description": "Date of this database version", + "bsonType": "date" + }, "antibodies": { "description": "Total number of antibodies.", "bsonType": "int" }, "species": { - "description": "Total number of species.", - "bsonType": "int" + "description": "List of species.", + "bsonType": "array", + "items": { + "bsonType": "string", + "pattern": "^[A-Z][a-z]+\\s[a-z]+$" + } }, "sources": { - "description": "Total number of sources.", - "bsonType": "int" + "description": "List of sources.", + "bsonType": "array", + "items": { + "bsonType": "string", + "pattern": "^.+$" + } }, "antibodiesPerSpecies": { "description": "Total number of antibodies per species. Keys must follow the species name pattern.", @@ -36,6 +48,24 @@ } } }, + "antibodiesPerHeavySegment": { + "description": "Total number of antibodies per heavy segment.", + "bsonType": "object", + "patternProperties": { + "^IGHV[0-9]+$": { + "bsonType": "int" + } + } + }, + "antibodiesPerLightSegment": { + "description": "Total number of antibodies per light segment.", + "bsonType": "object", + "patternProperties": { + "^IG[LK]V[0-9]+$": { + "bsonType": "int" + } + } + }, "antibodiesOnlyInSource": { "description": "Total number of antibodies that are unique for a given source and thus not repeated elsewhere.", "bsonType": "object", diff --git a/src/server/utils/archiveBuilderWorker.js b/src/server/utils/archiveBuilderWorker.js index 68fed5cd0357dadbbb8e72c228f94c4b76627417..a13de23a6abbe5868686b45c24d69ce2a5994931 100644 --- a/src/server/utils/archiveBuilderWorker.js +++ b/src/server/utils/archiveBuilderWorker.js @@ -1,3 +1,19 @@ +// ABSD +// Copyright (C) 2023 Institut Pasteur +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + import { createReadStream, createWriteStream } from "node:fs" import { rm } from "node:fs/promises" import { tmpdir } from "node:os" @@ -17,22 +33,17 @@ const dataFormat = workerData.archiveFormat const tmpFileExt = dataFormat === 'fasta' ? '.fasta' : '.json' const tmpFileName = `${workerData.archiveId}${tmpFileExt}` const tmpFilePath = join(tmpDir, tmpFileName) - const archiveName = `${tmpFileName}.gz` const archivePath = join(tmpDir, archiveName) -// TO DO: this should not be handled there but in the route, -// before the ArchiveBuilder intialisation -const projection = dataFormat === 'fasta' - ? workerData.requestFields.fasta - : workerData.requestFields.json - const mongoURI = `mongodb://${envConfig.ABSD_DB_HOST}:${envConfig.ABSD_DB_PORT}/${envConfig.ABSD_DB_NAME}` const database = await MongoClient.connect(mongoURI) const dataStream = database .db() .collection('antibodies') - .find(workerData.requestWhere, { projection }) + .find(workerData.requestWhere, { + projection: workerData.requestFields + }) .stream() const transformStream = dataFormat === 'fasta' diff --git a/src/server/utils/databaseIsSync.js b/src/server/utils/databaseIsSync.js new file mode 100644 index 0000000000000000000000000000000000000000..fbeaacf1f0ea23d2ea6f18ca859642ec989ae9ff --- /dev/null +++ b/src/server/utils/databaseIsSync.js @@ -0,0 +1,52 @@ +// ABSD +// Copyright (C) 2023 Institut Pasteur +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +import { readdir } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +/** + * Check if the database is already there and synchronized with the + * last version of the data by checking the stats version date. + * Used mainly to check if the database should be rebuilt or not. + * @param {object} fastify - The Fastify server object + * @returns {boolean} If the database is sync or not + */ +export default async function databaseIsSync(fastify) { + // Get the stats of the current database + const dbStats = await fastify.mongo.db + .collection('statistics') + .findOne() + + // Stop here if the database does not exist + if (!dbStats?.versionDate) return false + + // Then fetch the stats of the last stats file for comparison + const currentPath = dirname(fileURLToPath(import.meta.url)) + const dataFolderPath = resolve(currentPath, '../../../data') + const dataFolder = await readdir(dataFolderPath) + + const statsFilePath = dataFolder + .filter((file) => { return file.endsWith('-stats.json') }) + .sort() + .reverse() + .map(file => { return resolve(dataFolderPath, file) }) + .shift() + + const lastStatsDate = new Date(statsFilePath.match(/(\d{4}-\d{2}-\d{2})-stats/)[1]) + + return dbStats.versionDate.getTime() === lastStatsDate.getTime() +}