From 6ec8891be5a9d1b4e444c1c5b8c011a4307bccdc Mon Sep 17 00:00:00 2001 From: EL <1175065040@qq.com> Date: Fri, 13 Mar 2026 03:29:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20E2E=20=E5=87=86=E5=A4=87?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=EF=BC=9A=20package.json=20test:e2e:prepare?= =?UTF-8?q?=20=E7=8E=B0=E5=9C=A8=E6=98=AF=20migrate=20reset=20--force=20&&?= =?UTF-8?q?=20prisma=20generate=20&&=20seed=20=E4=B8=BA=20seed=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E8=A1=A5=E5=85=85=20JS=20Prisma=20client=20?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=9A=20schema.prisma=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20seed=20=E5=9C=A8=20ESM/CJS=20=E4=B8=8B?= =?UTF-8?q?=E7=9A=84=20Prisma=20=E5=AF=BC=E5=85=A5=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=EF=BC=9A=20seed.mjs=20=E4=BF=AE=E5=A4=8D=20Jest=20=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E6=9C=AA=E5=8A=A0=E8=BD=BD=20.env=20=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E8=BF=9E=E5=88=B0=20127.0.0.1=20=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=20e2e-app.helper.ts=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=B9=E5=85=B7=E4=BE=9D=E8=B5=96=E2=80=9C=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E2=80=9D=E5=AF=BC=E8=87=B4=E8=A2=AB=E7=BB=84=E7=BB=87=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=94=B9=E5=90=8D=E5=90=8E=E5=A4=B1=E6=95=88=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=88=E6=94=B9=E4=B8=BA=E6=8C=89=20seed?= =?UTF-8?q?=20openId=20=E5=8F=8D=E6=9F=A5=EF=BC=89=EF=BC=9A=20e2e-fixtures?= =?UTF-8?q?.helper.ts=20=E4=BF=AE=E5=A4=8D=E7=BB=84=E7=BB=87=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=9A=84=E7=8A=B6=E6=80=81=E6=B1=A1=E6=9F=93=E4=B8=8E?= =?UTF-8?q?=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B9=B6=E6=94=B6?= =?UTF-8?q?=E6=95=9B=20afterAll=20=E8=B5=84=E6=BA=90=E9=87=8A=E6=94=BE?= =?UTF-8?q?=EF=BC=9A=20organization.e2e-spec.ts=20e2e-context.helper.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/e2e-testing.md | 60 + package.json | 8 +- pnpm-lock.yaml | 2374 +++++++++++++++++++++++ prisma/schema.prisma | 5 + prisma/seed.mjs | 517 +++-- test/e2e/fixtures/e2e-roles.ts | 59 + test/e2e/helpers/e2e-app.helper.ts | 41 + test/e2e/helpers/e2e-auth.helper.ts | 47 + test/e2e/helpers/e2e-context.helper.ts | 38 + test/e2e/helpers/e2e-fixtures.helper.ts | 195 ++ test/e2e/helpers/e2e-http.helper.ts | 37 + test/e2e/helpers/e2e-matrix.helper.ts | 32 + test/e2e/specs/auth.e2e-spec.ts | 129 ++ test/e2e/specs/organization.e2e-spec.ts | 729 +++++++ test/e2e/specs/patients.e2e-spec.ts | 185 ++ test/e2e/specs/tasks.e2e-spec.ts | 325 ++++ test/e2e/specs/users.e2e-spec.ts | 332 ++++ test/jest-e2e.config.cjs | 20 + test/tsconfig.e2e.json | 10 + 19 files changed, 5007 insertions(+), 136 deletions(-) create mode 100644 docs/e2e-testing.md create mode 100644 test/e2e/fixtures/e2e-roles.ts create mode 100644 test/e2e/helpers/e2e-app.helper.ts create mode 100644 test/e2e/helpers/e2e-auth.helper.ts create mode 100644 test/e2e/helpers/e2e-context.helper.ts create mode 100644 test/e2e/helpers/e2e-fixtures.helper.ts create mode 100644 test/e2e/helpers/e2e-http.helper.ts create mode 100644 test/e2e/helpers/e2e-matrix.helper.ts create mode 100644 test/e2e/specs/auth.e2e-spec.ts create mode 100644 test/e2e/specs/organization.e2e-spec.ts create mode 100644 test/e2e/specs/patients.e2e-spec.ts create mode 100644 test/e2e/specs/tasks.e2e-spec.ts create mode 100644 test/e2e/specs/users.e2e-spec.ts create mode 100644 test/jest-e2e.config.cjs create mode 100644 test/tsconfig.e2e.json diff --git a/docs/e2e-testing.md b/docs/e2e-testing.md new file mode 100644 index 0000000..c374473 --- /dev/null +++ b/docs/e2e-testing.md @@ -0,0 +1,60 @@ +# E2E 接口测试说明 + +## 1. 目标 + +- 覆盖 `src/**/*controller.ts` 当前全部 30 个业务接口。 +- 采用 `supertest + @nestjs/testing` 进行真实 HTTP E2E 测试。 +- 测试前固定执行数据库重置与 seed,确保结果可重复。 + +## 2. 风险提示 + +`pnpm test:e2e` 会执行: + +1. `prisma migrate reset --force` +2. `node prisma/seed.mjs` + +这会清空 `.env` 中 `DATABASE_URL` 指向数据库的全部数据,请仅在测试库执行。 + +## 3. 运行命令 + +```bash +pnpm test:e2e +``` + +仅重置数据库并注入 seed: + +```bash +pnpm test:e2e:prepare +``` + +监听模式: + +```bash +pnpm test:e2e:watch +``` + +## 4. 种子账号(默认密码:`Seed@1234`) + +- 系统管理员:`13800001000` +- 院管(医院 A):`13800001001` +- 主任(医院 A):`13800001002` +- 组长(医院 A):`13800001003` +- 医生(医院 A):`13800001004` +- 工程师(医院 A):`13800001005` + +## 5. 用例结构 + +- `test/e2e/specs/auth.e2e-spec.ts` +- `test/e2e/specs/users.e2e-spec.ts` +- `test/e2e/specs/organization.e2e-spec.ts` +- `test/e2e/specs/tasks.e2e-spec.ts` +- `test/e2e/specs/patients.e2e-spec.ts` + +## 6. 覆盖策略 + +- 受保护接口(27 个):每个接口覆盖 6 角色访问结果 + 未登录 401。 +- 非受保护接口(3 个):每个接口至少 1 个成功 + 1 个失败。 +- 关键行为额外覆盖: + - 任务状态机冲突(409) + - 患者 B 端角色可见性 + - 组织域院管作用域限制与删除冲突 diff --git a/package.json b/package.json index 7e05ba2..23b3d86 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main" + "start:prod": "node dist/main", + "test:e2e:prepare": "pnpm prisma migrate reset --force && pnpm prisma generate && node prisma/seed.mjs", + "test:e2e": "pnpm test:e2e:prepare && NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --runInBand", + "test:e2e:watch": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --watch" }, "dependencies": { "@nestjs/common": "^11.0.1", @@ -39,14 +42,17 @@ "@nestjs/testing": "^11.0.1", "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "globals": "^16.0.0", + "jest": "^30.3.0", "prettier": "^3.4.2", "prisma": "^7.4.2", "source-map-support": "^0.5.21", "supertest": "^7.0.0", + "ts-jest": "^29.4.6", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 388444a..2c19a0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@types/express': specifier: ^5.0.0 version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 @@ -87,6 +90,9 @@ importers: globals: specifier: ^16.0.0 version: 16.5.0 + jest: + specifier: ^30.3.0 + version: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) prettier: specifier: ^3.4.2 version: 3.8.1 @@ -99,6 +105,9 @@ importers: supertest: specifier: ^7.0.0 version: 7.2.2 + ts-jest: + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.2 version: 9.5.4(typescript@5.9.3)(webpack@5.104.1) @@ -149,10 +158,167 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} @@ -190,6 +356,15 @@ packages: '@electric-sql/pglite@0.3.15': resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@hono/node-server@1.19.9': resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} @@ -339,9 +514,106 @@ packages: '@types/node': optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -369,6 +641,9 @@ packages: resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} engines: {node: '>=16'} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nestjs/cli@11.0.16': resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} @@ -485,6 +760,14 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@prisma/adapter-pg@7.5.0': resolution: {integrity: sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A==} @@ -549,6 +832,15 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.1.1': + resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -571,6 +863,21 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} @@ -601,6 +908,18 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -634,6 +953,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} @@ -643,6 +965,118 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -752,24 +1186,47 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -786,6 +1243,31 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -815,6 +1297,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} @@ -828,6 +1313,13 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -865,6 +1357,14 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001778: resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} @@ -872,6 +1372,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -886,12 +1390,19 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} citty@0.2.1: resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -914,10 +1425,21 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -965,6 +1487,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1011,6 +1536,14 @@ packages: supports-color: optional: true + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deepmerge-ts@7.1.5: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} @@ -1040,6 +1573,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -1059,6 +1596,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -1071,9 +1611,16 @@ packages: electron-to-chromium@1.5.307: resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1115,6 +1662,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -1147,6 +1698,18 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -1170,6 +1733,9 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + file-type@21.3.0: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} @@ -1182,6 +1748,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1216,16 +1786,36 @@ packages: fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} @@ -1233,6 +1823,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -1240,10 +1834,19 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -1261,6 +1864,11 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1281,6 +1889,9 @@ packages: resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1288,6 +1899,10 @@ packages: http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1299,6 +1914,19 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1313,6 +1941,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -1327,6 +1959,10 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -1334,14 +1970,165 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1349,10 +2136,19 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -1383,6 +2179,10 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + libphonenumber-js@1.12.39: resolution: {integrity: sha512-MW79m7HuOqBk8mwytiXYTMELJiBbV3Zl9Y39dCCn1yC8K+WGNSq1QGvzywbylp5vGShEztMScCWHX/XFOS0rXg==} @@ -1401,6 +2201,10 @@ packages: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -1419,6 +2223,9 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -1435,10 +2242,16 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.4: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} @@ -1446,9 +2259,16 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1512,6 +2332,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1538,6 +2362,14 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1562,9 +2394,20 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + nypm@0.6.5: resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} engines: {node: '>=18'} @@ -1599,6 +2442,25 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1611,10 +2473,22 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -1685,6 +2559,18 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -1736,6 +2622,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prisma@7.4.2: resolution: {integrity: sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==} engines: {node: ^20.19 || ^22.12 || >=24.0} @@ -1763,6 +2653,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -1783,6 +2676,9 @@ packages: peerDependencies: react: ^19.2.4 + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -1804,14 +2700,26 @@ packages: remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -1847,6 +2755,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1897,6 +2809,13 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -1916,10 +2835,17 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1931,10 +2857,18 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -1942,10 +2876,26 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -1982,6 +2932,10 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -2007,10 +2961,17 @@ packages: engines: {node: '>=10'} hasBin: true + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2023,6 +2984,33 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + ts-loader@9.5.4: resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} engines: {node: '>=12.0.0'} @@ -2055,6 +3043,18 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -2071,6 +3071,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -2090,6 +3095,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2105,6 +3113,10 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -2121,6 +3133,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -2151,25 +3166,55 @@ packages: engines: {node: '>= 8'} hasBin: true + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} @@ -2239,8 +3284,189 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + '@borewit/text-codec@0.2.2': {} '@chevrotain/cst-dts-gen@10.5.0': @@ -2275,6 +3501,22 @@ snapshots: '@electric-sql/pglite@0.3.15': {} + '@emnapi/core@1.9.0': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + '@hono/node-server@1.19.9(hono@4.11.4)': dependencies: hono: 4.11.4 @@ -2419,11 +3661,212 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + + '@jest/core@30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.3.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.3.0': {} + + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-mock: 30.3.0 + + '@jest/expect-utils@30.3.0': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.1.1 + '@types/node': 22.19.15 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.3.0': + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 22.19.15 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.3.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.19.15 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + + '@jest/snapshot-utils@30.3.0': + dependencies: + '@jest/types': 30.3.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.3.0': + dependencies: + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.3.0': + dependencies: + '@jest/test-result': 30.3.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + slash: 3.0.0 + + '@jest/transform@30.3.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.15 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.11': @@ -2452,6 +3895,13 @@ snapshots: chevrotain: 10.5.0 lilconfig: 2.1.0 + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nestjs/cli@11.0.16(@types/node@22.19.15)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) @@ -2577,6 +4027,11 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + '@prisma/adapter-pg@7.5.0': dependencies: '@prisma/driver-adapter-utils': 7.5.0 @@ -2669,6 +4124,16 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.34.48': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.1.1': + dependencies: + '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} '@tokenizer/inflate@0.4.1': @@ -2688,6 +4153,32 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/bcrypt@6.0.0': dependencies: '@types/node': 22.19.15 @@ -2730,6 +4221,21 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.3.0 + pretty-format: 30.3.0 + '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.10': @@ -2768,6 +4274,8 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 22.19.15 + '@types/stack-utils@2.0.3': {} + '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 @@ -2782,6 +4290,73 @@ snapshots: '@types/validator@13.15.10': {} + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -2917,18 +4492,37 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + ansis@4.2.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + append-field@1.0.0: {} arg@4.1.3: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-timsort@1.0.3: {} @@ -2939,6 +4533,58 @@ snapshots: aws-ssl-profiles@1.1.2: {} + babel-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.3.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -2977,6 +4623,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.4: dependencies: balanced-match: 4.0.4 @@ -2993,6 +4643,14 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -3035,6 +4693,10 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + caniuse-lite@1.0.30001778: {} chalk@4.1.2: @@ -3042,6 +4704,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + char-regex@1.0.2: {} + chardet@2.1.1: {} chevrotain@10.5.0: @@ -3059,12 +4723,16 @@ snapshots: chrome-trace-event@1.0.4: {} + ci-info@4.4.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 citty@0.2.1: {} + cjs-module-lexer@2.2.0: {} + class-transformer@0.5.1: {} class-validator@0.15.1: @@ -3087,8 +4755,18 @@ snapshots: cli-width@4.1.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3128,6 +4806,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -3164,6 +4844,8 @@ snapshots: dependencies: ms: 2.1.3 + dedent@1.7.2: {} + deepmerge-ts@7.1.5: {} deepmerge@4.3.1: {} @@ -3182,6 +4864,8 @@ snapshots: destr@2.0.5: {} + detect-newline@3.1.0: {} + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -3199,6 +4883,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -3212,8 +4898,12 @@ snapshots: electron-to-chromium@1.5.307: {} + emittery@0.13.1: {} + emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} @@ -3248,6 +4938,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -3269,6 +4961,29 @@ snapshots: events@3.3.0: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + express@5.2.1: dependencies: accepts: 2.0.0 @@ -3316,6 +5031,10 @@ snapshots: fast-uri@3.1.0: {} + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + file-type@21.3.0: dependencies: '@tokenizer/inflate': 0.4.1 @@ -3340,6 +5059,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -3388,12 +5112,21 @@ snapshots: fs-monkey@1.1.0: {} + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} generate-function@2.3.1: dependencies: is-property: 1.0.2 + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3407,6 +5140,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-package-type@0.1.0: {} + get-port-please@3.2.0: {} get-proto@1.0.1: @@ -3414,6 +5149,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + giget@2.0.0: dependencies: citty: 0.1.6 @@ -3425,12 +5162,30 @@ snapshots: glob-to-regexp@0.4.1: {} + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@13.0.0: dependencies: minimatch: 10.2.4 minipass: 7.1.3 path-scurry: 2.0.2 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@16.5.0: {} gopd@1.2.0: {} @@ -3441,6 +5196,15 @@ snapshots: graphmatch@1.1.1: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3455,6 +5219,8 @@ snapshots: hono@4.11.4: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -3465,6 +5231,8 @@ snapshots: http-status-codes@2.3.0: {} + human-signals@2.1.0: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -3476,6 +5244,18 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ipaddr.js@1.9.1: {} @@ -3484,6 +5264,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-fn@2.1.0: {} + is-interactive@1.0.0: {} is-number@7.0.0: {} @@ -3492,26 +5274,383 @@ snapshots: is-property@1.0.2: {} + is-stream@2.0.1: {} + is-unicode-supported@0.1.0: {} isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterare@1.2.1: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.3.0: + dependencies: + execa: 5.1.1 + jest-util: 30.3.0 + p-limit: 3.1.0 + + jest-circus@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + p-limit: 3.1.0 + pretty-format: 30.3.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + parse-json: 5.2.0 + pretty-format: 30.3.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.15 + ts-node: 10.9.2(@types/node@22.19.15)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 + + jest-environment-node@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + + jest-haste-map@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.3 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.3.0 + + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + jest-util: 30.3.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + optionalDependencies: + jest-resolve: 30.3.0 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.3.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.3.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.3.0: + dependencies: + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.3.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 30.3.0 + graceful-fs: 4.2.11 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 + semver: 7.7.4 + synckit: 0.11.12 + transitivePeerDependencies: + - supports-color + + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-validate@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.3.0 + + jest-watcher@30.3.0: + dependencies: + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 22.19.15 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.3.0 + string-length: 4.0.2 + jest-worker@27.5.1: dependencies: '@types/node': 22.19.15 merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@30.3.0: + dependencies: + '@types/node': 22.19.15 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.3.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jiti@2.6.1: {} js-tokens@4.0.0: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-parse-even-better-errors@2.3.1: {} json-schema-traverse@0.4.1: {} @@ -3552,6 +5691,8 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + leven@3.1.0: {} + libphonenumber-js@1.12.39: {} lilconfig@2.1.0: {} @@ -3562,6 +5703,10 @@ snapshots: loader-runner@4.3.1: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -3574,6 +5719,8 @@ snapshots: lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} + lodash.once@4.1.1: {} lodash@4.17.21: {} @@ -3587,16 +5734,30 @@ snapshots: long@5.3.2: {} + lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru.min@1.1.4: {} magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + make-error@1.3.6: {} + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + math-intrinsics@1.1.0: {} media-typer@0.3.0: {} @@ -3642,6 +5803,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + minimist@1.2.8: {} minipass@7.1.3: {} @@ -3673,6 +5838,10 @@ snapshots: dependencies: lru.min: 1.1.4 + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -3689,8 +5858,16 @@ snapshots: node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} + node-releases@2.0.36: {} + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + nypm@0.6.5: dependencies: citty: 0.2.1 @@ -3729,6 +5906,22 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3742,8 +5935,17 @@ snapshots: parseurl@1.3.3: {} + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.2.6 @@ -3810,6 +6012,14 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + pkg-types@2.3.0: dependencies: confbox: 0.2.4 @@ -3844,6 +6054,12 @@ snapshots: prettier@3.8.1: {} + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@prisma/config': 7.4.2 @@ -3875,6 +6091,8 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@7.0.1: {} + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -3898,6 +6116,8 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-is@18.3.1: {} + react@19.2.4: {} readable-stream@3.6.2: @@ -3914,10 +6134,18 @@ snapshots: remeda@2.33.4: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -3962,6 +6190,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + semver@6.3.1: {} + semver@7.7.4: {} send@1.2.1: @@ -4031,6 +6261,13 @@ snapshots: signal-exit@4.1.0: {} + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -4044,20 +6281,37 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + sqlstring@2.3.3: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + statuses@2.0.2: {} std-env@3.10.0: {} streamsearch@1.1.0: {} + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -4066,8 +6320,18 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -4117,6 +6381,10 @@ snapshots: symbol-observable@4.0.0: {} + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + tapable@2.3.0: {} terser-webpack-plugin@5.4.0(webpack@5.104.1): @@ -4134,8 +6402,16 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.5 + tinyexec@1.0.2: {} + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4148,6 +6424,26 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + jest-util: 30.3.0 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1): dependencies: chalk: 4.1.2 @@ -4191,6 +6487,12 @@ snapshots: tslib@2.8.1: {} + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -4206,6 +6508,9 @@ snapshots: typescript@5.9.3: {} + uglify-js@3.19.3: + optional: true + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -4218,6 +6523,30 @@ snapshots: unpipe@1.0.0: {} + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -4232,6 +6561,12 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -4240,6 +6575,10 @@ snapshots: vary@1.1.2: {} + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -4289,20 +6628,55 @@ snapshots: dependencies: isexe: 2.0.0 + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + xtend@4.0.2: {} + y18n@5.0.8: {} + + yallist@3.1.1: {} + yargs-parser@21.1.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yn@3.1.1: {} + yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} zeptomatch@2.1.0: diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aae5e93..f4ec725 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,6 +3,11 @@ generator client { output = "../src/generated/prisma" } +// 兼容 seed 脚本在 Node.js 直接运行时使用 @prisma/client runtime。 +generator seed_client { + provider = "prisma-client-js" +} + datasource db { provider = "postgresql" } diff --git a/prisma/seed.mjs b/prisma/seed.mjs index 60b26c9..d7c9047 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -1,10 +1,10 @@ import 'dotenv/config'; import { PrismaPg } from '@prisma/adapter-pg'; import { hash } from 'bcrypt'; -import { PrismaClient } from '../src/generated/prisma/client.js'; -import { DeviceStatus, Role } from '../src/generated/prisma/enums.js'; +import prismaClientPackage from '@prisma/client'; + +const { DeviceStatus, PrismaClient, Role, TaskStatus } = prismaClientPackage; -// Keep the seed executable with the same pg driver adapter used by PrismaService. const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error('DATABASE_URL is required to run seed'); @@ -14,177 +14,424 @@ const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }), }); -async function main() { - // Default seed login password (plain): Seed@1234 - const seedPasswordHash = await hash('Seed@1234', 12); +const SEED_PASSWORD_PLAIN = 'Seed@1234'; - // Seed a baseline organization tree for local/demo usage. - const hospital = - (await prisma.hospital.findFirst({ where: { name: 'Demo Hospital' } })) ?? - (await prisma.hospital.create({ - data: { name: 'Demo Hospital' }, - })); +async function ensureHospital(name) { + return ( + (await prisma.hospital.findFirst({ where: { name } })) ?? + prisma.hospital.create({ data: { name } }) + ); +} - const department = +async function ensureDepartment(hospitalId, name) { + return ( (await prisma.department.findFirst({ - where: { - hospitalId: hospital.id, - name: 'Neurosurgery', - }, + where: { hospitalId, name }, })) ?? - (await prisma.department.create({ - data: { - hospitalId: hospital.id, - name: 'Neurosurgery', - }, - })); + prisma.department.create({ + data: { hospitalId, name }, + }) + ); +} - const group = +async function ensureGroup(departmentId, name) { + return ( (await prisma.group.findFirst({ - where: { - departmentId: department.id, - name: 'Shift-A', - }, + where: { departmentId, name }, })) ?? - (await prisma.group.create({ - data: { - departmentId: department.id, - name: 'Shift-A', - }, - })); + prisma.group.create({ + data: { departmentId, name }, + }) + ); +} - // Use openId as idempotent unique key for seeded users. - const systemAdmin = await prisma.user.upsert({ - where: { openId: 'seed-system-admin-openid' }, - update: { - name: 'System Admin', - phone: '13800000000', +async function upsertUserByOpenId(openId, data) { + return prisma.user.upsert({ + where: { openId }, + update: data, + create: { + ...data, + openId, + }, + }); +} + +async function ensurePatient({ + hospitalId, + doctorId, + name, + phone, + idCardHash, +}) { + const existing = await prisma.patient.findFirst({ + where: { + hospitalId, + phone, + idCardHash, + }, + }); + + if (existing) { + if (existing.doctorId !== doctorId || existing.name !== name) { + return prisma.patient.update({ + where: { id: existing.id }, + data: { doctorId, name }, + }); + } + return existing; + } + + return prisma.patient.create({ + data: { + hospitalId, + doctorId, + name, + phone, + idCardHash, + }, + }); +} + +async function main() { + const seedPasswordHash = await hash(SEED_PASSWORD_PLAIN, 12); + + const hospitalA = await ensureHospital('Seed Hospital A'); + const hospitalB = await ensureHospital('Seed Hospital B'); + + const departmentA1 = await ensureDepartment(hospitalA.id, 'Neurosurgery-A1'); + const departmentA2 = await ensureDepartment(hospitalA.id, 'Cardiology-A2'); + const departmentB1 = await ensureDepartment(hospitalB.id, 'Neurosurgery-B1'); + + const groupA1 = await ensureGroup(departmentA1.id, 'Shift-A1'); + const groupA2 = await ensureGroup(departmentA2.id, 'Shift-A2'); + const groupB1 = await ensureGroup(departmentB1.id, 'Shift-B1'); + + const systemAdmin = await upsertUserByOpenId('seed-system-admin-openid', { + name: 'Seed System Admin', + phone: '13800001000', + passwordHash: seedPasswordHash, + role: Role.SYSTEM_ADMIN, + hospitalId: null, + departmentId: null, + groupId: null, + }); + + const hospitalAdminA = await upsertUserByOpenId( + 'seed-hospital-admin-a-openid', + { + name: 'Seed Hospital Admin A', + phone: '13800001001', passwordHash: seedPasswordHash, - role: Role.SYSTEM_ADMIN, - hospitalId: null, + role: Role.HOSPITAL_ADMIN, + hospitalId: hospitalA.id, departmentId: null, groupId: null, }, - create: { - name: 'System Admin', - phone: '13800000000', - passwordHash: seedPasswordHash, - openId: 'seed-system-admin-openid', - role: Role.SYSTEM_ADMIN, - }, + ); + + await upsertUserByOpenId('seed-hospital-admin-b-openid', { + name: 'Seed Hospital Admin B', + phone: '13800001101', + passwordHash: seedPasswordHash, + role: Role.HOSPITAL_ADMIN, + hospitalId: hospitalB.id, + departmentId: null, + groupId: null, }); - await prisma.user.upsert({ - where: { openId: 'seed-hospital-admin-openid' }, + const directorA = await upsertUserByOpenId('seed-director-a-openid', { + name: 'Seed Director A', + phone: '13800001002', + passwordHash: seedPasswordHash, + role: Role.DIRECTOR, + hospitalId: hospitalA.id, + departmentId: departmentA1.id, + groupId: null, + }); + + const leaderA = await upsertUserByOpenId('seed-leader-a-openid', { + name: 'Seed Leader A', + phone: '13800001003', + passwordHash: seedPasswordHash, + role: Role.LEADER, + hospitalId: hospitalA.id, + departmentId: departmentA1.id, + groupId: groupA1.id, + }); + + const doctorA = await upsertUserByOpenId('seed-doctor-a-openid', { + name: 'Seed Doctor A', + phone: '13800001004', + passwordHash: seedPasswordHash, + role: Role.DOCTOR, + hospitalId: hospitalA.id, + departmentId: departmentA1.id, + groupId: groupA1.id, + }); + + const doctorA2 = await upsertUserByOpenId('seed-doctor-a2-openid', { + name: 'Seed Doctor A2', + phone: '13800001204', + passwordHash: seedPasswordHash, + role: Role.DOCTOR, + hospitalId: hospitalA.id, + departmentId: departmentA1.id, + groupId: groupA1.id, + }); + + const doctorA3 = await upsertUserByOpenId('seed-doctor-a3-openid', { + name: 'Seed Doctor A3', + phone: '13800001304', + passwordHash: seedPasswordHash, + role: Role.DOCTOR, + hospitalId: hospitalA.id, + departmentId: departmentA2.id, + groupId: groupA2.id, + }); + + const doctorB = await upsertUserByOpenId('seed-doctor-b-openid', { + name: 'Seed Doctor B', + phone: '13800001104', + passwordHash: seedPasswordHash, + role: Role.DOCTOR, + hospitalId: hospitalB.id, + departmentId: departmentB1.id, + groupId: groupB1.id, + }); + + const engineerA = await upsertUserByOpenId('seed-engineer-a-openid', { + name: 'Seed Engineer A', + phone: '13800001005', + passwordHash: seedPasswordHash, + role: Role.ENGINEER, + hospitalId: hospitalA.id, + departmentId: null, + groupId: null, + }); + + const engineerB = await upsertUserByOpenId('seed-engineer-b-openid', { + name: 'Seed Engineer B', + phone: '13800001105', + passwordHash: seedPasswordHash, + role: Role.ENGINEER, + hospitalId: hospitalB.id, + departmentId: null, + groupId: null, + }); + + const patientA1 = await ensurePatient({ + hospitalId: hospitalA.id, + doctorId: doctorA.id, + name: 'Seed Patient A1', + phone: '13800002001', + idCardHash: 'seed-id-card-cross-hospital', + }); + + const patientA2 = await ensurePatient({ + hospitalId: hospitalA.id, + doctorId: doctorA2.id, + name: 'Seed Patient A2', + phone: '13800002002', + idCardHash: 'seed-id-card-a2', + }); + + const patientA3 = await ensurePatient({ + hospitalId: hospitalA.id, + doctorId: doctorA3.id, + name: 'Seed Patient A3', + phone: '13800002003', + idCardHash: 'seed-id-card-a3', + }); + + const patientB1 = await ensurePatient({ + hospitalId: hospitalB.id, + doctorId: doctorB.id, + name: 'Seed Patient B1', + phone: '13800002001', + idCardHash: 'seed-id-card-cross-hospital', + }); + + const deviceA1 = await prisma.device.upsert({ + where: { snCode: 'SEED-SN-A-001' }, update: { - name: 'Hospital Admin', - phone: '13800000001', - passwordHash: seedPasswordHash, - role: Role.HOSPITAL_ADMIN, - hospitalId: hospital.id, - departmentId: department.id, - groupId: group.id, + patientId: patientA1.id, + currentPressure: 118, + status: DeviceStatus.ACTIVE, }, create: { - name: 'Hospital Admin', - phone: '13800000001', - passwordHash: seedPasswordHash, - openId: 'seed-hospital-admin-openid', - role: Role.HOSPITAL_ADMIN, - hospitalId: hospital.id, - departmentId: department.id, - groupId: group.id, + snCode: 'SEED-SN-A-001', + patientId: patientA1.id, + currentPressure: 118, + status: DeviceStatus.ACTIVE, }, }); - const doctor = await prisma.user.upsert({ - where: { openId: 'seed-doctor-openid' }, + const deviceA2 = await prisma.device.upsert({ + where: { snCode: 'SEED-SN-A-002' }, update: { - name: 'Doctor Demo', - phone: '13800000002', - passwordHash: seedPasswordHash, - role: Role.DOCTOR, - hospitalId: hospital.id, - departmentId: department.id, - groupId: group.id, + patientId: patientA2.id, + currentPressure: 112, + status: DeviceStatus.ACTIVE, }, create: { - name: 'Doctor Demo', - phone: '13800000002', - passwordHash: seedPasswordHash, - openId: 'seed-doctor-openid', - role: Role.DOCTOR, - hospitalId: hospital.id, - departmentId: department.id, - groupId: group.id, + snCode: 'SEED-SN-A-002', + patientId: patientA2.id, + currentPressure: 112, + status: DeviceStatus.ACTIVE, }, }); - await prisma.user.upsert({ - where: { openId: 'seed-engineer-openid' }, - update: { - name: 'Engineer Demo', - phone: '13800000009', - passwordHash: seedPasswordHash, - role: Role.ENGINEER, - hospitalId: hospital.id, - departmentId: null, - groupId: null, - }, - create: { - name: 'Engineer Demo', - phone: '13800000009', - passwordHash: seedPasswordHash, - openId: 'seed-engineer-openid', - role: Role.ENGINEER, - hospitalId: hospital.id, - }, - }); - - const patient = - (await prisma.patient.findFirst({ - where: { - hospitalId: hospital.id, - phone: '13800000003', - idCardHash: 'seed-id-card-hash', - }, - })) ?? - (await prisma.patient.create({ - data: { - hospitalId: hospital.id, - doctorId: doctor.id, - name: 'Patient Demo', - phone: '13800000003', - idCardHash: 'seed-id-card-hash', - }, - })); - await prisma.device.upsert({ - where: { snCode: 'SEED-SN-001' }, + where: { snCode: 'SEED-SN-A-003' }, update: { - patientId: patient.id, - currentPressure: 110, + patientId: patientA3.id, + currentPressure: 109, status: DeviceStatus.ACTIVE, }, create: { - snCode: 'SEED-SN-001', - patientId: patient.id, - currentPressure: 110, + snCode: 'SEED-SN-A-003', + patientId: patientA3.id, + currentPressure: 109, status: DeviceStatus.ACTIVE, }, }); + const deviceB1 = await prisma.device.upsert({ + where: { snCode: 'SEED-SN-B-001' }, + update: { + patientId: patientB1.id, + currentPressure: 121, + status: DeviceStatus.ACTIVE, + }, + create: { + snCode: 'SEED-SN-B-001', + patientId: patientB1.id, + currentPressure: 121, + status: DeviceStatus.ACTIVE, + }, + }); + + await prisma.device.upsert({ + where: { snCode: 'SEED-SN-A-004' }, + update: { + patientId: patientA1.id, + currentPressure: 130, + status: DeviceStatus.INACTIVE, + }, + create: { + snCode: 'SEED-SN-A-004', + patientId: patientA1.id, + currentPressure: 130, + status: DeviceStatus.INACTIVE, + }, + }); + + // 清理与种子设备关联的历史任务,保证 seed 可重复执行且生命周期夹具稳定。 + const seedTaskItems = await prisma.taskItem.findMany({ + where: { + deviceId: { + in: [deviceA1.id, deviceB1.id], + }, + }, + select: { taskId: true }, + }); + const seedTaskIds = Array.from( + new Set(seedTaskItems.map((item) => item.taskId)), + ); + if (seedTaskIds.length > 0) { + await prisma.task.deleteMany({ + where: { + id: { + in: seedTaskIds, + }, + }, + }); + } + + const lifecycleTaskA = await prisma.task.create({ + data: { + status: TaskStatus.COMPLETED, + creatorId: doctorA.id, + engineerId: engineerA.id, + hospitalId: hospitalA.id, + items: { + create: [ + { + deviceId: deviceA1.id, + oldPressure: 118, + targetPressure: 120, + }, + ], + }, + }, + include: { items: true }, + }); + + const lifecycleTaskB = await prisma.task.create({ + data: { + status: TaskStatus.PENDING, + creatorId: doctorB.id, + engineerId: engineerB.id, + hospitalId: hospitalB.id, + items: { + create: [ + { + deviceId: deviceB1.id, + oldPressure: 121, + targetPressure: 119, + }, + ], + }, + }, + include: { items: true }, + }); + console.log( JSON.stringify( { ok: true, - hospitalId: hospital.id, - departmentId: department.id, - groupId: group.id, - systemAdminId: systemAdmin.id, - doctorId: doctor.id, - patientId: patient.id, - seedPasswordPlain: 'Seed@1234', + seedPasswordPlain: SEED_PASSWORD_PLAIN, + hospitals: { + hospitalAId: hospitalA.id, + hospitalBId: hospitalB.id, + }, + departments: { + departmentA1Id: departmentA1.id, + departmentA2Id: departmentA2.id, + departmentB1Id: departmentB1.id, + }, + groups: { + groupA1Id: groupA1.id, + groupA2Id: groupA2.id, + groupB1Id: groupB1.id, + }, + users: { + systemAdminId: systemAdmin.id, + hospitalAdminAId: hospitalAdminA.id, + directorAId: directorA.id, + leaderAId: leaderA.id, + doctorAId: doctorA.id, + doctorA2Id: doctorA2.id, + doctorA3Id: doctorA3.id, + doctorBId: doctorB.id, + engineerAId: engineerA.id, + engineerBId: engineerB.id, + }, + patients: { + patientA1Id: patientA1.id, + patientA2Id: patientA2.id, + patientA3Id: patientA3.id, + patientB1Id: patientB1.id, + }, + devices: { + deviceA1Id: deviceA1.id, + deviceA2Id: deviceA2.id, + deviceB1Id: deviceB1.id, + }, + tasks: { + lifecycleTaskAId: lifecycleTaskA.id, + lifecycleTaskBId: lifecycleTaskB.id, + }, }, null, 2, diff --git a/test/e2e/fixtures/e2e-roles.ts b/test/e2e/fixtures/e2e-roles.ts new file mode 100644 index 0000000..f28f9e5 --- /dev/null +++ b/test/e2e/fixtures/e2e-roles.ts @@ -0,0 +1,59 @@ +import { Role } from '../../../src/generated/prisma/enums.js'; + +export const E2E_SEED_PASSWORD = 'Seed@1234'; + +export const E2E_ROLE_LIST = [ + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + Role.ENGINEER, +] as const; + +export type E2ERole = (typeof E2E_ROLE_LIST)[number]; + +export interface E2ESeedCredential { + role: E2ERole; + phone: string; + password: string; + hospitalId?: number; +} + +export const E2E_SEED_CREDENTIALS: Record = { + [Role.SYSTEM_ADMIN]: { + role: Role.SYSTEM_ADMIN, + phone: '13800001000', + password: E2E_SEED_PASSWORD, + }, + [Role.HOSPITAL_ADMIN]: { + role: Role.HOSPITAL_ADMIN, + phone: '13800001001', + password: E2E_SEED_PASSWORD, + hospitalId: 1, + }, + [Role.DIRECTOR]: { + role: Role.DIRECTOR, + phone: '13800001002', + password: E2E_SEED_PASSWORD, + hospitalId: 1, + }, + [Role.LEADER]: { + role: Role.LEADER, + phone: '13800001003', + password: E2E_SEED_PASSWORD, + hospitalId: 1, + }, + [Role.DOCTOR]: { + role: Role.DOCTOR, + phone: '13800001004', + password: E2E_SEED_PASSWORD, + hospitalId: 1, + }, + [Role.ENGINEER]: { + role: Role.ENGINEER, + phone: '13800001005', + password: E2E_SEED_PASSWORD, + hospitalId: 1, + }, +}; diff --git a/test/e2e/helpers/e2e-app.helper.ts b/test/e2e/helpers/e2e-app.helper.ts new file mode 100644 index 0000000..0b1b41b --- /dev/null +++ b/test/e2e/helpers/e2e-app.helper.ts @@ -0,0 +1,41 @@ +import 'dotenv/config'; +import { BadRequestException, ValidationPipe } from '@nestjs/common'; +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { AppModule } from '../../../src/app.module.js'; +import { HttpExceptionFilter } from '../../../src/common/http-exception.filter.js'; +import { MESSAGES } from '../../../src/common/messages.js'; +import { ResponseEnvelopeInterceptor } from '../../../src/common/response-envelope.interceptor.js'; + +export async function createE2eApp(): Promise { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + const app = moduleRef.createNestApplication(); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + exceptionFactory: (errors) => { + const messages = errors + .flatMap((error) => Object.values(error.constraints ?? {})) + .filter((item): item is string => Boolean(item)); + + return new BadRequestException( + messages.length > 0 + ? messages.join(';') + : MESSAGES.DEFAULT_BAD_REQUEST, + ); + }, + }), + ); + + app.useGlobalFilters(new HttpExceptionFilter()); + app.useGlobalInterceptors(new ResponseEnvelopeInterceptor()); + + await app.init(); + return app; +} diff --git a/test/e2e/helpers/e2e-auth.helper.ts b/test/e2e/helpers/e2e-auth.helper.ts new file mode 100644 index 0000000..8855ea6 --- /dev/null +++ b/test/e2e/helpers/e2e-auth.helper.ts @@ -0,0 +1,47 @@ +import type { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { + E2E_ROLE_LIST, + type E2ERole, + E2E_SEED_CREDENTIALS, +} from '../fixtures/e2e-roles.js'; +import { expectSuccessEnvelope } from './e2e-http.helper.js'; + +export type E2EAccessTokenMap = Record; + +export async function loginAsRole( + app: INestApplication, + role: E2ERole, +): Promise { + const credential = E2E_SEED_CREDENTIALS[role]; + const payload: Record = { + phone: credential.phone, + password: credential.password, + role: credential.role, + }; + + if (credential.hospitalId != null) { + payload.hospitalId = credential.hospitalId; + } + + const response = await request(app.getHttpServer()) + .post('/auth/login') + .send(payload); + + expectSuccessEnvelope(response, 201); + expect(response.body.data?.accessToken).toEqual(expect.any(String)); + + return response.body.data.accessToken as string; +} + +export async function loginAllRoles( + app: INestApplication, +): Promise { + const tokenEntries = await Promise.all( + E2E_ROLE_LIST.map( + async (role) => [role, await loginAsRole(app, role)] as const, + ), + ); + + return Object.fromEntries(tokenEntries) as E2EAccessTokenMap; +} diff --git a/test/e2e/helpers/e2e-context.helper.ts b/test/e2e/helpers/e2e-context.helper.ts new file mode 100644 index 0000000..c98826f --- /dev/null +++ b/test/e2e/helpers/e2e-context.helper.ts @@ -0,0 +1,38 @@ +import type { INestApplication } from '@nestjs/common'; +import { PrismaService } from '../../../src/prisma.service.js'; +import { loginAllRoles, type E2EAccessTokenMap } from './e2e-auth.helper.js'; +import { createE2eApp } from './e2e-app.helper.js'; +import { + loadSeedFixtures, + type E2ESeedFixtures, +} from './e2e-fixtures.helper.js'; + +export interface E2EContext { + app: INestApplication; + prisma: PrismaService; + tokens: E2EAccessTokenMap; + fixtures: E2ESeedFixtures; +} + +export async function createE2EContext(): Promise { + const app = await createE2eApp(); + const prisma = app.get(PrismaService); + const fixtures = await loadSeedFixtures(prisma); + const tokens = await loginAllRoles(app); + + return { + app, + prisma, + fixtures, + tokens, + }; +} + +export async function closeE2EContext(ctx?: E2EContext) { + if (!ctx) { + return; + } + + await ctx.prisma.$disconnect(); + await ctx.app.close(); +} diff --git a/test/e2e/helpers/e2e-fixtures.helper.ts b/test/e2e/helpers/e2e-fixtures.helper.ts new file mode 100644 index 0000000..95c3413 --- /dev/null +++ b/test/e2e/helpers/e2e-fixtures.helper.ts @@ -0,0 +1,195 @@ +import { NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../../src/prisma.service.js'; + +export interface E2ESeedFixtures { + hospitalAId: number; + hospitalBId: number; + departmentA1Id: number; + departmentA2Id: number; + departmentB1Id: number; + groupA1Id: number; + groupA2Id: number; + groupB1Id: number; + users: { + systemAdminId: number; + hospitalAdminAId: number; + directorAId: number; + leaderAId: number; + doctorAId: number; + doctorA2Id: number; + doctorA3Id: number; + doctorBId: number; + engineerAId: number; + engineerBId: number; + }; + patients: { + patientA1Id: number; + patientA2Id: number; + patientA3Id: number; + patientB1Id: number; + }; + devices: { + deviceA1Id: number; + deviceA2Id: number; + deviceA3Id: number; + deviceA4InactiveId: number; + deviceB1Id: number; + }; +} + +interface SeedUserScope { + id: number; + hospitalId: number | null; + departmentId: number | null; + groupId: number | null; +} + +async function requireUserScope( + prisma: PrismaService, + openId: string, +): Promise { + const user = await prisma.user.findUnique({ + where: { openId }, + select: { + id: true, + hospitalId: true, + departmentId: true, + groupId: true, + }, + }); + if (!user) { + throw new NotFoundException(`Seed user not found: ${openId}`); + } + return user; +} + +async function requireDeviceId( + prisma: PrismaService, + snCode: string, +): Promise { + const device = await prisma.device.findUnique({ + where: { snCode }, + select: { id: true }, + }); + if (!device) { + throw new NotFoundException(`Seed device not found: ${snCode}`); + } + return device.id; +} + +async function requirePatientId( + prisma: PrismaService, + hospitalId: number, + phone: string, + idCardHash: string, +): Promise { + const patient = await prisma.patient.findFirst({ + where: { hospitalId, phone, idCardHash }, + select: { id: true }, + }); + if (!patient) { + throw new NotFoundException( + `Seed patient not found: ${phone}/${idCardHash}`, + ); + } + return patient.id; +} + +export async function loadSeedFixtures( + prisma: PrismaService, +): Promise { + const systemAdmin = await requireUserScope( + prisma, + 'seed-system-admin-openid', + ); + const hospitalAdminA = await requireUserScope( + prisma, + 'seed-hospital-admin-a-openid', + ); + const directorA = await requireUserScope(prisma, 'seed-director-a-openid'); + const leaderA = await requireUserScope(prisma, 'seed-leader-a-openid'); + const doctorA = await requireUserScope(prisma, 'seed-doctor-a-openid'); + const doctorA2 = await requireUserScope(prisma, 'seed-doctor-a2-openid'); + const doctorA3 = await requireUserScope(prisma, 'seed-doctor-a3-openid'); + const doctorB = await requireUserScope(prisma, 'seed-doctor-b-openid'); + const engineerA = await requireUserScope(prisma, 'seed-engineer-a-openid'); + const engineerB = await requireUserScope(prisma, 'seed-engineer-b-openid'); + + const hospitalAId = hospitalAdminA.hospitalId; + const hospitalBId = doctorB.hospitalId; + const departmentA1Id = doctorA.departmentId; + const departmentA2Id = doctorA3.departmentId; + const departmentB1Id = doctorB.departmentId; + const groupA1Id = doctorA.groupId; + const groupA2Id = doctorA3.groupId; + const groupB1Id = doctorB.groupId; + + if ( + hospitalAId == null || + hospitalBId == null || + departmentA1Id == null || + departmentA2Id == null || + departmentB1Id == null || + groupA1Id == null || + groupA2Id == null || + groupB1Id == null + ) { + throw new NotFoundException('Seed user scope is incomplete'); + } + + return { + hospitalAId, + hospitalBId, + departmentA1Id, + departmentA2Id, + departmentB1Id, + groupA1Id, + groupA2Id, + groupB1Id, + users: { + systemAdminId: systemAdmin.id, + hospitalAdminAId: hospitalAdminA.id, + directorAId: directorA.id, + leaderAId: leaderA.id, + doctorAId: doctorA.id, + doctorA2Id: doctorA2.id, + doctorA3Id: doctorA3.id, + doctorBId: doctorB.id, + engineerAId: engineerA.id, + engineerBId: engineerB.id, + }, + patients: { + patientA1Id: await requirePatientId( + prisma, + hospitalAId, + '13800002001', + 'seed-id-card-cross-hospital', + ), + patientA2Id: await requirePatientId( + prisma, + hospitalAId, + '13800002002', + 'seed-id-card-a2', + ), + patientA3Id: await requirePatientId( + prisma, + hospitalAId, + '13800002003', + 'seed-id-card-a3', + ), + patientB1Id: await requirePatientId( + prisma, + hospitalBId, + '13800002001', + 'seed-id-card-cross-hospital', + ), + }, + devices: { + deviceA1Id: await requireDeviceId(prisma, 'SEED-SN-A-001'), + deviceA2Id: await requireDeviceId(prisma, 'SEED-SN-A-002'), + deviceA3Id: await requireDeviceId(prisma, 'SEED-SN-A-003'), + deviceA4InactiveId: await requireDeviceId(prisma, 'SEED-SN-A-004'), + deviceB1Id: await requireDeviceId(prisma, 'SEED-SN-B-001'), + }, + }; +} diff --git a/test/e2e/helpers/e2e-http.helper.ts b/test/e2e/helpers/e2e-http.helper.ts new file mode 100644 index 0000000..147a5c8 --- /dev/null +++ b/test/e2e/helpers/e2e-http.helper.ts @@ -0,0 +1,37 @@ +import type { Response } from 'supertest'; + +export function expectSuccessEnvelope(response: Response, status: number) { + expect(response.status).toBe(status); + expect(response.body).toEqual( + expect.objectContaining({ + code: 0, + msg: '成功', + }), + ); + expect(response.body).toHaveProperty('data'); +} + +export function expectErrorEnvelope( + response: Response, + status: number, + messageIncludes?: string, +) { + expect(response.status).toBe(status); + expect(response.body.code).toBe(status); + expect(response.body.data).toBeNull(); + + if (messageIncludes) { + expect(String(response.body.msg)).toContain(messageIncludes); + } +} + +export function uniqueSeedValue(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +} + +export function uniquePhone(): string { + const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}` + .replace(/\D/g, '') + .slice(-10); + return `1${suffix.padStart(10, '0')}`.slice(0, 11); +} diff --git a/test/e2e/helpers/e2e-matrix.helper.ts b/test/e2e/helpers/e2e-matrix.helper.ts new file mode 100644 index 0000000..3106478 --- /dev/null +++ b/test/e2e/helpers/e2e-matrix.helper.ts @@ -0,0 +1,32 @@ +import type { Response } from 'supertest'; +import { E2E_ROLE_LIST, type E2ERole } from '../fixtures/e2e-roles.js'; +import type { E2EAccessTokenMap } from './e2e-auth.helper.js'; + +interface RoleMatrixCase { + name: string; + tokens: E2EAccessTokenMap; + expectedStatusByRole: Record; + sendAsRole: (role: E2ERole, token: string) => Promise; + sendWithoutToken: () => Promise; + expectedStatusWithoutToken?: number; +} + +export async function assertRoleMatrix(matrixCase: RoleMatrixCase) { + for (const role of E2E_ROLE_LIST) { + const response = await matrixCase.sendAsRole(role, matrixCase.tokens[role]); + const expectedStatus = matrixCase.expectedStatusByRole[role]; + const isSuccess = expectedStatus >= 200 && expectedStatus < 300; + + expect(response.status).toBe(expectedStatus); + expect(response.body.code).toBe(isSuccess ? 0 : expectedStatus); + } + + const unauthorizedResponse = await matrixCase.sendWithoutToken(); + const unauthorizedStatus = matrixCase.expectedStatusWithoutToken ?? 401; + expect(unauthorizedResponse.status).toBe(unauthorizedStatus); + expect(unauthorizedResponse.body.code).toBe( + unauthorizedStatus >= 200 && unauthorizedStatus < 300 + ? 0 + : unauthorizedStatus, + ); +} diff --git a/test/e2e/specs/auth.e2e-spec.ts b/test/e2e/specs/auth.e2e-spec.ts new file mode 100644 index 0000000..82fccb0 --- /dev/null +++ b/test/e2e/specs/auth.e2e-spec.ts @@ -0,0 +1,129 @@ +import request from 'supertest'; +import { Role } from '../../../src/generated/prisma/enums.js'; +import { + closeE2EContext, + createE2EContext, + type E2EContext, +} from '../helpers/e2e-context.helper.js'; +import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; +import { + expectErrorEnvelope, + expectSuccessEnvelope, + uniquePhone, + uniqueSeedValue, +} from '../helpers/e2e-http.helper.js'; + +describe('AuthController (e2e)', () => { + let ctx: E2EContext; + + beforeAll(async () => { + ctx = await createE2EContext(); + }); + + afterAll(async () => { + await closeE2EContext(ctx); + }); + + describe('POST /auth/register', () => { + it('成功:注册医生账号', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/auth/register') + .send({ + name: uniqueSeedValue('Auth 注册医生'), + phone: uniquePhone(), + password: 'Seed@1234', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + openId: uniqueSeedValue('auth-register-openid'), + }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.role).toBe(Role.DOCTOR); + }); + + it('失败:参数不合法返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/auth/register') + .send({ + name: 'bad-register', + phone: '13800009999', + password: '123', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + }); + + expectErrorEnvelope(response, 400, 'password 长度至少 8 位'); + }); + }); + + describe('POST /auth/login', () => { + it('成功:seed 账号登录并拿到 token', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/auth/login') + .send({ + phone: '13800001004', + password: 'Seed@1234', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.accessToken).toEqual(expect.any(String)); + expect(response.body.data.actor.role).toBe(Role.DOCTOR); + }); + + it('失败:密码错误返回 401', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/auth/login') + .send({ + phone: '13800001004', + password: 'Seed@12345', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + }); + + expectErrorEnvelope(response, 401, '手机号、角色或密码错误'); + }); + }); + + describe('GET /auth/me', () => { + it('成功:已登录用户可读取当前信息', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/auth/me') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.role).toBe(Role.DOCTOR); + }); + + it('失败:未登录返回 401', async () => { + const response = await request(ctx.app.getHttpServer()).get('/auth/me'); + expectErrorEnvelope(response, 401, '缺少 Bearer Token'); + }); + + it('角色矩阵:6 角色都可访问,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /auth/me role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 200, + [Role.LEADER]: 200, + [Role.DOCTOR]: 200, + [Role.ENGINEER]: 200, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get('/auth/me') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/auth/me'), + }); + }); + }); +}); diff --git a/test/e2e/specs/organization.e2e-spec.ts b/test/e2e/specs/organization.e2e-spec.ts new file mode 100644 index 0000000..cb7e7ef --- /dev/null +++ b/test/e2e/specs/organization.e2e-spec.ts @@ -0,0 +1,729 @@ +import request from 'supertest'; +import { Role } from '../../../src/generated/prisma/enums.js'; +import { + closeE2EContext, + createE2EContext, + type E2EContext, +} from '../helpers/e2e-context.helper.js'; +import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; +import { + expectErrorEnvelope, + expectSuccessEnvelope, + uniqueSeedValue, +} from '../helpers/e2e-http.helper.js'; + +describe('Organization Controllers (e2e)', () => { + let ctx: E2EContext; + + beforeAll(async () => { + ctx = await createE2EContext(); + }); + + afterAll(async () => { + await closeE2EContext(ctx); + }); + + describe('HospitalsController', () => { + describe('POST /b/organization/hospitals', () => { + it('成功:SYSTEM_ADMIN 可创建医院', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/hospitals') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ name: uniqueSeedValue('组织-医院') }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.name).toContain('组织-医院'); + }); + + it('失败:非系统管理员创建返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/hospitals') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: uniqueSeedValue('组织-医院-失败') }); + + expectErrorEnvelope(response, 403, '无权限执行当前操作'); + }); + + it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/organization/hospitals role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 400, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/organization/hospitals') + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/organization/hospitals') + .send({}), + }); + }); + }); + + describe('GET /b/organization/hospitals', () => { + it('成功:SYSTEM_ADMIN 可查询医院列表', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/b/organization/hospitals') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data).toHaveProperty('list'); + }); + + it('失败:未登录返回 401', async () => { + const response = await request(ctx.app.getHttpServer()).get( + '/b/organization/hospitals', + ); + expectErrorEnvelope(response, 401, '缺少 Bearer Token'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/hospitals role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get('/b/organization/hospitals') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/b/organization/hospitals'), + }); + }); + }); + + describe('GET /b/organization/hospitals/:id', () => { + it('成功:HOSPITAL_ADMIN 可查询本院详情', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(ctx.fixtures.hospitalAId); + }); + + it('失败:HOSPITAL_ADMIN 查询他院返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/hospitals/${ctx.fixtures.hospitalBId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/hospitals/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`) + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get( + `/b/organization/hospitals/${ctx.fixtures.hospitalAId}`, + ), + }); + }); + }); + + describe('PATCH /b/organization/hospitals/:id', () => { + it('成功:HOSPITAL_ADMIN 可更新本院名称', async () => { + const originalName = 'Seed Hospital A'; + const nextName = uniqueSeedValue('医院更新'); + + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: nextName }); + + expectSuccessEnvelope(response, 200); + + const rollbackResponse = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: originalName }); + expectSuccessEnvelope(rollbackResponse, 200); + }); + + it('失败:HOSPITAL_ADMIN 更新他院返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/hospitals/${ctx.fixtures.hospitalBId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: uniqueSeedValue('跨院更新失败') }); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'PATCH /b/organization/hospitals/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .patch('/b/organization/hospitals/99999999') + .set('Authorization', `Bearer ${token}`) + .send({ name: 'matrix-hospital-patch' }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .patch('/b/organization/hospitals/99999999') + .send({ name: 'matrix-hospital-patch' }), + }); + }); + }); + + describe('DELETE /b/organization/hospitals/:id', () => { + it('成功:SYSTEM_ADMIN 可删除空医院', async () => { + const createResponse = await request(ctx.app.getHttpServer()) + .post('/b/organization/hospitals') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ name: uniqueSeedValue('医院待删') }); + expectSuccessEnvelope(createResponse, 201); + + const targetId = createResponse.body.data.id as number; + const deleteResponse = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/hospitals/${targetId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(deleteResponse, 200); + expect(deleteResponse.body.data.id).toBe(targetId); + }); + + it('失败:HOSPITAL_ADMIN 删除医院返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '无权限执行当前操作'); + }); + + it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'DELETE /b/organization/hospitals/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .delete('/b/organization/hospitals/99999999') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).delete( + '/b/organization/hospitals/99999999', + ), + }); + }); + }); + }); + + describe('DepartmentsController', () => { + describe('POST /b/organization/departments', () => { + it('成功:HOSPITAL_ADMIN 可在本院创建科室', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/departments') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ + name: uniqueSeedValue('组织-科室'), + hospitalId: ctx.fixtures.hospitalAId, + }); + + expectSuccessEnvelope(response, 201); + }); + + it('失败:HOSPITAL_ADMIN 跨院创建返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/departments') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ + name: uniqueSeedValue('组织-跨院科室失败'), + hospitalId: ctx.fixtures.hospitalBId, + }); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/organization/departments role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 400, + [Role.HOSPITAL_ADMIN]: 400, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/organization/departments') + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/organization/departments') + .send({}), + }); + }); + }); + + describe('GET /b/organization/departments', () => { + it('成功:HOSPITAL_ADMIN 可查询本院科室列表', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/b/organization/departments') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data).toHaveProperty('list'); + }); + + it('失败:未登录返回 401', async () => { + const response = await request(ctx.app.getHttpServer()).get( + '/b/organization/departments', + ); + expectErrorEnvelope(response, 401, '缺少 Bearer Token'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/departments role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get('/b/organization/departments') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/b/organization/departments'), + }); + }); + }); + + describe('GET /b/organization/departments/:id', () => { + it('成功:SYSTEM_ADMIN 可查询科室详情', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(ctx.fixtures.departmentA1Id); + }); + + it('失败:HOSPITAL_ADMIN 查询他院科室返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/departments/${ctx.fixtures.departmentB1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/departments/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`) + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get( + `/b/organization/departments/${ctx.fixtures.departmentA1Id}`, + ), + }); + }); + }); + + describe('PATCH /b/organization/departments/:id', () => { + it('成功:HOSPITAL_ADMIN 可更新本院科室', async () => { + const originalName = 'Cardiology-A2'; + const nextName = uniqueSeedValue('科室更新'); + + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/departments/${ctx.fixtures.departmentA2Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: nextName }); + + expectSuccessEnvelope(response, 200); + + const rollbackResponse = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/departments/${ctx.fixtures.departmentA2Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: originalName }); + expectSuccessEnvelope(rollbackResponse, 200); + }); + + it('失败:HOSPITAL_ADMIN 更新他院科室返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/departments/${ctx.fixtures.departmentB1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: uniqueSeedValue('跨院科室更新失败') }); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'PATCH /b/organization/departments/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .patch('/b/organization/departments/99999999') + .set('Authorization', `Bearer ${token}`) + .send({ name: 'matrix-department-patch' }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .patch('/b/organization/departments/99999999') + .send({ name: 'matrix-department-patch' }), + }); + }); + }); + + describe('DELETE /b/organization/departments/:id', () => { + it('成功:SYSTEM_ADMIN 可删除无关联科室', async () => { + const createResponse = await request(ctx.app.getHttpServer()) + .post('/b/organization/departments') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + name: uniqueSeedValue('科室待删'), + hospitalId: ctx.fixtures.hospitalAId, + }); + expectSuccessEnvelope(createResponse, 201); + + const targetId = createResponse.body.data.id as number; + const deleteResponse = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/departments/${targetId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(deleteResponse, 200); + }); + + it('失败:存在关联数据删除返回 409', async () => { + const response = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectErrorEnvelope(response, 409, '存在关联数据,无法删除'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'DELETE /b/organization/departments/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .delete('/b/organization/departments/99999999') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).delete( + '/b/organization/departments/99999999', + ), + }); + }); + }); + }); + + describe('GroupsController', () => { + describe('POST /b/organization/groups', () => { + it('成功:HOSPITAL_ADMIN 可创建小组', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/groups') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ + name: uniqueSeedValue('组织-小组'), + departmentId: ctx.fixtures.departmentA1Id, + }); + + expectSuccessEnvelope(response, 201); + }); + + it('失败:HOSPITAL_ADMIN 跨院创建小组返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/organization/groups') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ + name: uniqueSeedValue('组织-跨院小组失败'), + departmentId: ctx.fixtures.departmentB1Id, + }); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/organization/groups role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 400, + [Role.HOSPITAL_ADMIN]: 400, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/organization/groups') + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/organization/groups') + .send({}), + }); + }); + }); + + describe('GET /b/organization/groups', () => { + it('成功:SYSTEM_ADMIN 可查询小组列表', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/b/organization/groups') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data).toHaveProperty('list'); + }); + + it('失败:未登录返回 401', async () => { + const response = await request(ctx.app.getHttpServer()).get( + '/b/organization/groups', + ); + expectErrorEnvelope(response, 401, '缺少 Bearer Token'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/groups role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get('/b/organization/groups') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/b/organization/groups'), + }); + }); + }); + + describe('GET /b/organization/groups/:id', () => { + it('成功:HOSPITAL_ADMIN 可查询本院小组详情', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/groups/${ctx.fixtures.groupA1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(ctx.fixtures.groupA1Id); + }); + + it('失败:HOSPITAL_ADMIN 查询他院小组返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/b/organization/groups/${ctx.fixtures.groupB1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/organization/groups/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get(`/b/organization/groups/${ctx.fixtures.groupA1Id}`) + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get( + `/b/organization/groups/${ctx.fixtures.groupA1Id}`, + ), + }); + }); + }); + + describe('PATCH /b/organization/groups/:id', () => { + it('成功:HOSPITAL_ADMIN 可更新本院小组', async () => { + const originalName = 'Shift-A2'; + const nextName = uniqueSeedValue('小组更新'); + + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/groups/${ctx.fixtures.groupA2Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: nextName }); + + expectSuccessEnvelope(response, 200); + + const rollbackResponse = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/groups/${ctx.fixtures.groupA2Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: originalName }); + expectSuccessEnvelope(rollbackResponse, 200); + }); + + it('失败:HOSPITAL_ADMIN 更新他院小组返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/b/organization/groups/${ctx.fixtures.groupB1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) + .send({ name: uniqueSeedValue('跨院小组更新失败') }); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'PATCH /b/organization/groups/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .patch('/b/organization/groups/99999999') + .set('Authorization', `Bearer ${token}`) + .send({ name: 'matrix-group-patch' }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .patch('/b/organization/groups/99999999') + .send({ name: 'matrix-group-patch' }), + }); + }); + }); + + describe('DELETE /b/organization/groups/:id', () => { + it('成功:SYSTEM_ADMIN 可删除无关联小组', async () => { + const createResponse = await request(ctx.app.getHttpServer()) + .post('/b/organization/groups') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + name: uniqueSeedValue('小组待删'), + departmentId: ctx.fixtures.departmentA1Id, + }); + expectSuccessEnvelope(createResponse, 201); + + const targetId = createResponse.body.data.id as number; + const deleteResponse = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/groups/${targetId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(deleteResponse, 200); + }); + + it('失败:HOSPITAL_ADMIN 删除他院小组返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .delete(`/b/organization/groups/${ctx.fixtures.groupB1Id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'DELETE /b/organization/groups/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .delete('/b/organization/groups/99999999') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).delete( + '/b/organization/groups/99999999', + ), + }); + }); + }); + }); +}); diff --git a/test/e2e/specs/patients.e2e-spec.ts b/test/e2e/specs/patients.e2e-spec.ts new file mode 100644 index 0000000..878928b --- /dev/null +++ b/test/e2e/specs/patients.e2e-spec.ts @@ -0,0 +1,185 @@ +import request from 'supertest'; +import { Role } from '../../../src/generated/prisma/enums.js'; +import { + closeE2EContext, + createE2EContext, + type E2EContext, +} from '../helpers/e2e-context.helper.js'; +import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; +import { + expectErrorEnvelope, + expectSuccessEnvelope, +} from '../helpers/e2e-http.helper.js'; + +describe('Patients Controllers (e2e)', () => { + let ctx: E2EContext; + + beforeAll(async () => { + ctx = await createE2EContext(); + }); + + afterAll(async () => { + await closeE2EContext(ctx); + }); + + describe('GET /b/patients', () => { + it('成功:按角色返回正确可见性范围', async () => { + const systemAdminResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .query({ hospitalId: ctx.fixtures.hospitalAId }) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + expectSuccessEnvelope(systemAdminResponse, 200); + const systemPatientIds = ( + systemAdminResponse.body.data as Array<{ id: number }> + ).map((item) => item.id); + expect(systemPatientIds).toEqual( + expect.arrayContaining([ + ctx.fixtures.patients.patientA1Id, + ctx.fixtures.patients.patientA2Id, + ctx.fixtures.patients.patientA3Id, + ]), + ); + + const hospitalAdminResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + expectSuccessEnvelope(hospitalAdminResponse, 200); + const hospitalPatientIds = ( + hospitalAdminResponse.body.data as Array<{ id: number }> + ).map((item) => item.id); + expect(hospitalPatientIds).toEqual( + expect.arrayContaining([ + ctx.fixtures.patients.patientA1Id, + ctx.fixtures.patients.patientA2Id, + ctx.fixtures.patients.patientA3Id, + ]), + ); + + const directorResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`); + expectSuccessEnvelope(directorResponse, 200); + const directorPatientIds = ( + directorResponse.body.data as Array<{ id: number }> + ).map((item) => item.id); + expect(directorPatientIds).toEqual( + expect.arrayContaining([ + ctx.fixtures.patients.patientA1Id, + ctx.fixtures.patients.patientA2Id, + ]), + ); + expect(directorPatientIds).not.toContain( + ctx.fixtures.patients.patientA3Id, + ); + + const leaderResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`); + expectSuccessEnvelope(leaderResponse, 200); + const leaderPatientIds = ( + leaderResponse.body.data as Array<{ id: number }> + ).map((item) => item.id); + expect(leaderPatientIds).toEqual( + expect.arrayContaining([ + ctx.fixtures.patients.patientA1Id, + ctx.fixtures.patients.patientA2Id, + ]), + ); + expect(leaderPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id); + + const doctorResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`); + expectSuccessEnvelope(doctorResponse, 200); + const doctorPatientIds = ( + doctorResponse.body.data as Array<{ id: number }> + ).map((item) => item.id); + expect(doctorPatientIds).toContain(ctx.fixtures.patients.patientA1Id); + expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA2Id); + expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id); + + const engineerResponse = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`); + expectErrorEnvelope(engineerResponse, 403, '无权限执行当前操作'); + }); + + it('失败:SYSTEM_ADMIN 不传 hospitalId 返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectErrorEnvelope( + response, + 400, + '系统管理员查询必须显式传入 hospitalId', + ); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR/LEADER/DOCTOR 可访问,ENGINEER 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /b/patients role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 200, + [Role.LEADER]: 200, + [Role.DOCTOR]: 200, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (role, token) => { + const req = request(ctx.app.getHttpServer()) + .get('/b/patients') + .set('Authorization', `Bearer ${token}`); + + if (role === Role.SYSTEM_ADMIN) { + req.query({ hospitalId: ctx.fixtures.hospitalAId }); + } + + return req; + }, + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/b/patients'), + }); + }); + }); + + describe('GET /c/patients/lifecycle', () => { + it('成功:可按 phone + idCardHash 查询跨院生命周期', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/c/patients/lifecycle') + .query({ + phone: '13800002001', + idCardHash: 'seed-id-card-cross-hospital', + }); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.phone).toBe('13800002001'); + expect(response.body.data.idCardHash).toBe('seed-id-card-cross-hospital'); + expect(response.body.data.patientCount).toBeGreaterThanOrEqual(2); + expect(Array.isArray(response.body.data.lifecycle)).toBe(true); + }); + + it('失败:参数缺失返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/c/patients/lifecycle') + .query({ + phone: '13800002001', + }); + + expectErrorEnvelope(response, 400, 'idCardHash 必须是字符串'); + }); + + it('失败:不存在患者返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/c/patients/lifecycle') + .query({ + phone: '13800009999', + idCardHash: 'not-exists-idcard-hash', + }); + + expectErrorEnvelope(response, 404, '未找到匹配的患者档案'); + }); + }); +}); diff --git a/test/e2e/specs/tasks.e2e-spec.ts b/test/e2e/specs/tasks.e2e-spec.ts new file mode 100644 index 0000000..cfff46c --- /dev/null +++ b/test/e2e/specs/tasks.e2e-spec.ts @@ -0,0 +1,325 @@ +import request from 'supertest'; +import { Role, TaskStatus } from '../../../src/generated/prisma/enums.js'; +import { + closeE2EContext, + createE2EContext, + type E2EContext, +} from '../helpers/e2e-context.helper.js'; +import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; +import { + expectErrorEnvelope, + expectSuccessEnvelope, +} from '../helpers/e2e-http.helper.js'; + +describe('BTasksController (e2e)', () => { + let ctx: E2EContext; + + beforeAll(async () => { + ctx = await createE2EContext(); + }); + + afterAll(async () => { + await closeE2EContext(ctx); + }); + + async function publishPendingTask(deviceId: number, targetPressure: number) { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/publish') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ + items: [ + { + deviceId, + targetPressure, + }, + ], + }); + + expectSuccessEnvelope(response, 201); + return response.body.data as { id: number; status: TaskStatus }; + } + + describe('POST /b/tasks/publish', () => { + it('成功:DOCTOR 可发布任务', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/publish') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ + engineerId: ctx.fixtures.users.engineerAId, + items: [ + { + deviceId: ctx.fixtures.devices.deviceA2Id, + targetPressure: 126, + }, + ], + }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.status).toBe(TaskStatus.PENDING); + }); + + it('失败:发布跨院设备任务返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/publish') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ + items: [ + { + deviceId: ctx.fixtures.devices.deviceB1Id, + targetPressure: 120, + }, + ], + }); + + expectErrorEnvelope(response, 404, '存在设备不在当前医院或设备不存在'); + }); + + it('角色矩阵:仅 DOCTOR 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/tasks/publish role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 403, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 400, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/tasks/publish') + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).post('/b/tasks/publish').send({}), + }); + }); + }); + + describe('POST /b/tasks/accept', () => { + it('成功:ENGINEER 可接收待处理任务', async () => { + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA2Id, + 127, + ); + + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.status).toBe(TaskStatus.ACCEPTED); + expect(response.body.data.engineerId).toBe( + ctx.fixtures.users.engineerAId, + ); + }); + + it('失败:接收不存在任务返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: 99999999 }); + + expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院'); + }); + + it('状态机失败:重复接收返回 409', async () => { + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA3Id, + 122, + ); + + const firstAccept = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + expectSuccessEnvelope(firstAccept, 201); + + const secondAccept = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + + expectErrorEnvelope(secondAccept, 409, '仅待接收任务可执行接收'); + }); + + it('角色矩阵:仅 ENGINEER 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/tasks/accept role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 403, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 404, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${token}`) + .send({ taskId: 99999999 }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .send({ taskId: 99999999 }), + }); + }); + }); + + describe('POST /b/tasks/complete', () => { + it('成功:ENGINEER 完成已接收任务并同步设备压力', async () => { + const targetPressure = 135; + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA1Id, + targetPressure, + ); + + const acceptResponse = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + expectSuccessEnvelope(acceptResponse, 201); + + const completeResponse = await request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + + expectSuccessEnvelope(completeResponse, 201); + expect(completeResponse.body.data.status).toBe(TaskStatus.COMPLETED); + + const device = await ctx.prisma.device.findUnique({ + where: { id: ctx.fixtures.devices.deviceA1Id }, + select: { currentPressure: true }, + }); + expect(device?.currentPressure).toBe(targetPressure); + }); + + it('失败:完成不存在任务返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: 99999999 }); + + expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院'); + }); + + it('状态机失败:未接收任务直接完成返回 409', async () => { + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA2Id, + 124, + ); + + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + + expectErrorEnvelope(response, 409, '仅已接收任务可执行完成'); + }); + + it('角色矩阵:仅 ENGINEER 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/tasks/complete role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 403, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 404, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .set('Authorization', `Bearer ${token}`) + .send({ taskId: 99999999 }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .send({ taskId: 99999999 }), + }); + }); + }); + + describe('POST /b/tasks/cancel', () => { + it('成功:DOCTOR 可取消自己创建的任务', async () => { + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA3Id, + 120, + ); + + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/cancel') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ taskId: task.id }); + + expectSuccessEnvelope(response, 201); + expect(response.body.data.status).toBe(TaskStatus.CANCELLED); + }); + + it('失败:取消不存在任务返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/b/tasks/cancel') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ taskId: 99999999 }); + + expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院'); + }); + + it('状态机失败:已完成任务不可取消返回 409', async () => { + const task = await publishPendingTask( + ctx.fixtures.devices.deviceA2Id, + 123, + ); + + const acceptResponse = await request(ctx.app.getHttpServer()) + .post('/b/tasks/accept') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + expectSuccessEnvelope(acceptResponse, 201); + + const completeResponse = await request(ctx.app.getHttpServer()) + .post('/b/tasks/complete') + .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) + .send({ taskId: task.id }); + expectSuccessEnvelope(completeResponse, 201); + + const cancelResponse = await request(ctx.app.getHttpServer()) + .post('/b/tasks/cancel') + .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) + .send({ taskId: task.id }); + + expectErrorEnvelope(cancelResponse, 409, '仅待接收/已接收任务可取消'); + }); + + it('角色矩阵:仅 DOCTOR 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /b/tasks/cancel role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 403, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 404, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/b/tasks/cancel') + .set('Authorization', `Bearer ${token}`) + .send({ taskId: 99999999 }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .post('/b/tasks/cancel') + .send({ taskId: 99999999 }), + }); + }); + }); +}); diff --git a/test/e2e/specs/users.e2e-spec.ts b/test/e2e/specs/users.e2e-spec.ts new file mode 100644 index 0000000..63c4105 --- /dev/null +++ b/test/e2e/specs/users.e2e-spec.ts @@ -0,0 +1,332 @@ +import request from 'supertest'; +import { Role } from '../../../src/generated/prisma/enums.js'; +import { + closeE2EContext, + createE2EContext, + type E2EContext, +} from '../helpers/e2e-context.helper.js'; +import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; +import { + expectErrorEnvelope, + expectSuccessEnvelope, + uniquePhone, + uniqueSeedValue, +} from '../helpers/e2e-http.helper.js'; + +describe('UsersController + BUsersController (e2e)', () => { + let ctx: E2EContext; + + beforeAll(async () => { + ctx = await createE2EContext(); + }); + + afterAll(async () => { + await closeE2EContext(ctx); + }); + + async function createDoctorUser(token: string) { + const response = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${token}`) + .send({ + name: uniqueSeedValue('用户-医生'), + phone: uniquePhone(), + password: 'Seed@1234', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + openId: uniqueSeedValue('users-doctor-openid'), + }); + + expectSuccessEnvelope(response, 201); + return response.body.data as { id: number; name: string }; + } + + async function createEngineerUser(token: string) { + const response = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${token}`) + .send({ + name: uniqueSeedValue('用户-工程师'), + phone: uniquePhone(), + password: 'Seed@1234', + role: Role.ENGINEER, + hospitalId: ctx.fixtures.hospitalAId, + openId: uniqueSeedValue('users-engineer-openid'), + }); + + expectSuccessEnvelope(response, 201); + return response.body.data as { id: number; name: string }; + } + + describe('POST /users', () => { + it('成功:SYSTEM_ADMIN 可创建用户', async () => { + await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]); + }); + + it('失败:参数校验失败返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + name: 'bad-user', + phone: '123', + password: 'short', + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + }); + + expectErrorEnvelope(response, 400, 'phone 必须是合法手机号'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'POST /users role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 400, + [Role.HOSPITAL_ADMIN]: 400, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).post('/users').send({}), + }); + }); + }); + + describe('GET /users', () => { + it('成功:SYSTEM_ADMIN 可查询用户列表', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/users') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(Array.isArray(response.body.data)).toBe(true); + }); + + it('失败:未登录返回 401', async () => { + const response = await request(ctx.app.getHttpServer()).get('/users'); + expectErrorEnvelope(response, 401, '缺少 Bearer Token'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /users role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get('/users') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get('/users'), + }); + }); + }); + + describe('GET /users/:id', () => { + it('成功:SYSTEM_ADMIN 可查询用户详情', async () => { + const response = await request(ctx.app.getHttpServer()) + .get(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId); + }); + + it('失败:查询不存在用户返回 404', async () => { + const response = await request(ctx.app.getHttpServer()) + .get('/users/99999999') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectErrorEnvelope(response, 404, '用户不存在'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'GET /users/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 200, + [Role.HOSPITAL_ADMIN]: 200, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .get(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).get( + `/users/${ctx.fixtures.users.doctorAId}`, + ), + }); + }); + }); + + describe('PATCH /users/:id', () => { + it('成功:SYSTEM_ADMIN 可更新用户姓名', async () => { + const created = await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]); + const nextName = uniqueSeedValue('更新后医生名'); + + const response = await request(ctx.app.getHttpServer()) + .patch(`/users/${created.id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ name: nextName }); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.name).toBe(nextName); + }); + + it('失败:非医生调整科室/小组返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch(`/users/${ctx.fixtures.users.engineerAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + }); + + expectErrorEnvelope(response, 400, '仅医生允许调整科室/小组归属'); + }); + + it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'PATCH /users/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 404, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .patch('/users/99999999') + .set('Authorization', `Bearer ${token}`) + .send({ name: 'matrix-patch' }), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .patch('/users/99999999') + .send({ name: 'matrix-patch' }), + }); + }); + }); + + describe('DELETE /users/:id', () => { + it('成功:SYSTEM_ADMIN 可删除用户', async () => { + const created = await createEngineerUser(ctx.tokens[Role.SYSTEM_ADMIN]); + const response = await request(ctx.app.getHttpServer()) + .delete(`/users/${created.id}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.id).toBe(created.id); + }); + + it('失败:HOSPITAL_ADMIN 无法删除返回 403', async () => { + const response = await request(ctx.app.getHttpServer()) + .delete(`/users/${ctx.fixtures.users.doctorAId}`) + .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); + + expectErrorEnvelope(response, 403, '无权限执行当前操作'); + }); + + it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'DELETE /users/:id role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 404, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .delete('/users/99999999') + .set('Authorization', `Bearer ${token}`), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()).delete('/users/99999999'), + }); + }); + }); + + describe('PATCH /b/users/:id/assign-engineer-hospital', () => { + it('成功:SYSTEM_ADMIN 可绑定工程师医院', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch( + `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, + ) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ hospitalId: ctx.fixtures.hospitalAId }); + + expectSuccessEnvelope(response, 200); + expect(response.body.data.hospitalId).toBe(ctx.fixtures.hospitalAId); + expect(response.body.data.role).toBe(Role.ENGINEER); + }); + + it('失败:目标用户不是工程师返回 400', async () => { + const response = await request(ctx.app.getHttpServer()) + .patch( + `/b/users/${ctx.fixtures.users.doctorAId}/assign-engineer-hospital`, + ) + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ hospitalId: ctx.fixtures.hospitalAId }); + + expectErrorEnvelope(response, 400, '目标用户不是工程师'); + }); + + it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { + await assertRoleMatrix({ + name: 'PATCH /b/users/:id/assign-engineer-hospital role matrix', + tokens: ctx.tokens, + expectedStatusByRole: { + [Role.SYSTEM_ADMIN]: 400, + [Role.HOSPITAL_ADMIN]: 403, + [Role.DIRECTOR]: 403, + [Role.LEADER]: 403, + [Role.DOCTOR]: 403, + [Role.ENGINEER]: 403, + }, + sendAsRole: async (_role, token) => + request(ctx.app.getHttpServer()) + .patch( + `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, + ) + .set('Authorization', `Bearer ${token}`) + .send({}), + sendWithoutToken: async () => + request(ctx.app.getHttpServer()) + .patch( + `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, + ) + .send({}), + }); + }); + }); +}); diff --git a/test/jest-e2e.config.cjs b/test/jest-e2e.config.cjs new file mode 100644 index 0000000..bbd1c9c --- /dev/null +++ b/test/jest-e2e.config.cjs @@ -0,0 +1,20 @@ +module.exports = { + rootDir: '../', + testEnvironment: 'node', + moduleFileExtensions: ['js', 'json', 'ts'], + testRegex: 'test/e2e/specs/.*\\.e2e-spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + useESM: true, + tsconfig: '/test/tsconfig.e2e.json', + }, + ], + }, + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + maxWorkers: 1, +}; diff --git a/test/tsconfig.e2e.json b/test/tsconfig.e2e.json new file mode 100644 index 0000000..0519235 --- /dev/null +++ b/test/tsconfig.e2e.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node", "jest"], + "noEmit": true + }, + "include": ["./e2e/**/*.ts", "../src/**/*.ts"] +}