From 420aa223428c4e36b58b65f0cbd4ee4c123f4d3c Mon Sep 17 00:00:00 2001 From: Citrons <909229731@qq.com> Date: Thu, 13 Aug 2026 17:26:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(GOV-009):=20=E5=BB=BA=E7=AB=8B=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=A5=91=E7=BA=A6=E4=B8=8E=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierignore | 11 + AGENTS.md | 2 +- deploy/compose/development.yml | 6 +- deploy/env/development/api.env.example | 1 + deploy/env/development/worker.env.example | 1 + deploy/env/production/api.env.example | 1 + deploy/env/production/worker.env.example | 1 + eslint.config.mjs | 6 + pnpm-lock.yaml | 726 ++++++++++++++++++ server/apps/api/src/api.module.ts | 27 +- server/apps/api/src/health.controller.ts | 22 + server/apps/api/src/health.service.ts | 51 ++ server/apps/api/src/main.ts | 1 + server/apps/api/test/health.e2e-spec.ts | 85 ++ server/apps/api/test/openapi.test.ts | 29 + server/libs/config/src/runtime-config.test.ts | 15 + server/libs/config/src/runtime-config.ts | 3 +- server/libs/http-contracts/src/health.dto.ts | 37 + .../src/disabled-health.client.ts | 17 + .../src/disabled-message-bus.adapter.test.ts | 23 + .../src/disabled-message-bus.adapter.ts | 26 + .../infrastructure/src/health-clients.test.ts | 135 ++++ .../infrastructure/src/message-bus.adapter.ts | 11 + .../infrastructure/src/mysql-health.client.ts | 75 ++ .../infrastructure/src/redis-health.client.ts | 58 ++ .../infrastructure/src/typeorm-options.ts | 23 + .../src/message-envelope.test.ts | 43 ++ .../message-contracts/src/message-envelope.ts | 39 + server/openapi/openapi.json | 74 ++ server/package.json | 11 +- server/tools/export-openapi.ts | 65 ++ server/vitest.config.mts | 8 +- 32 files changed, 1625 insertions(+), 8 deletions(-) create mode 100644 .prettierignore create mode 100644 server/apps/api/src/health.controller.ts create mode 100644 server/apps/api/src/health.service.ts create mode 100644 server/apps/api/test/health.e2e-spec.ts create mode 100644 server/apps/api/test/openapi.test.ts create mode 100644 server/libs/http-contracts/src/health.dto.ts create mode 100644 server/libs/infrastructure/src/disabled-health.client.ts create mode 100644 server/libs/infrastructure/src/disabled-message-bus.adapter.test.ts create mode 100644 server/libs/infrastructure/src/disabled-message-bus.adapter.ts create mode 100644 server/libs/infrastructure/src/health-clients.test.ts create mode 100644 server/libs/infrastructure/src/message-bus.adapter.ts create mode 100644 server/libs/infrastructure/src/mysql-health.client.ts create mode 100644 server/libs/infrastructure/src/redis-health.client.ts create mode 100644 server/libs/infrastructure/src/typeorm-options.ts create mode 100644 server/libs/message-contracts/src/message-envelope.test.ts create mode 100644 server/libs/message-contracts/src/message-envelope.ts create mode 100644 server/openapi/openapi.json create mode 100644 server/tools/export-openapi.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c76f96a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# 自动生成或不可变发布产物 +AGENTS.md +pnpm-lock.yaml +database/schema-baselines/ + +# 历史功能档案与人类维护文档不由代码格式器改写,追溯内容保持原样。 +docs/ + +# 已有治理脚本保留原历史格式;其行为由专项测试门禁覆盖。 +tools/database/ +tools/governance/ diff --git a/AGENTS.md b/AGENTS.md index f56e2d5..7eccdd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **a-share-analysis** (1220 symbols, 1576 relationships, 36 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **a-share-analysis** (1372 symbols, 1907 relationships, 42 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/deploy/compose/development.yml b/deploy/compose/development.yml index 76bd07c..32fac6c 100644 --- a/deploy/compose/development.yml +++ b/deploy/compose/development.yml @@ -4,13 +4,13 @@ services: redis: image: redis:7.2.5-alpine container_name: a-share-analysis-dev-redis - command: ["redis-server", "--appendonly", "yes"] + command: ['redis-server', '--appendonly', 'yes'] ports: - - "127.0.0.1:6379:6379" + - '127.0.0.1:6379:6379' volumes: - ../../.local/redis:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 5s timeout: 3s retries: 10 diff --git a/deploy/env/development/api.env.example b/deploy/env/development/api.env.example index fc391e7..9ac7ff0 100644 --- a/deploy/env/development/api.env.example +++ b/deploy/env/development/api.env.example @@ -22,6 +22,7 @@ MYSQL_USERNAME= MYSQL_PASSWORD= MYSQL_CHARSET=utf8mb4 MYSQL_COLLATION=utf8mb4_general_ci +MYSQL_TIMEZONE=+08:00 # 第一阶段关闭付费 AI;启用后 BASE_URL、API_KEY 和 MODEL 变为必填。 AI_ENABLED=false diff --git a/deploy/env/development/worker.env.example b/deploy/env/development/worker.env.example index 80a1660..140e13b 100644 --- a/deploy/env/development/worker.env.example +++ b/deploy/env/development/worker.env.example @@ -21,6 +21,7 @@ MYSQL_USERNAME= MYSQL_PASSWORD= MYSQL_CHARSET=utf8mb4 MYSQL_COLLATION=utf8mb4_general_ci +MYSQL_TIMEZONE=+08:00 ROCKETMQ_ENABLED=false ROCKETMQ_NAMESRV_ADDR=127.0.0.1:9876 diff --git a/deploy/env/production/api.env.example b/deploy/env/production/api.env.example index a1b7c72..d9154b5 100644 --- a/deploy/env/production/api.env.example +++ b/deploy/env/production/api.env.example @@ -20,6 +20,7 @@ MYSQL_USERNAME= MYSQL_PASSWORD= MYSQL_CHARSET=utf8mb4 MYSQL_COLLATION=utf8mb4_general_ci +MYSQL_TIMEZONE=+08:00 AI_ENABLED=false AI_BASE_URL= diff --git a/deploy/env/production/worker.env.example b/deploy/env/production/worker.env.example index 008102a..3d6e21d 100644 --- a/deploy/env/production/worker.env.example +++ b/deploy/env/production/worker.env.example @@ -21,6 +21,7 @@ MYSQL_USERNAME= MYSQL_PASSWORD= MYSQL_CHARSET=utf8mb4 MYSQL_COLLATION=utf8mb4_general_ci +MYSQL_TIMEZONE=+08:00 ROCKETMQ_ENABLED=false ROCKETMQ_NAMESRV_ADDR= diff --git a/eslint.config.mjs b/eslint.config.mjs index 92844f5..615ab24 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,4 +25,10 @@ export default [ 'vue/multi-word-component-names': 'off', }, }, + { + files: ['**/*.{js,mjs,cjs,ts,tsx}'], + rules: { + 'vue/one-component-per-file': 'off', + }, + }, ]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b56f794..bd61f75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,15 +38,30 @@ importers: '@nestjs/platform-express': specifier: 11.1.29 version: 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29) + '@nestjs/swagger': + specifier: 11.4.6 + version: 11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.2.2) + '@nestjs/typeorm': + specifier: 11.0.3 + version: 11.0.3(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.11.1)(mysql2@3.23.3(@types/node@22.20.0))) dotenv: specifier: 16.6.1 version: 16.6.1 + ioredis: + specifier: 5.11.1 + version: 5.11.1 + mysql2: + specifier: 3.23.3 + version: 3.23.3(@types/node@22.20.0) reflect-metadata: specifier: 0.2.2 version: 0.2.2 rxjs: specifier: 7.8.2 version: 7.8.2 + typeorm: + specifier: 0.3.31 + version: 0.3.31(ioredis@5.11.1)(mysql2@3.23.3(@types/node@22.20.0)) devDependencies: '@nestjs/cli': specifier: 11.0.24 @@ -57,6 +72,15 @@ importers: '@types/node': specifier: 22.20.0 version: 22.20.0 + '@types/supertest': + specifier: 7.2.1 + version: 7.2.1 + prettier: + specifier: 3.9.6 + version: 3.9.6 + supertest: + specifier: 7.2.2 + version: 7.2.2 ts-loader: specifier: 9.6.2 version: 9.6.2(typescript@5.9.3)(webpack@5.106.2) @@ -584,6 +608,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -611,11 +638,15 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [linux] + libc: [glibc] '@nestjs/cli@11.0.24': resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==} @@ -661,6 +692,19 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/platform-express@11.1.29': resolution: {integrity: sha512-OngsAjdzM+SbbmP1XojKfBgl0Lj7u8b7IK5duwpFymzNP5EmjS8ujvqck9M8h0L14I91aRIFSjIpVfHj8WVtqw==} peerDependencies: @@ -676,6 +720,23 @@ packages: prettier: optional: true + '@nestjs/swagger@11.4.6': + resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@11.1.29': resolution: {integrity: sha512-u+aEp90Ly3uQbLoicZs+ZOrJTyvaD0fbd6dpnSgFZz5Bf77gbKKfr+xFE2JxqZuIH15HlBrqDL7/OL2qL8oG+A==} peerDependencies: @@ -689,6 +750,19 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/typeorm@11.0.3': + resolution: {integrity: sha512-zJ+E5l7auVVA7c0PsvcMdyvRPKTUqU5s2ToYmOA2QEsXQ42qbUGtK4+1HlRfpHqBkCSXP+phiH4luvf9DyJNog==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 || ^1.0.0-dev + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -704,6 +778,9 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@parcel/watcher-android-arm64@2.6.0': resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} engines: {node: '>= 10.0.0'} @@ -733,36 +810,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.6.0': resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.6.0': resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.6.0': resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.6.0': resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.6.0': resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.6.0': resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} @@ -823,66 +906,79 @@ packages: resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.4': resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.4': resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.4': resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.4': resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.4': resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.4': resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.4': resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.4': resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.4': resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.4': resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.4': resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.4': resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.62.4': resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} @@ -914,6 +1010,12 @@ packages: cpu: [x64] os: [win32] + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -924,6 +1026,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -945,9 +1050,18 @@ packages: '@types/lodash@4.17.25': resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} + + '@types/supertest@7.2.1': + resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} + '@typescript-eslint/eslint-plugin@8.46.2': resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1245,6 +1359,14 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -1254,6 +1376,9 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1261,6 +1386,17 @@ packages: async-validator@4.2.5: resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1314,6 +1450,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1330,6 +1469,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -1384,10 +1527,18 @@ 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'} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1395,6 +1546,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1414,6 +1569,9 @@ packages: resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1444,6 +1602,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -1495,6 +1656,9 @@ packages: date-fns@4.4.0: resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1507,6 +1671,14 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + 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 + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1521,6 +1693,18 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1529,6 +1713,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1594,6 +1781,10 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1762,6 +1953,10 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1773,6 +1968,14 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1801,6 +2004,13 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + 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'} @@ -1827,6 +2037,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + 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.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -1857,10 +2072,17 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1924,6 +2146,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1931,6 +2157,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1957,6 +2187,13 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -1965,6 +2202,9 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1997,6 +2237,10 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -2071,6 +2315,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -2081,6 +2328,10 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} + 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'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2114,6 +2365,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2134,6 +2389,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2173,12 +2433,22 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mysql2@3.23.3: + resolution: {integrity: sha512-ehp9HEKr4wVJaBOUVxNFa+CNrsCCCZ6363/jbGhb7WpEmSRNIXjHBjFs5K2s2cXn7j/RhDieejsQJ6nfUwD6vQ==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + naive-ui@2.44.1: resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==} engines: {node: '>=20'} peerDependencies: vue: ^3.0.0 + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2345,6 +2615,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-selector-parser@6.1.4: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} @@ -2400,9 +2674,21 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + 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'} @@ -2485,9 +2771,18 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2541,9 +2836,20 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + sql-escaper@1.5.1: + resolution: {integrity: sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2589,10 +2895,18 @@ packages: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2601,6 +2915,9 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + swagger-ui-dist@5.32.8: + resolution: {integrity: sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -2689,6 +3006,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2761,9 +3082,68 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeorm@0.3.31: + resolution: {integrity: sha512-6u9EFtdLBgHjnPm78NStVeM+I/1MolTzKykDDcydzKUkh6E++YS6XViU/fePJbvDvEGU4Xq34KOM/CLeer9I2A==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 + ioredis: ^5.0.4 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 || ^7.0.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 || ^5.0.14 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 || ^6.0.0 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + typescript-eslint@8.46.2: resolution: {integrity: sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2807,6 +3187,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2976,6 +3360,10 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3028,10 +3416,18 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3438,6 +3834,8 @@ snapshots: optionalDependencies: '@types/node': 22.20.0 + '@ioredis/commands@1.10.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3470,6 +3868,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true @@ -3534,6 +3934,11 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29) + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/platform-express@11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)': dependencies: '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3559,6 +3964,18 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 5.2.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.8 + '@nestjs/testing@11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-express@11.1.29)': dependencies: '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3567,6 +3984,16 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29) + '@nestjs/typeorm@11.0.3(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.11.1)(mysql2@3.23.3(@types/node@22.20.0)))': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.31(ioredis@5.11.1)(mysql2@3.23.3(@types/node@22.20.0)) + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3581,6 +4008,10 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -3720,6 +4151,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.4': optional: true + '@scarf/scarf@1.4.0': {} + + '@sqltools/formatter@1.2.5': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -3734,6 +4169,8 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/cookiejar@2.1.5': {} + '@types/deep-eql@4.0.2': {} '@types/eslint-scope@3.7.7': @@ -3756,10 +4193,24 @@ snapshots: '@types/lodash@4.17.25': {} + '@types/methods@1.1.4': {} + '@types/node@22.20.0': dependencies: undici-types: 6.21.0 + '@types/superagent@8.1.11': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.20.0 + form-data: 4.0.6 + + '@types/supertest@7.2.1': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.11 + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4158,16 +4609,30 @@ snapshots: ansis@4.2.0: {} + ansis@4.3.1: {} + + app-root-path@3.1.0: {} + append-field@1.0.0: {} argparse@2.0.1: {} array-timsort@1.0.3: {} + asap@2.0.6: {} + assertion-error@2.0.1: {} async-validator@4.2.5: {} + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -4232,6 +4697,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4245,6 +4715,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4295,14 +4772,26 @@ 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: {} + cluster-key-slot@1.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} commander@2.20.3: {} @@ -4316,6 +4805,8 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 + component-emitter@1.3.1: {} + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -4340,6 +4831,8 @@ snapshots: cookie@0.7.2: {} + cookiejar@2.1.4: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -4391,12 +4884,16 @@ snapshots: date-fns@4.4.0: {} + dayjs@1.11.21: {} + debug@4.4.3: dependencies: ms: 2.1.3 decimal.js@10.6.0: {} + dedent@1.7.2: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -4407,11 +4904,26 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.1.2: optional: true + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -4464,6 +4976,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -4712,6 +5231,10 @@ snapshots: flatted@3.4.4: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4734,6 +5257,20 @@ snapshots: typescript: 5.9.3 webpack: 5.106.2 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -4754,6 +5291,12 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4795,6 +5338,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.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.6: dependencies: minimatch: 10.2.6 @@ -4817,8 +5369,16 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -4880,10 +5440,24 @@ snapshots: ini@1.3.8: {} + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} + is-callable@1.2.7: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4900,10 +5474,18 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + is-unicode-supported@0.1.0: {} is-what@5.5.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} iterare@1.2.1: {} @@ -4938,6 +5520,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0: dependencies: cssstyle: 4.6.0 @@ -5019,12 +5605,16 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} lru-cache@11.5.2: {} + lru.min@1.1.4: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5049,6 +5639,8 @@ snapshots: merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5066,6 +5658,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@2.6.0: {} + mimic-fn@2.1.0: {} minimatch@10.2.6: @@ -5099,6 +5693,17 @@ snapshots: mute-stream@2.0.0: {} + mysql2@3.23.3(@types/node@22.20.0): + dependencies: + '@types/node': 22.20.0 + aws-ssl-profiles: 1.1.2 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.5.1 + naive-ui@2.44.1(vue@3.5.41(typescript@5.9.3)): dependencies: '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) @@ -5121,6 +5726,10 @@ snapshots: vue: 3.5.41(typescript@5.9.3) vueuc: 0.4.65(vue@3.5.41(typescript@5.9.3)) + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -5265,6 +5874,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + postcss-selector-parser@6.1.4: dependencies: cssesc: 3.0.0 @@ -5315,8 +5926,16 @@ snapshots: readdirp@5.1.1: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -5446,8 +6065,23 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5501,8 +6135,14 @@ snapshots: speakingurl@14.0.1: {} + sql-escaper@1.5.1: {} + + sql-highlight@6.1.0: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -5545,10 +6185,32 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 4.0.6 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.3 + transitivePeerDependencies: + - supports-color + superjson@2.2.6: dependencies: copy-anything: 4.0.5 + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5557,6 +6219,10 @@ snapshots: dependencies: has-flag: 4.0.0 + swagger-ui-dist@5.32.8: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} @@ -5599,6 +6265,12 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5672,8 +6344,38 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typedarray@0.0.6: {} + typeorm@0.3.31(ioredis@5.11.1)(mysql2@3.23.3(@types/node@22.20.0)): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 4.3.1 + app-root-path: 3.1.0 + buffer: 6.0.3 + dayjs: 1.11.21 + debug: 4.4.3 + dedent: 1.7.2 + dotenv: 16.6.1 + glob: 10.5.0 + reflect-metadata: 0.2.2 + sha.js: 2.4.12 + sql-highlight: 6.1.0 + tslib: 2.8.1 + uuid: 11.1.1 + yargs: 17.7.3 + optionalDependencies: + ioredis: 5.11.1 + mysql2: 3.23.3(@types/node@22.20.0) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + typescript-eslint@8.46.2(eslint@9.39.5)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) @@ -5711,6 +6413,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.1: {} + vary@1.1.2: {} vdirs@0.1.8(vue@3.5.41(typescript@5.9.3)): @@ -5919,6 +6623,16 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5958,8 +6672,20 @@ snapshots: xmlchars@2.2.0: {} + y18n@5.0.8: {} + yargs-parser@21.1.1: {} + yargs@17.7.3: + 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 + yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.3: {} diff --git a/server/apps/api/src/api.module.ts b/server/apps/api/src/api.module.ts index 30ade51..1e12d5e 100644 --- a/server/apps/api/src/api.module.ts +++ b/server/apps/api/src/api.module.ts @@ -1,4 +1,29 @@ import { Module } from '@nestjs/common'; -@Module({}) +import { loadApiProcessConfig } from '../../../libs/config/src/load-process-env'; +import { DisabledHealthClient } from '../../../libs/infrastructure/src/disabled-health.client'; +import { createMysqlHealthClient } from '../../../libs/infrastructure/src/mysql-health.client'; +import { createRedisHealthClient } from '../../../libs/infrastructure/src/redis-health.client'; +import { HealthController } from './health.controller'; +import { HealthService, MYSQL_HEALTH_PORT, REDIS_HEALTH_PORT } from './health.service'; + +const apiConfig = loadApiProcessConfig(); + +@Module({ + controllers: [HealthController], + providers: [ + HealthService, + { + provide: REDIS_HEALTH_PORT, + useFactory: () => createRedisHealthClient(apiConfig.redis), + }, + { + provide: MYSQL_HEALTH_PORT, + useFactory: () => + apiConfig.mysql.enabled + ? createMysqlHealthClient(apiConfig.mysql) + : new DisabledHealthClient('MySQL 未启用'), + }, + ], +}) export class ApiModule {} diff --git a/server/apps/api/src/health.controller.ts b/server/apps/api/src/health.controller.ts new file mode 100644 index 0000000..24d8c77 --- /dev/null +++ b/server/apps/api/src/health.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, HttpException, HttpStatus, Inject } from '@nestjs/common'; +import { ApiOkResponse, ApiServiceUnavailableResponse, ApiTags } from '@nestjs/swagger'; + +import { HealthResponseDto } from '../../../libs/http-contracts/src/health.dto'; +import { HealthService } from './health.service'; + +@ApiTags('health') +@Controller('health') +export class HealthController { + constructor(@Inject(HealthService) private readonly healthService: HealthService) {} + + @Get() + @ApiOkResponse({ type: HealthResponseDto }) + @ApiServiceUnavailableResponse({ type: HealthResponseDto }) + async getHealth(): Promise { + const response = await this.healthService.check(); + if (response.status === 'unavailable') { + throw new HttpException(response, HttpStatus.SERVICE_UNAVAILABLE); + } + return response; + } +} diff --git a/server/apps/api/src/health.service.ts b/server/apps/api/src/health.service.ts new file mode 100644 index 0000000..0b91b24 --- /dev/null +++ b/server/apps/api/src/health.service.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable } from '@nestjs/common'; + +import type { + DependencyHealthDto, + HealthResponseDto, +} from '../../../libs/http-contracts/src/health.dto'; + +export const REDIS_HEALTH_PORT = Symbol('REDIS_HEALTH_PORT'); +export const MYSQL_HEALTH_PORT = Symbol('MYSQL_HEALTH_PORT'); + +export interface DependencyHealthPort { + check(): Promise; +} + +@Injectable() +export class HealthService { + constructor( + @Inject(REDIS_HEALTH_PORT) private readonly redis: DependencyHealthPort, + @Inject(MYSQL_HEALTH_PORT) private readonly mysql: DependencyHealthPort, + ) {} + + async check(): Promise { + const [redis, mysql] = await Promise.all([ + this.checkSafely(this.redis), + this.checkSafely(this.mysql), + ]); + const status = + redis.status === 'healthy' && mysql.status !== 'unavailable' ? 'healthy' : 'unavailable'; + + return { + status, + process: 'api', + checkedAt: new Date().toISOString(), + dependencies: { + redis, + mysql, + rocketMq: { status: 'disabled', detail: 'GOV-009 尚未启用 RocketMQ 客户端' }, + }, + }; + } + + private async checkSafely(port: DependencyHealthPort): Promise { + try { + const result = await port.check(); + if (result.status === 'healthy' || result.status === 'disabled') return result; + } catch { + // 外部错误可能包含 DSN、用户名或密码;HTTP 健康响应只暴露固定安全文案。 + } + return { status: 'unavailable', detail: '依赖检查失败' }; + } +} diff --git a/server/apps/api/src/main.ts b/server/apps/api/src/main.ts index ddb664b..27ffaf7 100644 --- a/server/apps/api/src/main.ts +++ b/server/apps/api/src/main.ts @@ -18,6 +18,7 @@ interface BootstrapApiOptions { export async function bootstrapApi(options: BootstrapApiOptions): Promise { const app = await NestFactory.create(ApiModule, { logger: options.logger }); + app.setGlobalPrefix('api/v1'); app.enableShutdownHooks(); await app.listen(options.port ?? options.config.api.port, options.config.api.host); return app; diff --git a/server/apps/api/test/health.e2e-spec.ts b/server/apps/api/test/health.e2e-spec.ts new file mode 100644 index 0000000..3dfae6d --- /dev/null +++ b/server/apps/api/test/health.e2e-spec.ts @@ -0,0 +1,85 @@ +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { HealthController } from '../src/health.controller'; +import { + HealthService, + MYSQL_HEALTH_PORT, + REDIS_HEALTH_PORT, + type DependencyHealthPort, +} from '../src/health.service'; + +describe('GET /api/v1/health', () => { + let app: INestApplication | undefined; + + afterEach(async () => { + await app?.close(); + app = undefined; + }); + + async function createApp( + redis: DependencyHealthPort, + mysql: DependencyHealthPort, + ): Promise { + const moduleRef = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + HealthService, + { provide: REDIS_HEALTH_PORT, useValue: redis }, + { provide: MYSQL_HEALTH_PORT, useValue: mysql }, + ], + }).compile(); + const nestApp = moduleRef.createNestApplication(); + nestApp.setGlobalPrefix('api/v1'); + await nestApp.init(); + return nestApp; + } + + it('依赖健康时返回 200 与 RocketMQ disabled', async () => { + app = await createApp( + { check: async () => ({ status: 'healthy', detail: 'PONG' }) }, + { check: async () => ({ status: 'healthy', detail: 'MySQL 5.7' }) }, + ); + + const response = await request(app.getHttpServer()).get('/api/v1/health').expect(200); + + expect(response.body).toMatchObject({ + status: 'healthy', + process: 'api', + dependencies: { + redis: { status: 'healthy' }, + mysql: { status: 'healthy' }, + rocketMq: { status: 'disabled' }, + }, + }); + }); + + it('依赖失败时返回 503 且不泄漏错误中的敏感值', async () => { + app = await createApp( + { check: async () => ({ status: 'unavailable', detail: 'TEST_SENSITIVE_VALUE' }) }, + { check: async () => ({ status: 'healthy', detail: 'MySQL 5.7' }) }, + ); + + const response = await request(app.getHttpServer()).get('/api/v1/health').expect(503); + const serialized = JSON.stringify(response.body); + + expect(response.body.status).toBe('unavailable'); + expect(serialized).not.toContain('TEST_SENSITIVE_VALUE'); + }); + + it('有意禁用的可选依赖不降低 API 总体状态', async () => { + app = await createApp( + { check: async () => ({ status: 'healthy', detail: 'PONG' }) }, + { check: async () => ({ status: 'disabled', detail: 'MySQL 未启用' }) }, + ); + + const response = await request(app.getHttpServer()).get('/api/v1/health').expect(200); + + expect(response.body).toMatchObject({ + status: 'healthy', + dependencies: { mysql: { status: 'disabled' } }, + }); + }); +}); diff --git a/server/apps/api/test/openapi.test.ts b/server/apps/api/test/openapi.test.ts new file mode 100644 index 0000000..f89fafe --- /dev/null +++ b/server/apps/api/test/openapi.test.ts @@ -0,0 +1,29 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { createOpenApiDocument } from '../../../tools/export-openapi'; + +describe('Server OpenAPI 单向发布', () => { + it('从真实 Controller 和 DTO 生成健康接口契约', async () => { + const document = await createOpenApiDocument(); + + expect(document.paths).toHaveProperty('/api/v1/health'); + expect(document.components?.schemas).toHaveProperty('HealthResponseDto'); + }); + + it('仓库内 OpenAPI 文件是可解析的同一契约', async () => { + const content = await readFile( + path.resolve(__dirname, '../../../openapi/openapi.json'), + 'utf8', + ); + const document = JSON.parse(content) as { + paths: Record; + components?: { schemas?: Record }; + }; + + expect(document.paths).toHaveProperty('/api/v1/health'); + expect(document.components?.schemas).toHaveProperty('HealthResponseDto'); + }); +}); diff --git a/server/libs/config/src/runtime-config.test.ts b/server/libs/config/src/runtime-config.test.ts index e83fc99..0099fb0 100644 --- a/server/libs/config/src/runtime-config.test.ts +++ b/server/libs/config/src/runtime-config.test.ts @@ -157,4 +157,19 @@ describe('进程级运行配置', () => { expect(serialized).not.toContain('password'); expect(serialized).not.toContain('apiKey'); }); + + it('RocketMQ 在未落地已验证 Node 客户端前禁止启用', () => { + expect(() => + loadSchedulerConfig({ + APP_ENV: 'development', + LOG_LEVEL: 'debug', + SCHEDULER_TIMEZONE: 'Asia/Shanghai', + SCHEDULER_ENABLED: 'true', + ...baseRedisEnv, + ...disabledRocketMqEnv, + ROCKETMQ_ENABLED: 'true', + ROCKETMQ_SEND_TIMEOUT_MS: '3000', + }), + ).toThrow(/RocketMQ Node 客户端/); + }); }); diff --git a/server/libs/config/src/runtime-config.ts b/server/libs/config/src/runtime-config.ts index 383a333..478bf57 100644 --- a/server/libs/config/src/runtime-config.ts +++ b/server/libs/config/src/runtime-config.ts @@ -191,7 +191,8 @@ function loadRocketMqConfig(env: EnvironmentSource): RocketMqRuntimeConfig { const nameServerAddress = optionalText(env, 'ROCKETMQ_NAMESRV_ADDR'); const endpoint = optionalText(env, 'ROCKETMQ_ENDPOINT'); - // GOV-009 尚不选择 Node RocketMQ 客户端;启用配置只在后续 Adapter 落地时允许启动。 + // GOV-009 尚未选定通过真实链路验证的 Node 客户端,禁止以内存队列或空实现伪装启用。 + if (enabled) throw new Error('RocketMQ Node 客户端尚未落地验证,当前禁止启用'); if (enabled && !nameServerAddress && !endpoint) { throw new Error('ROCKETMQ_NAMESRV_ADDR 或 ROCKETMQ_ENDPOINT 至少填写一个'); } diff --git a/server/libs/http-contracts/src/health.dto.ts b/server/libs/http-contracts/src/health.dto.ts new file mode 100644 index 0000000..1a4f339 --- /dev/null +++ b/server/libs/http-contracts/src/health.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export type HealthStatus = 'healthy' | 'unavailable'; +export type DependencyStatus = HealthStatus | 'disabled'; + +export class DependencyHealthDto { + @ApiProperty({ type: String, enum: ['healthy', 'unavailable', 'disabled'] }) + status!: DependencyStatus; + + @ApiProperty({ type: String }) + detail!: string; +} + +export class HealthDependenciesDto { + @ApiProperty({ type: () => DependencyHealthDto }) + redis!: DependencyHealthDto; + + @ApiProperty({ type: () => DependencyHealthDto }) + mysql!: DependencyHealthDto; + + @ApiProperty({ type: () => DependencyHealthDto }) + rocketMq!: DependencyHealthDto; +} + +export class HealthResponseDto { + @ApiProperty({ type: String, enum: ['healthy', 'unavailable'] }) + status!: HealthStatus; + + @ApiProperty({ type: String, enum: ['api'] }) + process!: 'api'; + + @ApiProperty({ type: String, format: 'date-time' }) + checkedAt!: string; + + @ApiProperty({ type: () => HealthDependenciesDto }) + dependencies!: HealthDependenciesDto; +} diff --git a/server/libs/infrastructure/src/disabled-health.client.ts b/server/libs/infrastructure/src/disabled-health.client.ts new file mode 100644 index 0000000..28eb234 --- /dev/null +++ b/server/libs/infrastructure/src/disabled-health.client.ts @@ -0,0 +1,17 @@ +import type { DependencyHealthDto } from '../../http-contracts/src/health.dto'; + +export class DisabledHealthClient { + constructor(private readonly detail: string) {} + + async check(): Promise { + return { status: 'disabled', detail: this.detail }; + } + + async close(): Promise { + return Promise.resolve(); + } + + async onModuleDestroy(): Promise { + await this.close(); + } +} diff --git a/server/libs/infrastructure/src/disabled-message-bus.adapter.test.ts b/server/libs/infrastructure/src/disabled-message-bus.adapter.test.ts new file mode 100644 index 0000000..9b34a13 --- /dev/null +++ b/server/libs/infrastructure/src/disabled-message-bus.adapter.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { DisabledMessageBusAdapter, MessageBusDisabledError } from './disabled-message-bus.adapter'; + +describe('禁用消息总线 Adapter', () => { + it('拒绝发布和订阅且不提供内存队列降级', async () => { + const adapter = new DisabledMessageBusAdapter(); + + await expect( + adapter.publish({ + version: 1, + eventType: 'market.snapshot.updated', + idempotencyKey: 'snapshot:1', + occurredAt: '2026-08-13T01:30:00.000Z', + payload: { market: 'A_SHARE' }, + }), + ).rejects.toBeInstanceOf(MessageBusDisabledError); + await expect( + adapter.subscribe('market.snapshot.updated', async () => undefined), + ).rejects.toBeInstanceOf(MessageBusDisabledError); + await expect(adapter.close()).resolves.toBeUndefined(); + }); +}); diff --git a/server/libs/infrastructure/src/disabled-message-bus.adapter.ts b/server/libs/infrastructure/src/disabled-message-bus.adapter.ts new file mode 100644 index 0000000..5f9f2d9 --- /dev/null +++ b/server/libs/infrastructure/src/disabled-message-bus.adapter.ts @@ -0,0 +1,26 @@ +import type { MessageEnvelope } from '../../message-contracts/src/message-envelope'; +import type { MessageBusAdapter, MessageHandler } from './message-bus.adapter'; + +export class MessageBusDisabledError extends Error { + constructor(operation: 'publish' | 'subscribe') { + super(`RocketMQ 当前未启用,拒绝执行 ${operation}`); + this.name = 'MessageBusDisabledError'; + } +} + +export class DisabledMessageBusAdapter implements MessageBusAdapter { + async publish(message: Readonly>): Promise { + void message; + throw new MessageBusDisabledError('publish'); + } + + async subscribe(eventType: string, handler: MessageHandler): Promise { + void eventType; + void handler; + throw new MessageBusDisabledError('subscribe'); + } + + async close(): Promise { + return Promise.resolve(); + } +} diff --git a/server/libs/infrastructure/src/health-clients.test.ts b/server/libs/infrastructure/src/health-clients.test.ts new file mode 100644 index 0000000..1b57508 --- /dev/null +++ b/server/libs/infrastructure/src/health-clients.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from 'vitest'; + +import { MysqlHealthClient, MYSQL_HEALTH_QUERY } from './mysql-health.client'; +import { RedisHealthClient } from './redis-health.client'; +import { buildTypeOrmOptions } from './typeorm-options'; + +describe('基础设施只读健康客户端', () => { + it('Redis 仅执行 PING 并返回健康状态', async () => { + const commands: string[] = []; + const client = new RedisHealthClient({ + status: 'wait', + connect: async () => { + commands.push('CONNECT'); + }, + ping: async () => { + commands.push('PING'); + return 'PONG'; + }, + quit: async () => 'OK', + }); + + await expect(client.check()).resolves.toEqual({ status: 'healthy', detail: 'PONG' }); + expect(commands).toEqual(['CONNECT', 'PING']); + }); + + it('MySQL 使用固定元数据查询并校验 5.7 与数据库口径', async () => { + const queries: string[] = []; + const client = new MysqlHealthClient( + { + query: async (sql: string) => { + queries.push(sql); + return [ + [ + { + version: '5.7.42-log', + databaseName: 'a_share_analysis_dev', + charset: 'utf8mb4', + collation: 'utf8mb4_general_ci', + timezone: '+08:00', + }, + ], + [], + ]; + }, + end: async () => undefined, + }, + 'a_share_analysis_dev', + ); + + await expect(client.check()).resolves.toEqual({ + status: 'healthy', + detail: 'MySQL 5.7 / a_share_analysis_dev / +08:00', + }); + expect(queries).toEqual([MYSQL_HEALTH_QUERY]); + }); + + it('MySQL 拒绝版本、库名或连接口径漂移', async () => { + const createClient = (override: Record) => + new MysqlHealthClient( + { + query: async () => [ + [ + { + version: '5.7.42', + databaseName: 'a_share_analysis_dev', + charset: 'utf8mb4', + collation: 'utf8mb4_general_ci', + timezone: '+08:00', + ...override, + }, + ], + [], + ], + end: async () => undefined, + }, + 'a_share_analysis_dev', + ); + + await expect(createClient({ version: '8.0.40' }).check()).rejects.toThrow(/MySQL 5.7/); + await expect(createClient({ databaseName: 'a_share_analysis' }).check()).rejects.toThrow( + /a_share_analysis_dev/, + ); + await expect(createClient({ collation: 'utf8mb4_0900_ai_ci' }).check()).rejects.toThrow( + /utf8mb4_general_ci/, + ); + }); + + it('MySQL 按当前配置校验生产库名', async () => { + const client = new MysqlHealthClient( + { + query: async () => [ + [ + { + version: '5.7.42', + databaseName: 'a_share_analysis', + charset: 'utf8mb4', + collation: 'utf8mb4_general_ci', + timezone: '+08:00', + }, + ], + [], + ], + end: async () => undefined, + }, + 'a_share_analysis', + ); + + await expect(client.check()).resolves.toMatchObject({ status: 'healthy' }); + }); +}); + +describe('TypeORM MySQL 5.7 选项', () => { + it('禁止自动同步并固定字符集、排序规则和东八区', () => { + const options = buildTypeOrmOptions({ + enabled: true, + host: '127.0.0.1', + port: 3306, + database: 'a_share_analysis_dev', + username: 'dev-user', + password: 'TEST_SENSITIVE_VALUE', + charset: 'utf8mb4', + collation: 'utf8mb4_general_ci', + timezone: '+08:00', + }); + + expect(options).toMatchObject({ + type: 'mysql', + charset: 'utf8mb4', + timezone: '+08:00', + synchronize: false, + migrationsRun: false, + }); + expect(options.extra).toMatchObject({ collation: 'utf8mb4_general_ci' }); + }); +}); diff --git a/server/libs/infrastructure/src/message-bus.adapter.ts b/server/libs/infrastructure/src/message-bus.adapter.ts new file mode 100644 index 0000000..db39b24 --- /dev/null +++ b/server/libs/infrastructure/src/message-bus.adapter.ts @@ -0,0 +1,11 @@ +import type { MessageEnvelope } from '../../message-contracts/src/message-envelope'; + +export type MessageHandler = ( + message: Readonly>, +) => Promise; + +export interface MessageBusAdapter { + publish(message: Readonly>): Promise; + subscribe(eventType: string, handler: MessageHandler): Promise; + close(): Promise; +} diff --git a/server/libs/infrastructure/src/mysql-health.client.ts b/server/libs/infrastructure/src/mysql-health.client.ts new file mode 100644 index 0000000..d008304 --- /dev/null +++ b/server/libs/infrastructure/src/mysql-health.client.ts @@ -0,0 +1,75 @@ +import { createPool, type Pool } from 'mysql2/promise'; +import type { OnModuleDestroy } from '@nestjs/common'; + +import type { MysqlRuntimeConfig } from '../../config/src/runtime-config'; +import type { DependencyHealthDto } from '../../http-contracts/src/health.dto'; + +export const MYSQL_HEALTH_QUERY = `SELECT + VERSION() AS version, + DATABASE() AS databaseName, + @@character_set_database AS charset, + @@collation_database AS collation, + @@session.time_zone AS timezone`; + +interface MysqlQueryPort { + query(sql: string): Promise<[unknown, unknown]>; + end(): Promise; +} + +interface MysqlMetadataRow { + version: string; + databaseName: string; + charset: string; + collation: string; + timezone: string; +} + +export class MysqlHealthClient implements OnModuleDestroy { + constructor( + private readonly client: MysqlQueryPort, + private readonly expectedDatabase: string, + ) {} + + async check(): Promise { + const [rows] = await this.client.query(MYSQL_HEALTH_QUERY); + const row = (rows as unknown as MysqlMetadataRow[])[0]; + if (!row) throw new Error('MySQL 未返回元数据'); + if (!row.version.startsWith('5.7.')) throw new Error('MySQL 5.7.x 版本要求未满足'); + if (row.databaseName !== this.expectedDatabase) { + throw new Error(`MySQL 健康检查必须连接 ${this.expectedDatabase}`); + } + if (row.charset !== 'utf8mb4') throw new Error('MySQL 字符集必须为 utf8mb4'); + if (row.collation !== 'utf8mb4_general_ci') { + throw new Error('MySQL 排序规则必须为 utf8mb4_general_ci'); + } + if (row.timezone !== '+08:00') throw new Error('MySQL 会话时区必须为 +08:00'); + + return { + status: 'healthy', + detail: `MySQL 5.7 / ${row.databaseName} / ${row.timezone}`, + }; + } + + async close(): Promise { + await this.client.end(); + } + + async onModuleDestroy(): Promise { + await this.close(); + } +} + +export function createMysqlHealthClient(config: MysqlRuntimeConfig): MysqlHealthClient { + const pool: Pool = createPool({ + host: config.host, + port: config.port, + database: config.database, + user: config.username, + password: config.password, + charset: config.charset, + timezone: config.timezone, + connectionLimit: 2, + enableKeepAlive: false, + }); + return new MysqlHealthClient(pool, config.database); +} diff --git a/server/libs/infrastructure/src/redis-health.client.ts b/server/libs/infrastructure/src/redis-health.client.ts new file mode 100644 index 0000000..9f78bbe --- /dev/null +++ b/server/libs/infrastructure/src/redis-health.client.ts @@ -0,0 +1,58 @@ +import Redis from 'ioredis'; +import type { OnModuleDestroy } from '@nestjs/common'; + +import type { DependencyHealthDto } from '../../http-contracts/src/health.dto'; +import type { RedisRuntimeConfig } from '../../config/src/runtime-config'; + +interface RedisPingPort { + status: string; + connect(): Promise; + ping(): Promise; + quit(): Promise; +} + +export class RedisHealthClient implements OnModuleDestroy { + constructor(private readonly client: RedisPingPort) {} + + async check(): Promise { + if ( + this.client.status === 'wait' || + this.client.status === 'end' || + this.client.status === 'close' + ) { + await this.client.connect(); + } + const response = await this.client.ping(); + if (response !== 'PONG') throw new Error('Redis PING 未返回 PONG'); + return { status: 'healthy', detail: 'PONG' }; + } + + async close(): Promise { + if ( + this.client.status !== 'wait' && + this.client.status !== 'end' && + this.client.status !== 'close' + ) { + await this.client.quit(); + } + } + + async onModuleDestroy(): Promise { + await this.close(); + } +} + +export function createRedisHealthClient(config: RedisRuntimeConfig): RedisHealthClient { + const client = new Redis({ + host: config.host, + port: config.port, + username: config.username || undefined, + password: config.password || undefined, + db: config.database, + tls: config.tls ? {} : undefined, + lazyConnect: true, + enableOfflineQueue: false, + maxRetriesPerRequest: 0, + }); + return new RedisHealthClient(client); +} diff --git a/server/libs/infrastructure/src/typeorm-options.ts b/server/libs/infrastructure/src/typeorm-options.ts new file mode 100644 index 0000000..3870353 --- /dev/null +++ b/server/libs/infrastructure/src/typeorm-options.ts @@ -0,0 +1,23 @@ +import type { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'; + +import type { MysqlRuntimeConfig } from '../../config/src/runtime-config'; + +export function buildTypeOrmOptions(config: MysqlRuntimeConfig): MysqlConnectionOptions { + return { + type: 'mysql', + host: config.host, + port: config.port, + database: config.database, + username: config.username, + password: config.password, + charset: config.charset, + timezone: config.timezone, + synchronize: false, + migrationsRun: false, + entities: [], + migrations: [], + extra: { + collation: config.collation, + }, + }; +} diff --git a/server/libs/message-contracts/src/message-envelope.test.ts b/server/libs/message-contracts/src/message-envelope.test.ts new file mode 100644 index 0000000..938f333 --- /dev/null +++ b/server/libs/message-contracts/src/message-envelope.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { createMessageEnvelope } from './message-envelope'; + +describe('RocketMQ 消息信封', () => { + it('规范化并冻结版本 1 消息', () => { + const envelope = createMessageEnvelope({ + version: 1, + eventType: 'market.snapshot.updated', + idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00', + occurredAt: '2026-08-13T01:30:00.000Z', + payload: { market: 'A_SHARE' }, + }); + + expect(envelope).toEqual({ + version: 1, + eventType: 'market.snapshot.updated', + idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00', + occurredAt: '2026-08-13T01:30:00.000Z', + payload: { market: 'A_SHARE' }, + }); + expect(Object.isFrozen(envelope)).toBe(true); + expect(Object.isFrozen(envelope.payload)).toBe(true); + }); + + it.each([ + [{ version: 2 }, /version/], + [{ eventType: ' ' }, /eventType/], + [{ idempotencyKey: '' }, /idempotencyKey/], + [{ occurredAt: 'not-a-date' }, /occurredAt/], + ])('拒绝非法信封字段 %#', (override, expectedError) => { + expect(() => + createMessageEnvelope({ + version: 1, + eventType: 'market.snapshot.updated', + idempotencyKey: 'snapshot:1', + occurredAt: '2026-08-13T01:30:00.000Z', + payload: { market: 'A_SHARE' }, + ...override, + }), + ).toThrow(expectedError); + }); +}); diff --git a/server/libs/message-contracts/src/message-envelope.ts b/server/libs/message-contracts/src/message-envelope.ts new file mode 100644 index 0000000..a65a159 --- /dev/null +++ b/server/libs/message-contracts/src/message-envelope.ts @@ -0,0 +1,39 @@ +export interface MessageEnvelope { + version: 1; + eventType: string; + idempotencyKey: string; + occurredAt: string; + payload: TPayload; +} + +type MessageEnvelopeInput = Omit, 'version'> & { + version: number; +}; + +function deepFreeze(value: TValue): Readonly { + if (value && typeof value === 'object' && !Object.isFrozen(value)) { + for (const child of Object.values(value as Record)) deepFreeze(child); + Object.freeze(value); + } + return value; +} + +export function createMessageEnvelope( + input: MessageEnvelopeInput, +): Readonly> { + if (input.version !== 1) throw new Error('version 只支持 1'); + if (!input.eventType.trim()) throw new Error('eventType 不能为空'); + if (!input.idempotencyKey.trim()) throw new Error('idempotencyKey 不能为空'); + if (!input.occurredAt.trim() || Number.isNaN(Date.parse(input.occurredAt))) { + throw new Error('occurredAt 必须为合法 ISO 时间'); + } + if (input.payload === undefined) throw new Error('payload 不能为空'); + + return deepFreeze({ + version: 1, + eventType: input.eventType.trim(), + idempotencyKey: input.idempotencyKey.trim(), + occurredAt: new Date(input.occurredAt).toISOString(), + payload: input.payload, + }) as Readonly>; +} diff --git a/server/openapi/openapi.json b/server/openapi/openapi.json new file mode 100644 index 0000000..222ff7c --- /dev/null +++ b/server/openapi/openapi.json @@ -0,0 +1,74 @@ +{ + "components": { + "schemas": { + "DependencyHealthDto": { + "properties": { + "detail": { "type": "string" }, + "status": { + "enum": ["healthy", "unavailable", "disabled"], + "type": "string" + } + }, + "required": ["status", "detail"], + "type": "object" + }, + "HealthDependenciesDto": { + "properties": { + "mysql": { "$ref": "#/components/schemas/DependencyHealthDto" }, + "redis": { "$ref": "#/components/schemas/DependencyHealthDto" }, + "rocketMq": { "$ref": "#/components/schemas/DependencyHealthDto" } + }, + "required": ["redis", "mysql", "rocketMq"], + "type": "object" + }, + "HealthResponseDto": { + "properties": { + "checkedAt": { "format": "date-time", "type": "string" }, + "dependencies": { + "$ref": "#/components/schemas/HealthDependenciesDto" + }, + "process": { "enum": ["api"], "type": "string" }, + "status": { "enum": ["healthy", "unavailable"], "type": "string" } + }, + "required": ["status", "process", "checkedAt", "dependencies"], + "type": "object" + } + } + }, + "info": { + "contact": {}, + "description": "", + "title": "A 股市场分析工具 API", + "version": "0.0.8-draft" + }, + "openapi": "3.0.0", + "paths": { + "/api/v1/health": { + "get": { + "operationId": "HealthController_getHealth", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HealthResponseDto" } + } + }, + "description": "" + }, + "503": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HealthResponseDto" } + } + }, + "description": "" + } + }, + "tags": ["health"] + } + } + }, + "servers": [], + "tags": [] +} diff --git a/server/package.json b/server/package.json index 1d3b98a..1d50c7c 100644 --- a/server/package.json +++ b/server/package.json @@ -6,6 +6,7 @@ "dev:api": "tsx apps/api/src/main.ts", "dev:scheduler": "tsx apps/scheduler/src/main.ts", "dev:worker": "tsx apps/worker/src/main.ts", + "openapi:generate": "tsx tools/export-openapi.ts", "lint": "eslint .", "typecheck": "tsc --noEmit -p tsconfig.json", "test": "vitest run", @@ -16,14 +17,22 @@ "@nestjs/common": "11.1.29", "@nestjs/core": "11.1.29", "@nestjs/platform-express": "11.1.29", + "@nestjs/swagger": "11.4.6", + "@nestjs/typeorm": "11.0.3", "dotenv": "16.6.1", + "ioredis": "5.11.1", + "mysql2": "3.23.3", "reflect-metadata": "0.2.2", - "rxjs": "7.8.2" + "rxjs": "7.8.2", + "typeorm": "0.3.31" }, "devDependencies": { "@nestjs/cli": "11.0.24", "@nestjs/testing": "11.1.29", "@types/node": "22.20.0", + "@types/supertest": "7.2.1", + "prettier": "3.9.6", + "supertest": "7.2.2", "ts-loader": "9.6.2", "tsx": "4.20.6", "typescript": "5.9.3", diff --git a/server/tools/export-openapi.ts b/server/tools/export-openapi.ts new file mode 100644 index 0000000..d97be5e --- /dev/null +++ b/server/tools/export-openapi.ts @@ -0,0 +1,65 @@ +import 'reflect-metadata'; + +import { Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule, type OpenAPIObject } from '@nestjs/swagger'; +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { format } from 'prettier'; + +import { HealthController } from '../apps/api/src/health.controller'; +import { + HealthService, + MYSQL_HEALTH_PORT, + REDIS_HEALTH_PORT, +} from '../apps/api/src/health.service'; +import { DisabledHealthClient } from '../libs/infrastructure/src/disabled-health.client'; + +@Module({ + controllers: [HealthController], + providers: [ + HealthService, + { provide: REDIS_HEALTH_PORT, useValue: new DisabledHealthClient('文档生成不连接 Redis') }, + { provide: MYSQL_HEALTH_PORT, useValue: new DisabledHealthClient('文档生成不连接 MySQL') }, + ], +}) +class OpenApiModule {} + +function sortObject(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortObject); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries( + Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => [key, sortObject(child)]), + ); +} + +export async function createOpenApiDocument(): Promise { + const app = await NestFactory.create(OpenApiModule, { logger: false }); + app.setGlobalPrefix('api/v1'); + const options = new DocumentBuilder() + .setTitle('A 股市场分析工具 API') + .setVersion('0.0.8-draft') + .build(); + const document = SwaggerModule.createDocument(app, options); + await app.close(); + return sortObject(document) as OpenAPIObject; +} + +async function writeOpenApiDocument(): Promise { + const serverDirectory = path.resolve(__dirname, '..'); + const output = path.join(serverDirectory, 'openapi', 'openapi.json'); + const serialized = await format(JSON.stringify(await createOpenApiDocument()), { + parser: 'json', + }); + await mkdir(path.dirname(output), { recursive: true }); + await writeFile(output, serialized, 'utf8'); +} + +if (require.main === module) { + void writeOpenApiDocument().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/server/vitest.config.mts b/server/vitest.config.mts index 3e19c6d..4880976 100644 --- a/server/vitest.config.mts +++ b/server/vitest.config.mts @@ -3,6 +3,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', - include: ['apps/**/*.test.ts', 'apps/**/*.spec.ts', 'libs/**/*.test.ts', 'test/**/*.test.ts'], + include: [ + 'apps/**/*.test.ts', + 'apps/**/*.spec.ts', + 'apps/**/*.e2e-spec.ts', + 'libs/**/*.test.ts', + 'test/**/*.test.ts', + ], }, });