diff --git a/eslint.config.mjs b/eslint.config.mjs index c8ff0fc..31a4049 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,27 +2,30 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; import stylistic from '@stylistic/eslint-plugin'; import pluginSecurity from 'eslint-plugin-security'; +import { defineConfig, globalIgnores } from 'eslint/config'; -export default tseslint.config({ - ignores: ['dist'], - files: ['src/**/*.ts', 'prisma/seed/**/*.ts'], - extends: [ - eslint.configs.recommended, - tseslint.configs.strictTypeChecked, - tseslint.configs.stylisticTypeChecked, - pluginSecurity.configs.recommended, - ], - languageOptions: { - parserOptions: { project: true, tsconfigRootDir: import.meta.dirname }, +export default defineConfig([ + globalIgnores(['dist/']), + { + files: ['src/**/*.ts', 'prisma/seed/**/*.ts'], + extends: [ + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + pluginSecurity.configs.recommended, + ], + languageOptions: { + parserOptions: { project: true, tsconfigRootDir: import.meta.dirname }, + }, + plugins: { '@stylistic': stylistic }, + rules: { + '@stylistic/semi': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, }, - plugins: { '@stylistic': stylistic }, - rules: { - '@stylistic/semi': 'error', - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unsafe-assignment': 'error', - '@typescript-eslint/no-confusing-void-expression': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - }, -}); +]); diff --git a/package-lock.json b/package-lock.json index e8fff6c..ab56e51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "@prisma/client": "^6.17.1", + "@prisma/client": "^6.19.0", "@supabase/supabase-js": "^2.50.0", "bcryptjs": "^3.0.2", "cookie-parser": "^1.4.7", @@ -44,7 +44,7 @@ "dotenv-cli": "^8.0.0", "eslint": "^9.24.0", "eslint-plugin-security": "^3.0.1", - "prisma": "^6.17.1", + "prisma": "^6.19.0", "simple-git-hooks": "^2.12.1", "supertest": "^7.1.0", "tsc-alias": "^1.8.16", @@ -64,20 +64,20 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "license": "MIT", "dependencies": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "license": "MIT", "optional": true, "dependencies": { @@ -85,9 +85,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -102,9 +102,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -119,9 +119,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -136,9 +136,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -170,9 +170,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -187,9 +187,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -204,9 +204,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -221,9 +221,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -238,9 +238,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -255,9 +255,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -289,9 +289,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -306,9 +306,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -323,9 +323,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -340,9 +340,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -357,9 +357,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -391,9 +391,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -425,9 +425,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -441,10 +441,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -459,9 +476,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -476,9 +493,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -493,9 +510,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -510,9 +527,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -542,9 +559,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -552,13 +569,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -591,19 +608,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -614,9 +634,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -626,7 +646,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -662,9 +682,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -675,9 +695,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -685,13 +705,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -726,33 +746,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -768,9 +774,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -781,10 +787,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", - "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -800,13 +815,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", - "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -822,13 +837,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -842,9 +857,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -858,9 +873,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -874,9 +889,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -890,9 +905,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -905,10 +920,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -922,9 +953,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -938,9 +969,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -954,9 +985,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -970,9 +1001,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", - "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -988,13 +1019,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", - "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -1010,13 +1041,57 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", - "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -1032,13 +1107,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", - "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -1054,13 +1129,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", - "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -1076,13 +1151,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", - "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -1098,20 +1173,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", - "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.3" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1121,9 +1196,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", - "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -1140,9 +1215,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", - "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -1159,9 +1234,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", - "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1178,9 +1253,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, @@ -1236,9 +1311,9 @@ } }, "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1246,9 +1321,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz", - "integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.1.tgz", + "integrity": "sha512-4SXj4Oo6HyQkLUWT8Ke5R0PTAfVOKip5Roo+6+b2EDTkFg5be0FnBWiuRJc0BC0sRQIWGMLKW1XguhVfW/z3/A==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -1268,72 +1343,72 @@ } }, "node_modules/@prisma/config": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz", - "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.1.tgz", + "integrity": "sha512-bUL/aYkGXLwxVGhJmQMtslLT7KPEfUqmRa919fKI4wQFX4bIFUKiY8Jmio/2waAjjPYrtuDHa7EsNCnJTXxiOw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", - "effect": "3.16.12", + "effect": "3.18.4", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz", - "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.1.tgz", + "integrity": "sha512-h1JImhlAd/s5nhY/e9qkAzausWldbeT+e4nZF7A4zjDYBF4BZmKDt4y0jK7EZapqOm1kW7V0e9agV/iFDy3fWw==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz", - "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.1.tgz", + "integrity": "sha512-xy95dNJ7DiPf9IJ3oaVfX785nbFl7oNDzclUF+DIiJw6WdWCvPl0LPU0YqQLsrwv8N64uOQkH391ujo3wSo+Nw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.17.1", - "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "@prisma/fetch-engine": "6.17.1", - "@prisma/get-platform": "6.17.1" + "@prisma/debug": "6.19.1", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.1", + "@prisma/get-platform": "6.19.1" } }, "node_modules/@prisma/engines-version": { - "version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz", - "integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==", + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz", - "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.1.tgz", + "integrity": "sha512-mmgcotdaq4VtAHO6keov3db+hqlBzQS6X7tR7dFCbvXjLVTxBYdSJFRWz+dq7F9p6dvWyy1X0v8BlfRixyQK6g==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.17.1", - "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "@prisma/get-platform": "6.17.1" + "@prisma/debug": "6.19.1", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz", - "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.1.tgz", + "integrity": "sha512-zsg44QUiQAnFUyh6Fbt7c9HjMXHwFTqtrgcX7DAZmRgnkPyYT7Sh8Mn8D5PuuDYNtMOYcpLGg576MLfIORsBYw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.17.1" + "@prisma/debug": "6.19.1" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "cpu": [ "arm" ], @@ -1345,9 +1420,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "cpu": [ "arm64" ], @@ -1359,9 +1434,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "cpu": [ "arm64" ], @@ -1373,9 +1448,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "cpu": [ "x64" ], @@ -1387,9 +1462,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "cpu": [ "arm64" ], @@ -1401,9 +1476,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "cpu": [ "x64" ], @@ -1415,9 +1490,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "cpu": [ "arm" ], @@ -1429,9 +1504,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "cpu": [ "arm" ], @@ -1443,9 +1518,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "cpu": [ "arm64" ], @@ -1457,9 +1532,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "cpu": [ "arm64" ], @@ -1470,10 +1545,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "cpu": [ "loong64" ], @@ -1484,10 +1559,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "cpu": [ "ppc64" ], @@ -1499,9 +1574,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "cpu": [ "riscv64" ], @@ -1513,9 +1588,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "cpu": [ "riscv64" ], @@ -1527,9 +1602,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "cpu": [ "s390x" ], @@ -1541,9 +1616,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "cpu": [ "x64" ], @@ -1555,9 +1630,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "cpu": [ "x64" ], @@ -1568,10 +1643,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "cpu": [ "arm64" ], @@ -1583,9 +1672,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "cpu": [ "ia32" ], @@ -1596,10 +1685,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "cpu": [ "x64" ], @@ -1610,21 +1713,31 @@ "win32" ] }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, "license": "MIT" }, "node_modules/@stylistic/eslint-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz", - "integrity": "sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.4.1.tgz", + "integrity": "sha512-CEigAk7eOLyHvdgmpZsKFwtiqS2wFwI1fn4j09IU9GmD4euFM4jEBAViWeCqaNLlbX2k2+A/Fq9cje4HQBXuJQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.23.0", + "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "estraverse": "^5.3.0", @@ -1638,83 +1751,89 @@ } }, "node_modules/@supabase/auth-js": { - "version": "2.70.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", - "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", + "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/functions-js": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", - "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", + "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" - } - }, - "node_modules/@supabase/node-fetch": { - "version": "2.6.15", - "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", - "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" + "tslib": "2.8.1" }, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=20.0.0" } }, "node_modules/@supabase/postgrest-js": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", - "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", + "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/realtime-js": { - "version": "2.11.10", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz", - "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", + "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", + "tslib": "2.8.1", "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/storage-js": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", - "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", + "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/supabase-js": { - "version": "2.50.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz", - "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", + "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.70.0", - "@supabase/functions-js": "2.4.4", - "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.19.4", - "@supabase/realtime-js": "2.11.10", - "@supabase/storage-js": "2.7.1" + "@supabase/auth-js": "2.89.0", + "@supabase/functions-js": "2.89.0", + "@supabase/postgrest-js": "2.89.0", + "@supabase/realtime-js": "2.89.0", + "@supabase/storage-js": "2.89.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -1722,6 +1841,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1733,9 +1863,9 @@ } }, "node_modules/@types/cookie-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", - "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1750,15 +1880,22 @@ "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1791,29 +1928,28 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -1824,9 +1960,9 @@ } }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, @@ -1838,9 +1974,9 @@ "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "dev": true, "license": "MIT", "dependencies": { @@ -1855,13 +1991,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -1880,9 +2009,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz", - "integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1933,15 +2062,15 @@ } }, "node_modules/@types/phoenix": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", - "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", "license": "MIT" }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, @@ -1953,26 +2082,24 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "@types/node": "*" } }, "node_modules/@types/superagent": { @@ -2015,19 +2142,18 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -2039,23 +2165,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "engines": { @@ -2067,18 +2202,40 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2088,15 +2245,33 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2109,13 +2284,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", "dev": true, "license": "MIT", "engines": { @@ -2127,19 +2302,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -2150,20 +2326,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2174,18 +2350,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.50.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2196,14 +2372,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2212,13 +2389,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2227,7 +2404,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -2239,9 +2416,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -2252,27 +2429,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2281,27 +2459,27 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2327,7 +2505,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2466,9 +2643,9 @@ "license": "MIT" }, "node_modules/bcryptjs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", - "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", "license": "BSD-3-Clause", "bin": { "bcrypt": "bin/bcrypt" @@ -2488,23 +2665,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2591,36 +2772,6 @@ } } }, - "node_modules/c12/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2671,9 +2822,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -2684,7 +2835,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -2715,41 +2866,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/citty": { @@ -2763,19 +2892,23 @@ } }, "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "license": "MIT", "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2788,41 +2921,49 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "license": "MIT", "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" } }, "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" } }, "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" + "engines": { + "node": ">=12.20" } }, "node_modules/combined-stream": { @@ -2898,15 +3039,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2940,21 +3082,12 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -2991,9 +3124,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3068,9 +3201,9 @@ "license": "MIT" }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3169,9 +3302,9 @@ "license": "MIT" }, "node_modules/effect": { - "version": "3.16.12", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", - "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3258,9 +3391,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3271,31 +3404,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escape-html": { @@ -3318,26 +3452,24 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -3533,9 +3665,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3543,18 +3675,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -3584,10 +3717,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "devOptional": true, "license": "MIT" }, @@ -3673,9 +3815,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3683,11 +3825,14 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3730,9 +3875,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -3743,7 +3888,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/find-up": { @@ -3791,9 +3940,9 @@ "license": "MIT" }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -3928,9 +4077,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4017,13 +4166,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4084,31 +4226,48 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4163,12 +4322,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4241,19 +4394,26 @@ "license": "ISC" }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4285,12 +4445,12 @@ "license": "MIT" }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -4307,23 +4467,23 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -4440,20 +4600,20 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -4556,15 +4716,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimatch": { @@ -4672,13 +4836,13 @@ } }, "node_modules/mylas": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", - "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=16.0.0" }, "funding": { "type": "github", @@ -4757,13 +4921,6 @@ "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/nypm/node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "devOptional": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4962,12 +5119,13 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { @@ -4988,9 +5146,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -5017,9 +5175,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5055,9 +5213,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -5075,7 +5233,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5094,16 +5252,15 @@ } }, "node_modules/prisma": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz", - "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.1.tgz", + "integrity": "sha512-XRfmGzh6gtkc/Vq3LqZJcS2884dQQW3UhPo6jNRoiTW95FFQkXFg8vkYEy6og+Pyv0aY7zRQ7Wn1Cvr56XjhQQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@prisma/config": "6.17.1", - "@prisma/engines": "6.17.1" + "@prisma/config": "6.19.1", + "@prisma/engines": "6.19.1" }, "bin": { "prisma": "build/index.js" @@ -5216,18 +5373,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/rc9": { @@ -5256,29 +5413,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/regexp-tree": { @@ -5323,13 +5468,13 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5339,26 +5484,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" } }, @@ -5448,9 +5595,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5460,31 +5607,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -5494,6 +5645,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -5503,15 +5658,15 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", - "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -5520,40 +5675,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.2", - "@img/sharp-darwin-x64": "0.34.2", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.2", - "@img/sharp-linux-arm64": "0.34.2", - "@img/sharp-linux-s390x": "0.34.2", - "@img/sharp-linux-x64": "0.34.2", - "@img/sharp-linuxmusl-arm64": "0.34.2", - "@img/sharp-linuxmusl-x64": "0.34.2", - "@img/sharp-wasm32": "0.34.2", - "@img/sharp-win32-arm64": "0.34.2", - "@img/sharp-win32-ia32": "0.34.2", - "@img/sharp-win32-x64": "0.34.2" - } - }, - "node_modules/sharp/node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -5659,9 +5804,9 @@ "license": "ISC" }, "node_modules/simple-git-hooks": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/simple-git-hooks/-/simple-git-hooks-2.13.0.tgz", - "integrity": "sha512-N+goiLxlkHJlyaYEglFypzVNMaNplPAk5syu0+OPp/Bk6dwVoXF6FfOw2vO0Dp+JHsBaI+w6cm8TnFl2Hw6tDA==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/simple-git-hooks/-/simple-git-hooks-2.13.1.tgz", + "integrity": "sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5669,15 +5814,6 @@ "simple-git-hooks": "cli.js" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5715,18 +5851,18 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -5760,36 +5896,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", "dev": true, "license": "MIT", "dependencies": { - "component-emitter": "^1.3.0", + "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", - "debug": "^4.3.4", + "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^3.5.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.0" + "qs": "^6.11.2" }, "engines": { "node": ">=14.18.0" } }, "node_modules/supertest": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", - "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^9.0.1" + "superagent": "^10.2.3" }, "engines": { "node": ">=14.18.0" @@ -5822,21 +5971,24 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5846,9 +5998,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -5866,9 +6018,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -5897,12 +6049,6 @@ "node": ">=0.6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -5947,22 +6093,84 @@ "node": ">=16.20.2" } }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tsc-alias/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsx": { - "version": "4.19.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", - "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -6009,12 +6217,11 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6024,15 +6231,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", - "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz", + "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@typescript-eslint/utils": "8.32.0" + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6043,7 +6251,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/undici-types": { @@ -6096,25 +6304,24 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6123,14 +6330,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -6172,17 +6379,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -6195,32 +6402,34 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6236,8 +6445,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -6265,21 +6474,12 @@ } } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", @@ -6315,13 +6515,13 @@ } }, "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -6367,9 +6567,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6410,9 +6610,9 @@ } }, "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index cf04717..9270321 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "pg:up": "docker compose -f docker-compose.postgres.yml up" }, "dependencies": { - "@prisma/client": "^6.17.1", + "@prisma/client": "^6.19.0", "@supabase/supabase-js": "^2.50.0", "bcryptjs": "^3.0.2", "cookie-parser": "^1.4.7", @@ -82,7 +82,7 @@ "dotenv-cli": "^8.0.0", "eslint": "^9.24.0", "eslint-plugin-security": "^3.0.1", - "prisma": "^6.17.1", + "prisma": "^6.19.0", "simple-git-hooks": "^2.12.1", "supertest": "^7.1.0", "tsc-alias": "^1.8.16", diff --git a/prisma/migrations/20251109080320_add_messaging_models/migration.sql b/prisma/migrations/20251109080320_add_messaging_models/migration.sql new file mode 100644 index 0000000..eabe369 --- /dev/null +++ b/prisma/migrations/20251109080320_add_messaging_models/migration.sql @@ -0,0 +1,130 @@ +-- CreateEnum +CREATE TYPE "manager_roles" AS ENUM ('MODERATOR', 'OWNER'); + +-- CreateTable +CREATE TABLE "profiles" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "last_seen" TIMESTAMP(3) NOT NULL, + "visible" BOOLEAN NOT NULL DEFAULT true, + "tangible" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "follows" ( + "profile_id" UUID NOT NULL, + "follower_id" UUID NOT NULL, + "followed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "follows_pkey" PRIMARY KEY ("profile_id","follower_id") +); + +-- CreateTable +CREATE TABLE "profiles_managed_chats" ( + "profile_id" UUID NOT NULL, + "chatId" UUID NOT NULL, + "role" "manager_roles" NOT NULL +); + +-- CreateTable +CREATE TABLE "profiles_chats" ( + "profile_id" UUID NOT NULL, + "chat_id" UUID NOT NULL, + "joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "profiles_received_messages" ( + "profile_id" UUID NOT NULL, + "message_id" UUID NOT NULL, + "received_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "profiles_seen_messages" ( + "profile_id" UUID NOT NULL, + "message_id" UUID NOT NULL, + "seen_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "chats" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "chats_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "messages" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "chatId" UUID NOT NULL, + "profile_id" UUID, + "profile_name" TEXT NOT NULL, + "body" TEXT NOT NULL, + "image_id" UUID, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "messages_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_managed_chats_profile_id_chatId_key" ON "profiles_managed_chats"("profile_id", "chatId"); + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_chats_profile_id_chat_id_key" ON "profiles_chats"("profile_id", "chat_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_received_messages_profile_id_message_id_key" ON "profiles_received_messages"("profile_id", "message_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_seen_messages_profile_id_message_id_key" ON "profiles_seen_messages"("profile_id", "message_id"); + +-- AddForeignKey +ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "follows" ADD CONSTRAINT "follows_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_fkey" FOREIGN KEY ("follower_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_managed_chats" ADD CONSTRAINT "profiles_managed_chats_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_managed_chats" ADD CONSTRAINT "profiles_managed_chats_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "chats"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_chats" ADD CONSTRAINT "profiles_chats_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_chats" ADD CONSTRAINT "profiles_chats_chat_id_fkey" FOREIGN KEY ("chat_id") REFERENCES "chats"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_received_messages" ADD CONSTRAINT "profiles_received_messages_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_received_messages" ADD CONSTRAINT "profiles_received_messages_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "messages"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_seen_messages" ADD CONSTRAINT "profiles_seen_messages_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profiles_seen_messages" ADD CONSTRAINT "profiles_seen_messages_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "messages"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "chats"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "messages" ADD CONSTRAINT "messages_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "images"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20251209095909_enhance_profiles_chats_rel/migration.sql b/prisma/migrations/20251209095909_enhance_profiles_chats_rel/migration.sql new file mode 100644 index 0000000..844b957 --- /dev/null +++ b/prisma/migrations/20251209095909_enhance_profiles_chats_rel/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - A unique constraint covering the columns `[profile_name,chat_id]` on the table `profiles_chats` will be added. If there are existing duplicate values, this will fail. + - Added the required column `profile_name` to the `profiles_chats` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "profiles_chats" DROP CONSTRAINT "profiles_chats_profile_id_fkey"; + +-- DropIndex +DROP INDEX "profiles_chats_profile_id_chat_id_key"; + +-- AlterTable +ALTER TABLE "profiles_chats" ADD COLUMN "profile_name" TEXT NOT NULL, +ALTER COLUMN "profile_id" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "profiles_chats_profile_name_chat_id_key" ON "profiles_chats"("profile_name", "chat_id"); + +-- AddForeignKey +ALTER TABLE "profiles_chats" ADD CONSTRAINT "profiles_chats_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "profiles"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20251225114249_move_message_seen_received_to_chat_profile/migration.sql b/prisma/migrations/20251225114249_move_message_seen_received_to_chat_profile/migration.sql new file mode 100644 index 0000000..08192fc --- /dev/null +++ b/prisma/migrations/20251225114249_move_message_seen_received_to_chat_profile/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the `profiles_received_messages` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `profiles_seen_messages` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "profiles_received_messages" DROP CONSTRAINT "profiles_received_messages_message_id_fkey"; + +-- DropForeignKey +ALTER TABLE "profiles_received_messages" DROP CONSTRAINT "profiles_received_messages_profile_id_fkey"; + +-- DropForeignKey +ALTER TABLE "profiles_seen_messages" DROP CONSTRAINT "profiles_seen_messages_message_id_fkey"; + +-- DropForeignKey +ALTER TABLE "profiles_seen_messages" DROP CONSTRAINT "profiles_seen_messages_profile_id_fkey"; + +-- AlterTable +ALTER TABLE "profiles_chats" ADD COLUMN "last_received_at" TIMESTAMP(3), +ADD COLUMN "last_seen_at" TIMESTAMP(3); + +-- DropTable +DROP TABLE "profiles_received_messages"; + +-- DropTable +DROP TABLE "profiles_seen_messages"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1dc2276..6387d7a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,7 @@ model User { isAdmin Boolean @default(false) @map("is_admin") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + profile Profile? avatar Avatar? images Image[] posts Post[] @@ -27,6 +28,92 @@ model User { @@map("users") } +model Profile { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String @unique @map("user_id") @db.Uuid + lastSeen DateTime @updatedAt @map("last_seen") + visible Boolean @default(true) + tangible Boolean @default(true) + chats ProfilesChats[] + managedChats ProfilesManagedChats[] + sentMessages Message[] + followers Follows[] @relation("profile") + following Follows[] @relation("follower") + + @@map("profiles") +} + +model Follows { + profile Profile @relation("profile", fields: [profileId], references: [id], onDelete: Cascade) + profileId String @map("profile_id") @db.Uuid + follower Profile @relation("follower", fields: [followerId], references: [id], onDelete: Cascade) + followerId String @map("follower_id") @db.Uuid + followedAt DateTime @default(now()) @map("followed_at") + + @@id([profileId, followerId]) + @@map("follows") +} + +enum ManagerRole { + MODERATOR + OWNER + + @@map("manager_roles") +} + +model ProfilesManagedChats { + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) + profileId String @map("profile_id") @db.Uuid + chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) + chatId String @db.Uuid + role ManagerRole + + @@unique([profileId, chatId]) + @@map("profiles_managed_chats") +} + +model ProfilesChats { + profile Profile? @relation(fields: [profileId], references: [id], onDelete: SetNull) + profileId String? @map("profile_id") @db.Uuid + profileName String @map("profile_name") + chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) + chatId String @map("chat_id") @db.Uuid + joinedAt DateTime @default(now()) @map("joined_at") + lastReceivedAt DateTime? @map("last_received_at") + lastSeenAt DateTime? @map("last_seen_at") + + @@unique([profileName, chatId]) + @@map("profiles_chats") +} + +model Chat { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + messages Message[] + profiles ProfilesChats[] + managers ProfilesManagedChats[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("chats") +} + +model Message { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) + chatId String @db.Uuid + profile Profile? @relation(fields: [profileId], references: [id], onDelete: SetNull) + profileId String? @map("profile_id") @db.Uuid + profileName String @map("profile_name") + body String + image Image? @relation(fields: [imageId], references: [id], onDelete: SetNull) + imageId String? @map("image_id") @db.Uuid + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("messages") +} + model Comment { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid order Int @unique @default(autoincrement()) @@ -95,26 +182,27 @@ model VotesOnPosts { } model Image { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - order Int @unique @default(autoincrement()) - ownerId String @map("owner_id") @db.Uuid - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - storageFullPath String @map("storage_full_path") - storageId String @map("storage_id") - info String @default("") - alt String @default("") - src String @unique + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + order Int @unique @default(autoincrement()) + ownerId String @map("owner_id") @db.Uuid + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + storageFullPath String @map("storage_full_path") + storageId String @map("storage_id") + info String @default("") + alt String @default("") + src String @unique mimetype String size Int width Int height Int - xPos Int @default(0) @map("x_pos") - yPos Int @default(0) @map("y_pos") - scale Float @default(1.0) + xPos Int @default(0) @map("x_pos") + yPos Int @default(0) @map("y_pos") + scale Float @default(1.0) posts Post[] avatars Avatar[] + messages Message[] @@map("images") } diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts index c3f19e3..abb24c3 100644 --- a/prisma/seed/script.ts +++ b/prisma/seed/script.ts @@ -48,6 +48,7 @@ async function main() { await transClient.user.create({ data: { password: await bcrypt.hash(AUTHOR_PASSWORD, 10), + profile: { create: { lastSeen: new Date() } }, updatedAt: createdAt, isAdmin: true, createdAt, diff --git a/src/api/v1/characters/characters.service.ts b/src/api/v1/characters/characters.service.ts index be8df83..cb7f36c 100644 --- a/src/api/v1/characters/characters.service.ts +++ b/src/api/v1/characters/characters.service.ts @@ -41,7 +41,7 @@ export const updateFinder = async (id: CharacterFinder['id'], { name }: ValidFin }; const isCharacterFound = ({ x, y }: Point, { top, left, right, bottom }: CharacterRect) => { - return Boolean(x > left && y > top && x < right && y < bottom); + return x > left && y > top && x < right && y < bottom; }; const evaluateSelection = ( @@ -54,7 +54,8 @@ const evaluateSelection = ( for (const [characterName, selectedPoint] of selectionEntries) { const characterRect = characterRects.find((cr) => cr.name === characterName); const characterFound = characterRect ? isCharacterFound(selectedPoint, characterRect) : false; - evaluation[String(characterName)] = characterFound; + // eslint-disable-next-line security/detect-object-injection + evaluation[characterName] = characterFound; if (characterFound) foundCharactersNum++; } const allCharactersFound = foundCharactersNum > 0 && foundCharactersNum === characterRects.length; diff --git a/src/api/v1/chats/chat.schema.ts b/src/api/v1/chats/chat.schema.ts new file mode 100644 index 0000000..f29fecd --- /dev/null +++ b/src/api/v1/chats/chat.schema.ts @@ -0,0 +1,39 @@ +import * as Image from '@/lib/image'; +import { z } from 'zod'; + +const messageFields = { imagedata: Image.imageSchema.optional(), body: z.string().trim() }; + +export const messageSchema = z.object({ ...messageFields, body: messageFields.body.nonempty() }); + +export type ValidMessage = z.output; + +export const optionalMessageSchema = z + .object({ + ...messageFields, + body: messageFields.body.optional(), + }) + .transform((data) => { + const body = data.body ?? ''; + return { ...data, body }; + }); + +export type ValidOptionalMessage = z.output; + +const chatFields = { profiles: z.array(z.string().trim().uuid()), message: messageSchema }; + +export const chatSchema = z.object(chatFields); + +export type ValidChat = z.output; + +export const chatWithOptionalMessageSchema = z + .object({ + ...chatFields, + message: optionalMessageSchema.optional(), + }) + .transform((data): ValidChat => { + const body = data.message?.body ?? ''; + const message = data.message ? { ...data.message, body } : { body }; + return { ...data, message }; + }); + +export type ValidOptionalChat = z.output; diff --git a/src/api/v1/chats/chats.router.ts b/src/api/v1/chats/chats.router.ts new file mode 100644 index 0000000..82bab58 --- /dev/null +++ b/src/api/v1/chats/chats.router.ts @@ -0,0 +1,99 @@ +import * as Types from '@/types'; +import * as Utils from '@/lib/utils'; +import * as Image from '@/lib/image'; +import * as Schema from './chat.schema'; +import * as Storage from '@/lib/storage'; +import * as Service from './chats.service'; +import * as Middlewares from '@/middlewares'; +import { Request, Response, Router } from 'express'; + +export const chatsRouter = Router(); + +const prepareImageData = async ( + req: Request & { file?: Express.Multer.File }, + imagedata: Schema.ValidMessage['imagedata'], + user: Types.PublicUser, +) => { + let uploadedImage: Storage.UploadedImageData | undefined; + let imageData: Types.ImageFullData | undefined; + if (req.file) { + const file = await Image.getValidImageFileFormReq(req); + imageData = { ...(imagedata ?? {}), ...Image.getImageMetadata(file) }; + uploadedImage = await Storage.uploadImage(file, user); + } + return [imageData, uploadedImage] as const; +}; + +chatsRouter.get('/', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const filters = Utils.getBasePaginationFiltersFromReqQuery(req); + const chats = await Service.getUserChats(userId, filters); + res.json(chats); +}); + +chatsRouter.get('/members/:profileId', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const chats = await Service.getUserChatsByMemberProfileId(userId, req.params.profileId); + res.json(chats); +}); + +chatsRouter.get('/:id', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const chat = await Service.getUserChatById(userId, req.params.id); + res.json(chat); +}); + +chatsRouter.get('/:id/messages', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const filters = Utils.getBasePaginationFiltersFromReqQuery(req); + const messages = await Service.getUserChatMessages(userId, req.params.id, filters); + res.json(messages); +}); + +chatsRouter.get('/:id/messages/:msgId', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const msg = await Service.getUserChatMessageById(userId, req.params.id, req.params.msgId); + res.json(msg); +}); + +chatsRouter.post( + '/', + Middlewares.authValidator, + Middlewares.createFileProcessor('image'), + async (req: Request, res: Response) => { + const user = Utils.getCurrentUserFromReq(req)!; + const chatData = (req.file ? Schema.chatWithOptionalMessageSchema : Schema.chatSchema).parse( + req.body, + ); + const preparedImageData = await prepareImageData(req, chatData.message.imagedata, user); + const createdChat = await Service.createChat(user, chatData, ...preparedImageData); + res.status(201).json(createdChat); + }, +); + +chatsRouter.patch('/:id/seen', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + res.json(await Service.updateProfileChatLastSeenDate(userId, req.params.id)); +}); + +chatsRouter.post( + '/:id/messages', + Middlewares.authValidator, + Middlewares.createFileProcessor('image'), + async (req: Request, res: Response) => { + const user = Utils.getCurrentUserFromReq(req)!; + const { imagedata, ...msgData } = ( + req.file ? Schema.optionalMessageSchema : Schema.messageSchema + ).parse(req.body); + const preparedImageData = await prepareImageData(req, imagedata, user); + const msgArgs = [user, req.params.id, msgData, ...preparedImageData] as const; + const createdMessage = await Service.createUserChatMessage(...msgArgs); + res.status(201).json(createdMessage); + }, +); + +chatsRouter.delete('/:id', Middlewares.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + await Service.deleteChat(userId, req.params.id); + res.status(204).send(); +}); diff --git a/src/api/v1/chats/chats.service.ts b/src/api/v1/chats/chats.service.ts new file mode 100644 index 0000000..72034d4 --- /dev/null +++ b/src/api/v1/chats/chats.service.ts @@ -0,0 +1,417 @@ +import * as Types from '@/types'; +import * as Utils from '@/lib/utils'; +import * as Image from '@/lib/image'; +import * as Schema from './chat.schema'; +import * as Storage from '@/lib/storage'; +import { Chat, Image as ImageType, Message, Prisma, Profile, User } from '@/../prisma/client'; +import { AppNotFoundError } from '@/lib/app-error'; +import logger from '@/lib/logger'; +import db from '@/lib/db'; + +const createPaginationArgs = ( + filters: Types.BasePaginationFilters & { orderBy: 'createdAt' | 'updatedAt' }, + limit = 10 +) => { + return { + ...(filters.cursor ? { cursor: { id: filters.cursor }, skip: 1 } : {}), + orderBy: { [filters.orderBy]: filters.sort ?? 'desc' }, + take: filters.limit ?? limit, + }; +}; + +const createMessageAggregation = () => { + return { profile: Utils.profileAggregation, image: true } as const; +}; + +const createChatAggregation = () => { + return { + profiles: { include: { profile: Utils.profileAggregation } }, + managers: { include: { profile: Utils.profileAggregation } }, + messages: { + ...createPaginationArgs({ orderBy: 'createdAt' }), + include: createMessageAggregation(), + }, + }; +}; + +const createCurrentProfileChatArgs = ( + currentProfile: Prisma.ProfileGetPayload<{ include: { user: true } }>, + now: Date +) => { + return { + profileName: currentProfile.user.username, + profileId: currentProfile.id, + lastReceivedAt: now, + lastSeenAt: now, + }; +}; + +const prepareChat = ( + chat: Prisma.ChatGetPayload<{ include: ReturnType }>, + currentProfile: Profile +) => { + if (currentProfile.tangible) { + chat.profiles = chat.profiles.map((p) => { + if ((currentProfile.tangible && p.profile?.tangible) || p.profileId === currentProfile.id) { + return p; + } + return { ...p, lastSeenAt: null }; + }); + } else { + chat.profiles = chat.profiles.map((p) => + p.profileId === currentProfile.id ? p : { ...p, lastSeenAt: null } + ); + } + return chat; +}; + +export const createChat = async ( + user: Types.PublicUser, + data: Schema.ValidChat, + imageData?: Types.ImageFullData, + uploadedImage?: Storage.UploadedImageData +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + // Get current user's profile + const currentProfile = await tx.profile.findUnique({ + where: { userId: user.id }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const currentProfileChatArgs = createCurrentProfileChatArgs(currentProfile, new Date()); + // Get all the other profiles + const otherProfiles = await tx.profile.findMany({ + where: { id: { in: data.profiles } }, + include: { user: true }, + distinct: 'id', + }); + const chatAggregation = createChatAggregation(); + const messageArgs = { + profileName: currentProfile.user.username, + body: data.message.body, + imageId: + imageData && uploadedImage + ? ( + await tx.image.create({ + data: Image.getImageUpsertData(uploadedImage, imageData, user), + }) + ).id + : undefined, + profileId: currentProfile.id, + }; + // Find chats with the same owner and same group of profiles (there should be at most one) + const existentChats = await tx.chat.findMany({ + where: { + managers: { some: { profileId: currentProfile.id, role: 'OWNER' } }, + profiles: { + every: { + profileName: { + in: [currentProfile.user.username, ...otherProfiles.map((p) => p.user.username)], + }, + }, + }, + }, + include: chatAggregation, + }); + // Upsert a chat + let chat: Prisma.ChatGetPayload<{ include: typeof chatAggregation }>; + if (existentChats.length > 0) { + // Return the 1st found chat, and, if there are more than one chat found, delete the rest + const selectedChatIndex = existentChats.findIndex((c) => c.messages.length > 0); + const selectedChat = existentChats[selectedChatIndex > 0 ? selectedChatIndex : 0]; + if (existentChats.length > 1) { + const chatIdsToDel: string[] = []; + existentChats.forEach(({ id }, i) => i !== selectedChatIndex && chatIdsToDel.push(id)); + tx.chat.deleteMany({ where: { id: { in: chatIdsToDel } } }).catch(logger.error); + } + chat = await tx.chat.update({ + where: { id: selectedChat.id }, + data: { + messages: { create: messageArgs }, + profiles: { + update: { + where: { + profileName_chatId: { + profileName: currentProfile.user.username, + chatId: selectedChat.id, + }, + }, + data: currentProfileChatArgs, + }, + }, + }, + include: chatAggregation, + }); + } else { + // Create new chat, because there are no chats for this group of profiles + chat = await tx.chat.create({ + data: { + managers: { create: { profileId: currentProfile.id, role: 'OWNER' } }, + profiles: { + createMany: { + data: [ + currentProfileChatArgs, + ...otherProfiles.map((p) => ({ profileName: p.user.username, profileId: p.id })), + ], + skipDuplicates: true, + }, + }, + messages: { create: messageArgs }, + }, + include: chatAggregation, + }); + } + return prepareChat(chat, currentProfile); + }) + ); +}; + +export const deleteChat = async (userId: User['id'], chatId: Chat['id']) => { + await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const chat = await tx.chat.findUnique({ + where: { id: chatId, profiles: { some: { profile: { userId } } } }, + include: { profiles: { include: { profile: true } } }, + }); + if (chat) { + if (chat.profiles.length < 2) { + await tx.chat.delete({ where: { id: chatId } }); + } else { + const profile = chat.profiles.find((p) => p.profile?.userId === userId); + if (profile) { + const { profileName } = profile; + await tx.profilesChats.delete({ + where: { profileName_chatId: { chatId, profileName } }, + }); + } + } + } + }) + ); +}; + +export const getUserChats = async ( + userId: User['id'], + filters: Types.BasePaginationFilters = {} +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ where: { userId } }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileId = currentProfile.id; + const chatIds = await tx.chat.findMany({ + where: { profiles: { some: { profileId } } }, + select: { id: true }, + }); + await tx.profilesChats.updateMany({ + where: { chatId: { in: chatIds.map((c) => c.id) }, profile: { userId } }, + data: { lastReceivedAt: new Date() }, + }); + const chats = await tx.chat.findMany({ + ...createPaginationArgs({ ...filters, orderBy: 'updatedAt' }), + where: { profiles: { some: { profileId } } }, + include: createChatAggregation(), + }); + return chats.map((chat) => prepareChat(chat, currentProfile)); + }) + ); +}; + +export const getUserChatById = async (userId: User['id'], chatId: Chat['id']) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ + where: { userId }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileId = currentProfile.id; + let chat; + try { + await tx.profilesChats.update({ + where: { profileName_chatId: { chatId, profileName: currentProfile.user.username } }, + data: { lastReceivedAt: new Date() }, + }); + chat = await tx.chat.findUnique({ + where: { id: chatId, profiles: { some: { profileId } } }, + include: createChatAggregation(), + }); + if (!chat) throw new AppNotFoundError('Chat not found'); + } catch { + throw new AppNotFoundError('Chat not found'); + } + return prepareChat(chat, currentProfile); + }) + ); +}; + +export const getUserChatsByMemberProfileId = async ( + userId: User['id'], + profileId: Profile['id'] +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + try { + const memberProfile = await tx.profile.findUnique({ where: { id: profileId } }); + if (!memberProfile) throw new AppNotFoundError('Chat member profile not found'); + } catch { + throw new AppNotFoundError('Chat member profile not found'); + } + await tx.profilesChats.updateManyAndReturn({ + where: { + profile: { userId }, + chat: { + AND: [ + { profiles: { some: { profileId } } }, + { profiles: { some: { profile: { userId } } } }, + ], + }, + }, + data: { lastReceivedAt: new Date() }, + }); + const currentProfileWithChats = await tx.profile.findUnique({ + where: { userId }, + include: { + chats: { + where: { chat: { profiles: { some: { profileId } } } }, + include: { chat: { include: createChatAggregation() } }, + }, + }, + }); + if (!currentProfileWithChats) throw new AppNotFoundError('Profile not found'); + return currentProfileWithChats.chats.map((c) => prepareChat(c.chat, currentProfileWithChats)); + }) + ); +}; + +export const getUserChatMessages = async ( + userId: User['id'], + chatId: Chat['id'], + filters: Types.BasePaginationFilters = {} +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ + where: { userId }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileName = currentProfile.user.username; + const profileId = currentProfile.id; + let chat; + try { + chat = await tx.chat.findUnique({ where: { id: chatId } }); + if (!chat) throw new AppNotFoundError('Chat not found'); + } catch { + throw new AppNotFoundError('Chat not found'); + } + await tx.profilesChats.update({ + where: { profileName_chatId: { profileName, chatId } }, + data: { lastReceivedAt: new Date() }, + }); + return await tx.message.findMany({ + ...createPaginationArgs({ ...filters, orderBy: 'createdAt' }), + where: { chat: { id: chatId, profiles: { some: { profileId } } } }, + include: createMessageAggregation(), + }); + }) + ); +}; + +export const getUserChatMessageById = async ( + userId: User['id'], + chatId: Chat['id'], + msgId: Message['id'] +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ + where: { userId }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileName = currentProfile.user.username; + const profileId = currentProfile.id; + await tx.profilesChats.update({ + where: { profileName_chatId: { profileName, chatId } }, + data: { lastReceivedAt: new Date() }, + }); + const message = await tx.message.findUnique({ + where: { id: msgId, chat: { id: chatId, profiles: { some: { profileId } } } }, + include: { ...createMessageAggregation() }, + }); + if (!message) throw new AppNotFoundError('Message not found'); + return message; + }) + ); +}; + +export const createUserChatMessage = async ( + user: Types.PublicUser, + chatId: Chat['id'], + { body }: Schema.ValidMessage, + imageData?: Types.ImageFullData, + uploadedImage?: Storage.UploadedImageData +) => { + return await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ + where: { userId: user.id }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileName = currentProfile.user.username; + const profileId = currentProfile.id; + let imageId: ImageType['id'] | undefined; + if (imageData && uploadedImage) { + const image = await tx.image.create({ + data: Image.getImageUpsertData(uploadedImage, imageData, user), + }); + imageId = image.id; + } + const now = new Date(); + await tx.chat.update({ + where: { id: chatId }, + data: { + updatedAt: now, + profiles: { + update: { + where: { profileName_chatId: { profileName, chatId } }, + data: createCurrentProfileChatArgs(currentProfile, now), + }, + }, + }, + }); + return await tx.message.create({ + data: { + profileName: currentProfile.user.username, + profileId, + imageId, + chatId, + body, + }, + include: createMessageAggregation(), + }); + }) + ); +}; + +export const updateProfileChatLastSeenDate = async (userId: User['id'], chatId: Chat['id']) => { + const now = new Date(); + await Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const currentProfile = await tx.profile.findUnique({ + where: { userId }, + include: { user: true }, + }); + if (!currentProfile) throw new AppNotFoundError('Profile not found'); + const profileName = currentProfile.user.username; + await tx.profilesChats.update({ + where: { profileName_chatId: { profileName, chatId } }, + data: { lastSeenAt: now }, + }); + }) + ); + return now; +}; diff --git a/src/api/v1/chats/index.ts b/src/api/v1/chats/index.ts new file mode 100644 index 0000000..25076a1 --- /dev/null +++ b/src/api/v1/chats/index.ts @@ -0,0 +1,3 @@ +export * from './chat.schema'; +export * from './chats.router'; +export * from './chats.service'; diff --git a/src/api/v1/index.ts b/src/api/v1/index.ts index a678d1c..6c909ae 100644 --- a/src/api/v1/index.ts +++ b/src/api/v1/index.ts @@ -1,16 +1,22 @@ +import { profilesRouter, lastSeenUpdater } from './profiles'; import { charactersRouter } from './characters'; import { imagesRouter } from './images'; import { statsRouter } from './stats'; import { usersRouter } from './users'; import { postsRouter } from './posts'; +import { chatsRouter } from './chats'; import { authRouter } from './auth'; import { Router } from 'express'; export const apiRouter = Router(); +apiRouter.use(lastSeenUpdater); + apiRouter.use('/auth', authRouter); apiRouter.use('/users', usersRouter); apiRouter.use('/posts', postsRouter); +apiRouter.use('/chats', chatsRouter); apiRouter.use('/stats', statsRouter); apiRouter.use('/images', imagesRouter); +apiRouter.use('/profiles', profilesRouter); apiRouter.use('/characters', charactersRouter); diff --git a/src/api/v1/profiles/index.ts b/src/api/v1/profiles/index.ts new file mode 100644 index 0000000..0a85276 --- /dev/null +++ b/src/api/v1/profiles/index.ts @@ -0,0 +1,4 @@ +export * from './profile.schema'; +export * from './profiles.router'; +export * from './profiles.service'; +export * from './profiles.middlewares'; diff --git a/src/api/v1/profiles/profile.schema.ts b/src/api/v1/profiles/profile.schema.ts new file mode 100644 index 0000000..104ce41 --- /dev/null +++ b/src/api/v1/profiles/profile.schema.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const profileSchema = z + .object({ + tangible: z.boolean(), + visible: z.boolean(), + }) + .partial(); + +export type ValidProfile = z.output; + +export const followingSchema = z.object({ profileId: z.string().trim().uuid() }); + +export type ValidFollowing = z.output; diff --git a/src/api/v1/profiles/profiles.middlewares.ts b/src/api/v1/profiles/profiles.middlewares.ts new file mode 100644 index 0000000..7b2c7d7 --- /dev/null +++ b/src/api/v1/profiles/profiles.middlewares.ts @@ -0,0 +1,18 @@ +import * as Middlewares from '@/middlewares'; +import { Request, Response, NextFunction } from 'express'; +import logger from '@/lib/logger'; +import db from '@/lib/db'; + +export const lastSeenUpdater = [ + Middlewares.optionalAuthValidator, + async (req: Request, res: Response, next: NextFunction) => { + if (req.user && 'id' in req.user && typeof req.user.id === 'string') { + try { + await db.profile.update({ where: { userId: req.user.id }, data: { lastSeen: new Date() } }); + } catch (error) { + logger.error(error); + } + } + next(); + }, +]; diff --git a/src/api/v1/profiles/profiles.router.ts b/src/api/v1/profiles/profiles.router.ts new file mode 100644 index 0000000..642aab0 --- /dev/null +++ b/src/api/v1/profiles/profiles.router.ts @@ -0,0 +1,47 @@ +import * as Utils from '@/lib/utils'; +import * as Schema from './profile.schema'; +import * as Service from './profiles.service'; +import * as Validators from '@/middlewares/validators'; +import { Router } from 'express'; + +export const profilesRouter = Router(); + +profilesRouter.get('/', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const filters = Utils.getProfileFiltersFromReqQuery(req); + res.json(await Service.getAllProfiles(userId, filters)); +}); + +profilesRouter.get('/following', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const filters = Utils.getProfileFiltersFromReqQuery(req); + res.json(await Service.getAllFollowing(userId, filters)); +}); + +profilesRouter.get('/followers', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + const filters = Utils.getProfileFiltersFromReqQuery(req); + res.json(await Service.getAllFollowers(userId, filters)); +}); + +profilesRouter.get('/:idOrUsername', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + res.json(await Service.getProfileByIdOrUsername(req.params.idOrUsername, userId)); +}); + +profilesRouter.patch('/', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + res.json(await Service.updateProfileByUserId(userId, Schema.profileSchema.parse(req.body))); +}); + +profilesRouter.post('/following/:profileId', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + await Service.createFollowing(userId, Schema.followingSchema.parse(req.params)); + res.status(201).json(); +}); + +profilesRouter.delete('/following/:profileId', Validators.authValidator, async (req, res) => { + const userId = Utils.getCurrentUserIdFromReq(req)!; + await Service.deleteFollowing(userId, Schema.followingSchema.parse(req.params)); + res.status(204).send(); +}); diff --git a/src/api/v1/profiles/profiles.service.ts b/src/api/v1/profiles/profiles.service.ts new file mode 100644 index 0000000..e84197e --- /dev/null +++ b/src/api/v1/profiles/profiles.service.ts @@ -0,0 +1,179 @@ +import * as Types from '@/types'; +import * as Utils from '@/lib/utils'; +import * as Schema from './profile.schema'; +import * as AppError from '@/lib/app-error'; +import { Prisma, Profile, User } from '@/../prisma/client'; +import db from '@/lib/db'; + +type ProfilePayload = Prisma.ProfileGetPayload<{ + include: typeof Utils.profileAggregation.include & { + followers: { include: { follower: typeof Utils.profileAggregation } }; + }; +}>; + +export const prepareProfileData = ( + data: ProfilePayload | ProfilePayload[], + userId: User['id'], +): Types.PublicProfile | Types.PublicProfile[] => { + const arrayGiven = Array.isArray(data); + const profiles = arrayGiven ? data : [data]; + const preparedProfiles = profiles.map(({ followers, ...profile }) => { + if (!profile.visible) profile.lastSeen = profile.user.createdAt; + return { + ...profile, + followedByCurrentUser: followers.some((f) => f.follower.userId === userId), + }; + }); + return arrayGiven ? preparedProfiles : preparedProfiles[0]; +}; + +const getProfilePaginationArgs = (filters: Types.ProfileFilters, limit = 10) => { + return { + orderBy: { user: { username: filters.sort ?? 'asc' } }, + ...(filters.cursor ? { cursor: { id: filters.cursor }, skip: 1 } : {}), + take: filters.limit ?? limit, + }; +}; + +const getExtendedProfileAggregationArgs = (userId: User['id']) => { + return { + include: { + ...Utils.profileAggregation.include, + followers: { + where: { follower: { userId } }, + include: { follower: Utils.profileAggregation }, + }, + }, + }; +}; + +const getNameFilterArgs = (nameFilter: Types.ProfileFilters['name']) => { + if (nameFilter) { + const args = { contains: nameFilter, mode: 'insensitive' as const }; + return { OR: [{ username: args }, { fullname: args }] }; + } + return {}; +}; + +export const getAllProfiles = async (userId: User['id'], filters: Types.ProfileFilters = {}) => { + return prepareProfileData( + await Utils.handleDBKnownErrors( + db.profile.findMany({ + ...getProfilePaginationArgs(filters), + ...getExtendedProfileAggregationArgs(userId), + where: { user: getNameFilterArgs(filters.name) }, + }), + ), + userId, + ); +}; + +export const getProfileById = async (profileId: Profile['id'], userId: User['id']) => { + const profile = await Utils.handleDBKnownErrors( + db.profile.findUnique({ + ...getExtendedProfileAggregationArgs(userId), + where: { id: profileId }, + }), + ); + if (profile) return prepareProfileData(profile, userId); + throw new AppError.AppNotFoundError('Profile not found'); +}; + +export const getProfileByUsername = async (username: User['username'], userId: User['id']) => { + const profiles = await Utils.handleDBKnownErrors( + db.profile.findMany({ + ...getExtendedProfileAggregationArgs(userId), + where: { user: { username } }, + }), + ); + if (profiles.length === 1) return prepareProfileData(profiles[0], userId); + throw new AppError.AppNotFoundError('Profile not found'); +}; + +export const getProfileByIdOrUsername = async (idOrUsername: string, userId: User['id']) => { + try { + return await getProfileByUsername(idOrUsername, userId); + } catch (error) { + if (error instanceof AppError.AppNotFoundError) { + return await getProfileById(idOrUsername, userId); + } + throw error; + } +}; + +export const updateProfileByUserId = async (userId: User['id'], data: Schema.ValidProfile) => { + return prepareProfileData( + await Utils.handleDBKnownErrors( + db.profile.update({ + ...getExtendedProfileAggregationArgs(userId), + where: { userId }, + data, + }), + ), + userId, + ); +}; + +export const createFollowing = async (userId: User['id'], { profileId }: Schema.ValidFollowing) => { + try { + await Utils.handleDBKnownErrors( + db.profile.update({ + ...Utils.profileAggregation, + where: { userId }, + data: { following: { create: { profileId } } }, + }), + ); + } catch (error) { + if (error instanceof AppError.AppUniqueConstraintViolationError) return; + throw error; + } +}; + +export const deleteFollowing = async (userId: User['id'], { profileId }: Schema.ValidFollowing) => { + try { + await Utils.handleDBKnownErrors( + db.$transaction(async (prismaClient) => { + const currentProfile = await prismaClient.profile.findUnique({ where: { userId } }); + if (!currentProfile) throw new AppError.AppNotFoundError('Profile not found'); + await prismaClient.follows.delete({ + where: { profileId_followerId: { followerId: currentProfile.id, profileId } }, + }); + }), + ); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2017') return; + throw error; + } +}; + +export const getAllFollowing = async (userId: User['id'], filters: Types.ProfileFilters = {}) => { + return prepareProfileData( + await Utils.handleDBKnownErrors( + db.profile.findMany({ + ...getProfilePaginationArgs(filters), + ...getExtendedProfileAggregationArgs(userId), + where: { + followers: { some: { follower: { userId } } }, + user: getNameFilterArgs(filters.name), + }, + }), + ), + userId, + ); +}; + +export const getAllFollowers = async (userId: User['id'], filters: Types.ProfileFilters = {}) => { + return prepareProfileData( + await Utils.handleDBKnownErrors( + db.profile.findMany({ + ...getProfilePaginationArgs(filters), + ...getExtendedProfileAggregationArgs(userId), + where: { + following: { some: { profile: { userId } } }, + user: getNameFilterArgs(filters.name), + }, + }), + ), + userId, + ); +}; diff --git a/src/api/v1/users/users.service.ts b/src/api/v1/users/users.service.ts index a55c8b3..239a3a8 100644 --- a/src/api/v1/users/users.service.ts +++ b/src/api/v1/users/users.service.ts @@ -22,6 +22,7 @@ export const createUser = async ({ data: { ...data, password: await hashPassword(password), + profile: { create: { lastSeen: new Date() } }, ...(avatarId ? { avatar: { create: { imageId: avatarId } } } : {}), }, ...Utils.userAggregation, @@ -31,9 +32,7 @@ export const createUser = async ({ return user; }; -export const findUserById = async ( - id: string -): Promise => { +export const findUserById = async (id: string): Promise => { const dbQuery = db.user.findUnique({ where: { id }, ...Utils.userAggregation, @@ -42,9 +41,7 @@ export const findUserById = async ( return user; }; -export const findUserByUsername = async ( - username: string -): Promise => { +export const findUserByUsername = async (username: string): Promise => { const dbQuery = db.user.findUnique({ where: { username }, ...Utils.userAggregation, @@ -73,35 +70,44 @@ export const findUserByIdOrByUsernameOrThrow = async (idOrUsername: string) => { return user; }; -export const updateUser = async ( - id: string, - { avatarId, password, ...data }: Types.UpdateUserOutput -) => { - const dbQuery = db.user.update({ - where: { id }, - data: { - ...data, - ...(password && typeof password === 'string' - ? { password: await hashPassword(password) } - : {}), - ...(avatarId - ? { - avatar: { - connectOrCreate: { - where: { userId: id }, - create: { imageId: avatarId }, - }, - }, - } - : {}), - }, - ...Utils.userAggregation, - }); - const handlerOptions = { - notFoundErrMsg: 'User not found', - uniqueFieldName: 'username', - }; - return await Utils.handleDBKnownErrors(dbQuery, handlerOptions); +export const updateUser = (id: string, { avatarId, password, ...data }: Types.UpdateUserOutput) => { + return Utils.handleDBKnownErrors( + db.$transaction(async (tx) => { + const updatedUser = await tx.user.update({ + where: { id }, + data: { + ...data, + ...(password && typeof password === 'string' + ? { password: await hashPassword(password) } + : {}), + ...(avatarId + ? { + avatar: { + connectOrCreate: { + where: { userId: id }, + create: { imageId: avatarId }, + }, + }, + } + : {}), + }, + ...Utils.userAggregation, + }); + if (data.username) { + const profileNameUpdateArgs = { + where: { profile: { userId: id } }, + data: { profileName: data.username }, + }; + await tx.profilesChats.updateMany(profileNameUpdateArgs); + await tx.message.updateMany(profileNameUpdateArgs); + } + return updatedUser; + }), + { + notFoundErrMsg: 'User not found', + uniqueFieldName: 'username', + } + ); }; export const deleteUser = async (id: string): Promise => { diff --git a/src/lib/passport.ts b/src/lib/passport.ts index 007d787..1e6b490 100644 --- a/src/lib/passport.ts +++ b/src/lib/passport.ts @@ -16,6 +16,7 @@ passport.use( (username, password, done) => { db.user .findUnique({ + ...Utils.userAggregation, where: { username: username }, omit: { password: false }, }) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5447b83..d39297e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -58,8 +58,12 @@ export const handleDBKnownErrors = async ( return post; }; +export const getCurrentUserFromReq = (req: Request) => { + return req.user as Types.PublicUser | undefined; +}; + export const getCurrentUserIdFromReq = (req: Request) => { - return (req.user as Types.PublicUser | undefined)?.id; + return getCurrentUserFromReq(req)?.id; }; export const getTextFilterFromReqQuery = (req: Request) => { @@ -88,20 +92,29 @@ export const getTagsFilterFromReqQuery = (req: Request) => { .safeParse(req.query.tags).data; }; -export const getPaginationFiltersFromReqQuery = ( - req: Request -): Types.PaginationFilters => { - const { cursor, sort, limit } = req.query; +export const getBasePaginationFiltersFromReqQuery = (req: Request): Types.BasePaginationFilters => { + return { + sort: z.literal('asc').or(z.literal('desc')).safeParse(req.query.sort).data, + limit: z.coerce.number().int().min(0).safeParse(req.query.limit).data, + cursor: z.string().trim().uuid().safeParse(req.query.cursor).data, + }; +}; + +export const getPaginationFiltersFromReqQuery = (req: Request): Types.PaginationFilters => { + return { + ...getBasePaginationFiltersFromReqQuery(req), + cursor: z.coerce.number().int().min(0).safeParse(req.query.cursor).data, + }; +}; + +export const getProfileFiltersFromReqQuery = (req: Request): Types.ProfileFilters => { return { - sort: z.literal('asc').or(z.literal('desc')).safeParse(sort).data, - cursor: z.coerce.number().int().min(0).safeParse(cursor).data, - limit: z.coerce.number().int().min(0).safeParse(limit).data, + ...getBasePaginationFiltersFromReqQuery(req), + name: z.string().trim().safeParse(req.query.name).data, }; }; -export const getCommonFiltersFromReqQuery = ( - req: Request -): Types.PaginationFilters => { +export const getCommonFiltersFromReqQuery = (req: Request): Types.PaginationFilters => { return { currentUserId: getCurrentUserIdFromReq(req), authorId: getAuthorIdFilterFromReqQuery(req), @@ -109,9 +122,7 @@ export const getCommonFiltersFromReqQuery = ( }; }; -export const getCommentFiltersFromReqQuery = ( - req: Request -): Types.CommentFilters => { +export const getCommentFiltersFromReqQuery = (req: Request): Types.CommentFilters => { return { ...getCommonFiltersFromReqQuery(req), text: getTextFilterFromReqQuery(req), @@ -133,10 +144,7 @@ export const getPostFiltersFromReqQuery = (req: Request): Types.PostFilters => { }; }; -export const getPaginationArgs = ( - filters: Types.PaginationFilters = {}, - take = 3 -) => { +export const getPaginationArgs = (filters: Types.PaginationFilters = {}, take = 3) => { return { orderBy: { order: filters.sort ?? 'asc' }, take: filters.limit ?? take, @@ -154,10 +162,17 @@ export const userAggregation: Types.UserAggregation = { avatar: { select: { image: { omit: { storageId: true, storageFullPath: true } } }, }, + profile: true, }, omit: { password: true }, }; +export const profileAggregation: Types.ProfileAggregation = { + include: { + user: { ...userAggregation, include: { ...userAggregation.include, profile: false } }, + }, +}; + export const fieldsToIncludeWithImage: Types.ImageDataToAggregate = { _count: { select: { posts: true } }, owner: userAggregation, diff --git a/src/tests/api/setup.ts b/src/tests/api/setup.ts index ee5f2d6..073e092 100644 --- a/src/tests/api/setup.ts +++ b/src/tests/api/setup.ts @@ -51,8 +51,8 @@ export const setup = async (signinUrl: string, expApp: App = app) => { const password = bcrypt.hashSync(data.password, 10); return await db.user.create({ omit: { password: false, isAdmin: false }, - include: { avatar: true, images: true }, - data: { ...data, password }, + include: { avatar: true, images: true, profile: true }, + data: { ...data, password, profile: data.profile ?? { create: { lastSeen: new Date() } } }, }); }; diff --git a/src/tests/api/v1/auth.int.test.ts b/src/tests/api/v1/auth.int.test.ts index 782eb4f..9daecac 100644 --- a/src/tests/api/v1/auth.int.test.ts +++ b/src/tests/api/v1/auth.int.test.ts @@ -1,6 +1,6 @@ import { it, expect, describe, afterAll, beforeAll, vi } from 'vitest'; import { SIGNIN_URL, VERIFY_URL, SIGNED_IN_USER_URL } from './utils'; -import { AppErrorResponse, AuthResponse } from '@/types'; +import { AppErrorResponse, AuthResponse, PublicUser } from '@/types'; import { User } from '@/../prisma/client'; import jwt from 'jsonwebtoken'; import setup from '../setup'; @@ -55,15 +55,14 @@ describe('Authentication endpoint', async () => { const res = await api.post(SIGNIN_URL).send(userData); const resBody = res.body as AuthResponse; const resUser = resBody.user; - const resJwtPayload = jwt.decode( - resBody.token.replace(/^Bearer /, '') - ) as User; + const resJwtPayload = jwt.decode(resBody.token.replace(/^Bearer /, '')) as User; expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(200); expect(Object.keys(resUser)).not.toContain('password'); expect(resUser.username).toBe(userData.username); expect(resUser.fullname).toBe(userData.fullname); expect(resUser.isAdmin).toStrictEqual(false); + expect(resUser.profile).toBeTruthy(); expect(resBody.token).toMatch(/^Bearer /i); expect(resJwtPayload.id).toStrictEqual(dbUser.id); expect(resJwtPayload.isAdmin).toStrictEqual(false); @@ -77,19 +76,15 @@ describe('Authentication endpoint', async () => { describe(`GET ${VERIFY_URL}`, () => { it('should verify a valid, fresh token and respond with `true`', async () => { - const signinResBody = (await api.post(SIGNIN_URL).send(userData)) - .body as AuthResponse; - const res = await api - .get(VERIFY_URL) - .set('Authorization', signinResBody.token); + const signinResBody = (await api.post(SIGNIN_URL).send(userData)).body as AuthResponse; + const res = await api.get(VERIFY_URL).set('Authorization', signinResBody.token); expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(200); expect(res.body).toBe(true); }); it('should not verify an invalid token and respond 401', async () => { - const signinResBody = (await api.post(SIGNIN_URL).send(userData)) - .body as AuthResponse; + const signinResBody = (await api.post(SIGNIN_URL).send(userData)).body as AuthResponse; const res = await api .get(VERIFY_URL) .set('Authorization', signinResBody.token.replace(/\../, '.x')); @@ -97,15 +92,12 @@ describe('Authentication endpoint', async () => { }); it('should not verify an expired token and respond 401', async () => { - const signinResBody = (await api.post(SIGNIN_URL).send(userData)) - .body as AuthResponse; + const signinResBody = (await api.post(SIGNIN_URL).send(userData)).body as AuthResponse; vi.useFakeTimers(); const now = new Date(); const future = new Date(now.setFullYear(now.getFullYear() + 3)); vi.setSystemTime(future); - const res = await api - .get(VERIFY_URL) - .set('Authorization', signinResBody.token); + const res = await api.get(VERIFY_URL).set('Authorization', signinResBody.token); expect(res.statusCode).toBe(401); }); }); @@ -120,16 +112,14 @@ describe('Authentication endpoint', async () => { }); it('should respond with 401 if the JWT is invalid', async () => { - const res = await api - .get(SIGNED_IN_USER_URL) - .set('Authorization', 'blah'); + const res = await api.get(SIGNED_IN_USER_URL).set('Authorization', 'blah'); assertUnauthorizedErrorRes(res); }); it('should respond with current signed in user data base on the JWT', async () => { const { authorizedApi } = await prepForAuthorizedTest(userData); const res = await authorizedApi.get(SIGNED_IN_USER_URL); - const resBody = res.body as User; + const resBody = res.body as User & PublicUser; expect(res.statusCode).toBe(200); expect(res.type).toMatch(/json/); expect(resBody.password).toBeUndefined(); @@ -137,6 +127,7 @@ describe('Authentication endpoint', async () => { expect(resBody.isAdmin).toStrictEqual(false); expect(resBody.username).toBe(dbUser.username); expect(resBody.fullname).toBe(dbUser.fullname); + expect(resBody.profile).toBeTruthy(); }); }); }); diff --git a/src/tests/api/v1/chats.int.test.ts b/src/tests/api/v1/chats.int.test.ts new file mode 100644 index 0000000..9604423 --- /dev/null +++ b/src/tests/api/v1/chats.int.test.ts @@ -0,0 +1,1123 @@ +/* eslint-disable security/detect-object-injection */ +import * as Types from '@/types'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { Prisma, Chat, Message } from '@/../prisma/client'; +import { CHATS_URL, SIGNIN_URL } from './utils'; +import { faker } from '@faker-js/faker'; +import setup from '@/tests/api/setup'; +import db from '@/lib/db'; +import fs from 'node:fs'; + +interface MessageIncludeFields { + seenBy: { include: { profile: { include: { user: true } } } }; + profile: { include: { user: true } }; + image: true; +} + +type ChatFullData = Prisma.ChatGetPayload<{ + include: { + messages: { include: MessageIncludeFields }; + profiles: { include: { profile: { include: { user: true } } } }; + managers: { include: { profile: { include: { user: true } } } }; + }; +}>; + +type ChatWithProfiles = Prisma.ChatGetPayload<{ include: { profiles: true } }>; + +type MessageFullData = Prisma.MessageGetPayload<{ include: MessageIncludeFields }>; + +const PAGE_LEN = 10; +const EXTRA_LEN = 3; +const ITEMS_LEN = PAGE_LEN + EXTRA_LEN; + +const assertMessage = (msg: MessageFullData, expectedMsgId: Message['id']) => { + expect(msg.id).toBe(expectedMsgId); + expect(msg.profileName).toBeTruthy(); + expect(msg.profile!.user.username).toBeTruthy(); + expect(msg.profileName).toBe(msg.profile!.user.username); + if (msg.image) expect(msg.image).not.toHaveProperty('owner'); +}; + +const assertChat = ( + chat: ChatFullData, + expectedChatId: Chat['id'], + msgCount = PAGE_LEN, + profilesCount = 2, +) => { + expect(chat.id).toBe(expectedChatId); + expect(chat.managers).toBeInstanceOf(Array); + expect(chat.managers).toHaveLength(1); + expect(chat.profiles).toBeInstanceOf(Array); + expect(chat.profiles).toHaveLength(profilesCount); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages).toHaveLength(msgCount); + for (const manager of chat.managers) { + expect(manager.profile.user.password).toBeUndefined(); + expect(manager.profile.user.username).toBeTruthy(); + } + for (const member of chat.profiles) { + expect(member.profile!.user.password).toBeUndefined(); + expect(member.profile!.user.username).toBeTruthy(); + } + for (const msg of chat.messages) assertMessage(msg, msg.id); +}; + +const assertChatMembersTangibility = (chat: ChatFullData) => { + for (const member of chat.profiles) { + expect(member.lastReceivedAt).toBeTruthy(); + if (member.profile?.tangible) { + expect(member.lastSeenAt).toBeTruthy(); + } else { + expect(member.lastSeenAt).toBeNull(); + } + } +}; + +const assertReceivedDateUpdated = ( + newChat: ChatWithProfiles, + oldChat: ChatWithProfiles, + receiverName: string, +) => { + for (const newChatMember of newChat.profiles) { + const oldChatMember = oldChat.profiles.find( + (p) => p.profileName === newChatMember.profileName, + )!; + if (newChatMember.profileName === receiverName) { + expect(new Date(oldChatMember.lastReceivedAt!).getTime()).toBeLessThan( + new Date(newChatMember.lastReceivedAt!).getTime(), + ); + } else { + expect(new Date(oldChatMember.lastReceivedAt!).getTime()).toBe( + new Date(newChatMember.lastReceivedAt!).getTime(), + ); + } + } +}; + +const assertSeenDateUpdated = async (chat: ChatWithProfiles, receiverName: string) => { + for (const member of chat.profiles) { + const dbMember = (await db.profilesChats.findUnique({ + where: { profileName_chatId: { profileName: member.profileName, chatId: chat.id } }, + }))!; + if (member.profileName === receiverName) { + expect(dbMember.lastSeenAt!.getTime()).toBeGreaterThan( + new Date(member.lastSeenAt!).getTime(), + ); + } else { + expect(dbMember.lastSeenAt!.getTime()).toBe(new Date(member.lastSeenAt!).getTime()); + } + } +}; + +describe('Chats endpoints', async () => { + const { + userOneData, + userTwoData, + storageData, + xUserData, + imagedata, + dbUserOne, + dbUserTwo, + dbXUser, + dbAdmin, + imgData, + api, + createUser, + createImage, + deleteAllUsers, + deleteAllImages, + assertImageData, + prepForAuthorizedTest, + assertNotFoundErrorRes, + assertUnauthorizedErrorRes, + assertResponseWithValidationError, + } = await setup(SIGNIN_URL); + + const { storage } = storageData; + + afterEach(vi.clearAllMocks); + + afterAll(async () => { + await db.chat.deleteMany({}); + await deleteAllImages(); + await deleteAllUsers(); + }); + + const createFakeUser = (usernameBlacklist: string[] = [], data: Record = {}) => { + const password = typeof data.password === 'string' ? data.password : faker.internet.password(); + const fullname = typeof data.fullName === 'string' ? data.fullName : faker.person.fullName(); + let username: string; + if (typeof data.username === 'string') username = data.username; + else { + username = faker.person.firstName(); + do { + username = faker.person.firstName(); + } while (usernameBlacklist.includes(username)); + } + return createUser({ username, fullname, password, profile: { create: { tangible: false } } }); + }; + + const createChat = async (msg = 'Hi!', users = [dbUserOne, dbUserTwo], withImage = true) => { + const profileName = users[0].username; + const profileId = users[0].profile!.id; + const imageId = msg && withImage ? (await createImage(imgData)).id : undefined; + return await db.chat.create({ + data: { + profiles: { + createMany: { + data: users.map((u) => ({ + profileName: u.username, + profileId: u.profile!.id, + lastReceivedAt: new Date(), + lastSeenAt: new Date(), + })), + }, + }, + managers: { create: { profileId: users[0].profile!.id, role: 'OWNER' } }, + ...(msg ? { messages: { create: { body: msg, profileId, profileName, imageId } } } : {}), + }, + include: { profiles: true, managers: true, messages: true }, + }); + }; + + const createMessage = async (chatId: Chat['id'], dbUser: typeof dbUserOne, withImage = true) => { + const profileId = dbUser.profile!.id; + return db.message.create({ + data: { + imageId: withImage ? (await createImage(imgData)).id : undefined, + profileName: dbUser.username, + body: faker.lorem.sentence(), + profileId, + chatId, + }, + }); + }; + + const dbUsers = [dbUserTwo, dbXUser, dbAdmin]; + const usedUsernames: string[] = []; + for (let i = 0; i < ITEMS_LEN; i++) { + dbUsers[i] = dbUsers[i] ?? (await createFakeUser(usedUsernames)); + usedUsernames.push(dbUsers[i].username); + } + + const intangibleUserData = { + password: faker.internet.password(), + fullname: 'Intangible User', + username: 'intangible_user', + profile: { create: { tangible: false } }, + }; + + describe('GET', () => { + const dbChats: Awaited>[] = []; + const dbMsgs: Message[] = []; + + beforeAll(async () => { + await db.chat.deleteMany({}); + for (const dbUser of dbUsers) { + // Ignore the initial, chat-owner message to be added with the other messages + const chat = await createChat('', [dbUserOne, dbUser]); + for (let j = 0; j < ITEMS_LEN; j++) { + dbMsgs.push(await createMessage(chat.id, dbUsers[j])); + } + dbChats.push(chat); + } + }); + + describe(CHATS_URL, () => { + it('should respond with 401 on unauthorized request', async () => { + const res = await api.get(CHATS_URL); + assertUnauthorizedErrorRes(res); + }); + + it('should respond desc-paginated chats with their profiles, managers, and 1st messages page', async () => { + const pages = [{ len: PAGE_LEN }, { len: EXTRA_LEN }]; + let firstChat: ChatFullData | undefined; + let cursor: Chat['id'] | undefined; + for (const page of pages) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}${cursor ? '?cursor=' + cursor : ''}`); + const resBody = res.body as ChatFullData[]; + cursor = resBody.at(-1)!.id; + firstChat ??= resBody[0]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(resBody).toBeInstanceOf(Array); + expect(resBody).toHaveLength(page.len); + for (const c of resBody) { + assertChat(c, c.id); + assertChatMembersTangibility(c); + const dbChat = dbChats.find((dbc) => dbc.id === c.id)!; + assertReceivedDateUpdated(c, dbChat, userOneData.username); + } + } + expect(firstChat!.id).toBe(dbChats.at(-1)!.id); + }); + + it('should respond custom-asc-paginated chats with their profiles, managers, and 1st messages page', async () => { + let firstChat: ChatFullData | undefined; + let cursor: Chat['id'] | undefined; + const limit = 2; + const pages: { len: number }[] = Array.from({ length: Math.ceil(ITEMS_LEN / limit) }).map( + (_, i, arr) => ({ len: i < arr.length - 1 ? limit : ITEMS_LEN - i * limit }), + ); + for (const page of pages) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${CHATS_URL}?sort=asc&limit=${limit}${cursor ? '&cursor=' + cursor : ''}`, + ); + const resBody = res.body as ChatFullData[]; + cursor = resBody.at(-1)!.id; + firstChat ??= resBody[0]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(resBody).toBeInstanceOf(Array); + expect(resBody).toHaveLength(page.len); + for (const c of resBody) { + assertChat(c, c.id); + assertChatMembersTangibility(c); + const dbChat = dbChats.find((dbc) => dbc.id === c.id)!; + assertReceivedDateUpdated(c, dbChat, userOneData.username); + } + } + expect(firstChat!.id).toBe(dbChats[0].id); + }); + }); + + describe(`${CHATS_URL}/members/:profileId`, () => { + it('should respond with 401 on unauthorized request', async () => { + const res = await api.get(`${CHATS_URL}/members/${crypto.randomUUID()}`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 404 if the given member id is not exist', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/members/${crypto.randomUUID()}`); + assertNotFoundErrorRes(res); + }); + + it('should respond all the current user chats that include the given member`s profile id', async () => { + const memberProfileId = dbUserTwo.profile!.id; + const dbChatId1 = dbChats.find((c) => + c.profiles.some((p) => p.profileId === memberProfileId), + )!.id; + const dbChat1 = (await db.chat.findUnique({ + where: { id: dbChatId1 }, + include: { profiles: true }, + }))!; + const dbChat2 = await createChat('Hello #2', [dbUserOne, dbUserTwo, dbXUser]); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/members/${memberProfileId}`); + const resBody = res.body as ChatFullData[]; + const resChat1 = resBody.find((c) => c.id === dbChat1.id)!; + const resChat2 = resBody.find((c) => c.id === dbChat2.id)!; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(resBody).toBeInstanceOf(Array); + expect(resBody).toHaveLength(2); + expect(resBody.every((c) => c.id === dbChat1.id || c.id === dbChat2.id)).toBe(true); + for (const c of resBody) { + if (c.id === dbChat2.id) assertChat(c, c.id, 1, 3); + else assertChat(c, c.id); + assertChatMembersTangibility(c); + } + assertReceivedDateUpdated(resChat1, dbChat1, userOneData.username); + assertReceivedDateUpdated(resChat2, dbChat2, userOneData.username); + }); + }); + + describe(`${CHATS_URL}/:id`, () => { + it('should respond with 401 on unauthorized request', async () => { + const res = await api.get(`${CHATS_URL}/${dbChats[0].id}`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 404 on unknown chat id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${crypto.randomUUID()}`); + assertNotFoundErrorRes(res); + }); + + it('should respond with 404 if the current user not a chat member', async () => { + const chat = dbChats[0]; + const password = faker.internet.password(); + const { id, username } = await createFakeUser(usedUsernames, { password }); + const { authorizedApi } = await prepForAuthorizedTest({ username, password }); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}`); + assertNotFoundErrorRes(res); + await db.user.delete({ where: { id } }); + }); + + it('should respond with 200 and the requested chat', async () => { + const chat = (await db.chat.findUnique({ + where: { id: dbChats[0].id }, + include: { profiles: true }, + }))!; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}`); + const resBody = res.body as ChatFullData; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertChat(resBody, chat.id); + assertChatMembersTangibility(resBody); + assertReceivedDateUpdated(resBody, chat, userOneData.username); + }); + }); + + describe(`${CHATS_URL}/:id/messages`, () => { + it('should respond with 401 on unauthorized request', async () => { + const res = await api.get(`${CHATS_URL}/${dbChats[0].id}/messages`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 404 and an empty array on unknown chat id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${crypto.randomUUID()}/messages`); + assertNotFoundErrorRes(res); + }); + + it('should respond with 404 if the current user not a chat member', async () => { + const chat = dbChats[0]; + const password = faker.internet.password(); + const { id, username } = await createFakeUser(usedUsernames, { password }); + const { authorizedApi } = await prepForAuthorizedTest({ username, password }); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}/messages`); + assertNotFoundErrorRes(res); + await db.user.delete({ where: { id } }); + }); + + it('should respond with 200 and a desc-paginated messages with their profiles, seen-by, and images', async () => { + const pages = [{ len: PAGE_LEN }, { len: EXTRA_LEN }]; + let firstMessage: MessageFullData | undefined; + let cursor: Message['id'] | undefined; + const chat = dbChats[0]; + for (const page of pages) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${CHATS_URL}/${chat.id}/messages${cursor ? '?cursor=' + cursor : ''}`, + ); + const resBody = res.body as MessageFullData[]; + cursor = resBody.at(-1)!.id; + firstMessage ??= resBody[0]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(resBody).toBeInstanceOf(Array); + expect(resBody).toHaveLength(page.len); + resBody.forEach((m) => assertMessage(m, m.id)); + } + expect(firstMessage?.id).toBe(dbMsgs.at(ITEMS_LEN - 1)!.id); + const updatedChat = (await db.chat.findUnique({ + where: { id: chat.id }, + include: { profiles: true }, + }))!; + assertReceivedDateUpdated(updatedChat, chat, userOneData.username); + }); + + it('should respond with 200 and a custom-asc-paginated messages with their profiles, seen-by and images', async () => { + let firstMessage: MessageFullData | undefined; + let cursor: Message['id'] | undefined; + const chat = dbChats[0]; + const limit = 2; + const pages: { len: number }[] = Array.from({ length: Math.ceil(ITEMS_LEN / limit) }).map( + (_, i, arr) => ({ len: i < arr.length - 1 ? limit : ITEMS_LEN - i * limit }), + ); + for (const page of pages) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${CHATS_URL}/${chat.id}/messages?sort=asc&limit=${limit}${ + cursor ? '&cursor=' + cursor : '' + }`, + ); + const resBody = res.body as MessageFullData[]; + cursor = resBody.at(-1)!.id; + firstMessage ??= resBody[0]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(resBody).toBeInstanceOf(Array); + expect(resBody).toHaveLength(page.len); + resBody.forEach((m) => assertMessage(m, m.id)); + } + expect(firstMessage!.id).toBe(dbMsgs[0].id); + const updatedChat = (await db.chat.findUnique({ + where: { id: chat.id }, + include: { profiles: true }, + }))!; + assertReceivedDateUpdated(updatedChat, chat, userOneData.username); + }); + }); + + describe(`${CHATS_URL}/:id/messages/:msgId`, () => { + it('should respond with 401 on unauthorized request', async () => { + const chat = dbChats[0]; + const msg = dbMsgs[0]; + const res = await api.get(`${CHATS_URL}/${chat.id}/messages/${msg.id}`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 404 on unknown chat id', async () => { + const chat = { id: crypto.randomUUID() }; + const msg = dbMsgs[0]; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}/messages/${msg.id}`); + assertNotFoundErrorRes(res); + }); + + it('should respond with 404 on unknown message id', async () => { + const chat = dbChats[0]; + const msg = { id: crypto.randomUUID() }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}/messages/${msg.id}`); + assertNotFoundErrorRes(res); + }); + + it('should respond with 404 if the current user not a chat member', async () => { + const chat = dbChats[0]; + const msg = dbMsgs[0]; + const password = faker.internet.password(); + const { id, username } = await createFakeUser(usedUsernames, { password }); + const { authorizedApi } = await prepForAuthorizedTest({ username, password }); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}/messages/${msg.id}`); + assertNotFoundErrorRes(res); + await db.user.delete({ where: { id } }); + }); + + it('should respond with 200 and a message', async () => { + const chat = dbChats[0]; + const msg = dbMsgs[0]; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${CHATS_URL}/${chat.id}/messages/${msg.id}`); + const updatedChat = (await db.chat.findUnique({ + where: { id: chat.id }, + include: { profiles: true }, + }))!; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertMessage(res.body as MessageFullData, msg.id); + assertReceivedDateUpdated(updatedChat, chat, userOneData.username); + }); + }); + }); + + describe(`POST ${CHATS_URL}/:id/messages`, () => { + let dbChat: Awaited>; + const msgData = { body: "What's up?" }; + + beforeAll(async () => { + await db.chat.deleteMany({}); + dbChat = await createChat('', [dbUserOne, dbUserTwo]); + }); + + afterAll(async () => { + await db.chat.deleteMany({}); + }); + + afterEach(async () => { + await db.message.deleteMany({}); + await deleteAllImages(); + }); + + it('should respond with 401 on unauthorized request', async () => { + const res = await api.post(`${CHATS_URL}/${dbChat.id}/messages`).send(msgData); + assertUnauthorizedErrorRes(res); + expect(await db.message.findMany({})).toHaveLength(0); + }); + + it('should respond with 400 on request without message data', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${CHATS_URL}/${dbChat.id}/messages`); + expect(await db.message.findMany({})).toHaveLength(0); + assertResponseWithValidationError(res); + }); + + it('should respond with 400 on request without message body', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${CHATS_URL}/${dbChat.id}/messages`).send({}); + assertResponseWithValidationError(res, 'body'); + expect(await db.message.findMany({})).toHaveLength(0); + }); + + it('should respond with 404 on non-existent chat id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${crypto.randomUUID()}/messages`) + .send(msgData); + assertNotFoundErrorRes(res); + expect(await db.message.findMany({})).toHaveLength(0); + }); + + it('should respond with 400 on request with invalid image type', async () => { + const stream = fs.createReadStream('src/tests/files/ugly.txt'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${dbChat.id}/messages`) + .field(msgData) + .field(preparedImgData) + .attach('image', stream); + const resBody = res.body as Types.AppErrorResponse; + expect(res.type).toMatch(/json/); + expect(res.statusCode).toBe(400); + expect(resBody.error.message).toMatch(/invalid image/i); + expect(storage.upload).not.toHaveBeenCalledOnce(); + }); + + it('should respond with 400 on request with too large image file', async () => { + const stream = fs.createReadStream('src/tests/files/bad.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${dbChat.id}/messages`) + .field(msgData) + .field(preparedImgData) + .attach('image', stream); + const resBody = res.body as Types.AppErrorResponse; + expect(res.type).toMatch(/json/); + expect(res.statusCode).toBe(400); + expect(resBody.error.message).toMatch(/too large/i); + expect(storage.upload).not.toHaveBeenCalledOnce(); + }); + + it('should respond with 201 and create message, and update the sender profile chat-last-received/seen date', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${CHATS_URL}/${dbChat.id}/messages`).send(msgData); + const updatedDBChat = (await db.chat.findUnique({ + where: { id: dbChat.id }, + include: { profiles: true }, + }))!; + const dbMsgs = await db.message.findMany({}); + const resBody = res.body as MessageFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbMsgs).toHaveLength(1); + expect(resBody.id).toBe(dbMsgs[0].id); + await assertSeenDateUpdated(dbChat as ChatFullData, userOneData.username); + assertReceivedDateUpdated(updatedDBChat, dbChat, userOneData.username); + }); + + it('should respond with 201, create message without image, and ignore `imagedata` field without an image file', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${dbChat.id}/messages`) + .send({ ...msgData, imagedata }); + const dbMsgs = await db.message.findMany({ include: { image: true } }); + const resBody = res.body as MessageFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbMsgs).toHaveLength(1); + expect(resBody.image).toBeNull(); + expect(resBody.imageId).toBeNull(); + expect(resBody.id).toBe(dbMsgs[0].id); + expect(storage.upload).not.toHaveBeenCalled(); + }); + + it('should respond with 201 and create a message with an image', async () => { + const stream = fs.createReadStream('src/tests/files/good.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${dbChat.id}/messages`) + .field(msgData) + .field(preparedImgData) + .attach('image', stream); + const dbMsgs = await db.message.findMany({ include: { image: true } }); + const resBody = res.body as MessageFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbMsgs).toHaveLength(1); + expect(resBody.id).toBe(dbMsgs[0].id); + expect(resBody.imageId).toBe(dbMsgs[0].imageId); + assertImageData(Object.assign(res, { body: resBody.image }), { ...imgData, ...imagedata }); + expect(storage.upload).toHaveBeenCalledOnce(); + expect(storage.upload.mock.calls.at(-1)?.at(-1)).toHaveProperty('upsert', false); + }); + + it('should respond with 201 and create an empty message with an image', async () => { + const stream = fs.createReadStream('src/tests/files/good.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(`${CHATS_URL}/${dbChat.id}/messages`) + .field(preparedImgData) + .attach('image', stream); + const dbMsgs = await db.message.findMany({ include: { image: true } }); + const resBody = res.body as MessageFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(resBody.body).toBe(''); + expect(dbMsgs).toHaveLength(1); + expect(resBody.id).toBe(dbMsgs[0].id); + expect(resBody.body).toBe(dbMsgs[0].body); + expect(resBody.imageId).toBe(dbMsgs[0].imageId); + assertImageData(Object.assign(res, { body: resBody.image }), { ...imgData, ...imagedata }); + expect(storage.upload).toHaveBeenCalledOnce(); + expect(storage.upload.mock.calls.at(-1)?.at(-1)).toHaveProperty('upsert', false); + }); + }); + + describe(`POST ${CHATS_URL}`, () => { + afterEach(async () => { + await db.chat.deleteMany({}); + await db.image.deleteMany({}); + await db.user.deleteMany({ where: { username: intangibleUserData.username } }); + }); + + it('should respond with 401 on unauthorized request', async () => { + const res = await api.post(CHATS_URL); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 400 on request with an empty object', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send({}); + assertResponseWithValidationError(res, '', 2); + }); + + it('should respond with 400 on request with invalid profiles data', async () => { + const message = { body: 'Hello!' }; + const invalidData = [{ profiles: null }, { profiles: ['blah'] }, { profiles: [7] }]; + for (const data of invalidData) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send({ ...data, message }); + assertResponseWithValidationError(res, 'profiles', 1); + } + }); + + it('should respond with 400 on request with an empty message', async () => { + const testData = { profiles: [crypto.randomUUID()], message: { body: '' } }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(testData); + assertResponseWithValidationError(res, 'body', 1); + }); + + it('should respond with 400 on request with an invalid message', async () => { + const invalidData = [{ message: 'Hello!' }, { message: true }, { message: 7 }]; + for (const data of invalidData) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(CHATS_URL) + .send({ ...data, profiles: [dbUserTwo.profile!.id] }); + assertResponseWithValidationError(res, 'message', 1); + } + }); + + it('should respond with 400 on request with invalid image type', async () => { + const stream = fs.createReadStream('src/tests/files/ugly.txt'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const chatData = { 'profiles[0]': dbUserTwo.profile!.id, 'message[body]': 'Hello!' }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(CHATS_URL) + .field(chatData) + .field(preparedImgData) + .attach('image', stream); + const resBody = res.body as Types.AppErrorResponse; + expect(res.type).toMatch(/json/); + expect(res.statusCode).toBe(400); + expect(resBody.error.message).toMatch(/invalid image/i); + expect(storage.upload).not.toHaveBeenCalledOnce(); + }); + + it('should respond with 400 on request with too large image file', async () => { + const stream = fs.createReadStream('src/tests/files/bad.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const chatData = { 'profiles[0]': dbUserTwo.profile!.id, 'message[body]': 'Hello!' }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(CHATS_URL) + .field(chatData) + .field(preparedImgData) + .attach('image', stream); + const resBody = res.body as Types.AppErrorResponse; + expect(res.type).toMatch(/json/); + expect(res.statusCode).toBe(400); + expect(resBody.error.message).toMatch(/too large/i); + expect(storage.upload).not.toHaveBeenCalledOnce(); + }); + + it('should create new chat with a message that have seen by the its sender, ignoring any unknown IDs', async () => { + const chatData = { + profiles: [dbUserTwo.profile!.id, crypto.randomUUID()], + message: { body: 'Hello!' }, + }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(1); + assertChat(chat, dbChats[0].id, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages[0].id).toBe(dbMsgs[0].id); + }); + + it('should create a self-chat if the given profiles array is empty or has an unknown profile id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const profilesData = [[], [crypto.randomUUID()]]; + for (const profiles of profilesData) { + const data = { profiles, message: { body: 'Hello!' } }; + const res = await authorizedApi.post(CHATS_URL).send(data); + const dbChats = await db.chat.findMany({ include: { profiles: true } }); + const dbMsgs = await db.message.findMany({}); + await db.chat.deleteMany({}); + await db.message.deleteMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(1); + assertChat(chat, dbChats[0].id, 1, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.profiles[0].profileName).toBe(dbUserOne.username); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages[0].id).toBe(dbMsgs[0].id); + expect(chat.messages[0].body).toBe(data.message.body); + } + }); + + it('should use an already exist chat, and eliminate any last-seen-date intangible profiles', async () => { + const intangibleUser = await createUser(intangibleUserData); + const chatMembers = [dbUserOne, intangibleUser, dbUserTwo]; + const oldChat = await createChat('', chatMembers); + await createMessage(oldChat.id, dbUserOne); + const chatData = { + profiles: chatMembers.map((u) => u.profile!.id), + message: { body: 'Whats up?' }, + }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({ orderBy: { createdAt: 'desc' } }); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(2); + assertChat(chat, dbChats[0].id, 2, 3); + assertChatMembersTangibility(chat); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages).toHaveLength(2); + }); + + it('should use an already exist chat, and include the last-seen-date for the current, intangible profile only', async () => { + const intangibleUser = await createUser(intangibleUserData); + const chatMembers = [intangibleUser, dbUserOne, dbUserTwo]; + const oldChat = await createChat('', chatMembers); + await createMessage(oldChat.id, dbUserOne); + const chatData = { + profiles: chatMembers.map((u) => u.profile!.id), + message: { body: 'Whats up?' }, + }; + const { authorizedApi } = await prepForAuthorizedTest(intangibleUserData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({ orderBy: { createdAt: 'desc' } }); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(2); + assertChat(chat, dbChats[0].id, 2, 3); + for (const member of chat.profiles) { + expect(member.lastReceivedAt).toBeTruthy(); + if (member.profileName === intangibleUser.username) { + expect(member.lastSeenAt).toBeTruthy(); + } else { + expect(member.lastSeenAt).toBeNull(); + } + } + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages).toHaveLength(2); + }); + + it('should use an already exist chat that has a message and delete the any other duplications', async () => { + for (let i = 0; i < 3; i++) await createChat(i % 2 > 0 ? 'Hi!' : ''); + const chatData = { profiles: [dbUserTwo.profile!.id], message: { body: 'Hello!' } }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(2); + assertChat(chat, dbChats[0].id, 2); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + }); + + it('should create new chat with a non-image message, and ignore `imagedata` field without an image file', async () => { + const profileId = dbUserTwo.profile!.id; + const chatData = { profiles: [profileId], message: { body: 'Hello!' }, imagedata }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(1); + assertChat(chat, dbChats[0].id, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + assertMessage(chat.messages[0], dbMsgs[0].id); + }); + + it('should create new chat with an image a non-empty message', async () => { + const stream = fs.createReadStream('src/tests/files/good.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const chatData = { 'profiles[0]': dbUserTwo.profile!.id, 'message[body]': 'Hello!' }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(CHATS_URL) + .field(chatData) + .field(preparedImgData) + .attach('image', stream); + const dbMsgs = await db.message.findMany({ include: { image: true } }); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(1); + assertChat(chat, dbChats[0].id, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages[0].image).toBeTruthy(); + assertMessage(chat.messages[0], dbMsgs[0].id); + expect(chat.messages[0].imageId).toBe(dbMsgs[0].imageId); + expect(storage.upload).toHaveBeenCalledOnce(); + expect(storage.upload.mock.calls.at(-1)?.at(-1)).toHaveProperty('upsert', false); + }); + + it('should create new chat with an image and an empty message', async () => { + const stream = fs.createReadStream('src/tests/files/good.jpg'); + const preparedImgData = Object.fromEntries( + Object.entries(imagedata).map(([k, v]) => [`imagedata[${k}]`, v]), + ); + const chatData = { 'profiles[0]': dbUserTwo.profile!.id }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi + .post(CHATS_URL) + .field(chatData) + .field(preparedImgData) + .attach('image', stream); + const dbMsgs = await db.message.findMany({ include: { image: true } }); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(1); + expect(dbMsgs).toHaveLength(1); + assertChat(chat, dbChats[0].id, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + expect(chat.messages).toBeInstanceOf(Array); + expect(chat.messages[0].image).toBeTruthy(); + expect(chat.messages[0].body).toBe(''); + assertMessage(chat.messages[0], dbMsgs[0].id); + expect(chat.messages[0].imageId).toBe(dbMsgs[0].imageId); + expect(storage.upload).toHaveBeenCalledOnce(); + expect(storage.upload.mock.calls.at(-1)?.at(-1)).toHaveProperty('upsert', false); + }); + + it('should create multiple chats sequentially, each of which have seen/received by the its owner', async () => { + const profileIds = [dbUserTwo.profile!.id, dbXUser.profile!.id, dbAdmin.profile!.id]; + for (let i = 0; i < profileIds.length; i++) { + const iterNum = i + 1; + const chatData = { profiles: [profileIds[i]], message: { body: 'Hello!' } }; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(CHATS_URL).send(chatData); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + const chat = res.body as ChatFullData; + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(dbChats).toHaveLength(iterNum); + expect(dbMsgs).toHaveLength(iterNum); + assertChat(chat, dbChats[i].id, 1); + expect(chat.profiles[0].lastSeenAt).toBeTruthy(); + expect(chat.profiles[0].lastReceivedAt).toBeTruthy(); + expect(dbMsgs[i].chatId).toBe(dbChats[i].id); + expect(chat.messages).toBeInstanceOf(Array); + assertMessage(chat.messages[0], dbMsgs[i].id); + } + }); + }); + + describe(`PATCH ${CHATS_URL}/:id/seen`, () => { + let dbChat: Awaited>; + + const assertProfileChatDatesUnmodified = async () => { + const dbChatNow = (await db.chat.findUnique({ + where: { id: dbChat.id }, + include: { profiles: true }, + }))!; + expect(dbChatNow.profiles.map((p) => [p.lastSeenAt, p.lastReceivedAt])).toStrictEqual( + dbChat.profiles.map((p) => [p.lastSeenAt, p.lastReceivedAt]), + ); + }; + + const assertProfileChatSeenDateUpdated = async ( + updatedProfileId: Types.PublicProfile['id'], + updatedDate: string, + ) => { + const dbChatNow = (await db.chat.findUnique({ + where: { id: dbChat.id }, + include: { profiles: true }, + }))!; + const chatProfile = dbChat.profiles.find((p) => p.profileId === updatedProfileId)!; + const chatProfileNow = dbChatNow.profiles.find((p) => p.profileId === updatedProfileId)!; + expect(chatProfileNow.lastReceivedAt!.getTime()).toBe(chatProfile.lastReceivedAt!.getTime()); + expect(chatProfileNow.lastSeenAt!.getTime()).toBeGreaterThan( + chatProfile.lastSeenAt!.getTime(), + ); + expect(chatProfileNow.lastSeenAt!.toISOString()).toBe(updatedDate); + }; + + beforeAll(async () => { + await db.chat.deleteMany({}); + dbChat = await createChat('', [dbUserOne, dbUserTwo]); + }); + + afterAll(async () => { + await db.chat.deleteMany({}); + }); + + it('should respond with 401 on unauthorized request', async () => { + const res = await api.patch(`${CHATS_URL}/${dbChat.id}/seen`); + assertUnauthorizedErrorRes(res); + await assertProfileChatDatesUnmodified(); + }); + + it('should respond with 404 on non-existent chat id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(`${CHATS_URL}/${crypto.randomUUID()}/seen`); + assertNotFoundErrorRes(res); + await assertProfileChatDatesUnmodified(); + }); + + it('should respond with 404 on request from none chat member', async () => { + const { authorizedApi } = await prepForAuthorizedTest(xUserData); + const res = await authorizedApi.patch(`${CHATS_URL}/${crypto.randomUUID()}/seen`); + assertNotFoundErrorRes(res); + await assertProfileChatDatesUnmodified(); + }); + + it('should set last seen date', async () => { + for (const [ud, dbu] of [ + [userOneData, dbUserOne], + [userTwoData, dbUserTwo], + ] as const) { + const { authorizedApi } = await prepForAuthorizedTest(ud); + const res = await authorizedApi.patch(`${CHATS_URL}/${dbChat.id}/seen`); + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + await assertProfileChatSeenDateUpdated(dbu.profile!.id, res.body as string); + } + }); + + it('should update last seen date', async () => { + await db.profilesChats.updateMany({ + where: { chatId: dbChat.id }, + data: { lastSeenAt: new Date() }, + }); + for (const [ud, dbu] of [ + [userOneData, dbUserOne], + [userTwoData, dbUserTwo], + ] as const) { + const { authorizedApi } = await prepForAuthorizedTest(ud); + const res = await authorizedApi.patch(`${CHATS_URL}/${dbChat.id}/seen`); + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + await assertProfileChatSeenDateUpdated(dbu.profile!.id, res.body as string); + } + }); + }); + + describe(`DELETE ${CHATS_URL}/:id`, () => { + afterEach(async () => { + await db.chat.deleteMany({}); + }); + + it('should respond with 401 on unauthorized request', async () => { + const chatId = (await createChat()).id; + const res = await api.delete(`${CHATS_URL}/${chatId}`); + await api.delete(`${CHATS_URL}/${chatId}`); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + assertUnauthorizedErrorRes(res); + expect(dbMsgs).toHaveLength(1); + expect(dbChats).toHaveLength(1); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + }); + + it('should respond with 204, and do nothing, on request with a non-existent chat id', async () => { + await createChat(); + const chatId = crypto.randomUUID(); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.delete(`${CHATS_URL}/${chatId}`); + await authorizedApi.delete(`${CHATS_URL}/${chatId}`); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + expect(res.body).toStrictEqual({}); + expect(res.statusCode).toBe(204); + expect(dbMsgs).toHaveLength(1); + expect(dbChats).toHaveLength(1); + expect(dbMsgs[0].chatId).toBe(dbChats[0].id); + }); + + it('should respond with 204 and delete the profile from chat, delete an empty chat, and be idempotent', async () => { + await createChat(); + const usersData = [userOneData, userTwoData]; + const users = [dbUserOne, dbUserTwo]; + const chat = await createChat('Hi!', users); + for (let i = 0; i < users.length; i++) { + const { authorizedApi } = await prepForAuthorizedTest(usersData[i]); + const res = await authorizedApi.delete(`${CHATS_URL}/${chat.id}`); + await authorizedApi.delete(`${CHATS_URL}/${chat.id}`); + const dbMsgs = await db.message.findMany({}); + const dbChats = await db.chat.findMany({}); + expect(res.body).toStrictEqual({}); + expect(res.statusCode).toBe(204); + if (i === users.length - 1) { + expect(dbMsgs).toHaveLength(1); + expect(dbChats).toHaveLength(1); + } else { + expect(dbMsgs).toHaveLength(2); + expect(dbChats).toHaveLength(2); + } + } + expect(await db.message.findMany({})).toHaveLength(1); + expect(await db.chat.findMany({})).toHaveLength(1); + }); + }); +}); diff --git a/src/tests/api/v1/profiles.int.test.ts b/src/tests/api/v1/profiles.int.test.ts new file mode 100644 index 0000000..317f46d --- /dev/null +++ b/src/tests/api/v1/profiles.int.test.ts @@ -0,0 +1,466 @@ +/* eslint-disable security/detect-object-injection */ +import * as Types from '@/types'; +import { BASE_URL, PROFILES_URL, SIGNIN_URL } from './utils'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { faker } from '@faker-js/faker'; +import setup from '../setup'; +import db from '@/lib/db'; + +const assertPublicProfile = ( + profile: Types.PublicProfile, + props: Partial<{ tangible: boolean; visible: boolean; followedByCurrentUser: boolean }> = {}, +) => { + props.visible ??= true; + props.tangible ??= true; + props.followedByCurrentUser ??= false; + expect(profile.followedByCurrentUser).toBe(props.followedByCurrentUser); + expect(profile.tangible).toBe(props.tangible); + expect(profile.visible).toBe(props.visible); + expect(profile.userId).toBeTypeOf('string'); + expect(profile.id).toBeTypeOf('string'); + expect(profile.userId).toHaveLength(36); + expect(profile.id).toHaveLength(36); + expect(profile.user.id).toHaveLength(36); + expect(profile.user.id).toBeTypeOf('string'); + if (profile.visible) { + expect(new Date(profile.lastSeen).getTime()).toBeLessThanOrEqual(Date.now()); + } else { + expect(profile.lastSeen).toBe(profile.user.createdAt); + } +}; + +describe('Profile', async () => { + const { + userOneData, + dbUserOne, + dbUserTwo, + dbXUser, + dbAdmin, + api, + deleteAllUsers, + prepForAuthorizedTest, + assertNotFoundErrorRes, + assertInvalidIdErrorRes, + assertUnauthorizedErrorRes, + assertResponseWithValidationError, + } = await setup(SIGNIN_URL); + + const compareString = (a: string, b: string) => { + for (let i = 0; i < a.length; i++) { + const result = a.charCodeAt(i) - b.charCodeAt(i); + if (result !== 0) return result; + } + return a.length - b.length; + }; + + const dbAllUsersSorted = [dbUserOne, dbUserTwo, dbXUser, dbAdmin].sort((a, b) => + compareString(a.username, b.username), + ); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('endpoints', () => { + describe('GET', () => { + beforeAll(async () => { + const dbUserOneProfileId = dbUserOne.profile!.id; + const data = [ + { profileId: dbUserTwo.profile!.id, followerId: dbUserOneProfileId }, + { profileId: dbXUser.profile!.id, followerId: dbUserOneProfileId }, + { profileId: dbAdmin.profile!.id, followerId: dbUserOneProfileId }, + { profileId: dbUserOneProfileId, followerId: dbUserTwo.profile!.id }, + { profileId: dbUserOneProfileId, followerId: dbXUser.profile!.id }, + { profileId: dbUserOneProfileId, followerId: dbAdmin.profile!.id }, + ]; + await db.follows.createMany({ data }); + }); + + afterAll(async () => { + await db.follows.deleteMany({}); + }); + + describe(PROFILES_URL, () => { + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.get(PROFILES_URL); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with profiles list in ascending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(PROFILES_URL); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(dbAllUsersSorted.length); + for (let i = 0; i < profiles.length; i++) { + const followedByCurrentUser = profiles[i].id !== dbUserOne.profile!.id; + assertPublicProfile(profiles[i], { followedByCurrentUser }); + expect(profiles[i].id).toBe(dbAllUsersSorted[i].profile!.id); + } + }); + + it('should respond with profiles list in descending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}?sort=desc`); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBeGreaterThan(1); + for (let i = 0; i < profiles.length; i++) { + const followedByCurrentUser = profiles[i].id !== dbUserOne.profile!.id; + assertPublicProfile(profiles[i], { followedByCurrentUser }); + expect(profiles[i].id).toBe( + dbAllUsersSorted[dbAllUsersSorted.length - 1 - i].profile!.id, + ); + } + }); + + it('should respond with a list of the last profile only, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${PROFILES_URL}?limit=1&cursor=${dbAllUsersSorted.at(-2)!.profile!.id}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(dbAllUsersSorted.at(-1)!.profile!.id); + }); + + it('should respond with a list of profiles that matches the query name, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + for (const user of [dbUserTwo, dbXUser, dbAdmin]) { + const res = await authorizedApi.get( + `${PROFILES_URL}?name=${user.username.slice(0, -2)}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(user.profile!.id); + } + }); + }); + + const getProfileTestData = [ + { key: 'id', value: dbUserOne.profile!.id }, + { key: 'username', value: dbUserOne.username }, + ]; + for (const { key, value } of getProfileTestData) { + describe(`${PROFILES_URL}/:${key}`, () => { + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.get(`${PROFILES_URL}/${value}`); + assertUnauthorizedErrorRes(res); + }); + + it(`should respond with 400 on an invalid ${key}`, async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/invalid`); + assertInvalidIdErrorRes(res); + }); + + it('should respond with a profile', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/${value}`); + const profile = res.body as Types.PublicProfile; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertPublicProfile(profile); + }); + }); + } + + describe(`${PROFILES_URL}/following`, () => { + const sortedUsers = dbAllUsersSorted.filter((u) => u.id !== dbUserOne.id); + + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.get(`${PROFILES_URL}/following`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with following profiles list in ascending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/following`); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(3); + for (let i = 0; i < profiles.length; i++) { + assertPublicProfile(profiles[i], { followedByCurrentUser: true }); + expect(profiles[i].id).toBe(sortedUsers[i].profile!.id); + } + }); + + it('should respond with following profiles list in descending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/following?sort=desc`); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(3); + for (let i = 0; i < profiles.length; i++) { + assertPublicProfile(profiles[i], { followedByCurrentUser: true }); + expect(profiles[i].id).toBe(sortedUsers[sortedUsers.length - 1 - i].profile!.id); + } + }); + + it('should respond with a list of the last following profile only, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${PROFILES_URL}/following?limit=1&cursor=${sortedUsers.at(-2)!.profile!.id}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(sortedUsers.at(-1)!.profile!.id); + }); + + it('should respond with a list of the following profiles that matches the name query, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + for (const user of [dbUserTwo, dbXUser, dbAdmin]) { + const res = await authorizedApi.get( + `${PROFILES_URL}/following?name=${user.username.slice(0, -2)}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(user.profile!.id); + } + }); + }); + + describe(`${PROFILES_URL}/followers`, () => { + const sortedUsers = dbAllUsersSorted.filter((u) => u.id !== dbUserOne.id); + + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.get(`${PROFILES_URL}/followers`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with followers profiles list in ascending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/followers`); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(3); + for (let i = 0; i < profiles.length; i++) { + assertPublicProfile(profiles[i], { followedByCurrentUser: true }); + expect(profiles[i].id).toBe(sortedUsers[i].profile!.id); + } + }); + + it('should respond with followers profiles list in descending order, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get(`${PROFILES_URL}/followers?sort=desc`); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(3); + for (let i = 0; i < profiles.length; i++) { + assertPublicProfile(profiles[i], { followedByCurrentUser: true }); + expect(profiles[i].id).toBe(sortedUsers[sortedUsers.length - 1 - i].profile!.id); + } + }); + + it('should respond with a list of the last followers profile only, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.get( + `${PROFILES_URL}/followers?limit=1&cursor=${sortedUsers.at(-2)!.profile!.id}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(sortedUsers.at(-1)!.profile!.id); + }); + + it('should respond with a list of the followers profiles that matches the name query, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + for (const user of [dbUserTwo, dbXUser, dbAdmin]) { + const res = await authorizedApi.get( + `${PROFILES_URL}/followers?name=${user.username.slice(0, -2)}`, + ); + const profiles = res.body as Types.PublicProfile[]; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + expect(profiles.length).toBe(1); + assertPublicProfile(profiles[0], { followedByCurrentUser: true }); + expect(profiles[0].id).toBe(user.profile!.id); + } + }); + }); + }); + + describe(`PATCH ${PROFILES_URL}`, () => { + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.patch(PROFILES_URL); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 400 on an authenticated request with invalid data', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(PROFILES_URL).send({ tangible: '' }); + assertResponseWithValidationError(res, 'tangible'); + }); + + it('should respond with 404 on an authenticated request, for non-existent profile', async () => { + await db.profile.delete({ where: { userId: dbUserOne.id } }); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(PROFILES_URL).send({}); + assertNotFoundErrorRes(res); + dbUserOne.profile = await db.profile.create({ + data: { userId: dbUserOne.id, lastSeen: new Date() }, + }); + }); + + it('should respond with a profile has updated tangibility, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(PROFILES_URL).send({ tangible: false }); + const profile = res.body as Types.PublicProfile; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertPublicProfile(profile, { tangible: false, visible: true }); + await db.profile.update({ where: { userId: dbUserOne.id }, data: { tangible: true } }); + }); + + it('should respond with a profile has updated visibility, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(PROFILES_URL).send({ visible: false }); + const profile = res.body as Types.PublicProfile; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertPublicProfile(profile, { tangible: true, visible: false }); + await db.profile.update({ where: { userId: dbUserOne.id }, data: { visible: true } }); + }); + + it('should respond with an unmodified profile, on an authenticated request', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.patch(PROFILES_URL).send({}); + const profile = res.body as Types.PublicProfile; + expect(res.statusCode).toBe(200); + expect(res.type).toMatch(/json/); + assertPublicProfile(profile); + }); + }); + + describe(`POST ${PROFILES_URL}/following`, () => { + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.post(`${PROFILES_URL}/following/${crypto.randomUUID()}`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 400 on an authenticated request with invalid profile id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${PROFILES_URL}/following/123`); + assertResponseWithValidationError(res, 'profileId'); + }); + + it('should respond with 400 on an authenticated request, for non-existent profile id', async () => { + const profileId = crypto.randomUUID(); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${PROFILES_URL}/following/${profileId}`); + assertInvalidIdErrorRes(res); + }); + + it('should respond with 201 and empty body, and create new follow', async () => { + const profileId = dbUserTwo.profile!.id; + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${PROFILES_URL}/following/${profileId}`); + const follows = await db.follows.findMany({}); + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(res.body).toBe(''); + expect(follows).toHaveLength(1); + expect(follows[0].profileId).toBe(profileId); + expect(follows[0].followerId).toBe(dbUserOne.profile!.id); + await db.follows.deleteMany({}); + }); + + it('should respond with 201 and empty body, and not create new follow if it exists', async () => { + const profileId = dbUserTwo.profile!.id; + await db.follows.create({ data: { profileId, followerId: dbUserOne.profile!.id } }); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.post(`${PROFILES_URL}/following/${profileId}`); + const follows = await db.follows.findMany({}); + expect(res.statusCode).toBe(201); + expect(res.type).toMatch(/json/); + expect(res.body).toBe(''); + expect(follows).toHaveLength(1); + expect(follows[0].profileId).toBe(profileId); + expect(follows[0].followerId).toBe(dbUserOne.profile!.id); + await db.follows.deleteMany({}); + }); + }); + + describe(`DELETE ${PROFILES_URL}/following`, () => { + it('should respond with 401 on an unauthenticated request', async () => { + const res = await api.delete(`${PROFILES_URL}/following/${crypto.randomUUID()}`); + assertUnauthorizedErrorRes(res); + }); + + it('should respond with 400 on an authenticated request with invalid profile id', async () => { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.delete(`${PROFILES_URL}/following/not_id`); + assertResponseWithValidationError(res, 'profileId'); + }); + + it('should respond 404 if the following/profile not exists', async () => { + const ids = [crypto.randomUUID(), dbUserTwo.profile!.id]; + for (const profileId of ids) { + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.delete(`${PROFILES_URL}/following/${profileId}`); + assertNotFoundErrorRes(res); + expect(await db.follows.findMany({})).toHaveLength(0); + } + }); + + it('should respond 204 after deleting an existent following', async () => { + const profileId = dbUserTwo.profile!.id; + await db.follows.create({ data: { profileId, followerId: dbUserOne.profile!.id } }); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + const res = await authorizedApi.delete(`${PROFILES_URL}/following/${profileId}`); + const follows = await db.follows.findMany({}); + expect(res.statusCode).toBe(204); + expect(res.type).toBe(''); + expect(res.body).toStrictEqual({}); + expect(follows).toHaveLength(0); + }); + }); + }); + + describe('middlewares', () => { + describe('lastSeenUpdater', () => { + it('should not update the profile last-seen date on any unauthorized request', async () => { + const methods = ['get', 'put', 'post', 'delete'] as const; + const lastSeen = faker.date.past({ years: 10 }); + for (const m of methods) { + await db.profile.update({ where: { userId: dbUserOne.id }, data: { lastSeen } }); + await api[m](BASE_URL); + const dbProfile = await db.profile.findUnique({ where: { userId: dbUserOne.id } }); + expect(dbProfile!.lastSeen.getTime()).toBe(lastSeen.getTime()); + } + }); + + it('should update the profile last-seen date on any authorized request', async () => { + const methods = ['get', 'put', 'post', 'delete'] as const; + const lastSeen = faker.date.past({ years: 10 }); + for (const m of methods) { + await db.profile.update({ where: { userId: dbUserOne.id }, data: { lastSeen } }); + const { authorizedApi } = await prepForAuthorizedTest(userOneData); + await authorizedApi[m](BASE_URL); + const dbProfile = await db.profile.findUnique({ where: { userId: dbUserOne.id } }); + expect(dbProfile!.lastSeen.getTime()).toBeGreaterThan(lastSeen.getTime()); + } + }); + }); + }); +}); diff --git a/src/tests/api/v1/stats.int.test.ts b/src/tests/api/v1/stats.int.test.ts index 8f62541..16c2f72 100644 --- a/src/tests/api/v1/stats.int.test.ts +++ b/src/tests/api/v1/stats.int.test.ts @@ -12,7 +12,6 @@ describe('Statistics endpoints', async () => { await setup(SIGNIN_URL); afterEach(async () => { - vi.useRealTimers(); await db.creation.deleteMany({}); await db.visitor.deleteMany({}); }); @@ -30,7 +29,7 @@ describe('Statistics endpoints', async () => { const models = Object.values(Model); for (let i = 0; i < monthCount; i++) { // Set system time to the date of the previous month - vi.setSystemTime(new Date().setMonth(new Date().getMonth() - 1)); + vi.setSystemTime(new Date().setMonth(new Date().getMonth() - 1, 1)); // Register new creation entries in all models for (const model of models) { const data: Prisma.CreationCreateInput[] = []; @@ -69,6 +68,8 @@ describe('Statistics endpoints', async () => { expect(entry.count).toBe(monthlyEntryCount); } } + vi.setSystemTime(vi.getRealSystemTime()); + vi.useRealTimers(); }); }); }); diff --git a/src/tests/api/v1/users.int.test.ts b/src/tests/api/v1/users.int.test.ts index 7f68106..062d7e6 100644 --- a/src/tests/api/v1/users.int.test.ts +++ b/src/tests/api/v1/users.int.test.ts @@ -1,12 +1,4 @@ -import { - it, - expect, - describe, - afterAll, - afterEach, - beforeEach, - TestFunction, -} from 'vitest'; +import { it, expect, describe, afterAll, afterEach, beforeEach, TestFunction } from 'vitest'; import { AppErrorResponse, AuthResponse, PublicUser } from '@/types'; import { SIGNIN_URL, USERS_URL, ADMIN_SECRET } from './utils'; import { Image, Prisma, User } from '@/../prisma/client'; @@ -45,18 +37,14 @@ describe('User endpoints', async () => { describe(`POST ${USERS_URL}`, () => { for (const field of Object.keys(newUserData).filter((k) => k !== 'bio')) { it(`should not create a user without ${field}`, async () => { - const res = await api - .post(USERS_URL) - .send({ ...newUserData, [field]: undefined }); + const res = await api.post(USERS_URL).send({ ...newUserData, [field]: undefined }); assertResponseWithValidationError(res, field); expect(await db.user.findMany()).toHaveLength(0); }); } it(`should not create a user with wrong password confirmation`, async () => { - const res = await api - .post(USERS_URL) - .send({ ...userData, confirm: 'blah' }); + const res = await api.post(USERS_URL).send({ ...userData, confirm: 'blah' }); assertResponseWithValidationError(res, 'confirm'); expect(await db.user.findMany()).toHaveLength(0); }); @@ -64,9 +52,7 @@ describe('User endpoints', async () => { const invalidUsernames = ['user-x', 'user x', 'user@x', 'user(x)']; for (const username of invalidUsernames) { it(`should not create a user while the username having a space`, async () => { - const res = await api - .post(USERS_URL) - .send({ ...newUserData, username }); + const res = await api.post(USERS_URL).send({ ...newUserData, username }); assertResponseWithValidationError(res, 'username'); expect(await db.user.findMany()).toHaveLength(0); }); @@ -83,9 +69,7 @@ describe('User endpoints', async () => { }); it('should not create an admin user', async () => { - const res = await api - .post(USERS_URL) - .send({ ...newUserData, secret: 'not_admin' }); + const res = await api.post(USERS_URL).send({ ...newUserData, secret: 'not_admin' }); assertResponseWithValidationError(res, 'secret'); expect(await db.user.findMany()).toHaveLength(0); }); @@ -103,15 +87,14 @@ describe('User endpoints', async () => { where: { id: resUser.id }, omit: { password: false }, }); - const resJwtPayload = jwt.decode( - resBody.token.replace(/^Bearer /, '') - ) as User; + const resJwtPayload = jwt.decode(resBody.token.replace(/^Bearer /, '')) as User; expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(201); expect(resUser.avatar).toBeDefined(); expect(resUser.isAdmin).toStrictEqual(isAdmin); expect(resUser.username).toBe(newUserData.username); expect(resUser.fullname).toBe(newUserData.fullname); + expect(resUser.profile!.id).toBeTypeOf('string'); expect(resBody.token).toMatch(/^Bearer /i); expect(resJwtPayload.isAdmin).toStrictEqual(isAdmin); expect(resJwtPayload.id).toStrictEqual(dbUser.id); @@ -137,12 +120,8 @@ describe('User endpoints', async () => { const res = await api.post(`${USERS_URL}/guest`); const resBody = res.body as AuthResponse; const resUser = resBody.user; - const dbUser = (await db.user.findMany({ omit: { password: false } })).at( - -1 - ) as User; - const resJwtPayload = jwt.decode( - resBody.token.replace(/^Bearer /, '') - ) as User; + const dbUser = (await db.user.findMany({ omit: { password: false } })).at(-1) as User; + const resJwtPayload = jwt.decode(resBody.token.replace(/^Bearer /, '')) as User; expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(201); expect(resUser.avatar).toBeDefined(); @@ -156,6 +135,7 @@ describe('User endpoints', async () => { expect(resJwtPayload.isAdmin).toStrictEqual(false); expect(Object.keys(resUser)).not.toContain('password'); expect(dbUser.password).toMatch(/^\$2[a|b|x|y]\$.{56}/); + expect(resUser.profile!.id).toBeTypeOf('string'); expect(resUser.bio).toBe(resUser.bio); expect(dbUser.bio).toBe(resUser.bio); expect(dbUser.isAdmin).toBe(false); @@ -187,6 +167,7 @@ describe('User endpoints', async () => { expect(users[1].avatar).toBeDefined(); expect(users[1].username).toBe(userData.username); expect(users[1].fullname).toBe(userData.fullname); + for (const { profile } of users) expect(profile!.id).toBeTypeOf('string'); await db.user.delete({ where: { id: dbUser.id } }); }); }); @@ -218,6 +199,7 @@ describe('User endpoints', async () => { expect(resUser.isAdmin).toStrictEqual(false); expect(resUser.username).toBe(dbUser.username); expect(resUser.fullname).toBe(dbUser.fullname); + expect(resUser.profile!.id).toBeTypeOf('string'); expect(Object.keys(resUser)).not.toContain('password'); } }); @@ -235,6 +217,7 @@ describe('User endpoints', async () => { expect(resUser.isAdmin).toStrictEqual(false); expect(resUser.username).toBe(dbUser.username); expect(resUser.fullname).toBe(dbUser.fullname); + expect(resUser.profile!.id).toBeTypeOf('string'); expect(Object.keys(resUser)).not.toContain('password'); } }); @@ -245,7 +228,7 @@ describe('User endpoints', async () => { for (let i = 0; i < 1000; i++) longString += 'x'; let dbXImg: Omit; - let dbUser: User; + let dbUser: Awaited>; const getAllFields = () => { return Object.entries({ @@ -278,6 +261,14 @@ describe('User endpoints', async () => { credentials: { username: string; password: string } ) => { return async () => { + const profileId = dbUser.profile!.id; + const profileName = dbUser.username; + const dbChat = await db.chat.create({ + data: { + profiles: { create: { profileId, profileName } }, + messages: { create: { body: 'Hello!', profileId, profileName } }, + }, + }); const { authorizedApi, signedInUserData: { token }, @@ -288,8 +279,12 @@ describe('User endpoints', async () => { const updatedDBUser = (await db.user.findUnique({ where: { id: dbUser.id }, omit: { password: false }, - include: { avatar: { select: { image: true } } }, - })) as User & { avatar: { image: Image } }; + include: { avatar: { select: { image: true } }, profile: true }, + }))!; + const updatedDBChat = (await db.chat.findUnique({ + where: { id: dbChat.id }, + include: { messages: true, profiles: true }, + }))!; expect(res.statusCode).toBe(200); expect(JSON.stringify((res.body as AuthResponse).user)).toBe( JSON.stringify({ @@ -298,15 +293,17 @@ describe('User endpoints', async () => { }) ); expect((res.body as AuthResponse).token).toBe(token); - expect((res.body as AuthResponse).user.avatar?.image.id).toBe( - dbXImg.id - ); - expect(updatedDBUser.avatar.image.id).toBe(dbXImg.id); + expect((res.body as AuthResponse).user.avatar?.image.id).toBe(dbXImg.id); + expect(updatedDBUser.avatar?.image.id).toBe(dbXImg.id); const updatedFields = Object.keys(data); if (updatedFields.includes('username')) { expect(updatedDBUser.username).toBe(data.username); + expect(updatedDBChat.profiles[0].profileName).toBe(data.username); + expect(updatedDBChat.profiles[0].profileId).toBe(dbUser.profile!.id); } else { expect(updatedDBUser.username).toBe(dbUser.username); + expect(updatedDBChat.profiles[0].profileName).toBe(dbUser.username); + expect(updatedDBChat.profiles[0].profileId).toBe(dbUser.profile!.id); } if (updatedFields.includes('fullname')) { expect(updatedDBUser.fullname).toBe(data.fullname); @@ -314,9 +311,7 @@ describe('User endpoints', async () => { expect(updatedDBUser.fullname).toBe(dbUser.fullname); } if (updatedFields.includes('password')) { - expect( - bcrypt.compareSync(data.password as string, updatedDBUser.password) - ).toBe(true); + expect(bcrypt.compareSync(data.password as string, updatedDBUser.password)).toBe(true); } else { expect(updatedDBUser.password).toBe(dbUser.password); } @@ -335,9 +330,7 @@ describe('User endpoints', async () => { ) => { return async () => { const { authorizedApi } = await prepForAuthorizedTest(credentials); - const res = await authorizedApi - .patch(`${USERS_URL}/${dbUser.id}`) - .send(data); + const res = await authorizedApi.patch(`${USERS_URL}/${dbUser.id}`).send(data); const issues = res.body as z.ZodIssue[]; expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(400); @@ -355,9 +348,7 @@ describe('User endpoints', async () => { it('should respond with 401, on a request with non-owner/admin JWT', async () => { const { authorizedApi } = await prepForAuthorizedTest(xUserData); for (const field of getAllFields()) { - const res = await authorizedApi - .patch(`${USERS_URL}/${dbUser.id}`) - .send(field); + const res = await authorizedApi.patch(`${USERS_URL}/${dbUser.id}`).send(field); assertUnauthorizedErrorRes(res); } }); @@ -366,9 +357,7 @@ describe('User endpoints', async () => { const username = 'foobar'; await createUser({ ...userData, username }); const { authorizedApi } = await prepForAuthorizedTest(userData); - const res = await authorizedApi - .patch(`${USERS_URL}/${dbUser.id}`) - .send({ username }); + const res = await authorizedApi.patch(`${USERS_URL}/${dbUser.id}`).send({ username }); const resBody = res.body as AppErrorResponse; expect(res.type).toMatch(/json/); expect(res.statusCode).toBe(400); @@ -383,20 +372,12 @@ describe('User endpoints', async () => { it( 'should not change username if the given is too short, on request with owner/admin JWT', - createTestForNotUpdateInvalidField( - { username: 'x' }, - /username/i, - userData - ) + createTestForNotUpdateInvalidField({ username: 'x' }, /username/i, userData) ); it( 'should not change username if the given is too long, on request with owner/admin JWT', - createTestForNotUpdateInvalidField( - { username: longString }, - /username/i, - adminData - ) + createTestForNotUpdateInvalidField({ username: longString }, /username/i, adminData) ); it( @@ -406,28 +387,17 @@ describe('User endpoints', async () => { it( 'should not change fullname if the given is too short, on request with owner/admin JWT', - createTestForNotUpdateInvalidField( - { fullname: 'x' }, - /fullname/i, - userData - ) + createTestForNotUpdateInvalidField({ fullname: 'x' }, /fullname/i, userData) ); it( 'should not change fullname if the given is too long, on request with owner/admin JWT', - createTestForNotUpdateInvalidField( - { fullname: longString }, - /fullname/i, - adminData - ) + createTestForNotUpdateInvalidField({ fullname: longString }, /fullname/i, adminData) ); it( 'should change password, on request with owner/admin JWT', - createTestForUpdateField( - { password: 'aB@32121', confirm: 'aB@32121' }, - userData - ) + createTestForUpdateField({ password: 'aB@32121', confirm: 'aB@32121' }, userData) ); it( @@ -464,11 +434,7 @@ describe('User endpoints', async () => { it( 'should not change admin state if given a wrong secret, on request with owner/admin JWT', - createTestForNotUpdateInvalidField( - { secret: 'not_admin' }, - /secret/i, - adminData - ) + createTestForNotUpdateInvalidField({ secret: 'not_admin' }, /secret/i, adminData) ); it( @@ -497,6 +463,8 @@ describe('User endpoints', async () => { const { authorizedApi } = await prepForAuthorizedTest(userData); const res = await authorizedApi.delete(`${USERS_URL}/${dbUser.id}`); expect(res.statusCode).toBe(204); + expect(await db.user.findUnique({ where: { id: dbUser.id } })).toBeNull(); + expect(await db.profile.findUnique({ where: { id: dbUser.profile!.id } })).toBeNull(); }); it('should respond with 204 if found a user and deleted it, on request with admin JWT', async () => { @@ -505,6 +473,8 @@ describe('User endpoints', async () => { const { authorizedApi } = await prepForAuthorizedTest(adminData); const res = await authorizedApi.delete(`${USERS_URL}/${dbUser.id}`); expect(res.statusCode).toBe(204); + expect(await db.user.findUnique({ where: { id: dbUser.id } })).toBeNull(); + expect(await db.profile.findUnique({ where: { id: dbUser.profile!.id } })).toBeNull(); }); it('should respond with 401 if not found a user, on request with owner JWT', async () => { diff --git a/src/tests/api/v1/utils.ts b/src/tests/api/v1/utils.ts index 754e332..4041227 100644 --- a/src/tests/api/v1/utils.ts +++ b/src/tests/api/v1/utils.ts @@ -8,8 +8,10 @@ export * from '../../../lib/config'; export const BASE_URL = '/api/v1'; export const USERS_URL = `${BASE_URL}/users`; export const POSTS_URL = `${BASE_URL}/posts`; +export const CHATS_URL = `${BASE_URL}/chats`; export const STATS_URL = `${BASE_URL}/stats`; export const IMAGES_URL = `${BASE_URL}/images`; +export const PROFILES_URL = `${BASE_URL}/profiles`; export const SIGNIN_URL = `${BASE_URL}/auth/signin`; export const VERIFY_URL = `${BASE_URL}/auth/verify`; export const CHARACTERS_URL = `${BASE_URL}/characters`; diff --git a/src/types.ts b/src/types.ts index 31844b3..4b9388b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface UserSensitiveDataToOmit { export interface UserDataToAggregate { avatar: { select: { image: { omit: ImageSensitiveDataToOmit } } }; + profile: boolean; } export interface UserAggregation { @@ -28,6 +29,14 @@ export type PublicUser = Prisma.UserGetPayload<{ omit: UserAggregation['omit']; }>; +export interface ProfileAggregation { + include: { user: UserAggregation }; +} + +export type PublicProfile = Prisma.ProfileGetPayload<{ + include: ProfileAggregation['include']; +}> & { followedByCurrentUser: boolean }; + export interface ImageSensitiveDataToOmit { storageId: true; storageFullPath: true; @@ -112,12 +121,18 @@ export interface BaseFilters { authorId?: string; } -export interface PaginationFilters extends BaseFilters { +export interface BasePaginationFilters { sort?: Prisma.SortOrder; - cursor?: number; + cursor?: CursorType; limit?: number; } +export interface ProfileFilters extends BasePaginationFilters { + name?: string; +} + +export interface PaginationFilters extends BaseFilters, BasePaginationFilters {} + export type TagsFilter = string[]; export interface PostFilters extends PaginationFilters {