diff --git a/.github/workflows/ci-regression.yml b/.github/workflows/ci-regression.yml index 2d5773d7..1bd6603f 100644 --- a/.github/workflows/ci-regression.yml +++ b/.github/workflows/ci-regression.yml @@ -35,6 +35,7 @@ jobs: OCR_PROVIDER: mock OCR_MAX_ATTEMPTS: 3 DELEGATION_REVOKE_CRON: "0 * * * *" + REGRESSION_REPORT_DIR: /tmp/hyb-oa-regression steps: - name: Checkout @@ -79,7 +80,16 @@ jobs: - name: Run full regression suite working-directory: server run: | - RUN_AUDIT_REGRESSION=1 RUN_OCR_REGRESSION=1 ./scripts/regression-all.sh + mkdir -p "$REGRESSION_REPORT_DIR" + RUN_AUDIT_REGRESSION=1 RUN_OCR_REGRESSION=1 ./scripts/regression-all.sh --json-report "$REGRESSION_REPORT_DIR/report.json" + + - name: Upload regression report artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: regression-report-${{ github.run_id }}-${{ github.sha }} + path: /tmp/hyb-oa-regression/report.json + if-no-files-found: warn - name: Stop server if: always() diff --git a/.gitignore b/.gitignore index 2a4352fd..505d5324 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ deploy/trialrun/backups # Playwright artifacts client/test-results client/playwright-report + +# Vitest coverage artifacts +server/coverage +server/.vitest diff --git a/deploy/prometheus/alerts-slo.yml b/deploy/prometheus/alerts-slo.yml new file mode 100644 index 00000000..8fe3314d --- /dev/null +++ b/deploy/prometheus/alerts-slo.yml @@ -0,0 +1,47 @@ +groups: + - name: hyb-oa-slo + rules: + - alert: HybOAApiAvailabilityLow + expr: | + ( + sum(rate(http_server_requests_total{route=~"/api/.*",status!~"5.."}[5m])) + / + clamp_min(sum(rate(http_server_requests_total{route=~"/api/.*"}[5m])), 0.001) + ) < 0.999 + for: 10m + labels: + severity: warning + slo: availability + annotations: + summary: "hyb-oa API availability below SLO" + description: "API availability fell below 99.9% for 10m. Check recent releases and feature-flag changes." + + - alert: HybOADashboardP99High + expr: | + histogram_quantile( + 0.99, + sum by (le, route) ( + rate(http_server_request_duration_seconds_bucket{route=~"/api/dashboard/.*"}[5m]) + ) + ) > 0.5 + for: 15m + labels: + severity: warning + slo: latency + annotations: + summary: "hyb-oa dashboard P99 latency above SLO" + description: "Dashboard API P99 exceeded 500ms for 15m. Investigate DB load and aggregation queries." + + - alert: HybOAApi5xxSpike + expr: | + sum(rate(http_server_requests_total{route=~"/api/.*",status=~"5.."}[5m])) + / + clamp_min(sum(rate(http_server_requests_total{route=~"/api/.*"}[5m])), 0.001) + > 0.01 + for: 5m + labels: + severity: critical + slo: availability + annotations: + summary: "hyb-oa API 5xx ratio spike" + description: "API 5xx ratio is above 1% for 5m. Consider force_off for high-risk feature flags." diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md index e554437e..5cfc2548 100644 --- a/docs/feature-matrix.md +++ b/docs/feature-matrix.md @@ -1,8 +1,8 @@ -# hyb-oa 实现状态矩阵(P5-P12) +# hyb-oa 实现状态矩阵(P5-P13) -**快照日期**:2026-02-11 -**范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4`,共 92 项。 -**事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv`(执行合同) +**快照日期**:2026-02-12 +**范围**:`F1-F42`(含 `F19b/F19c/F19d`)+ `R1-S2` + `P9A1-P9D1` + `P10E1-P10G1` + `G0-G6` + `H1` + `M1-M4` + `P13-0-P13-8`,共 101 项。 +**事实源**:`issues/2026-02-09_13-39-02-p5-blueprint-v3.csv` + `issues/2026-02-09_23-30-00-p6-p8-remaining-work-master.csv` + `issues/2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv` + `issues/2026-02-10_10-35-30-p10-production-hardening.csv` + `issues/2026-02-11-p11-p12-governance-mobile.csv` + `issues/2026-02-12_01-09-41-p13-l5-governance.csv`(执行合同) **说明**: - 本矩阵用于全局读图(审计/交接/回归覆盖追踪),不替代 `issues/*.csv` 的执行合同职责。 @@ -105,3 +105,13 @@ | M2 | 移动拍照与离线上传队列 | §16(移动端治理) | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11 | DONE | manual + client:typecheck/build | iOS前台恢复重放 | | M3 | 移动审批快捷处理 | §16(移动端治理) | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11 | DONE | manual + client:typecheck/build | none | | M4 | P12验收与收口 | §16(状态治理) | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11-p11-p12-governance-mobile.csv | 2026-02-11 | DONE | docs/mobile/pwa-regression-checklist.md | none | + +| P13-0 | P13执行合同建档 | §16(执行合同治理) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | manual | none | +| P13-1 | 后端单元测试框架引入 | §16(测试工程化) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | cd server && npm test | Vitest底座落地 | +| P13-2 | 核心服务单元测试补齐 | §16(测试工程化) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | cd server && npm run test:coverage | 覆盖 featureFlags/verification/integrations dispatcher | +| P13-3 | OpenAPI自动文档接入 | §16(治理可观测性) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | /api/docs + /api/docs/json(非生产) | 双闸条件:非生产+开关开启 | +| P13-4 | SLI-SLO与错误预算落地 | §16(非功能治理) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | docs/ops/slo.md + deploy/prometheus/alerts-slo.yml | none | +| P13-5 | 回归脚本结构化报告输出 | §16(回归工程化) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | server/scripts/regression-all.sh --json-report | 报告 schema_version=1.0 | +| P13-6 | CI回归报告归档 | §16(CI治理) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | .github/workflows/ci-regression.yml artifact | run_id+sha 可追溯 | +| P13-7 | 变更失败率统计脚本 | §16(DORA 指标治理) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | python3 scripts/cfr_report.py --window 7d,30d | failed_commit=回归失败或24h热修 | +| P13-8 | P13验收与矩阵收口 | §16(状态治理) | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12_01-09-41-p13-l5-governance.csv | 2026-02-12 | DONE | check-minimal + regression-all + validate-feature-matrix | none | diff --git a/docs/ops/ci-regression-reporting.md b/docs/ops/ci-regression-reporting.md new file mode 100644 index 00000000..2e1642d8 --- /dev/null +++ b/docs/ops/ci-regression-reporting.md @@ -0,0 +1,38 @@ +# CI 回归报告归档说明(P13-6) + +更新时间:2026-02-12 + +## 目标 + +在 `ci-regression` 流水线中产出并归档结构化回归报告,确保每次回归可追溯到具体 `run_id` 与 `commit SHA`。 + +## 产物 + +- 回归脚本输出:`/tmp/hyb-oa-regression/report.json` +- Artifact 名称:`regression-report--` +- JSON 契约:`schema_version = "1.0"` + +## 字段约定 + +报告包含以下关键字段: + +- `schema_version` +- `generated_at` +- `git.sha` +- `git.branch` +- `overall.status | duration_ms | passed | failed | skipped | total` +- `steps[]`:`id | name | enabled | status | exit_code | duration_ms | started_at | ended_at | command` + +## 回归命令 + +在 CI 中使用: + +```bash +RUN_AUDIT_REGRESSION=1 RUN_OCR_REGRESSION=1 ./scripts/regression-all.sh --json-report /tmp/hyb-oa-regression/report.json +``` + +## 排障建议 + +- 回归失败时优先下载 artifact,检查 `overall.failed` 与 `steps[].status="failed"`。 +- 若报告未生成,检查回归步骤是否在脚本启动前失败(例如服务未就绪)。 +- 若报告字段缺失,优先比对 `server/scripts/lib/regression-report.sh` 与 schema 版本。 diff --git a/docs/ops/dora-metrics.md b/docs/ops/dora-metrics.md new file mode 100644 index 00000000..29f603de --- /dev/null +++ b/docs/ops/dora-metrics.md @@ -0,0 +1,49 @@ +# DORA 指标(CFR)落地说明(P13-7) + +更新时间:2026-02-12 + +## 指标口径 + +- `run_cfr = failed_runs / total_runs` +- `commit_cfr = failed_commits / total_commits` +- `failed_commit` 判定: + 1. 对应回归 run 失败;或 + 2. 该 commit 在回归时间后 24 小时内出现热修记录。 + +## 数据输入 + +- 回归 JSON:来自 `server/scripts/regression-all.sh --json-report` +- 可选热修 CSV:`commit_sha,occurred_at` + +## 生成命令 + +```bash +python3 scripts/cfr_report.py \ + --input-dir artifacts/regression \ + --window 7d,30d \ + --output-json /tmp/cfr.json \ + --output-md /tmp/cfr.md +``` + +带热修记录: + +```bash +python3 scripts/cfr_report.py \ + --input-dir artifacts/regression \ + --window 30d \ + --hotfix-csv data/hotfix-events.csv \ + --output-json /tmp/cfr.json +``` + +## 输出结构(JSON) + +- `generated_at` +- `input_dir` +- `reports_scanned` +- `hotfix_events` +- `windows..{total_runs,failed_runs,run_cfr,run_cfr_pct,total_commits,failed_commits,commit_cfr,commit_cfr_pct}` + +## 使用建议 + +- 周报使用 `7d`,月报使用 `30d`。 +- 当 `commit_cfr_pct` 持续高于阈值(例如 15%)时,暂停高风险灰度并补回归样本。 diff --git a/docs/ops/non-functional-baseline.md b/docs/ops/non-functional-baseline.md index de4bb634..43485932 100644 --- a/docs/ops/non-functional-baseline.md +++ b/docs/ops/non-functional-baseline.md @@ -1,6 +1,6 @@ -# 非功能基线(P10.5) +# 非功能基线(P10.5/P13) -更新时间:2026-02-11 +更新时间:2026-02-12 ## 安全基线 - 使用 `server/scripts/security-baseline.sh` 检查关键安全配置与核心路由语法。 @@ -20,6 +20,14 @@ - 平均响应(avg)< 500ms - 最大响应(max)< 1500ms +## SLI/SLO 与错误预算(P13) +- 详细定义见:`docs/ops/slo.md` +- 首版目标: + - 可用性(30天)>= 99.9% + - 核心 Dashboard API P99 < 500ms + - Feature Flag 更新成功率 >= 99.5% +- 告警规则文件:`deploy/prometheus/alerts-slo.yml` + ## 回滚原则 - 若非功能指标异常,先执行配置回滚而非代码回滚: 1. 将 `erp_sync` 和 `invoice_verification` 设为 `force_off`。 diff --git a/docs/ops/slo.md b/docs/ops/slo.md new file mode 100644 index 00000000..d59a96e9 --- /dev/null +++ b/docs/ops/slo.md @@ -0,0 +1,101 @@ +# SLI / SLO 与错误预算(P13) + +更新时间:2026-02-12 + +## 1. 范围与目标 + +本版聚焦 P11/P13 治理链路(Feature Flags、ERP 同步、验真开关)和核心可观测接口,建立可执行的 SLI/SLO 与错误预算基线。 + +- 环境范围:生产环境(`NODE_ENV=production`) +- 统计窗口:滚动 30 天 +- 目标用途:发布评审、变更放行、故障复盘 + +## 2. SLI 定义 + +### SLI-1:API 可用性(Availability) + +- 指标定义: + - 分子:`http_server_requests_total{status!~"5.."}` + - 分母:`http_server_requests_total` +- 统计口径:按所有 `/api/*` 请求聚合 +- 采样周期:5 分钟 + +### SLI-2:核心接口延迟(Latency P99) + +- 指标定义:`histogram_quantile(0.99, rate(http_server_request_duration_seconds_bucket[5m]))` +- 核心接口: + - `/api/dashboard/expense-overview` + - `/api/dashboard/trends` + - `/api/dashboard/top-categories` + - `/api/dashboard/approval-efficiency` + +### SLI-3:治理开关变更成功率(Feature Flag Update Success) + +- 指标定义: + - 分子:`/api/system/feature-flags/:key` 返回 2xx 的次数 + - 分母:`/api/system/feature-flags/:key` 总调用次数 +- 目标:验证治理入口稳定性,避免灰度期管理面不稳定 + +## 3. SLO 目标 + +### SLO-A(可用性) + +- 目标:30 天可用性 >= 99.90% +- 对应最大不可用预算:0.10% +- 换算: + - 每 30 天预算约 43 分钟 + - 每 7 天预算约 10 分钟 + +### SLO-B(延迟) + +- 目标:核心 Dashboard 接口 P99 < 500ms +- 兜底上限:P99 不得持续超过 1500ms 超过 15 分钟 + +### SLO-C(治理变更稳定性) + +- 目标:Feature Flag 更新成功率 >= 99.5% +- 说明:若低于目标,暂停高风险灰度变更(ERP/验真) + +## 4. 错误预算策略 + +### 预算分级 + +- 正常区:剩余预算 > 50% + - 允许常规迭代与灰度 +- 预警区:剩余预算 20% ~ 50% + - 限制非必要发布频率,增加回归样本 +- 保护区:剩余预算 < 20% + - 冻结非紧急变更,仅允许故障修复 + +### 预算消耗触发动作 + +- 可用性预算触发: + 1. 立即复核最近 24h 发布与配置变更 + 2. 执行 `erp_sync` 与 `invoice_verification` 的 `force_off` 预案 +- 延迟预算触发: + 1. 优先检查 Dashboard 聚合查询与数据库负载 + 2. 降级非关键统计刷新频率 + +## 5. 监控与告警接入 + +- Prometheus 告警规则:`deploy/prometheus/alerts-slo.yml` +- 非功能脚本: + - `server/scripts/benchmark-dashboard.sh` + - `server/scripts/security-baseline.sh` +- 发布前最小检查: + - `./scripts/check-minimal.sh` + - `cd server && ./scripts/regression-feature-flags.sh` + +## 6. 回滚与变更约束 + +- 一级回滚(业务可用优先): + - 将 `erp_sync`、`invoice_verification` 置 `force_off` +- 二级回滚(系统保护): + - env 硬闸:`ERP_SYNC_ENABLED=false`、`INVOICE_VERIFICATION_ENABLED=false` +- 说明:保留审计与历史开关记录,不做 destructive 数据回滚 + +## 7. 后续演进(P13-5+) + +- 将 `regression-all.sh` 结构化结果纳入 SLO 评审输入 +- 形成 Change Failure Rate 周报/月报(DORA 指标) +- 将 SLO 预算状态纳入发布门禁 diff --git a/issues/2026-02-12_01-09-41-p13-l5-governance.csv b/issues/2026-02-12_01-09-41-p13-l5-governance.csv new file mode 100644 index 00000000..cbc7536d --- /dev/null +++ b/issues/2026-02-12_01-09-41-p13-l5-governance.csv @@ -0,0 +1,10 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +P13-0,P13执行合同建档,创建并冻结P13治理增强Issue合同及依赖关系,CSV通过校验且字段规范完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-02-12_01-09-41-p13-l5-governance.csv,manual,DONE,DONE,DONE,issues/2026-02-12_01-09-41-p13-l5-governance.csv,[],none +P13-1,后端单元测试框架引入,在server引入可执行单测框架与基础脚手架并设置npm test可运行,`cd server && npm test`可执行且至少包含1个示例测试通过,cd server && npm test,manual,DONE,DONE,DONE,server/package.json | server/vitest.config.* | server/tests/**,"[""P13-0""]",新增依赖: vitest@^2 + @vitest/coverage-v8@^2;回滚: 移除test脚本与依赖;已验收: npm test/test:coverage +P13-2,核心服务单元测试补齐,为高风险治理链路补单测: featureFlags/verification/integration dispatcher,核心链路覆盖并包含开关阻断/灰度/重试异常等关键分支,cd server && npm test -- --coverage,manual,DONE,DONE,DONE,server/src/services/featureFlags/index.js | server/src/services/verification/index.js | server/src/services/integrations/dispatcher.js | server/tests/unit/**,"[""P13-1""]",已补齐featureFlags/verification/dispatcher核心分支单测;验收通过: cd server && npm test && npm run test:coverage;回滚: 删除新增单测文件并恢复Issue状态 +P13-3,OpenAPI自动文档接入,接入Fastify Swagger并产出可访问或可导出的OpenAPI文档,非生产且OPENAPI_DOCS_ENABLED=true时,/api/docs 与 /api/docs/json 可访问,NODE_ENV=development OPENAPI_DOCS_ENABLED=true JOBS_ENABLED=false cd server && npm run start & sleep 3 && curl -sf http://127.0.0.1:3000/api/docs >/dev/null && curl -sf http://127.0.0.1:3000/api/docs/json | jq '.openapi' >/dev/null,manual,DONE,DONE,DONE,server/src/app.js | server/src/routes/featureFlags.js | server/src/routes/integrations.js | server/src/routes/invoicePool/verification.js | server/src/plugins/swagger.js | server/src/utils/openapi.js | server/src/config.js | server/.env.example | server/package.json,"[""P13-0""]","新增依赖: @fastify/swagger + @fastify/swagger-ui;回滚: OPENAPI_DOCS_ENABLED=false 或移除插件注册;已验收: /api/docs dev可用, prod隐藏" +P13-4,SLI-SLO与错误预算落地,将现有非功能基线文档升级为可执行SLO并提供告警规则草案,形成明确SLI/SLO/错误预算与Prometheus规则文件并可检查,test -f docs/ops/slo.md && test -f deploy/prometheus/alerts-slo.yml,manual,DONE,DONE,DONE,docs/ops/non-functional-baseline.md | docs/ops/slo.md | deploy/prometheus/alerts-slo.yml,"[""P13-0""]",已新增SLO文档与Prometheus告警规则;验收通过: test -f docs/ops/slo.md && test -f deploy/prometheus/alerts-slo.yml;回滚: 删除新增文档与规则并恢复Issue状态 +P13-5,回归脚本结构化报告输出,为regression-all增加JSON报告能力并标准化结果字段,可输出commit/时间戳/脚本项结果并可被CI消费,cd server && ./scripts/regression-all.sh --json-report /tmp/regression-report.json && jq . /tmp/regression-report.json >/dev/null,manual,DONE,DONE,DONE,server/scripts/regression-all.sh | server/scripts/lib/regression-report.sh,"[""P13-0""]",已支持--json-report并输出schema_version=1.0;支持enabled/failed/skipped结构化步骤;回滚: 不使用--json-report保持旧行为 +P13-6,CI回归报告归档,在GitHub Actions中上传回归JSON报告并关联commit SHA,CI产物可下载且报告包含失败项定位信息,gh run view --log || manual: 在Actions页面确认artifact存在,manual,DONE,DONE,DONE,.github/workflows/ci-regression.yml | docs/ops/ci-regression-reporting.md,"[""P13-5""]",ci-regression已输出/tmp/hyb-oa-regression/report.json并通过actions/upload-artifact归档;回滚: 移除artifact步骤 +P13-7,变更失败率统计脚本,基于回归JSON生成按时间窗口统计的Change Failure Rate报表,生成周/月维度失败率报表并可复算,"python3 scripts/cfr_report.py --input-dir artifacts/regression --window 7d,30d --output-json /tmp/cfr.json && jq -e '.windows[""30d""].run_cfr_pct >= 0' /tmp/cfr.json >/dev/null",manual,DONE,DONE,DONE,scripts/cfr_report.py | docs/ops/dora-metrics.md | docs/feature-matrix.md,"[""P13-6""]",新增CFR统计脚本,支持7d/30d窗口与可选hotfix-csv;failed_commit=回归失败或24h热修 +P13-8,P13验收与矩阵收口,执行P13相关校验并同步feature matrix与Issue状态,check-minimal+关键回归通过且矩阵一致性通过,./scripts/check-minimal.sh && cd server && RUN_AUDIT_REGRESSION=1 RUN_OCR_REGRESSION=1 RUN_FEATURE_FLAGS_REGRESSION=1 ./scripts/regression-all.sh --json-report /tmp/regression-report.json && cd /home/shunli/Projects/hyb-oa && python3 scripts/validate-feature-matrix.py,manual,DONE,DONE,DONE,docs/feature-matrix.md | scripts/validate-feature-matrix.py | issues/2026-02-12_01-09-41-p13-l5-governance.csv,"[""P13-2"",""P13-3"",""P13-4"",""P13-7""]",已纳入P13事实源并补齐P13-0~P13-8矩阵行;完成后统一三状态DONE diff --git a/issues/2026-02-13_00-11-56-p14-l5-operationalization.csv b/issues/2026-02-13_00-11-56-p14-l5-operationalization.csv new file mode 100644 index 00000000..f3f53f26 --- /dev/null +++ b/issues/2026-02-13_00-11-56-p14-l5-operationalization.csv @@ -0,0 +1,10 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +P14-0,P14执行合同建档,创建并校验P14执行合同CSV,冻结依赖关系与字段规范,CSV通过校验且字段完整,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-02-13_00-11-56-p14-l5-operationalization.csv,manual,DONE,DONE,DONE,issues/2026-02-13_00-11-56-p14-l5-operationalization.csv,[],none +P14-1,回归报告Schema固化,为regression JSON报告补schema校验器并固化schema_version=1.0,报告缺字段或类型不匹配时校验失败,cd server && ./scripts/regression-all.sh --json-report /tmp/r.json && python3 ../scripts/validate_regression_report.py /tmp/r.json,manual,TODO,TODO,TODO,server/scripts/lib/regression-report.sh | server/scripts/regression-all.sh | scripts/validate_regression_report.py | docs/ops/ci-regression-reporting.md,"[""P14-0""]",schema_version固定1.0;回滚: 仅保留基础report字段 +P14-2,CI回归摘要发布,在ci-regression输出Job Summary并保留artifact定位失败步骤,Actions Summary可直接查看失败步骤与报告下载链接,manual: 触发ci-regression并在Actions页面核验summary与artifact,manual,TODO,TODO,TODO,.github/workflows/ci-regression.yml | docs/ops/ci-regression-reporting.md,"[""P14-1""]",不引入外部SaaS +P14-3,CFR自动周/月报,将CFR计算纳入自动化并产出7d/30d报表,可稳定产出JSON与Markdown双格式报表并可复算,"python3 scripts/cfr_report.py --input-dir artifacts/regression --window 7d,30d --output-json /tmp/cfr.json --output-md /tmp/cfr.md",manual,TODO,TODO,TODO,scripts/cfr_report.py | docs/ops/dora-metrics.md | .github/workflows/ci-regression.yml,"[""P14-2""]",热修通过--hotfix-csv合并口径 +P14-4,高风险服务单测扩容,补齐featureFlags/verification/integrations/outbox/workflow util关键分支单测,新增至少20个测试并提升关键模块覆盖率,cd server && npm test && npm run test:coverage,manual,TODO,TODO,TODO,server/tests/unit/**/*.test.js | server/src/services/featureFlags/index.js | server/src/services/verification/index.js | server/src/services/integrations/dispatcher.js | server/src/services/integrations/outbox.js | server/src/services/integrations/erp.js | server/src/utils/workflow.js,"[""P14-0""]",仅纯单测,不连DB +P14-5,覆盖率门禁渐进,建立coverage gate策略:PR告警、main阻断,main分支低于阈值失败;PR仅提示不阻断,cd server && npm run test:coverage && node ../scripts/coverage_gate.js --mode=ci,manual,TODO,TODO,TODO,server/vitest.config.cjs | scripts/coverage_gate.js | .github/workflows/ci-minimal.yml | docs/ops/testing-strategy.md,"[""P14-4""]",初始阈值仅关键模块lines>=70%,全局先告警 +P14-6,OpenAPI覆盖扩展与漂移检查,扩展高频路由OpenAPI schema并增加导出与对比检查,/api/docs/json包含新增路径且导出结果可做diff,NODE_ENV=development OPENAPI_DOCS_ENABLED=true cd server && node scripts/export_openapi.js --out /tmp/openapi.json && jq '.paths|keys' /tmp/openapi.json,manual,TODO,TODO,TODO,server/src/routes/reimbursements.js | server/src/routes/workflow.js | server/src/routes/invoicePool/read.js | server/scripts/export_openapi.js | scripts/openapi_diff_check.py,"[""P14-0""]",不改业务语义,仅补schema +P14-7,分布式追踪最小落地,引入OTel trace基础链路与环境硬闸,默认关闭,开关开启可导出trace,关闭无行为变化,cd server && node --check src/observability/tracing.js && manual: 验证traceparent透传,manual,TODO,TODO,TODO,server/src/observability/tracing.js | server/src/app.js | server/src/index.js | server/src/config.js | server/.env.example | docs/ops/tracing.md,"[""P14-0""]",默认OTEL_TRACES_ENABLED=false,fail-safe +P14-8,P14验收与矩阵收口,执行P14全量校验并同步feature-matrix与执行合同状态,check-minimal+regression-all+matrix校验全部通过,./scripts/check-minimal.sh && cd server && ./scripts/regression-all.sh --json-report /tmp/regression-report.json && cd /home/shunli/Projects/hyb-oa && python3 scripts/validate-feature-matrix.py,manual,TODO,TODO,TODO,docs/feature-matrix.md | scripts/validate-feature-matrix.py | issues/*.csv,"[""P14-3"",""P14-5"",""P14-6"",""P14-7""]",三状态全DONE后关闭 diff --git a/scripts/cfr_report.py b/scripts/cfr_report.py new file mode 100755 index 00000000..27e12f10 --- /dev/null +++ b/scripts/cfr_report.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +"""Generate Change Failure Rate reports from regression JSON artifacts.""" + +from __future__ import annotations + +import argparse +import csv +import json +import re +import sys +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Iterable + +WINDOW_PATTERN = re.compile(r"^(\d+)d$") + + +@dataclass +class RegressionRun: + timestamp: datetime + commit_sha: str + failed: bool + report_file: Path + + +@dataclass +class HotfixEvent: + commit_sha: str + occurred_at: datetime + + +def parse_iso_datetime(value: str) -> datetime: + normalized = value.strip() + if normalized.endswith("Z"): + normalized = normalized[:-1] + "+00:00" + parsed = datetime.fromisoformat(normalized) + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(timezone.utc) + + +def parse_windows(value: str) -> list[int]: + windows: list[int] = [] + for chunk in value.split(","): + item = chunk.strip() + if not item: + continue + matched = WINDOW_PATTERN.match(item) + if not matched: + raise ValueError(f"invalid window '{item}', expected like 7d/30d/90d") + days = int(matched.group(1)) + if days <= 0: + raise ValueError(f"window days must be >0: {item}") + windows.append(days) + if not windows: + windows.append(30) + return sorted(set(windows)) + + +def load_regression_runs(input_dir: Path) -> list[RegressionRun]: + runs: list[RegressionRun] = [] + + for path in sorted(input_dir.rglob("*.json")): + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except Exception: + continue + + schema_version = str(payload.get("schema_version", "")).strip() + if schema_version != "1.0": + continue + + generated_at_raw = payload.get("generated_at") + overall = payload.get("overall") if isinstance(payload.get("overall"), dict) else {} + git = payload.get("git") if isinstance(payload.get("git"), dict) else {} + + if not generated_at_raw or not isinstance(generated_at_raw, str): + continue + + commit_sha = str(git.get("sha", "unknown")).strip() or "unknown" + status = str(overall.get("status", "")).strip().lower() + + try: + generated_at = parse_iso_datetime(generated_at_raw) + except Exception: + continue + + runs.append( + RegressionRun( + timestamp=generated_at, + commit_sha=commit_sha, + failed=status != "passed", + report_file=path, + ) + ) + + return sorted(runs, key=lambda x: x.timestamp) + + +def load_hotfix_events(path: Path | None) -> list[HotfixEvent]: + if path is None: + return [] + if not path.exists(): + raise FileNotFoundError(f"hotfix csv not found: {path}") + + events: list[HotfixEvent] = [] + with path.open(encoding="utf-8", newline="") as fp: + reader = csv.DictReader(fp) + for row in reader: + commit_sha = (row.get("commit_sha") or "").strip() + occurred_at_raw = (row.get("occurred_at") or "").strip() + if not commit_sha or not occurred_at_raw: + continue + try: + occurred_at = parse_iso_datetime(occurred_at_raw) + except Exception: + continue + events.append(HotfixEvent(commit_sha=commit_sha, occurred_at=occurred_at)) + + return sorted(events, key=lambda x: x.occurred_at) + + +def ratio(numerator: int, denominator: int) -> float: + if denominator == 0: + return 0.0 + return numerator / denominator + + +def has_hotfix_hit_within_24h(run_times: Iterable[datetime], events: list[HotfixEvent]) -> bool: + event_times = [event.occurred_at for event in events] + for run_time in run_times: + for event_time in event_times: + seconds = (event_time - run_time).total_seconds() + if 0 <= seconds <= 24 * 3600: + return True + return False + + +def build_window_metrics( + runs: list[RegressionRun], + hotfix_events: list[HotfixEvent], + now: datetime, + window_days: int, +) -> dict[str, object]: + cutoff = now - timedelta(days=window_days) + window_runs = [run for run in runs if run.timestamp >= cutoff] + failed_runs = [run for run in window_runs if run.failed] + + commits: dict[str, list[RegressionRun]] = {} + for run in window_runs: + commits.setdefault(run.commit_sha, []).append(run) + + hotfix_by_commit: dict[str, list[HotfixEvent]] = {} + for event in hotfix_events: + if event.occurred_at >= cutoff: + hotfix_by_commit.setdefault(event.commit_sha, []).append(event) + + failed_commit_count = 0 + for commit_sha, commit_runs in commits.items(): + run_failed = any(run.failed for run in commit_runs) + hotfix_hit = has_hotfix_hit_within_24h( + (run.timestamp for run in commit_runs), + hotfix_by_commit.get(commit_sha, []), + ) + if run_failed or hotfix_hit: + failed_commit_count += 1 + + total_runs = len(window_runs) + total_commits = len(commits) + failed_runs_count = len(failed_runs) + + run_cfr = ratio(failed_runs_count, total_runs) + commit_cfr = ratio(failed_commit_count, total_commits) + + return { + "window": f"{window_days}d", + "cutoff": cutoff.isoformat().replace("+00:00", "Z"), + "total_runs": total_runs, + "failed_runs": failed_runs_count, + "run_cfr": round(run_cfr, 6), + "run_cfr_pct": round(run_cfr * 100, 2), + "total_commits": total_commits, + "failed_commits": failed_commit_count, + "commit_cfr": round(commit_cfr, 6), + "commit_cfr_pct": round(commit_cfr * 100, 2), + } + + +def render_markdown(result: dict[str, object]) -> str: + lines = [ + "# Change Failure Rate 报告", + "", + f"- 生成时间: {result['generated_at']}", + f"- 输入目录: `{result['input_dir']}`", + f"- 报告总数: {result['reports_scanned']}", + "", + "| Window | Total Runs | Failed Runs | Run CFR | Total Commits | Failed Commits | Commit CFR |", + "| --- | ---: | ---: | ---: | ---: | ---: | ---: |", + ] + + windows: dict[str, dict[str, object]] = result["windows"] # type: ignore[assignment] + for key in sorted(windows.keys(), key=lambda x: int(x[:-1])): + item = windows[key] + lines.append( + f"| {key} | {item['total_runs']} | {item['failed_runs']} | {item['run_cfr_pct']}% | " + f"{item['total_commits']} | {item['failed_commits']} | {item['commit_cfr_pct']}% |" + ) + + return "\n".join(lines) + "\n" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate Change Failure Rate report") + parser.add_argument("--input-dir", required=True, help="Directory that stores regression JSON artifacts") + parser.add_argument("--window", default="30d", help="Comma separated windows, e.g. 7d,30d,90d") + parser.add_argument("--output-json", help="Optional output JSON file path") + parser.add_argument("--output-md", help="Optional output markdown file path") + parser.add_argument("--hotfix-csv", help="Optional hotfix CSV path (columns: commit_sha,occurred_at)") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + input_dir = Path(args.input_dir).resolve() + if not input_dir.exists(): + print(f"[ERR] input dir not found: {input_dir}", file=sys.stderr) + return 1 + + try: + windows = parse_windows(args.window) + except ValueError as exc: + print(f"[ERR] {exc}", file=sys.stderr) + return 1 + + try: + hotfix_events = load_hotfix_events(Path(args.hotfix_csv).resolve() if args.hotfix_csv else None) + except Exception as exc: + print(f"[ERR] {exc}", file=sys.stderr) + return 1 + + runs = load_regression_runs(input_dir) + now = datetime.now(timezone.utc) + + result: dict[str, object] = { + "generated_at": now.isoformat().replace("+00:00", "Z"), + "input_dir": str(input_dir), + "reports_scanned": len(runs), + "hotfix_events": len(hotfix_events), + "windows": {}, + } + + for days in windows: + result["windows"][f"{days}d"] = build_window_metrics(runs, hotfix_events, now, days) # type: ignore[index] + + rendered_json = json.dumps(result, ensure_ascii=False, indent=2) + "\n" + + if args.output_json: + output_json = Path(args.output_json) + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text(rendered_json, encoding="utf-8") + + if args.output_md: + output_md = Path(args.output_md) + output_md.parent.mkdir(parents=True, exist_ok=True) + output_md.write_text(render_markdown(result), encoding="utf-8") + + if not args.output_json and not args.output_md: + print(rendered_json, end="") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate-feature-matrix.py b/scripts/validate-feature-matrix.py index 5d201605..739bae82 100755 --- a/scripts/validate-feature-matrix.py +++ b/scripts/validate-feature-matrix.py @@ -24,9 +24,10 @@ CSV_FACT_SOURCES = [ ROOT / "issues" / "2026-02-10_09-42-31-p9-maintenance-performance-matrix.csv", ROOT / "issues" / "2026-02-10_10-35-30-p10-production-hardening.csv", ROOT / "issues" / "2026-02-11-p11-p12-governance-mobile.csv", + ROOT / "issues" / "2026-02-12_01-09-41-p13-l5-governance.csv", ] -ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z]*$") +ID_PATTERN = re.compile(r"^[A-Z][0-9][0-9A-Za-z-]*$") STATUS_INDEX = 6 diff --git a/server/.env.example b/server/.env.example index 4ef1324c..9649e902 100644 --- a/server/.env.example +++ b/server/.env.example @@ -25,6 +25,8 @@ MAX_UPLOAD_SIZE_BYTES="10485760" # ===== 单域托管 client/dist(试运行/生产建议)===== # SERVE_CLIENT_DIST="true" # CLIENT_DIST_DIR="../client/dist" +# OpenAPI 文档端点(建议仅非生产开启) +# OPENAPI_DOCS_ENABLED="false" # ===== 安全基线(默认:production=true,其它环境=false,可显式覆盖)===== # RATE_LIMIT_ENABLED="true" diff --git a/server/package-lock.json b/server/package-lock.json index 28d1c414..4cdf47b9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -18,6 +18,8 @@ "@fastify/multipart": "^9.3.0", "@fastify/rate-limit": "^10.3.0", "@fastify/static": "^9.0.0", + "@fastify/swagger": "^9.5.2", + "@fastify/swagger-ui": "^5.2.3", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", "bcryptjs": "^3.0.3", @@ -36,7 +38,23 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "dotenv": "^16.4.5" + "@vitest/coverage-v8": "^2.1.8", + "dotenv": "^16.4.5", + "vitest": "^2.1.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@aws-crypto/crc32": { @@ -1177,6 +1195,63 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", @@ -1237,6 +1312,397 @@ "@electric-sql/pglite": "0.3.15" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -1251,6 +1717,12 @@ "lodash.isnil": "^4.0.0" } }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@fast-csv/parse": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", @@ -1266,6 +1738,12 @@ "lodash.uniq": "^4.5.0" } }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@fastify/accept-negotiator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", @@ -1589,6 +2067,52 @@ "glob": "^13.0.0" } }, + "node_modules/@fastify/swagger": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.7.0.tgz", + "integrity": "sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.5.tgz", + "integrity": "sha512-ky3I0LAkXKX/prwSDpoQ3kscBKsj2Ha6Gp1/JfgQSqyx0bm9F2bE//XmGVGj2cR9l5hUjZYn60/hqn7e+OLgWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^9.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -1622,6 +2146,73 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -1659,6 +2250,17 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.3.0.tgz", @@ -1832,6 +2434,356 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@smithy/abort-controller": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", @@ -2870,12 +3822,25 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/react": { "version": "19.2.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", @@ -2886,6 +3851,166 @@ "csstype": "^3.2.2" } }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -2937,6 +4062,32 @@ } } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -3051,6 +4202,16 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3280,6 +4441,16 @@ } } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3293,6 +4464,23 @@ "node": ">= 0.4" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -3305,6 +4493,16 @@ "node": "*" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chevrotain": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", @@ -3343,6 +4541,26 @@ "consola": "^3.2.3" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3504,6 +4722,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -3626,6 +4854,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3645,6 +4880,13 @@ "fast-check": "^3.23.1" } }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -3681,6 +4923,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3708,12 +4957,61 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -3743,6 +5041,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -4026,6 +5334,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -4178,6 +5501,16 @@ "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.0.tgz", "integrity": "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA==" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4235,6 +5568,13 @@ "node": ">=16.9.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4351,6 +5691,16 @@ "node": ">= 10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -4381,6 +5731,76 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -4427,6 +5847,23 @@ "dequal": "^2.0.3" } }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4669,6 +6106,13 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", @@ -4702,6 +6146,44 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4842,6 +6324,25 @@ "node": ">=8.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -4957,6 +6458,12 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, "node_modules/openid-client": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.2.tgz", @@ -4970,6 +6477,13 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5016,6 +6530,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -5134,6 +6658,13 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/pino": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.0.tgz", @@ -5182,6 +6713,35 @@ "pathe": "^2.0.3" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", @@ -5544,6 +7104,51 @@ "node": "*" } }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5704,6 +7309,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5725,6 +7337,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -5743,6 +7365,13 @@ "node": ">= 0.6" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -5780,6 +7409,110 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strnum": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", @@ -5792,6 +7525,19 @@ ], "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -5837,6 +7583,93 @@ "node": ">=10" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thread-stream": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", @@ -5849,6 +7682,13 @@ "node": ">=20" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -5858,6 +7698,36 @@ "node": ">=18" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -5918,6 +7788,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/unzipper": { "version": "0.10.14", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", @@ -5999,6 +7878,176 @@ } } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -6030,6 +8079,121 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6051,6 +8215,21 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zeptomatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", diff --git a/server/package.json b/server/package.json index e426b069..3d17130d 100644 --- a/server/package.json +++ b/server/package.json @@ -8,7 +8,7 @@ "dev": "node src/index.js", "start": "node src/index.js", "worker": "node src/worker.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest run", "prisma:format": "prisma format", "prisma:validate": "prisma validate", "prisma:generate": "prisma generate", @@ -22,7 +22,9 @@ "job:ocr-once": "node src/scripts/run-ocr-once.js", "job:archive-unused-invoices": "node src/scripts/run-invoice-archive-once.js", "job:integration-outbox-once": "node src/scripts/run-integration-outbox-once.js", - "job:verification-once": "node src/scripts/run-verification-once.js" + "job:verification-once": "node src/scripts/run-verification-once.js", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "keywords": [], "author": "", @@ -51,9 +53,13 @@ "prisma": "^7.3.0", "tencentcloud-sdk-nodejs": "^4.1.14", "zod": "^4.3.6", - "zxcvbn": "^4.4.2" + "zxcvbn": "^4.4.2", + "@fastify/swagger": "^9.5.2", + "@fastify/swagger-ui": "^5.2.3" }, "devDependencies": { - "dotenv": "^16.4.5" + "dotenv": "^16.4.5", + "vitest": "^2.1.8", + "@vitest/coverage-v8": "^2.1.8" } } diff --git a/server/scripts/lib/regression-report.sh b/server/scripts/lib/regression-report.sh new file mode 100644 index 00000000..6f7483fc --- /dev/null +++ b/server/scripts/lib/regression-report.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +RR_ENABLED=0 +RR_OUTPUT_PATH="" +RR_TMP_DIR="" +RR_STEPS_TSV="" +RR_ROOT_DIR="" +RR_SCHEMA_VERSION="1.0" + +rr_now_iso() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +rr_now_ms() { + if date +%s%3N >/dev/null 2>&1; then + date +%s%3N + else + echo "$(( $(date +%s) * 1000 ))" + fi +} + +rr_sanitize() { + local value="${1//$'\t'/ }" + value="${value//$'\n'/ }" + printf '%s' "$value" +} + +rr_prepare() { + local output_path="$1" + local root_dir="$2" + + RR_ENABLED=1 + RR_OUTPUT_PATH="$output_path" + RR_ROOT_DIR="$root_dir" + RR_TMP_DIR="$(mktemp -d)" + RR_STEPS_TSV="$RR_TMP_DIR/steps.tsv" + : > "$RR_STEPS_TSV" +} + +rr_record_step() { + if [[ "$RR_ENABLED" != "1" ]]; then + return 0 + fi + + local step_id="$1" + local name="$2" + local enabled="$3" + local command="$4" + local status="$5" + local exit_code="$6" + local started_at="$7" + local ended_at="$8" + local duration_ms="$9" + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$(rr_sanitize "$step_id")" \ + "$(rr_sanitize "$name")" \ + "$(rr_sanitize "$enabled")" \ + "$(rr_sanitize "$command")" \ + "$(rr_sanitize "$status")" \ + "$(rr_sanitize "$exit_code")" \ + "$(rr_sanitize "$started_at")" \ + "$(rr_sanitize "$ended_at")" \ + "$(rr_sanitize "$duration_ms")" \ + >> "$RR_STEPS_TSV" +} + +rr_git_sha() { + if command -v git >/dev/null 2>&1 && git -C "$RR_ROOT_DIR" rev-parse --short HEAD >/dev/null 2>&1; then + git -C "$RR_ROOT_DIR" rev-parse --short HEAD + else + echo "unknown" + fi +} + +rr_git_branch() { + if command -v git >/dev/null 2>&1 && git -C "$RR_ROOT_DIR" rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then + git -C "$RR_ROOT_DIR" rev-parse --abbrev-ref HEAD + else + echo "unknown" + fi +} + +rr_finalize() { + if [[ "$RR_ENABLED" != "1" ]]; then + return 0 + fi + + local overall_status="$1" + local overall_duration_ms="$2" + local passed="$3" + local failed="$4" + local skipped="$5" + local total="$6" + + local output_dir + output_dir="$(dirname "$RR_OUTPUT_PATH")" + mkdir -p "$output_dir" + + local git_sha + local git_branch + local generated_at + + git_sha="$(rr_git_sha)" + git_branch="$(rr_git_branch)" + generated_at="$(rr_now_iso)" + + python3 - "$RR_STEPS_TSV" "$RR_OUTPUT_PATH" "$RR_SCHEMA_VERSION" "$generated_at" "$git_sha" "$git_branch" "$overall_status" "$overall_duration_ms" "$passed" "$failed" "$skipped" "$total" <<'PY' +import json +import sys +from pathlib import Path + +steps_path = Path(sys.argv[1]) +output_path = Path(sys.argv[2]) +schema_version = sys.argv[3] +generated_at = sys.argv[4] +git_sha = sys.argv[5] +git_branch = sys.argv[6] +overall_status = sys.argv[7] +overall_duration_ms = int(sys.argv[8]) +passed = int(sys.argv[9]) +failed = int(sys.argv[10]) +skipped = int(sys.argv[11]) +total = int(sys.argv[12]) + +steps = [] +if steps_path.exists(): + for line in steps_path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + parts = line.split("\t") + if len(parts) != 9: + continue + step_id, name, enabled, command, status, exit_code, started_at, ended_at, duration_ms = parts + steps.append( + { + "id": step_id, + "name": name, + "enabled": enabled == "1", + "status": status, + "exit_code": int(exit_code), + "duration_ms": int(duration_ms), + "started_at": started_at, + "ended_at": ended_at, + "command": command, + } + ) + +payload = { + "schema_version": schema_version, + "generated_at": generated_at, + "git": { + "sha": git_sha, + "branch": git_branch, + }, + "overall": { + "status": overall_status, + "duration_ms": overall_duration_ms, + "passed": passed, + "failed": failed, + "skipped": skipped, + "total": total, + }, + "steps": steps, +} + +output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") +PY +} + +rr_cleanup() { + if [[ -n "$RR_TMP_DIR" && -d "$RR_TMP_DIR" ]]; then + rm -rf "$RR_TMP_DIR" + fi +} diff --git a/server/scripts/regression-all.sh b/server/scripts/regression-all.sh index 8de4f44f..47c77723 100755 --- a/server/scripts/regression-all.sh +++ b/server/scripts/regression-all.sh @@ -2,6 +2,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# shellcheck source=scripts/lib/regression-report.sh +source "$SCRIPT_DIR/lib/regression-report.sh" + RUN_AUDIT_REGRESSION="${RUN_AUDIT_REGRESSION:-1}" RUN_OCR_REGRESSION="${RUN_OCR_REGRESSION:-1}" RUN_ORG_SETTINGS_REGRESSION="${RUN_ORG_SETTINGS_REGRESSION:-0}" @@ -12,97 +17,138 @@ RUN_BUDGET_REGRESSION="${RUN_BUDGET_REGRESSION:-0}" RUN_INTEGRATIONS_REGRESSION="${RUN_INTEGRATIONS_REGRESSION:-0}" RUN_FEATURE_FLAGS_REGRESSION="${RUN_FEATURE_FLAGS_REGRESSION:-0}" -echo "=== [1/7] Smoke Test ===" -"$SCRIPT_DIR/smoke-test.sh" -echo "" +JSON_REPORT_PATH="" -echo "=== [2/7] Dedup Regression ===" -"$SCRIPT_DIR/regression-dedup.sh" -echo "" +usage() { + cat <<'USAGE' +Usage: ./scripts/regression-all.sh [--json-report ] -echo "=== [3/7] Workflow Actions Regression ===" -"$SCRIPT_DIR/regression-workflow-actions.sh" -echo "" +Options: + --json-report Write structured JSON report (schema_version=1.0). +USAGE +} -if [[ "$RUN_AUDIT_REGRESSION" == "1" ]]; then - echo "=== [4/7] Audit Regression ===" - "$SCRIPT_DIR/regression-audit.sh" - echo "" -else - echo "=== [4/7] Audit Regression (skipped, RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION) ===" - echo "" +while [[ $# -gt 0 ]]; do + case "$1" in + --json-report) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "[ERROR] --json-report requires a file path" >&2 + exit 2 + fi + JSON_REPORT_PATH="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "[ERROR] Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +FAIL_FAST="1" +if [[ -n "$JSON_REPORT_PATH" ]]; then + FAIL_FAST="0" + rr_prepare "$JSON_REPORT_PATH" "$ROOT_DIR" + trap rr_cleanup EXIT fi -if [[ "$RUN_OCR_REGRESSION" == "1" ]]; then - echo "=== [5/7] OCR Regression ===" - "$SCRIPT_DIR/regression-ocr.sh" - echo "" -else - echo "=== [5/7] OCR Regression (skipped, RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION) ===" - echo "" +TOTAL_STEPS=12 +PASSED_STEPS=0 +FAILED_STEPS=0 +SKIPPED_STEPS=0 +RUN_STARTED_MS="$(rr_now_ms)" + +run_step() { + local index="$1" + local step_id="$2" + local step_name="$3" + local enabled="$4" + local command="$5" + local skip_reason="${6:-}" + + local step_started_ms + local step_ended_ms + local step_duration_ms + local step_started_at + local step_ended_at + local status + local exit_code + + step_started_ms="$(rr_now_ms)" + step_started_at="$(rr_now_iso)" + + if [[ "$enabled" != "1" ]]; then + echo "=== [$index/$TOTAL_STEPS] $step_name (skipped, $skip_reason) ===" + echo "" + status="skipped" + exit_code=0 + SKIPPED_STEPS=$((SKIPPED_STEPS + 1)) + else + echo "=== [$index/$TOTAL_STEPS] $step_name ===" + + set +e + eval "$command" + exit_code=$? + set -e + + if [[ "$exit_code" -eq 0 ]]; then + status="passed" + PASSED_STEPS=$((PASSED_STEPS + 1)) + else + status="failed" + FAILED_STEPS=$((FAILED_STEPS + 1)) + echo "[ERROR] $step_name failed (exit=$exit_code)" >&2 + fi + + echo "" + fi + + step_ended_ms="$(rr_now_ms)" + step_ended_at="$(rr_now_iso)" + step_duration_ms="$((step_ended_ms - step_started_ms))" + + rr_record_step "$step_id" "$step_name" "$enabled" "$command" "$status" "$exit_code" "$step_started_at" "$step_ended_at" "$step_duration_ms" + + if [[ "$status" == "failed" && "$FAIL_FAST" == "1" ]]; then + return "$exit_code" + fi + + return 0 +} + +run_step 1 "smoke" "Smoke Test" "1" '"$SCRIPT_DIR"/smoke-test.sh' +run_step 2 "dedup" "Dedup Regression" "1" '"$SCRIPT_DIR"/regression-dedup.sh' +run_step 3 "workflow_actions" "Workflow Actions Regression" "1" '"$SCRIPT_DIR"/regression-workflow-actions.sh' +run_step 4 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION" +run_step 5 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION" +run_step 6 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION" +run_step 7 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION" +run_step 8 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION" +run_step 9 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION" +run_step 10 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION" +run_step 11 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION" +run_step 12 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION" + +RUN_ENDED_MS="$(rr_now_ms)" +OVERALL_DURATION_MS="$((RUN_ENDED_MS - RUN_STARTED_MS))" + +OVERALL_STATUS="passed" +if [[ "$FAILED_STEPS" -gt 0 ]]; then + OVERALL_STATUS="failed" fi -if [[ "$RUN_ORG_SETTINGS_REGRESSION" == "1" ]]; then - echo "=== [6/7] Org + Settings Regression ===" - "$SCRIPT_DIR/regression-org-settings.sh" - echo "" -else - echo "=== [6/7] Org + Settings Regression (skipped, RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION) ===" - echo "" +if [[ -n "$JSON_REPORT_PATH" ]]; then + rr_finalize "$OVERALL_STATUS" "$OVERALL_DURATION_MS" "$PASSED_STEPS" "$FAILED_STEPS" "$SKIPPED_STEPS" "$TOTAL_STEPS" fi -if [[ "$RUN_AUTH_SECURITY_REGRESSION" == "1" ]]; then -echo "=== [7/7] Auth Security Regression ===" - "$SCRIPT_DIR/regression-auth-security.sh" - echo "" -else - echo "=== [7/7] Auth Security Regression (skipped, RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION) ===" - echo "" -fi - -if [[ "$RUN_VERIFICATION_REGRESSION" == "1" ]]; then - echo "=== [8/11] Verification Regression ===" - "$SCRIPT_DIR/regression-verification.sh" - echo "" -else - echo "=== [8/11] Verification Regression (skipped, RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION) ===" - echo "" -fi - -if [[ "$RUN_DASHBOARD_REGRESSION" == "1" ]]; then - echo "=== [9/11] Dashboard Regression ===" - "$SCRIPT_DIR/regression-dashboard.sh" - echo "" -else - echo "=== [9/11] Dashboard Regression (skipped, RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION) ===" - echo "" -fi - -if [[ "$RUN_BUDGET_REGRESSION" == "1" ]]; then - echo "=== [10/11] Budget Regression ===" - "$SCRIPT_DIR/regression-budget.sh" - echo "" -else - echo "=== [10/11] Budget Regression (skipped, RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION) ===" - echo "" -fi - -if [[ "$RUN_INTEGRATIONS_REGRESSION" == "1" ]]; then - echo "=== [11/11] Integrations Regression ===" - "$SCRIPT_DIR/regression-integrations.sh" - echo "" -else - echo "=== [11/11] Integrations Regression (skipped, RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION) ===" - echo "" -fi - -if [[ "$RUN_FEATURE_FLAGS_REGRESSION" == "1" ]]; then - echo "=== [12/12] Feature Flags Regression ===" - "$SCRIPT_DIR/regression-feature-flags.sh" - echo "" -else - echo "=== [12/12] Feature Flags Regression (skipped, RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION) ===" - echo "" +if [[ "$FAILED_STEPS" -gt 0 ]]; then + echo "[ERROR] Regressions finished with failures (passed=$PASSED_STEPS failed=$FAILED_STEPS skipped=$SKIPPED_STEPS)" >&2 + exit 1 fi echo "[OK] All regressions passed" diff --git a/server/src/app.js b/server/src/app.js index a205f708..9a0eaaba 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -14,6 +14,7 @@ const staticPlugin = require("@fastify/static"); const { getConfig } = require("./config"); const authPlugin = require("./plugins/auth"); const prismaPlugin = require("./plugins/prisma"); +const swaggerDocsPlugin = require("./plugins/swagger"); const authRoutes = require("./routes/auth"); const meRoutes = require("./routes/me"); const reimbursementRoutes = require("./routes/reimbursements"); @@ -38,6 +39,7 @@ const travelOrdersRoutes = require("./routes/travelOrders"); const { startJobs } = require("./jobs/jobSystem"); const { initMetrics } = require("./observability"); const { AppError } = require("./utils/AppError"); +const { buildDataEnvelopeSchema, buildErrorEnvelopeSchema } = require("./utils/openapi"); async function buildApp() { const config = getConfig(); @@ -180,11 +182,57 @@ async function buildApp() { await app.register(prismaPlugin); await app.register(authPlugin); - app.get("/health", async (request, reply) => { - return reply.send({ requestId: request.id, data: { ok: true } }); - }); + if (config.nodeEnv !== "production" && config.openapiDocsEnabled) { + await app.register(swaggerDocsPlugin); + app.log.info("OpenAPI docs enabled at /api/docs"); + } - app.get("/ready", async (request, reply) => { + app.get( + "/health", + { + schema: { + summary: "Health check", + tags: ["system"], + response: { + 200: buildDataEnvelopeSchema({ + type: "object", + required: ["ok"], + properties: { + ok: { type: "boolean" }, + }, + }), + }, + }, + }, + async (request, reply) => { + return reply.send({ requestId: request.id, data: { ok: true } }); + }, + ); + + app.get( + "/ready", + { + schema: { + summary: "Readiness check", + tags: ["system"], + response: { + 200: buildDataEnvelopeSchema({ + type: "object", + required: ["ok"], + properties: { + ok: { type: "boolean" }, + }, + }), + 503: buildErrorEnvelopeSchema({ + description: "数据库不可用", + codeExample: "SYS_503", + messageExample: "Service Unavailable", + detailsExample: { db: "unreachable" }, + }), + }, + }, + }, + async (request, reply) => { try { await app.prisma.$queryRaw`SELECT 1`; return reply.send({ requestId: request.id, data: { ok: true } }); @@ -197,7 +245,8 @@ async function buildApp() { details: { db: "unreachable" }, }); } - }); + }, + ); if (config.metricsEnabled) { initMetrics(app); diff --git a/server/src/config.js b/server/src/config.js index 310cd628..92549c02 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -66,6 +66,7 @@ function getConfig() { s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "", s3ForcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true", serveClientDist: process.env.SERVE_CLIENT_DIST === "true", + openapiDocsEnabled: process.env.OPENAPI_DOCS_ENABLED === "true", clientDistDir: path.resolve(serverRoot, clientDistDir), signedUrlTtlSeconds: Number(process.env.SIGNED_URL_TTL_SECONDS ?? 600), maxUploadSizeBytes: Number(process.env.MAX_UPLOAD_SIZE_BYTES ?? 10 * 1024 * 1024), diff --git a/server/src/plugins/swagger.js b/server/src/plugins/swagger.js new file mode 100644 index 00000000..7d3e0271 --- /dev/null +++ b/server/src/plugins/swagger.js @@ -0,0 +1,37 @@ +const fp = require("fastify-plugin"); +const fastifySwagger = require("@fastify/swagger"); +const fastifySwaggerUi = require("@fastify/swagger-ui"); +const pkg = require("../../package.json"); + +async function swaggerPlugin(fastify) { + await fastify.register(fastifySwagger, { + openapi: { + openapi: "3.0.3", + info: { + title: "hyb-oa API", + description: "hyb-oa 后端接口文档(治理链路优先)", + version: pkg.version, + }, + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + }, + }); + + await fastify.register(fastifySwaggerUi, { + routePrefix: "/api/docs", + staticCSP: true, + uiConfig: { + docExpansion: "list", + deepLinking: false, + }, + }); +} + +module.exports = fp(swaggerPlugin, { name: "swagger-docs" }); diff --git a/server/src/routes/featureFlags.js b/server/src/routes/featureFlags.js index a1346f93..cd638404 100644 --- a/server/src/routes/featureFlags.js +++ b/server/src/routes/featureFlags.js @@ -3,6 +3,10 @@ const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { buildAuditLogData } = require("../utils/audit"); const { sendData } = require("../utils/reply"); +const { + buildDataEnvelopeSchema, + buildErrorEnvelopeSchema, +} = require("../utils/openapi"); const { FEATURE_FLAG_KEY_LIST, OVERRIDE_MODE_LIST, @@ -24,6 +28,49 @@ const UPDATE_BODY_SCHEMA = z.object({ expectedVersion: z.coerce.number().int().min(1), }); +const FEATURE_FLAG_ITEM_SCHEMA = { + type: "object", + required: [ + "id", + "flagKey", + "overrideMode", + "rolloutType", + "departmentIds", + "note", + "version", + "createdAt", + "createdBy", + "updatedAt", + "updatedBy", + "envEnabled", + "effectiveEnabled", + "effectiveReason", + "matchedDepartmentId", + "requiresDepartment", + ], + properties: { + id: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }, + flagKey: { type: "string", enum: FEATURE_FLAG_KEY_LIST }, + overrideMode: { type: "string", enum: OVERRIDE_MODE_LIST }, + rolloutType: { type: "string", enum: ROLLOUT_TYPE_LIST }, + departmentIds: { + type: "array", + items: { type: "string", format: "uuid" }, + }, + note: { type: "string" }, + version: { type: "integer" }, + createdAt: { anyOf: [{ type: "string", format: "date-time" }, { type: "null" }] }, + createdBy: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }, + updatedAt: { anyOf: [{ type: "string", format: "date-time" }, { type: "null" }] }, + updatedBy: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }, + envEnabled: { type: "boolean" }, + effectiveEnabled: { type: "boolean" }, + effectiveReason: { type: "string" }, + matchedDepartmentId: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }, + requiresDepartment: { type: "boolean" }, + }, +}; + function parseKey(params) { const parsed = KEY_PARAM_SCHEMA.safeParse(params ?? {}); if (!parsed.success) { @@ -66,6 +113,31 @@ async function featureFlagsRoutes(fastify) { "/system/feature-flags", { preHandler: fastify.requirePermission("system.settings"), + schema: { + tags: ["system", "feature-flags"], + summary: "获取功能开关列表", + security: [{ bearerAuth: [] }], + response: { + 200: buildDataEnvelopeSchema( + { + type: "object", + required: ["items"], + properties: { + items: { + type: "array", + items: FEATURE_FLAG_ITEM_SCHEMA, + }, + }, + }, + "功能开关列表", + ), + 403: buildErrorEnvelopeSchema({ + description: "无权限", + codeExample: "PERM_001", + messageExample: "无权限", + }), + }, + }, }, async (request, reply) => { const items = await listFeatureFlags(fastify.prisma, fastify.config); @@ -77,6 +149,64 @@ async function featureFlagsRoutes(fastify) { "/system/feature-flags/:key", { preHandler: fastify.requirePermission("system.settings"), + schema: { + tags: ["system", "feature-flags"], + summary: "更新功能开关", + security: [{ bearerAuth: [] }], + params: { + type: "object", + required: ["key"], + properties: { + key: { type: "string", enum: FEATURE_FLAG_KEY_LIST }, + }, + }, + body: { + type: "object", + required: ["overrideMode", "rolloutType", "departmentIds", "expectedVersion"], + properties: { + overrideMode: { type: "string", enum: OVERRIDE_MODE_LIST }, + rolloutType: { type: "string", enum: ROLLOUT_TYPE_LIST }, + departmentIds: { + type: "array", + items: { type: "string", format: "uuid" }, + }, + note: { type: "string", maxLength: 500 }, + expectedVersion: { type: "integer", minimum: 1 }, + }, + }, + response: { + 200: buildDataEnvelopeSchema( + { + type: "object", + required: ["item"], + properties: { + item: FEATURE_FLAG_ITEM_SCHEMA, + }, + }, + "更新后的功能开关", + ), + 400: buildErrorEnvelopeSchema({ + description: "参数校验失败", + codeExample: "VALID_001", + messageExample: "参数校验失败", + }), + 404: buildErrorEnvelopeSchema({ + description: "开关不存在", + codeExample: "SYS_404", + messageExample: "Not Found", + }), + 409: buildErrorEnvelopeSchema({ + description: "版本冲突", + codeExample: "VALID_001", + messageExample: "参数校验失败", + detailsExample: { + featureFlag: "version_conflict", + expectedVersion: 2, + currentVersion: 3, + }, + }), + }, + }, }, async (request, reply) => { const key = parseKey(request.params); diff --git a/server/src/routes/integrations.js b/server/src/routes/integrations.js index 5c9cce2f..a3f6e6c0 100644 --- a/server/src/routes/integrations.js +++ b/server/src/routes/integrations.js @@ -3,16 +3,74 @@ const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const { buildAuditLogData } = require("../utils/audit"); +const { + buildDataEnvelopeSchema, + buildErrorEnvelopeSchema, +} = require("../utils/openapi"); const { enqueueIntegrationEvent } = require("../services/integrations/outbox"); const { buildErpPayload } = require("../services/integrations/erp"); const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags"); const { runIntegrationOutboxOnce } = require("../jobs/integrationOutboxJob"); +const ERP_SYNC_RESPONSE_SCHEMA = { + type: "object", + required: ["outboxId", "status", "duplicated"], + properties: { + outboxId: { type: "string", format: "uuid" }, + status: { type: "string" }, + duplicated: { type: "boolean" }, + }, +}; + async function integrationsRoutes(fastify) { fastify.post( "/integrations/erp/sync/:reimbursementId", { preHandler: fastify.requirePermission("integration.manage"), + schema: { + tags: ["integrations"], + summary: "手动触发 ERP 同步", + security: [{ bearerAuth: [] }], + params: { + type: "object", + required: ["reimbursementId"], + properties: { + reimbursementId: { type: "string", format: "uuid" }, + }, + }, + querystring: { + type: "object", + properties: { + runNow: { + anyOf: [{ type: "string", enum: ["1", "true"] }, { type: "boolean" }], + }, + }, + additionalProperties: true, + }, + response: { + 200: buildDataEnvelopeSchema(ERP_SYNC_RESPONSE_SCHEMA, "ERP 同步任务已入队"), + 400: buildErrorEnvelopeSchema({ + description: "参数校验失败", + codeExample: "VALID_001", + messageExample: "参数校验失败", + }), + 404: buildErrorEnvelopeSchema({ + description: "报销单不存在", + codeExample: "SYS_404", + messageExample: "Not Found", + }), + 409: buildErrorEnvelopeSchema({ + description: "状态不满足或开关阻断", + codeExample: "VALID_001", + messageExample: "参数校验失败", + detailsExample: { + integration: "feature_flag_blocked", + featureFlag: "erp_sync", + reason: "department_not_allowed", + }, + }), + }, + }, }, async (request, reply) => { const parsedId = z.string().uuid().safeParse(request.params?.reimbursementId); @@ -106,7 +164,11 @@ async function integrationsRoutes(fastify) { }), }); - const runNow = request.query?.runNow === "1" || request.query?.runNow === 1 || request.query?.runNow === true || request.query?.runNow === "true"; + const runNow = + request.query?.runNow === "1" || + request.query?.runNow === 1 || + request.query?.runNow === true || + request.query?.runNow === "true"; if (runNow) { await runIntegrationOutboxOnce(fastify.prisma, fastify.config, fastify.log); } diff --git a/server/src/routes/invoicePool/verification.js b/server/src/routes/invoicePool/verification.js index 4127cb07..c165937a 100644 --- a/server/src/routes/invoicePool/verification.js +++ b/server/src/routes/invoicePool/verification.js @@ -2,6 +2,7 @@ const { z } = require("zod"); const { AppError } = require("../../utils/AppError"); const { sendData } = require("../../utils/reply"); +const { buildDataEnvelopeSchema, buildErrorEnvelopeSchema, UUID_PARAM_SCHEMA } = require("../../utils/openapi"); const { buildAuditLogData } = require("../../utils/audit"); const { getProvider, evaluateVerificationAccess } = require("../../services/verification"); @@ -9,6 +10,27 @@ const { mapVerificationRecord } = require("./shared/mappers"); const { parseVerificationListQuery } = require("./shared/schemas"); const { triggerVerificationJob } = require("./shared/helpers"); +const VERIFY_RESPONSE_SCHEMA = { + type: "object", + required: ["invoiceId", "verificationId", "provider", "triggerMode"], + properties: { + invoiceId: { type: "string", format: "uuid" }, + verificationId: { type: "string", format: "uuid" }, + provider: { type: "string" }, + triggerMode: { type: "string" }, + }, +}; + +const RETRY_RESPONSE_SCHEMA = { + type: "object", + required: ["invoiceId", "verificationId", "status"], + properties: { + invoiceId: { type: "string", format: "uuid" }, + verificationId: { type: "string", format: "uuid" }, + status: { type: "string", enum: ["pending"] }, + }, +}; + function throwVerificationDisabled(reason) { throw new AppError({ code: "VALID_001", @@ -89,6 +111,31 @@ function registerInvoiceVerificationRoutes(fastify) { "/invoice-pool/:id/verify", { preHandler: fastify.requirePermission("invoice.verify"), + schema: { + tags: ["invoice-verification"], + summary: "触发发票验真", + security: [{ bearerAuth: [] }], + params: UUID_PARAM_SCHEMA, + response: { + 200: buildDataEnvelopeSchema(VERIFY_RESPONSE_SCHEMA, "验真任务已创建"), + 400: buildErrorEnvelopeSchema({ + description: "参数校验失败", + codeExample: "VALID_001", + messageExample: "参数校验失败", + }), + 404: buildErrorEnvelopeSchema({ + description: "票据不存在", + codeExample: "SYS_404", + messageExample: "Not Found", + }), + 409: buildErrorEnvelopeSchema({ + description: "触发受限(开关/频控/并发)", + codeExample: "VALID_001", + messageExample: "参数校验失败", + detailsExample: { verification: "disabled", featureFlag: "env_hard_off" }, + }), + }, + }, }, async (request, reply) => { const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id); @@ -319,6 +366,31 @@ function registerInvoiceVerificationRoutes(fastify) { "/invoice-pool/:id/verification/retry", { preHandler: fastify.requirePermission("invoice.verify"), + schema: { + tags: ["invoice-verification"], + summary: "重试发票验真", + security: [{ bearerAuth: [] }], + params: UUID_PARAM_SCHEMA, + response: { + 200: buildDataEnvelopeSchema(RETRY_RESPONSE_SCHEMA, "验真重试任务已创建"), + 400: buildErrorEnvelopeSchema({ + description: "参数校验失败", + codeExample: "VALID_001", + messageExample: "参数校验失败", + }), + 404: buildErrorEnvelopeSchema({ + description: "票据不存在", + codeExample: "SYS_404", + messageExample: "Not Found", + }), + 409: buildErrorEnvelopeSchema({ + description: "重试受限(开关/频控/重试次数)", + codeExample: "VALID_001", + messageExample: "参数校验失败", + detailsExample: { verification: "too_frequent" }, + }), + }, + }, }, async (request, reply) => { const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id); diff --git a/server/src/utils/openapi.js b/server/src/utils/openapi.js new file mode 100644 index 00000000..80c8f59d --- /dev/null +++ b/server/src/utils/openapi.js @@ -0,0 +1,89 @@ +const REQUEST_ID_SCHEMA = { + type: "string", + description: "服务端生成的请求追踪ID", +}; + +const ERROR_DETAILS_SCHEMA = { + type: "object", + additionalProperties: true, +}; + +const ERROR_BODY_SCHEMA = { + type: "object", + required: ["code", "message", "details"], + properties: { + code: { type: "string" }, + message: { type: "string" }, + details: ERROR_DETAILS_SCHEMA, + }, +}; + +function buildDataEnvelopeSchema(dataSchema, description = "OK") { + return { + description, + type: "object", + required: ["requestId", "data"], + properties: { + requestId: REQUEST_ID_SCHEMA, + data: dataSchema, + }, + }; +} + +function buildErrorEnvelopeSchema({ + description = "业务错误", + codeExample = "VALID_001", + messageExample = "参数校验失败", + detailsExample, +} = {}) { + const schema = { + description, + type: "object", + required: ["requestId", "error"], + properties: { + requestId: REQUEST_ID_SCHEMA, + error: { + ...ERROR_BODY_SCHEMA, + properties: { + ...ERROR_BODY_SCHEMA.properties, + code: { + ...ERROR_BODY_SCHEMA.properties.code, + example: codeExample, + }, + message: { + ...ERROR_BODY_SCHEMA.properties.message, + example: messageExample, + }, + details: { + ...ERROR_DETAILS_SCHEMA, + }, + }, + }, + }, + }; + + if (detailsExample !== undefined) { + schema.properties.error.properties.details.example = detailsExample; + } + + return schema; +} + +const UUID_PARAM_SCHEMA = { + type: "object", + required: ["id"], + properties: { + id: { + type: "string", + format: "uuid", + }, + }, +}; + +module.exports = { + REQUEST_ID_SCHEMA, + ERROR_BODY_SCHEMA, + buildDataEnvelopeSchema, + buildErrorEnvelopeSchema, + UUID_PARAM_SCHEMA, +}; diff --git a/server/tests/unit/featureFlags.evaluate.test.js b/server/tests/unit/featureFlags.evaluate.test.js new file mode 100644 index 00000000..ec14bfe5 --- /dev/null +++ b/server/tests/unit/featureFlags.evaluate.test.js @@ -0,0 +1,201 @@ +const { + FEATURE_FLAG_KEYS, + evaluateFeatureFlag, + listFeatureFlags, +} = require("../../src/services/featureFlags"); + +function buildRecord({ + flagKey, + overrideMode = "force_on", + rolloutType = "global", + departmentIds = [], +}) { + const now = new Date("2026-02-12T00:00:00.000Z"); + return { + id: `flag-${flagKey}`, + flag_key: flagKey, + override_mode: overrideMode, + rollout_type: rolloutType, + department_ids: departmentIds, + note: "unit-test", + version: 1, + created_at: now, + created_by: null, + updated_at: now, + updated_by: null, + }; +} + +function buildPrisma(recordMap = {}) { + return { + featureFlag: { + findUnique: async ({ where }) => recordMap[where.flag_key] ?? null, + findMany: async () => Object.values(recordMap), + }, + }; +} + +describe("evaluateFeatureFlag", () => { + it("returns env_hard_off when env gate is closed", async () => { + const result = await evaluateFeatureFlag({ + prisma: buildPrisma(), + config: { erpSyncEnabled: false, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + }); + + expect(result.enabled).toBe(false); + expect(result.reason).toBe("env_hard_off"); + }); + + it("returns missing_flag_record when record does not exist", async () => { + const result = await evaluateFeatureFlag({ + prisma: buildPrisma(), + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + }); + + expect(result.enabled).toBe(false); + expect(result.reason).toBe("missing_flag_record"); + }); + + it("returns flag_force_off when override mode is force_off", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.erpSync]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.erpSync, + overrideMode: "force_off", + }), + }); + + const result = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + }); + + expect(result.enabled).toBe(false); + expect(result.reason).toBe("flag_force_off"); + }); + + it("returns global enabled when inherit + global", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.erpSync]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.erpSync, + overrideMode: "inherit", + rolloutType: "global", + }), + }); + + const result = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + }); + + expect(result.enabled).toBe(true); + expect(result.reason).toBe("env_inherit_global_on"); + }); + + it("returns allowlist guards for department rollout", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.erpSync]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.erpSync, + rolloutType: "department_allowlist", + departmentIds: ["dept-a"], + }), + }); + + const missingDepartment = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + strictDepartment: true, + }); + + const blockedDepartment = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + departmentId: "dept-b", + strictDepartment: true, + }); + + const allowedDepartment = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.erpSync, + departmentId: "dept-a", + strictDepartment: true, + }); + + expect(missingDepartment.reason).toBe("department_missing"); + expect(blockedDepartment.reason).toBe("department_not_allowed"); + expect(allowedDepartment.reason).toBe("flag_force_on_department_allowed"); + expect(allowedDepartment.enabled).toBe(true); + }); + + it("returns department_deferred when strictDepartment is false", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.invoiceVerification]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.invoiceVerification, + rolloutType: "department_allowlist", + departmentIds: ["dept-a"], + }), + }); + + const result = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.invoiceVerification, + strictDepartment: false, + }); + + expect(result.enabled).toBe(true); + expect(result.reason).toBe("department_deferred"); + }); + + it("returns department_allowlist_empty when allowlist has no items", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.invoiceVerification]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.invoiceVerification, + rolloutType: "department_allowlist", + departmentIds: [], + }), + }); + + const result = await evaluateFeatureFlag({ + prisma, + config: { erpSyncEnabled: true, invoiceVerificationEnabled: true }, + flagKey: FEATURE_FLAG_KEYS.invoiceVerification, + strictDepartment: true, + }); + + expect(result.enabled).toBe(false); + expect(result.reason).toBe("department_allowlist_empty"); + }); +}); + +describe("listFeatureFlags", () => { + it("returns all known keys even if part of records are missing", async () => { + const prisma = buildPrisma({ + [FEATURE_FLAG_KEYS.erpSync]: buildRecord({ + flagKey: FEATURE_FLAG_KEYS.erpSync, + overrideMode: "inherit", + }), + }); + + const items = await listFeatureFlags(prisma, { + erpSyncEnabled: true, + invoiceVerificationEnabled: true, + }); + + expect(items).toHaveLength(2); + expect(items.map((item) => item.flagKey).sort()).toEqual([ + FEATURE_FLAG_KEYS.erpSync, + FEATURE_FLAG_KEYS.invoiceVerification, + ]); + + const missingItem = items.find((item) => item.flagKey === FEATURE_FLAG_KEYS.invoiceVerification); + expect(missingItem.id).toBeNull(); + expect(missingItem.effectiveEnabled).toBe(false); + }); +}); diff --git a/server/tests/unit/featureFlags.sanitize.test.js b/server/tests/unit/featureFlags.sanitize.test.js new file mode 100644 index 00000000..67bb9853 --- /dev/null +++ b/server/tests/unit/featureFlags.sanitize.test.js @@ -0,0 +1,18 @@ +const { sanitizeDepartmentIds } = require("../../src/services/featureFlags"); + +describe("sanitizeDepartmentIds", () => { + it("returns empty array when input is not array", () => { + expect(sanitizeDepartmentIds(null)).toEqual([]); + expect(sanitizeDepartmentIds("dept")).toEqual([]); + }); + + it("trims ids, removes empty values and deduplicates", () => { + const value = [" dept-a ", "dept-b", "", "dept-a", " "]; + expect(sanitizeDepartmentIds(value)).toEqual(["dept-a", "dept-b"]); + }); + + it("filters non-string items", () => { + const value = ["dept-a", 1, true, { id: "dept-b" }, "dept-c"]; + expect(sanitizeDepartmentIds(value)).toEqual(["dept-a", "dept-c"]); + }); +}); diff --git a/server/tests/unit/integrations.dispatcher.test.js b/server/tests/unit/integrations.dispatcher.test.js new file mode 100644 index 00000000..d7adb89b --- /dev/null +++ b/server/tests/unit/integrations.dispatcher.test.js @@ -0,0 +1,106 @@ +const { + IntegrationDispatchError, + dispatchOutboxEvent, +} = require("../../src/services/integrations/dispatcher"); + +describe("dispatchOutboxEvent", () => { + it("handles mock ERP success", async () => { + const result = await dispatchOutboxEvent({ + config: { + erpSyncEnabled: true, + erpSyncProvider: "mock", + }, + eventType: "erp.reimbursement.paid", + payload: { + reimbursementId: "rb-1", + formNumber: "BX-001", + }, + }); + + expect(result.channel).toBe("erp"); + expect(result.code).toBe("mock_synced"); + expect(result.response.externalId).toContain("BX-001"); + }); + + it("maps ERP disabled as non-retryable dispatch error", async () => { + await expect( + dispatchOutboxEvent({ + config: { + erpSyncEnabled: false, + erpSyncProvider: "mock", + }, + eventType: "erp.reimbursement.paid", + payload: { + reimbursementId: "rb-2", + formNumber: "BX-002", + }, + }), + ).rejects.toMatchObject({ + name: "IntegrationDispatchError", + code: "erp_sync_disabled", + retryable: false, + }); + }); + + it("maps invalid ERP provider as non-retryable dispatch error", async () => { + await expect( + dispatchOutboxEvent({ + config: { + erpSyncEnabled: true, + erpSyncProvider: "invalid-provider", + }, + eventType: "erp.reimbursement.paid", + payload: { + reimbursementId: "rb-3", + formNumber: "BX-003", + }, + }), + ).rejects.toMatchObject({ + name: "IntegrationDispatchError", + code: "erp_sync_provider_invalid", + retryable: false, + }); + }); + + it("handles IM notification outbox", async () => { + const result = await dispatchOutboxEvent({ + config: {}, + eventType: "notification.im", + payload: { + channelType: "wecom", + }, + }); + + expect(result.channel).toBe("im"); + expect(result.code).toBe("mock_sent"); + expect(result.response.channelType).toBe("wecom"); + }); + + it("throws non-retryable error for unsupported event", async () => { + await expect( + dispatchOutboxEvent({ + config: { erpSyncEnabled: true, erpSyncProvider: "mock" }, + eventType: "unsupported.event", + payload: {}, + }), + ).rejects.toMatchObject({ + name: "IntegrationDispatchError", + code: "unsupported_outbox_event", + retryable: false, + }); + }); + + it("preserves error class semantics", () => { + const error = new IntegrationDispatchError({ + code: "unit_test", + message: "unit test", + retryable: true, + statusCode: 503, + }); + + expect(error).toBeInstanceOf(Error); + expect(error.code).toBe("unit_test"); + expect(error.retryable).toBe(true); + expect(error.statusCode).toBe(503); + }); +}); diff --git a/server/tests/unit/verification.access.test.js b/server/tests/unit/verification.access.test.js new file mode 100644 index 00000000..b53af9ac --- /dev/null +++ b/server/tests/unit/verification.access.test.js @@ -0,0 +1,123 @@ +const { + FEATURE_FLAG_KEYS, +} = require("../../src/services/featureFlags"); +const { + evaluateVerificationAccess, + verifyInvoiceByProvider, +} = require("../../src/services/verification"); + +function buildFlagRecord({ + overrideMode = "force_on", + rolloutType = "global", + departmentIds = [], +}) { + const now = new Date("2026-02-12T00:00:00.000Z"); + return { + id: "verification-flag", + flag_key: FEATURE_FLAG_KEYS.invoiceVerification, + override_mode: overrideMode, + rollout_type: rolloutType, + department_ids: departmentIds, + note: "unit-test", + version: 1, + created_at: now, + created_by: null, + updated_at: now, + updated_by: null, + }; +} + +function buildPrisma(record) { + return { + featureFlag: { + findUnique: async () => record, + }, + }; +} + +describe("evaluateVerificationAccess", () => { + it("returns env_hard_off when env gate is closed", async () => { + const result = await evaluateVerificationAccess({ + prisma: buildPrisma(buildFlagRecord({})), + config: { + erpSyncEnabled: true, + invoiceVerificationEnabled: false, + }, + departmentId: "dept-a", + strictDepartment: true, + }); + + expect(result.enabled).toBe(false); + expect(result.reason).toBe("env_hard_off"); + }); + + it("returns allowlist decision for department rollout", async () => { + const prisma = buildPrisma( + buildFlagRecord({ rolloutType: "department_allowlist", departmentIds: ["dept-a"] }), + ); + + const blocked = await evaluateVerificationAccess({ + prisma, + config: { + erpSyncEnabled: true, + invoiceVerificationEnabled: true, + }, + departmentId: "dept-b", + strictDepartment: true, + }); + + const allowed = await evaluateVerificationAccess({ + prisma, + config: { + erpSyncEnabled: true, + invoiceVerificationEnabled: true, + }, + departmentId: "dept-a", + strictDepartment: true, + }); + + expect(blocked.enabled).toBe(false); + expect(blocked.reason).toBe("department_not_allowed"); + expect(allowed.enabled).toBe(true); + expect(allowed.reason).toBe("flag_force_on_department_allowed"); + }); +}); + +describe("verifyInvoiceByProvider", () => { + const invoice = { + invoice_code: "INV-001", + invoice_number: "NO-001", + total_amount: "100.00", + remarks: "ok", + }; + + it("uses mock provider and returns passed", async () => { + const result = await verifyInvoiceByProvider({ + config: { verificationProvider: "mock" }, + invoice, + }); + + expect(result.status).toBe("passed"); + expect(result.resultCode).toBe("verified"); + }); + + it("uses tax provider and returns provider-not-configured", async () => { + const result = await verifyInvoiceByProvider({ + config: { verificationProvider: "tax" }, + invoice, + }); + + expect(result.status).toBe("error"); + expect(result.resultCode).toBe("tax_provider_not_configured"); + }); + + it("returns unsupported provider error", async () => { + const result = await verifyInvoiceByProvider({ + config: { verificationProvider: "unknown" }, + invoice, + }); + + expect(result.status).toBe("error"); + expect(result.resultCode).toBe("provider_unsupported"); + }); +}); diff --git a/server/tests/unit/verification.provider.test.js b/server/tests/unit/verification.provider.test.js new file mode 100644 index 00000000..459c89e7 --- /dev/null +++ b/server/tests/unit/verification.provider.test.js @@ -0,0 +1,12 @@ +const { getProvider } = require("../../src/services/verification"); + +describe("getProvider", () => { + it("uses mock as default provider", () => { + expect(getProvider({})).toBe("mock"); + }); + + it("normalizes case and whitespace", () => { + expect(getProvider({ verificationProvider: " TAX " })).toBe("tax"); + expect(getProvider({ verificationProvider: "MoCk" })).toBe("mock"); + }); +}); diff --git a/server/vitest.config.cjs b/server/vitest.config.cjs new file mode 100644 index 00000000..d49b3468 --- /dev/null +++ b/server/vitest.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + test: { + environment: "node", + globals: true, + include: ["tests/unit/**/*.test.js"], + coverage: { + provider: "v8", + reportsDirectory: "coverage", + reporter: ["text", "json-summary", "html"], + }, + }, +};