From 71f5cfb3763e6e110d281c591bb61c890cd949ec Mon Sep 17 00:00:00 2001 From: Xianquan Date: Mon, 3 Aug 2026 16:01:36 +0800 Subject: [PATCH 01/14] chore: bump deps --- .node-version | 1 + package.json | 2 +- pnpm-lock.yaml | 1811 ++++++++++--------------------------------- pnpm-workspace.yaml | 4 +- pro | 2 +- 5 files changed, 402 insertions(+), 1418 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 000000000000..60ade1ae01e8 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.19.0 diff --git a/package.json b/package.json index 9e516f025636..e04e7275dacd 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,5 @@ "engines": { "node": ">=22.23.2" }, - "packageManager": "pnpm@10.33.4" + "packageManager": "pnpm@10.34.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22d538a5d701..35282835f0b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,9 +72,12 @@ catalogs: '@types/proxy-addr': specifier: 2.0.3 version: 2.0.3 + '@types/request-ip': + specifier: ^0.0.38 + version: 0.0.38 '@vitest/coverage-v8': specifier: ^4.1.5 - version: 4.1.5 + version: 4.1.10 '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0 @@ -116,7 +119,7 @@ catalogs: version: 23.16.8 ipaddr.js: specifier: ^2.4.0 - version: 2.4.0 + version: 2.5.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -145,14 +148,14 @@ catalogs: specifier: 8.0.7 version: 8.0.7 mongodb-memory-server: - specifier: ^10.1.4 - version: 10.1.4 + specifier: ^10.4.3 + version: 10.4.3 mongoose: - specifier: ^8.22.1 - version: 8.23.1 + specifier: ^8.24.2 + version: 8.24.2 nanoid: specifier: ^5.1.3 - version: 5.1.5 + version: 5.1.16 next: specifier: 16.3.0 version: 16.3.0 @@ -161,7 +164,7 @@ catalogs: version: 15.4.2 postcss: specifier: ^8.5.12 - version: 8.5.22 + version: 8.5.26 proxy-addr: specifier: 2.0.7 version: 2.0.7 @@ -186,6 +189,9 @@ catalogs: remend: specifier: ^1.3.0 version: 1.3.0 + request-ip: + specifier: ^3.3.0 + version: 3.3.0 tsdown: specifier: 0.21.10 version: 0.21.10 @@ -206,10 +212,10 @@ catalogs: version: 1.1.2 vitest: specifier: ^4.1.5 - version: 4.1.5 + version: 4.1.10 zod: specifier: ^4 - version: 4.1.12 + version: 4.4.3 lint: eslint: specifier: ^9 @@ -259,7 +265,7 @@ importers: version: 24.13.3 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.5) + version: 4.1.10(vitest@4.1.10) eslint: specifier: catalog:lint version: 9.39.4(jiti@2.7.0) @@ -280,7 +286,7 @@ importers: version: 16.4.0 mongodb-memory-server: specifier: 'catalog:' - version: 10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) next-i18next: specifier: 'catalog:' version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -301,7 +307,7 @@ importers: version: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) document: dependencies: @@ -395,7 +401,7 @@ importers: version: 18.3.0 postcss: specifier: 'catalog:' - version: 8.5.22 + version: 8.5.26 tailwindcss: specifier: ^4.1.11 version: 4.2.4 @@ -428,7 +434,7 @@ importers: version: 5.11.1 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/node': specifier: 'catalog:' @@ -438,7 +444,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) packages/global: dependencies: @@ -450,7 +456,7 @@ importers: version: 1.2.8 '@fastgpt-plugin/sdk-client': specifier: 0.1.0-alpha.5 - version: 0.1.0-alpha.5(zod@4.1.12) + version: 0.1.0-alpha.5(zod@4.4.3) '@fastgpt-sdk/sandbox-adapter': specifier: workspace:* version: link:../../sdk/sandbox-adapter @@ -471,7 +477,7 @@ importers: version: 0.1.13 ipaddr.js: specifier: 'catalog:' - version: 2.4.0 + version: 2.5.0 js-yaml: specifier: 'catalog:' version: 4.1.1 @@ -486,10 +492,10 @@ importers: version: 4.18.1 nanoid: specifier: 'catalog:' - version: 5.1.5 + version: 5.1.16 openai: specifier: 6.34.0 - version: 6.34.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12) + version: 6.34.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -498,10 +504,10 @@ importers: version: 3.1.0 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 zod-openapi: specifier: ^5.4.6 - version: 5.4.6(zod@4.1.12) + version: 5.4.6(zod@4.4.3) devDependencies: '@types/js-yaml': specifier: 'catalog:' @@ -547,16 +553,16 @@ importers: version: 2.0.8 '@mariozechner/pi-agent-core': specifier: ^0.67.3 - version: 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12) + version: 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3) '@mariozechner/pi-ai': specifier: ^0.67.3 - version: 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12) + version: 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3) '@maxmind/geoip2-node': specifier: ^6.3.4 version: 6.3.4 '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.26.0(zod@4.1.12) + version: 1.26.0(zod@4.4.3) '@node-rs/jieba': specifier: 'catalog:' version: 2.0.1 @@ -565,7 +571,7 @@ importers: version: 1.9.0 '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) '@xmldom/xmldom': specifier: ^0.8.10 version: 0.8.10 @@ -616,7 +622,7 @@ importers: version: 0.6.3 ipaddr.js: specifier: 'catalog:' - version: 2.4.0 + version: 2.5.0 joplin-turndown-plugin-gfm: specifier: ^1.0.12 version: 1.0.12 @@ -652,7 +658,7 @@ importers: version: 8.0.7 mongoose: specifier: 'catalog:' - version: 8.23.1(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) multer: specifier: 2.2.0 version: 2.2.0 @@ -706,7 +712,7 @@ importers: version: 3.4.0 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/async-retry': specifier: ^1.4.9 @@ -975,10 +981,10 @@ importers: version: 1.14.3 '@scalar/api-reference-react': specifier: ^0.9.59 - version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) + version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) '@tanstack/react-query': specifier: 'catalog:' version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1000,6 +1006,9 @@ importers: assert: specifier: ^2.0.0 version: 2.1.0 + autoprefixer: + specifier: ^10.4.19 + version: 10.5.4(postcss@8.5.26) axios: specifier: 'catalog:' version: 1.18.1 @@ -1056,10 +1065,10 @@ importers: version: 4.18.1 mongoose: specifier: 'catalog:' - version: 8.23.1(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) nanoid: specifier: 'catalog:' - version: 5.1.5 + version: 5.1.16 next: specifier: 'catalog:' version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) @@ -1117,6 +1126,9 @@ importers: sass: specifier: ^1.58.3 version: 1.85.1 + tailwindcss: + specifier: ^3.0 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.4) utf-8-validate: specifier: ^5.0.10 version: 5.0.10 @@ -1125,7 +1137,7 @@ importers: version: 0.6.2 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@faker-js/faker': specifier: ^9.0.3 @@ -1171,13 +1183,13 @@ importers: version: 0.25.12 eslint: specifier: catalog:lint - version: 9.39.4(jiti@2.7.0) + version: 9.39.4(jiti@1.21.7) eslint-config-next: specifier: catalog:lint - version: 16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 16.3.0(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) mongodb-memory-server: specifier: 'catalog:' - version: 10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) tsx: specifier: 'catalog:' version: 4.20.6 @@ -1186,7 +1198,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) pro/browser-sandbox: dependencies: @@ -1195,16 +1207,16 @@ importers: version: link:../../sdk/otel '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.26.0(zod@4.1.12) + version: 1.26.0(zod@4.4.3) '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) express: specifier: 'catalog:' version: 4.22.1 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/express': specifier: ^5.0.1 @@ -1214,7 +1226,7 @@ importers: version: 24.13.3 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.5) + version: 4.1.10(vitest@4.1.10) tsdown: specifier: 'catalog:' version: 0.21.10(typescript@6.0.3) @@ -1226,7 +1238,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) pro/llm_benchmark/content_benchmark: dependencies: @@ -1241,7 +1253,7 @@ importers: version: 3.4.0 nanoid: specifier: 'catalog:' - version: 5.1.5 + version: 5.1.16 devDependencies: '@types/node': specifier: 'catalog:' @@ -1254,7 +1266,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) pro/sso: dependencies: @@ -1366,7 +1378,7 @@ importers: version: 2.0.8 '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.26.0(zod@4.1.12) + version: 1.26.0(zod@4.4.3) '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1375,10 +1387,10 @@ importers: version: 2.0.1 '@scalar/api-reference-react': specifier: ^0.9.59 - version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12) + version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) '@tanstack/react-query': specifier: 'catalog:' version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1465,10 +1477,10 @@ importers: version: 8.0.7 mongoose: specifier: 'catalog:' - version: 8.23.1(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) nanoid: specifier: 'catalog:' - version: 5.1.5 + version: 5.1.16 next: specifier: 'catalog:' version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) @@ -1555,7 +1567,7 @@ importers: version: 6.0.3 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@next/bundle-analyzer': specifier: 16.1.6 @@ -1595,10 +1607,10 @@ importers: version: 15.5.13 eslint: specifier: catalog:lint - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: catalog:lint - version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) tailwindcss: specifier: ^3 version: 3.4.18(tsx@4.20.6)(yaml@2.8.4) @@ -1610,10 +1622,10 @@ importers: version: 6.0.3 typescript-eslint: specifier: catalog:lint - version: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + version: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) projects/code-sandbox: dependencies: @@ -1628,7 +1640,7 @@ importers: version: 2.0.12(hono@4.12.27) '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) acorn: specifier: ^8.15.0 version: 8.15.0 @@ -1661,14 +1673,14 @@ importers: version: 9.0.1 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/node': specifier: 'catalog:' version: 24.13.3 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.5) + version: 4.1.10(vitest@4.1.10) lodash: specifier: 4.17.23 version: 4.17.23 @@ -1683,7 +1695,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) projects/marketplace: dependencies: @@ -1725,7 +1737,7 @@ importers: version: link:../../packages/web '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) axios: specifier: 'catalog:' version: 1.18.1 @@ -1734,7 +1746,7 @@ importers: version: 23.16.8 mongoose: specifier: 'catalog:' - version: 8.23.1(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + version: 8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) next: specifier: 'catalog:' version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) @@ -1752,7 +1764,7 @@ importers: version: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@svgr/webpack': specifier: 'catalog:' @@ -1774,7 +1786,7 @@ importers: version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) postcss: specifier: 'catalog:' - version: 8.5.22 + version: 8.5.26 tailwindcss: specifier: ^3.4.1 version: 3.4.18(tsx@4.20.6)(yaml@2.8.4) @@ -1792,10 +1804,10 @@ importers: version: link:../../packages/global '@modelcontextprotocol/sdk': specifier: 'catalog:' - version: 1.26.0(zod@4.1.12) + version: 1.26.0(zod@4.4.3) '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + version: 0.13.10(typescript@6.0.3)(zod@4.4.3) chalk: specifier: 'catalog:' version: 5.6.2 @@ -1810,7 +1822,7 @@ importers: version: 4.22.1 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/express': specifier: ^5.0.1 @@ -1832,7 +1844,7 @@ importers: version: 7.29.0 zod: specifier: 'catalog:' - version: 4.1.12 + version: 4.4.3 devDependencies: '@types/node': specifier: 'catalog:' @@ -1848,7 +1860,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) scripts/icon: dependencies: @@ -1919,7 +1931,7 @@ importers: version: 24.13.3 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.5(vitest@4.1.5) + version: 4.1.10(vitest@4.1.10) tsdown: specifier: 'catalog:' version: 0.21.10(typescript@6.0.3) @@ -1928,7 +1940,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) sdk/storage: dependencies: @@ -2408,10 +2420,6 @@ packages: resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -2448,11 +2456,6 @@ packages: resolution: {integrity: sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.8': resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} @@ -2905,10 +2908,6 @@ packages: resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.8': resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} @@ -3268,21 +3267,12 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/runtime@1.11.2': resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -4406,9 +4396,6 @@ packages: react: ^18 react-dom: ^18 - '@mongodb-js/saslprep@1.2.0': - resolution: {integrity: sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==} - '@mongodb-js/saslprep@1.4.11': resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==} @@ -5691,277 +5678,139 @@ packages: '@rolldown/pluginutils@1.0.0-rc.17': resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.62.4': resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.62.4': resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.62.4': resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.62.4': resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.62.4': resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.4': resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} - cpu: [arm] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} - cpu: [arm] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm-musleabihf@4.62.4': resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm64-gnu@4.62.4': resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-musl@4.62.4': resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-loong64-gnu@4.62.4': resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-loong64-musl@4.62.4': resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.62.4': resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} - cpu: [ppc64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-ppc64-musl@4.62.4': resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.62.4': resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} - cpu: [riscv64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-riscv64-musl@4.62.4': resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.62.4': resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.4': resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} - cpu: [x64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-x64-musl@4.62.4': resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} - cpu: [x64] - os: [openbsd] - '@rollup/rollup-openbsd-x64@4.62.4': resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} - cpu: [arm64] - os: [openharmony] - '@rollup/rollup-openharmony-arm64@4.62.4': resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.62.4': resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.4': resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.4': resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.4': resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} cpu: [x64] @@ -6677,9 +6526,6 @@ packages: cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -6929,9 +6775,6 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.17.24': - resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==} - '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -7142,21 +6985,9 @@ packages: '@vitest/browser': optional: true - '@vitest/coverage-v8@4.1.5': - resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} - peerDependencies: - '@vitest/browser': 4.1.5 - vitest: 4.1.5 - peerDependenciesMeta: - '@vitest/browser': - optional: true - '@vitest/expect@4.1.10': resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/expect@4.1.5': - resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.1.10': resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: @@ -7168,47 +6999,21 @@ packages: vite: optional: true - '@vitest/mocker@4.1.5': - resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/pretty-format@4.1.10': resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/pretty-format@4.1.5': - resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.10': resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/runner@4.1.5': - resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.1.10': resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/snapshot@4.1.5': - resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.1.10': resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/spy@4.1.5': - resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - '@vitest/utils@4.1.5': - resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} - '@vladfrangu/async_event_emitter@2.4.7': resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -7624,6 +7429,13 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} + autoprefixer@10.5.4: + resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -7818,15 +7630,11 @@ packages: browser-or-node@2.1.1: resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bson@6.10.3: - resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==} - engines: {node: '>=16.20.1'} - bson@6.10.4: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} @@ -8886,8 +8694,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.341: - resolution: {integrity: sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==} + electron-to-chromium@1.5.403: + resolution: {integrity: sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -8976,9 +8784,6 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -9211,9 +9016,6 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -9224,10 +9026,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.1: - resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} - engines: {node: '>=18.0.0'} - eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} @@ -9244,10 +9042,6 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -9313,9 +9107,6 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - fast-xml-builder@1.3.1: resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} @@ -9443,9 +9234,6 @@ packages: flatted@2.0.2: resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -9529,6 +9317,9 @@ packages: resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} engines: {node: '>=0.8'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + framer-motion@9.1.7: resolution: {integrity: sha512-nKxBkIO4IPkMEqcBbbATxsVjwPYShKl051yhBv9628iAH6JLeHD0siBHxkL62oQzMC1+GNX73XtPjgP753ufuw==} peerDependencies: @@ -9714,12 +9505,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - get-tsconfig@4.14.3: resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} @@ -9959,10 +9744,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.11.7: - resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} - engines: {node: '>=16.9.0'} - hono@4.12.27: resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} @@ -10201,10 +9982,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - ipaddr.js@2.4.0: - resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} - engines: {node: '>= 10'} - ipaddr.js@2.5.0: resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} engines: {node: '>= 10'} @@ -11537,41 +11314,14 @@ packages: mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - mongodb-memory-server-core@10.1.4: - resolution: {integrity: sha512-o8fgY7ZalEd8pGps43fFPr/hkQu1L8i6HFEGbsTfA2zDOW0TopgpswaBCqDr0qD7ptibyPfB5DmC+UlIxbThzA==} + mongodb-memory-server-core@10.4.3: + resolution: {integrity: sha512-IPjlw73IoSYopnqBibQKxmAXMbOEPf5uGAOsBcaUiNH/TOI7V19WO+K7n5KYtnQ9FqzLGLpvwCGuPOTBSg4s5Q==} engines: {node: '>=16.20.1'} - mongodb-memory-server@10.1.4: - resolution: {integrity: sha512-+oKQ/kc3CX+816oPFRtaF0CN4vNcGKNjpOQe4bHo/21A3pMD+lC7Xz1EX5HP7siCX4iCpVchDMmCOFXVQSGkUg==} + mongodb-memory-server@10.4.3: + resolution: {integrity: sha512-CDZvFisXvGIigsIw5gqH6r9NI/zxGa/uRdutgUL/isuJh+inj0YXb7Ykw6oFMFzqgTJWb7x0I5DpzrqCstBWpg==} engines: {node: '>=16.20.1'} - mongodb@6.14.2: - resolution: {integrity: sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - mongodb@6.20.0: resolution: {integrity: sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==} engines: {node: '>=16.20.1'} @@ -11599,8 +11349,8 @@ packages: socks: optional: true - mongoose@8.23.1: - resolution: {integrity: sha512-gHSPD8qEwRmiXapK17hEnFWZdcFENMegHTcw5XIIg2+7R8eXQvdwSiMpD/A2oG8tKzFLLHyRXd8/eaDPAVwZgQ==} + mongoose@8.24.2: + resolution: {integrity: sha512-5+H3MSHNJCcr9M+lVplekrZ4/Dyn3N1dOpvgn9gMwvHJhunc4G8SQcBVd/btBQoVdspVNPjx8Pw03YWBv6uTJg==} engines: {node: '>=16.20.1'} mpath@0.9.0: @@ -11654,11 +11404,6 @@ packages: resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} engines: {node: '>=12.0.0'} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -11669,11 +11414,6 @@ packages: engines: {node: ^18 || >=20} hasBin: true - nanoid@5.1.5: - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} - engines: {node: ^18 || >=20} - hasBin: true - napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -11688,8 +11428,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} engines: {node: '>= 0.4.0'} neverpanic@0.0.8: @@ -11812,8 +11552,9 @@ packages: resolution: {integrity: sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==} engines: {node: '>=8.0.0'} - node-releases@2.0.37: - resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} nodemailer@7.0.13: resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} @@ -11903,9 +11644,6 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -12174,10 +11912,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-expression-matcher@1.6.2: resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} engines: {node: '>=14.0.0'} @@ -12292,10 +12026,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -12418,10 +12148,6 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.22: - resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.23: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} @@ -13171,11 +12897,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.62.4: resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -13288,16 +13009,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -13581,9 +13292,6 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -13743,9 +13451,6 @@ packages: strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - strnum@2.2.3: - resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} - strnum@2.4.2: resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==} @@ -13984,10 +13689,6 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} - engines: {node: '>=18'} - tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} @@ -13996,18 +13697,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - tinyrainbow@3.1.1: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} @@ -14063,10 +13756,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.0: - resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} - engines: {node: '>=18'} - tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -14280,9 +13969,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -14292,10 +13978,6 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} - engines: {node: '>=20.18.1'} - undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} @@ -14579,8 +14261,8 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - vite@6.2.2: - resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -14619,104 +14301,23 @@ packages: yaml: optional: true - vite@6.4.3: - resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - vitest@4.1.5: - resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.5 - '@vitest/browser-preview': 4.1.5 - '@vitest/browser-webdriverio': 4.1.5 - '@vitest/coverage-istanbul': 4.1.5 - '@vitest/coverage-v8': 4.1.5 - '@vitest/ui': 4.1.5 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -14815,10 +14416,6 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - whatwg-url@14.1.1: - resolution: {integrity: sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==} - engines: {node: '>=18'} - whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -15062,10 +14659,6 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yauzl@3.2.0: - resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} - engines: {node: '>=12'} - yauzl@3.4.0: resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} @@ -15158,28 +14751,28 @@ packages: snapshots: - '@ai-sdk/gateway@3.0.13(zod@4.1.12)': + '@ai-sdk/gateway@3.0.13(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.1.12) + '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) '@vercel/oidc': 3.1.0 - zod: 4.1.12 + zod: 4.4.3 - '@ai-sdk/provider-utils@4.0.5(zod@4.1.12)': + '@ai-sdk/provider-utils@4.0.5(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.2 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 - zod: 4.1.12 + zod: 4.4.3 '@ai-sdk/provider@3.0.2': dependencies: json-schema: 0.4.0 - '@ai-sdk/vue@3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.1.12)': + '@ai-sdk/vue@3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.4.3)': dependencies: - '@ai-sdk/provider-utils': 4.0.5(zod@4.1.12) - ai: 6.0.33(zod@4.1.12) + '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) + ai: 6.0.33(zod@4.4.3) swrv: 1.2.0(vue@3.5.40(typescript@6.0.3)) vue: 3.5.40(typescript@6.0.3) transitivePeerDependencies: @@ -15188,7 +14781,7 @@ snapshots: '@alibaba-group/opensandbox@0.1.10': dependencies: openapi-fetch: 0.14.1 - undici: 7.28.0 + undici: 7.29.0 '@alicloud/credentials@2.4.5': dependencies: @@ -15294,11 +14887,11 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 - '@anthropic-ai/sdk@0.73.0(zod@4.1.12)': + '@anthropic-ai/sdk@0.73.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.1.12 + zod: 4.4.3 '@apidevtools/json-schema-ref-parser@11.7.2': dependencies: @@ -15952,10 +15545,10 @@ snapshots: '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helpers': 7.26.10 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 '@babel/template': 7.26.9 '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -15966,16 +15559,16 @@ snapshots: '@babel/generator@7.26.10': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@8.0.0-rc.3': dependencies: - '@babel/parser': 8.0.0-rc.3 - '@babel/types': 8.0.0-rc.3 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@types/jsesc': 2.5.1 @@ -15983,13 +15576,13 @@ snapshots: '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/helper-compilation-targets@7.26.5': dependencies: '@babel/compat-data': 7.26.8 '@babel/helper-validator-option': 7.25.9 - browserslist: 4.28.2 + browserslist: 4.28.7 lru-cache: 5.1.1 semver: 6.3.1 @@ -16027,14 +15620,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -16042,14 +15635,14 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/helper-plugin-utils@7.26.5': {} @@ -16074,12 +15667,10 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-string-parser@8.0.0': {} @@ -16098,18 +15689,14 @@ snapshots: dependencies: '@babel/template': 7.26.9 '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helpers@7.26.10': dependencies: '@babel/template': 7.26.9 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.2': - dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/parser@7.29.8': dependencies: @@ -16117,7 +15704,7 @@ snapshots: '@babel/parser@8.0.0-rc.3': dependencies: - '@babel/types': 8.0.0-rc.3 + '@babel/types': 8.0.4 '@babel/parser@8.0.4': dependencies: @@ -16350,7 +15937,7 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -16463,7 +16050,7 @@ snapshots: '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.26.5 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -16631,7 +16218,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 esutils: 2.0.3 '@babel/preset-react@7.26.3(@babel/core@7.26.10)': @@ -16664,26 +16251,21 @@ snapshots: '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@babel/traverse@7.26.10': dependencies: '@babel/code-frame': 7.26.2 '@babel/generator': 7.26.10 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 '@babel/template': 7.26.9 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -16692,7 +16274,7 @@ snapshots: '@babel/types@8.0.0-rc.3': dependencies: '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.4 '@babel/types@8.0.4': dependencies: @@ -16789,7 +16371,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-fast-compare: 3.2.2 react-focus-lock: 2.13.6(@types/react@18.3.1)(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.1)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' @@ -16798,14 +16380,14 @@ snapshots: '@chakra-ui/styled-system@2.12.2(react@18.3.1)': dependencies: '@chakra-ui/utils': 2.2.4(react@18.3.1) - csstype: 3.1.3 + csstype: 3.2.3 transitivePeerDependencies: - react '@chakra-ui/styled-system@2.9.1': dependencies: '@chakra-ui/shared-utils': 2.0.5 - csstype: 3.1.3 + csstype: 3.2.3 lodash.mergewith: 4.6.2 '@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1)': @@ -17023,7 +16605,7 @@ snapshots: fingerprint-generator: 2.1.86 fingerprint-injector: 2.1.86(puppeteer@23.11.1(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)) lodash.merge: 4.6.2 - nanoid: 3.3.16 + nanoid: 3.3.18 ow: 0.28.2 p-limit: 3.1.0 proxy-chain: 2.7.1 @@ -17315,12 +16897,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@1.7.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -17331,16 +16907,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.1.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -17408,7 +16974,7 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} @@ -17648,9 +17214,9 @@ snapshots: '@faker-js/faker@9.9.0': {} - '@fastgpt-plugin/sdk-client@0.1.0-alpha.5(zod@4.1.12)': + '@fastgpt-plugin/sdk-client@0.1.0-alpha.5(zod@4.4.3)': dependencies: - zod: 4.1.12 + zod: 4.4.3 '@fidm/asn1@1.0.4': {} @@ -17697,14 +17263,14 @@ snapshots: '@fortaine/fetch-event-source@3.0.6': {} - '@google/genai@1.50.1(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + '@google/genai@1.50.1(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.5 ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: - '@modelcontextprotocol/sdk': 1.26.0(zod@4.1.12) + '@modelcontextprotocol/sdk': 1.26.0(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color @@ -17726,18 +17292,14 @@ snapshots: dependencies: tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.4) - '@headlessui/tailwindcss@0.2.2(tailwindcss@4.2.4)': - dependencies: - tailwindcss: 4.2.4 - '@headlessui/vue@1.7.23(vue@3.5.40(typescript@6.0.3))': dependencies: '@tanstack/vue-virtual': 3.13.12(vue@3.5.40(typescript@6.0.3)) vue: 3.5.40(typescript@6.0.3) - '@hono/node-server@1.19.9(hono@4.11.7)': + '@hono/node-server@1.19.9(hono@4.12.27)': dependencies: - hono: 4.11.7 + hono: 4.12.27 '@hono/node-server@2.0.12(hono@4.12.27)': dependencies: @@ -18046,7 +17608,7 @@ snapshots: '@kubernetes/client-node@1.4.0(bufferutil@4.1.0)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@types/js-yaml': 4.0.9 - '@types/node': 24.13.3 + '@types/node': 24.0.13 '@types/node-fetch': 2.6.13 '@types/stream-buffers': 3.0.8 form-data: 4.0.5 @@ -18279,9 +17841,9 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@mariozechner/pi-agent-core@0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12)': + '@mariozechner/pi-agent-core@0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3)': dependencies: - '@mariozechner/pi-ai': 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12) + '@mariozechner/pi-ai': 0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -18291,21 +17853,21 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12)': + '@mariozechner/pi-ai@0.67.3(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10)(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.1.12) + '@anthropic-ai/sdk': 0.73.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1030.0 - '@google/genai': 1.50.1(@modelcontextprotocol/sdk@1.26.0(zod@4.1.12))(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@google/genai': 1.50.1(@modelcontextprotocol/sdk@1.26.0(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@mistralai/mistralai': 1.14.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@sinclair/typebox': 0.34.49 ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) chalk: 5.6.2 - openai: 6.26.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12) + openai: 6.26.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.29.0 - zod-to-json-schema: 3.25.1(zod@4.1.12) + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -18356,33 +17918,33 @@ snapshots: '@mistralai/mistralai@1.14.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - zod: 4.1.12 - zod-to-json-schema: 3.25.1(zod@4.1.12) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - bufferutil - utf-8-validate '@mixmark-io/domino@2.2.0': {} - '@modelcontextprotocol/sdk@1.26.0(zod@4.1.12)': + '@modelcontextprotocol/sdk@1.26.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.7) + '@hono/node-server': 1.19.9(hono@4.12.27) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.6 - eventsource-parser: 3.0.1 + eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.11.7 + hono: 4.12.27 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.2 - zod: 4.1.12 - zod-to-json-schema: 3.25.1(zod@4.1.12) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -18397,10 +17959,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@mongodb-js/saslprep@1.2.0': - dependencies: - sparse-bitfield: 3.0.3 - '@mongodb-js/saslprep@1.4.11': dependencies: sparse-bitfield: 3.0.3 @@ -18476,9 +18034,9 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 optional: true '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -19599,153 +19157,78 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.17': {} - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true - '@rollup/rollup-android-arm-eabi@4.62.4': optional: true - '@rollup/rollup-android-arm64@4.62.2': - optional: true - '@rollup/rollup-android-arm64@4.62.4': optional: true - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true - '@rollup/rollup-darwin-arm64@4.62.4': optional: true - '@rollup/rollup-darwin-x64@4.62.2': - optional: true - '@rollup/rollup-darwin-x64@4.62.4': optional: true - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true - '@rollup/rollup-freebsd-arm64@4.62.4': optional: true - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true - '@rollup/rollup-freebsd-x64@4.62.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true - '@rollup/rollup-linux-arm64-musl@4.62.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true - '@rollup/rollup-linux-loong64-musl@4.62.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true - '@rollup/rollup-linux-x64-gnu@4.62.4': optional: true - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true - '@rollup/rollup-linux-x64-musl@4.62.4': optional: true - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true - '@rollup/rollup-openbsd-x64@4.62.4': optional: true - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true - '@rollup/rollup-openharmony-arm64@4.62.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.2': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.2': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true - '@rollup/rollup-win32-x64-gnu@4.62.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true - '@rollup/rollup-win32-x64-msvc@4.62.4': optional: true @@ -19758,9 +19241,9 @@ snapshots: fast-deep-equal: 3.1.3 lodash: 4.17.23 - '@scalar/agent-chat@0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12)': + '@scalar/agent-chat@0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3)': dependencies: - '@ai-sdk/vue': 3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.1.12) + '@ai-sdk/vue': 3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.4.3) '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3) '@scalar/components': 0.27.10(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3) '@scalar/helpers': 0.9.2 @@ -19774,45 +19257,7 @@ snapshots: '@scalar/validation': 0.6.2 '@scalar/workspace-store': 0.56.0(typescript@6.0.3) '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - ai: 6.0.33(zod@4.1.12) - js-base64: 3.7.8 - neverpanic: 0.0.8 - truncate-json: 3.0.1 - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - - '@scalar/agent-chat@0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@ai-sdk/vue': 3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.1.12) - '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/json-magic': 0.12.19 - '@scalar/openapi-types': 0.9.4 - '@scalar/schemas': 0.8.0 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/validation': 0.6.2 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - ai: 6.0.33(zod@4.1.12) + ai: 6.0.33(zod@4.4.3) js-base64: 3.7.8 neverpanic: 0.0.8 truncate-json: 3.0.1 @@ -19882,57 +19327,9 @@ snapshots: - typescript - universal-cookie - '@scalar/api-client@3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)': + '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3)': dependencies: - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/oas-utils': 0.19.9(typescript@6.0.3) - '@scalar/openapi-types': 0.9.4 - '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/typebox': 0.1.3 - '@scalar/types': 0.17.0 - '@scalar/use-codemirror': 0.14.14(typescript@6.0.3) - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - '@vueuse/integrations': 13.9.0(axios@1.18.1)(focus-trap@7.8.0)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.40(typescript@6.0.3)) - focus-trap: 7.8.0 - fuse.js: 7.1.0 - js-base64: 3.7.8 - jsonc-parser: 3.3.1 - nanoid: 5.1.16 - pretty-ms: 9.3.0 - radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) - set-cookie-parser: 3.1.0 - vue: 3.5.40(typescript@6.0.3) - yaml: 2.8.4 - zod: 4.4.3 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12) + '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) '@scalar/types': 0.17.0 react: 18.3.1 transitivePeerDependencies: @@ -19952,32 +19349,10 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) - '@scalar/types': 0.17.0 - react: 18.3.1 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - - '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12)': + '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/agent-chat': 0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12) + '@scalar/agent-chat': 0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3) '@scalar/blocks': 0.1.9(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3) '@scalar/code-highlight': 0.4.3 @@ -20018,50 +19393,6 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/agent-chat': 0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) - '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/code-highlight': 0.4.3 - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/oas-utils': 0.19.9(typescript@6.0.3) - '@scalar/schemas': 0.8.0 - '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/validation': 0.6.2 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@unhead/vue': 2.1.17(vue@3.5.40(typescript@6.0.3)) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - fuse.js: 7.1.0 - microdiff: 1.5.0 - nanoid: 5.1.16 - vue: 3.5.40(typescript@6.0.3) - yaml: 2.8.4 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - '@scalar/asyncapi-upgrader@0.1.4': dependencies: '@scalar/helpers': 0.9.2 @@ -20084,24 +19415,6 @@ snapshots: - tailwindcss - typescript - '@scalar/blocks@0.1.9(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@types/har-format': 1.2.16 - js-base64: 3.7.8 - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/code-highlight@0.4.3': dependencies: hast-util-to-text: 4.0.2 @@ -20144,28 +19457,6 @@ snapshots: - tailwindcss - typescript - '@scalar/components@0.27.10(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@floating-ui/utils': 0.2.10 - '@floating-ui/vue': 1.1.9(vue@3.5.40(typescript@6.0.3)) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/code-highlight': 0.4.3 - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/themes': 0.17.2 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - cva: 1.0.0-beta.4(typescript@6.0.3) - radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) - vue: 3.5.40(typescript@6.0.3) - vue-component-type-helpers: 3.3.9 - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/helpers@0.9.2': {} '@scalar/icons@0.7.5(typescript@6.0.3)': @@ -20221,21 +19512,6 @@ snapshots: - tailwindcss - typescript - '@scalar/sidebar@0.9.34(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/themes': 0.17.2 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/snippetz@0.9.24': dependencies: '@scalar/helpers': 0.9.2 @@ -20766,7 +20042,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@6.5.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 entities: 4.5.0 '@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1)': @@ -20807,10 +20083,10 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3-oss/env-core@0.13.10(typescript@6.0.3)(zod@4.1.12)': + '@t3-oss/env-core@0.13.10(typescript@6.0.3)(zod@4.4.3)': optionalDependencies: typescript: 6.0.3 - zod: 4.1.12 + zod: 4.4.3 '@tabby_ai/hijri-converter@1.0.5': {} @@ -20880,7 +20156,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 - postcss: 8.5.23 + postcss: 8.5.26 tailwindcss: 4.2.4 '@tanstack/query-core@4.36.1': {} @@ -21068,11 +20344,6 @@ snapshots: '@turbo/windows-arm64@2.10.8': optional: true - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -21108,7 +20379,7 @@ snapshots: '@types/cors@2.8.19': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/d3-array@3.2.1': {} @@ -21235,7 +20506,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/estree-jsx@1.0.5': dependencies: @@ -21247,7 +20518,7 @@ snapshots: '@types/express-serve-static-core@4.19.9': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -21301,7 +20572,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -21361,8 +20632,8 @@ snapshots: '@types/node-fetch@2.6.13': dependencies: - '@types/node': 20.17.24 - form-data: 4.0.5 + '@types/node': 20.19.43 + form-data: 4.0.6 '@types/node@12.20.55': {} @@ -21370,10 +20641,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.17.24': - dependencies: - undici-types: 6.19.8 - '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -21392,7 +20659,7 @@ snapshots: '@types/nodemailer@6.4.24': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/nprogress@0.2.3': {} @@ -21456,7 +20723,7 @@ snapshots: '@types/request-ip@0.0.38': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/retry@0.12.0': {} @@ -21464,7 +20731,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/send@0.17.4': dependencies: @@ -21479,7 +20746,7 @@ snapshots: '@types/stream-buffers@3.0.8': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/tough-cookie@4.0.5': {} @@ -21510,20 +20777,20 @@ snapshots: '@types/xml-encryption@1.2.4': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/xml2js@0.4.14': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/yauzl@2.10.3': dependencies: '@types/node': 20.19.43 - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) @@ -21629,7 +20896,7 @@ snapshots: debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -21689,21 +20956,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) - - '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.5 - ast-v8-to-istanbul: 1.0.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.2 - obug: 2.1.1 - std-env: 4.1.0 - tinyrainbow: 3.1.0 - vitest: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) '@vitest/expect@4.1.10': dependencies: @@ -21714,15 +20967,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/expect@4.1.5': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - chai: 6.2.2 - tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.10 @@ -21731,25 +20975,17 @@ snapshots: optionalDependencies: vite: 6.4.3(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) - '@vitest/mocker@4.1.5(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': - dependencies: - '@vitest/spy': 4.1.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) - - '@vitest/mocker@4.1.5(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': + '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': dependencies: - '@vitest/spy': 4.1.5 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) - '@vitest/mocker@4.1.5(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': + '@vitest/mocker@4.1.10(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4))': dependencies: - '@vitest/spy': 4.1.5 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -21759,20 +20995,11 @@ snapshots: dependencies: tinyrainbow: 3.1.1 - '@vitest/pretty-format@4.1.5': - dependencies: - tinyrainbow: 3.1.0 - '@vitest/runner@4.1.10': dependencies: '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/runner@4.1.5': - dependencies: - '@vitest/utils': 4.1.5 - pathe: 2.0.3 - '@vitest/snapshot@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -21780,29 +21007,14 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/snapshot@4.1.5': - dependencies: - '@vitest/pretty-format': 4.1.5 - '@vitest/utils': 4.1.5 - magic-string: 0.30.21 - pathe: 2.0.3 - '@vitest/spy@4.1.10': {} - '@vitest/spy@4.1.5': {} - '@vitest/utils@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 - '@vitest/utils@4.1.5': - dependencies: - '@vitest/pretty-format': 4.1.5 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - '@vladfrangu/async_event_emitter@2.4.7': {} '@vue/compiler-core@3.5.40': @@ -21827,7 +21039,7 @@ snapshots: '@vue/shared': 3.5.40 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.23 + postcss: 8.5.26 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.40': @@ -21928,7 +21140,7 @@ snapshots: '@grpc/grpc-js': 1.13.0 '@grpc/proto-loader': 0.7.13 '@petamoriken/float16': 3.9.2 - dayjs: 1.11.19 + dayjs: 1.11.21 generic-pool: 3.9.0 lru-cache: 9.1.2 protobufjs: 7.4.0 @@ -21952,6 +21164,10 @@ snapshots: dependencies: acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + acorn-walk@8.3.4: dependencies: acorn: 8.15.0 @@ -21960,8 +21176,7 @@ snapshots: acorn@8.16.0: {} - acorn@8.18.0: - optional: true + acorn@8.18.0: {} address@1.2.2: {} @@ -21989,7 +21204,7 @@ snapshots: dependencies: '@babel/runtime': 7.26.10 '@types/js-cookie': 3.0.6 - dayjs: 1.11.19 + dayjs: 1.11.21 intersection-observer: 0.12.2 js-cookie: 3.0.7 lodash: 4.17.23 @@ -22000,13 +21215,13 @@ snapshots: screenfull: 5.2.0 tslib: 2.8.1 - ai@6.0.33(zod@4.1.12): + ai@6.0.33(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 3.0.13(zod@4.1.12) + '@ai-sdk/gateway': 3.0.13(zod@4.4.3) '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.1.12) + '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) '@opentelemetry/api': 1.9.0 - zod: 4.1.12 + zod: 4.4.3 ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: @@ -22279,6 +21494,15 @@ snapshots: atomic-sleep@1.0.0: {} + autoprefixer@10.5.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.7 + caniuse-lite: 1.0.30001806 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -22294,7 +21518,7 @@ snapshots: axios@1.18.1: dependencies: follow-redirects: 1.16.0(debug@4.4.3) - form-data: 4.0.5 + form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -22442,7 +21666,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.3 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -22490,15 +21714,13 @@ snapshots: browser-or-node@2.1.1: {} - browserslist@4.28.2: + browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.11.1 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.341 - node-releases: 2.0.37 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - - bson@6.10.3: {} + electron-to-chromium: 1.5.403 + node-releases: 2.0.53 + update-browserslist-db: 1.2.3(browserslist@4.28.7) bson@6.10.4: {} @@ -22921,7 +22143,7 @@ snapshots: core-js-compat@3.41.0: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.7 core-js@3.13.1: {} @@ -23475,7 +22697,7 @@ snapshots: dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.26.10 - csstype: 3.1.3 + csstype: 3.2.3 dom-serializer@1.4.1: dependencies: @@ -23590,7 +22812,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.341: {} + electron-to-chromium@1.5.403: {} elkjs@0.9.3: {} @@ -23665,7 +22887,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -23721,8 +22943,6 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 - es-module-lexer@2.1.0: {} - es-module-lexer@2.3.2: {} es-object-atoms@1.1.1: @@ -23738,7 +22958,7 @@ snapshots: es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.2 + hasown: 2.0.4 es-to-primitive@1.3.0: dependencies: @@ -23840,26 +23060,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): - dependencies: - '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@1.21.7)) - eslint: 9.39.4(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@1.21.7)) - globals: 16.4.0 - typescript-eslint: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@2.7.0)) @@ -23880,18 +23080,18 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + eslint-config-next@16.3.0(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@2.7.0)) - eslint: 9.39.4(jiti@2.7.0) + '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@1.21.7)) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@1.21.7)) globals: 16.4.0 - typescript-eslint: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + typescript-eslint: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -23899,42 +23099,27 @@ snapshots: - eslint-import-resolver-webpack - eslint-plugin-import-x - supports-color - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@1.21.7)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) - get-tsconfig: 4.14.0 - is-bun-module: 1.3.0 - oxc-resolver: 5.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.16 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) - get-tsconfig: 4.14.0 + eslint: 9.39.4(jiti@1.21.7) + get-tsconfig: 4.14.3 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -23943,27 +23128,16 @@ snapshots: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 eslint: 9.39.4(jiti@2.7.0) - get-tsconfig: 4.14.0 + get-tsconfig: 4.14.3 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) - eslint: 9.39.4(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@1.21.7)) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 @@ -23975,43 +23149,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - eslint: 9.39.4(jiti@2.7.0) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): @@ -24026,7 +23171,7 @@ snapshots: eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) - hasown: 2.0.2 + hasown: 2.0.4 is-core-module: 2.16.1 is-glob: 4.0.3 minimatch: 3.1.5 @@ -24043,7 +23188,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24052,10 +23197,10 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - hasown: 2.0.2 + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + hasown: 2.0.4 is-core-module: 2.16.1 is-glob: 4.0.3 minimatch: 3.1.5 @@ -24081,7 +23226,7 @@ snapshots: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 9.39.4(jiti@1.21.7) - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 @@ -24100,7 +23245,7 @@ snapshots: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 9.39.4(jiti@2.7.0) - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 @@ -24111,22 +23256,22 @@ snapshots: eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@1.21.7)): dependencies: '@babel/core': 7.26.10 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 eslint: 9.39.4(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)): dependencies: '@babel/core': 7.26.10 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 eslint: 9.39.4(jiti@2.7.0) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -24140,7 +23285,7 @@ snapshots: es-iterator-helpers: 1.2.1 eslint: 9.39.4(jiti@1.21.7) estraverse: 5.3.0 - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 minimatch: 3.1.5 object.entries: 1.1.9 @@ -24162,7 +23307,7 @@ snapshots: es-iterator-helpers: 1.2.1 eslint: 9.39.4(jiti@2.7.0) estraverse: 5.3.0 - hasown: 2.0.2 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 minimatch: 3.1.5 object.entries: 1.1.9 @@ -24269,8 +23414,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -24342,8 +23487,6 @@ snapshots: eventemitter3@4.0.7: {} - eventemitter3@5.0.1: {} - eventemitter3@5.0.4: {} events-universal@1.0.1: @@ -24355,13 +23498,11 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.1: {} - eventsource-parser@3.1.0: {} eventsource@3.0.6: dependencies: - eventsource-parser: 3.0.1 + eventsource-parser: 3.1.0 execall@1.0.0: dependencies: @@ -24369,8 +23510,6 @@ snapshots: expand-template@2.0.3: {} - expect-type@1.3.0: {} - expect-type@1.4.0: {} express-rate-limit@8.2.1(express@5.2.1): @@ -24436,7 +23575,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.2 + qs: 6.15.3 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -24495,10 +23634,6 @@ snapshots: fast-uri@3.0.6: {} - fast-xml-builder@1.1.4: - dependencies: - path-expression-matcher: 1.5.0 - fast-xml-builder@1.3.1: dependencies: path-expression-matcher: 1.6.2 @@ -24519,9 +23654,9 @@ snapshots: fast-xml-parser@5.5.8: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.5.0 - strnum: 2.2.3 + fast-xml-builder: 1.3.1 + path-expression-matcher: 1.6.2 + strnum: 2.4.2 fastq@1.19.1: dependencies: @@ -24539,10 +23674,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -24660,13 +23791,11 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.4 keyv: 4.5.4 flatted@2.0.2: {} - flatted@3.3.3: {} - flatted@3.4.4: {} fn.name@1.1.0: {} @@ -24755,6 +23884,8 @@ snapshots: frac@1.1.2: {} + fraction.js@5.3.4: {} + framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -24876,7 +24007,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.2 + hasown: 2.0.4 is-callable: 1.2.7 functions-have-names@1.2.3: {} @@ -24977,14 +24108,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.6: - dependencies: - resolve-pkg-maps: 1.0.0 - - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 - get-tsconfig@4.14.3: dependencies: resolve-pkg-maps: 1.0.0 @@ -25357,7 +24480,7 @@ snapshots: header-generator@2.1.86: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.7 generative-bayesian-network: 2.1.86 ow: 0.28.2 tslib: 2.8.1 @@ -25378,8 +24501,6 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.7: {} - hono@4.12.27: {} hookable@6.1.1: {} @@ -25498,7 +24619,7 @@ snapshots: httpx@2.3.3: dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -25620,8 +24741,8 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 + hasown: 2.0.4 + side-channel: 1.1.1 internmap@1.0.1: {} @@ -25654,8 +24775,6 @@ snapshots: ipaddr.js@1.9.1: {} - ipaddr.js@2.4.0: {} - ipaddr.js@2.5.0: {} is-absolute-url@4.0.1: {} @@ -26071,7 +25190,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.4 + semver: 7.8.5 jsprim@1.4.2: dependencies: @@ -26251,16 +25370,16 @@ snapshots: dependencies: commander: 14.0.3 listr2: 9.0.5 - picomatch: 4.0.4 + picomatch: 4.0.5 string-argv: 0.3.2 - tinyexec: 1.1.2 + tinyexec: 1.3.0 yaml: 2.8.4 listr2@9.0.5: dependencies: cli-truncate: 5.2.0 colorette: 2.0.20 - eventemitter3: 5.0.1 + eventemitter3: 5.0.4 log-update: 6.1.0 rfdc: 1.4.1 wrap-ansi: 9.0.2 @@ -26415,8 +25534,8 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 source-map-js: 1.2.1 make-asynchronous@1.1.0: @@ -27341,7 +26460,7 @@ snapshots: eventemitter3: 5.0.4 fast-xml-parser: 5.11.0 ipaddr.js: 2.5.0 - lodash: 4.18.1 + lodash: 4.17.23 mime-types: 2.1.35 query-string: 7.1.3 stream-json: 1.9.1 @@ -27394,9 +26513,9 @@ snapshots: mongodb-connection-string-url@3.0.2: dependencies: '@types/whatwg-url': 11.0.5 - whatwg-url: 14.1.1 + whatwg-url: 14.2.0 - mongodb-memory-server-core@10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): + mongodb-memory-server-core@10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): dependencies: async-mutex: 0.5.0 camelcase: 6.3.0 @@ -27404,12 +26523,12 @@ snapshots: find-cache-dir: 3.3.2 follow-redirects: 1.16.0(debug@4.4.3) https-proxy-agent: 7.0.6 - mongodb: 6.14.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + mongodb: 6.20.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) new-find-package-json: 2.0.0 - semver: 7.8.0 + semver: 7.8.5 tar-stream: 3.1.7 tslib: 2.8.1 - yauzl: 3.2.0 + yauzl: 3.4.0 transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -27420,9 +26539,9 @@ snapshots: - socks - supports-color - mongodb-memory-server@10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): + mongodb-memory-server@10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): dependencies: - mongodb-memory-server-core: 10.1.4(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) + mongodb-memory-server-core: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/credential-providers' @@ -27434,15 +26553,6 @@ snapshots: - socks - supports-color - mongodb@6.14.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): - dependencies: - '@mongodb-js/saslprep': 1.2.0 - bson: 6.10.3 - mongodb-connection-string-url: 3.0.2 - optionalDependencies: - gcp-metadata: 5.3.0(encoding@0.1.13) - socks: 2.8.9 - mongodb@6.20.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): dependencies: '@mongodb-js/saslprep': 1.4.11 @@ -27452,7 +26562,7 @@ snapshots: gcp-metadata: 5.3.0(encoding@0.1.13) socks: 2.8.9 - mongoose@8.23.1(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): + mongoose@8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9): dependencies: bson: 6.10.4 kareem: 2.6.3 @@ -27536,14 +26646,10 @@ snapshots: dependencies: lru-cache: 7.18.3 - nanoid@3.3.16: {} - nanoid@3.3.18: {} nanoid@5.1.16: {} - nanoid@5.1.5: {} - napi-build-utils@2.0.0: {} natural-compare@1.4.0: {} @@ -27552,7 +26658,7 @@ snapshots: negotiator@1.0.0: {} - netmask@2.0.2: {} + netmask@2.1.1: {} neverpanic@0.0.8: {} @@ -27680,7 +26786,7 @@ snapshots: node-hex@1.0.1: {} - node-releases@2.0.37: {} + node-releases@2.0.53: {} nodemailer@7.0.13: {} @@ -27768,8 +26874,6 @@ snapshots: obuf@1.1.2: {} - obug@2.1.1: {} - obug@2.1.4: {} on-exit-leak-free@2.1.2: {} @@ -27817,15 +26921,15 @@ snapshots: transitivePeerDependencies: - encoding - openai@6.26.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12): + openai@6.26.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3): optionalDependencies: ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) - zod: 4.1.12 + zod: 4.4.3 - openai@6.34.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.1.12): + openai@6.34.0(ws@8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.4.3): optionalDependencies: ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) - zod: 4.1.12 + zod: 4.4.3 openapi-fetch@0.14.1: dependencies: @@ -28000,7 +27104,7 @@ snapshots: pac-resolver@7.0.1: dependencies: degenerator: 5.0.1 - netmask: 2.0.2 + netmask: 2.1.1 package-json-from-dist@1.0.1: {} @@ -28083,8 +27187,6 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.5.0: {} - path-expression-matcher@1.6.2: {} path-is-absolute@1.0.1: {} @@ -28185,8 +27287,6 @@ snapshots: picomatch@4.0.3: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} pify@2.3.0: {} @@ -28252,30 +27352,30 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.23): + postcss-import@15.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.23 + postcss: 8.5.26 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.1.0(postcss@8.5.23): + postcss-js@4.1.0(postcss@8.5.26): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.23 + postcss: 8.5.26 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.23)(tsx@4.20.6)(yaml@2.8.4): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.20.6)(yaml@2.8.4): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.23 + postcss: 8.5.26 tsx: 4.20.6 yaml: 2.8.4 - postcss-nested@6.2.0(postcss@8.5.23): + postcss-nested@6.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.23 + postcss: 8.5.26 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -28292,19 +27392,13 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.22: - dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.5.23: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -28529,11 +27623,11 @@ snapshots: qs@6.13.0: dependencies: - side-channel: 1.1.0 + side-channel: 1.1.1 qs@6.14.1: dependencies: - side-channel: 1.1.0 + side-channel: 1.1.1 qs@6.15.2: dependencies: @@ -28705,9 +27799,9 @@ snapshots: mdast-util-to-hast: 13.2.0 react: 18.3.1 remark-parse: 11.0.0 - remark-rehype: 11.1.1 + remark-rehype: 11.1.2 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -29301,37 +28395,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rollup@4.62.2: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 - fsevents: 2.3.3 - rollup@4.62.4: dependencies: '@types/estree': 1.0.9 @@ -29475,10 +28538,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} - - semver@7.8.0: {} - semver@7.8.5: {} send@0.19.0: @@ -29875,8 +28934,6 @@ snapshots: statuses@2.0.2: {} - std-env@4.1.0: {} - std-env@4.2.0: {} stdin-discarder@0.1.0: @@ -29959,7 +29016,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 10.4.0 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 string-width@7.2.0: dependencies: @@ -29992,7 +29049,7 @@ snapshots: internal-slot: 1.1.0 regexp.prototype.flags: 1.5.4 set-function-name: 2.0.2 - side-channel: 1.1.0 + side-channel: 1.1.1 string.prototype.repeat@1.0.0: dependencies: @@ -30072,8 +29129,6 @@ snapshots: strnum@1.1.2: {} - strnum@2.2.3: {} - strnum@2.4.2: dependencies: anynum: 1.0.1 @@ -30183,11 +29238,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.23 - postcss-import: 15.1.0(postcss@8.5.23) - postcss-js: 4.1.0(postcss@8.5.23) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.23)(tsx@4.20.6)(yaml@2.8.4) - postcss-nested: 6.2.0(postcss@8.5.23) + postcss: 8.5.26 + postcss-import: 15.1.0(postcss@8.5.26) + postcss-js: 4.1.0(postcss@8.5.26) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.20.6)(yaml@2.8.4) + postcss-nested: 6.2.0(postcss@8.5.26) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 @@ -30399,8 +29454,6 @@ snapshots: tinyexec@1.0.4: {} - tinyexec@1.1.2: {} - tinyexec@1.3.0: {} tinyglobby@0.2.15: @@ -30408,18 +29461,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - tinyrainbow@3.1.0: {} - tinyrainbow@3.1.1: {} tldts-core@6.1.86: {} @@ -30469,10 +29515,6 @@ snapshots: tr46@0.0.3: {} - tr46@5.1.0: - dependencies: - punycode: 2.3.1 - tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -30556,7 +29598,7 @@ snapshots: tsx@4.20.6: dependencies: esbuild: 0.25.12 - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.3 optionalDependencies: fsevents: 2.3.3 @@ -30666,7 +29708,7 @@ snapshots: typescript-eslint@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) @@ -30713,16 +29755,12 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.19.8: {} - undici-types@6.21.0: {} undici-types@7.18.2: {} undici-types@7.8.0: {} - undici@7.28.0: {} - undici@7.29.0: {} unescape@1.0.1: @@ -30844,9 +29882,9 @@ snapshots: dependencies: rolldown: 1.0.0-rc.17 - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 @@ -30895,12 +29933,12 @@ snapshots: urllib@4.9.0: dependencies: - form-data: 4.0.5 + form-data: 4.0.6 formstream: 1.5.2 mime-types: 2.1.35 qs: 6.15.2 type-fest: 4.41.0 - undici: 7.28.0 + undici: 7.29.0 ylru: 2.0.0 use-callback-ref@1.3.3(@types/react@18.3.1)(react@18.3.1): @@ -31063,21 +30101,6 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4): - dependencies: - esbuild: 0.25.12 - postcss: 8.5.23 - rollup: 4.62.2 - optionalDependencies: - '@types/node': 24.13.3 - fsevents: 2.3.3 - jiti: 2.7.0 - lightningcss: 1.32.0 - sass: 1.85.1 - terser: 5.39.0 - tsx: 4.20.6 - yaml: 2.8.4 - vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4): dependencies: esbuild: 0.25.12 @@ -31181,92 +30204,62 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 6.2.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 24.13.3 - '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) - jsdom: 26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - msw - - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.4 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.1.0 + picomatch: 4.0.5 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 vite: 6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 24.13.3 - '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) jsdom: 26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - msw - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + vitest@4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.1 + obug: 2.1.4 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.1.0 + picomatch: 4.0.5 + std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 vite: 6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 24.13.3 - '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) jsdom: 26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - msw @@ -31338,11 +30331,6 @@ snapshots: whatwg-mimetype@4.0.0: {} - whatwg-url@14.1.1: - dependencies: - tr46: 5.1.0 - webidl-conversions: 7.0.0 - whatwg-url@14.2.0: dependencies: tr46: 5.1.1 @@ -31599,11 +30587,6 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - yauzl@3.2.0: - dependencies: - buffer-crc32: 0.2.13 - pend: 1.2.0 - yauzl@3.4.0: dependencies: pend: 1.2.0 @@ -31626,21 +30609,21 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-openapi@5.4.6(zod@4.1.12): + zod-openapi@5.4.6(zod@4.4.3): dependencies: - zod: 4.1.12 + zod: 4.4.3 - zod-to-json-schema@3.25.1(zod@4.1.12): + zod-to-json-schema@3.25.1(zod@4.4.3): dependencies: - zod: 4.1.12 + zod: 4.4.3 zod-validation-error@3.4.0(zod@3.25.76): dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.1.12): + zod-validation-error@4.0.2(zod@4.4.3): dependencies: - zod: 4.1.12 + zod: 4.4.3 zod@3.23.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b57daac974d5..374c21533a69 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -64,8 +64,8 @@ catalog: mime: ^4.1.0 mime-types: 3.0.2 minio: 8.0.7 - mongodb-memory-server: ^10.1.4 - mongoose: ^8.22.1 + mongodb-memory-server: ^10.4.3 + mongoose: ^8.24.2 nanoid: ^5.1.3 next: 16.3.0 next-i18next: 15.4.2 diff --git a/pro b/pro index 65c90f06c137..ea04fa64a29c 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 65c90f06c137bbfa424532bbd90cb6e192d0b4eb +Subproject commit ea04fa64a29c69737b3a02ac14ed2a4f32e33a41 From 92bd0133bbed7f1f5a0c57ee43d538a8d973cba1 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Wed, 5 Aug 2026 14:11:00 +0800 Subject: [PATCH 02/14] chore: lockfile --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35282835f0b8..b08e0f4935db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23958,7 +23958,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 unist-util-visit: 5.0.0 - zod: 4.1.12 + zod: 4.4.3 optionalDependencies: next: 15.5.21(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) vite: 6.4.3(@types/node@24.0.13)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4) From d636f0bf4578699e563535692796d2d78a01a981 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Mon, 10 Aug 2026 10:29:33 +0800 Subject: [PATCH 03/14] wip: design mongodb refactor pattern --- .agents/design/dal-basic-architecture.md | 40 +++++++++++++ packages/dal/domain/index.ts | 4 ++ packages/dal/domain/types.ts | 14 +++++ packages/dal/domain/user.ts | 22 ++++++++ packages/dal/index.ts | 4 ++ packages/dal/mongodb/index.ts | 3 + packages/dal/mongodb/mappers/user.ts | 9 +++ packages/dal/mongodb/models/user.ts | 20 +++++++ packages/dal/mongodb/repositories/user.ts | 56 +++++++++++++++++++ packages/dal/mongodb/transaction.ts | 27 +++++++++ packages/dal/mongodb/types.ts | 3 + packages/dal/mongodb/utils.ts | 8 +++ packages/dal/package.json | 6 ++ packages/dal/ports/index.ts | 1 + packages/dal/ports/user.repository.ts | 10 ++++ packages/dal/test/mongodb-user-mapper.test.ts | 42 ++++++++++++++ packages/dal/transaction/index.ts | 1 + packages/dal/transaction/types.ts | 5 ++ pnpm-lock.yaml | 3 + 19 files changed, 278 insertions(+) create mode 100644 .agents/design/dal-basic-architecture.md create mode 100644 packages/dal/domain/index.ts create mode 100644 packages/dal/domain/types.ts create mode 100644 packages/dal/domain/user.ts create mode 100644 packages/dal/index.ts create mode 100644 packages/dal/mongodb/index.ts create mode 100644 packages/dal/mongodb/mappers/user.ts create mode 100644 packages/dal/mongodb/models/user.ts create mode 100644 packages/dal/mongodb/repositories/user.ts create mode 100644 packages/dal/mongodb/transaction.ts create mode 100644 packages/dal/mongodb/types.ts create mode 100644 packages/dal/mongodb/utils.ts create mode 100644 packages/dal/ports/index.ts create mode 100644 packages/dal/ports/user.repository.ts create mode 100644 packages/dal/test/mongodb-user-mapper.test.ts create mode 100644 packages/dal/transaction/index.ts create mode 100644 packages/dal/transaction/types.ts diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md new file mode 100644 index 000000000000..22f3cbec446d --- /dev/null +++ b/.agents/design/dal-basic-architecture.md @@ -0,0 +1,40 @@ +# DAL 基础架构(User 垂直切片) + +## 目标 + +让业务层依赖数据库无关的实体和 Repository 能力,MongoDB、SQL-like 数据库只负责实现适配器。 + +## 分层 + +```text +domain/ 数据库无关的实体、ID、输入输出 schema +ports/ Repository 接口(业务可依赖的最小能力) +mongodb/ Mongo Schema、ObjectId 转换、Repository 实现 +sql/ 后续增加 SQL 表结构、字段映射和 Repository 实现 +transaction/ 数据库无关的事务上下文和事务入口 +``` + +领域对象统一使用 `id: string`。MongoDB 的 `_id: ObjectId` 只在 `mongodb` 层出现,外键也在映射边界转换为字符串。 + +## User 当前实现 + +- `domain/user.ts`:`User`、`CreateUser`、`UpdateUser` +- `ports/user.repository.ts`:查询、创建、更新的最小 Repository 合约 +- `mongodb/models/user.ts`:Mongo 文档 schema,使用 `_id` +- `mongodb/mappers/user.ts`:Mongo 文档到领域对象的转换 +- `mongodb/repositories/user.ts`:通过注入 Mongoose Model 实现 Repository,便于测试 + +## 约束 + +1. 上层不得依赖 Mongo Model、Mongoose Query 或 Mongo 查询对象。 +2. `EntityId` 暂不限制为 24 位,以保留 UUID、SQL sequence 等后续实现空间。 +3. 数据库字段命名、ID 类型、查询参数转换必须集中在 adapter/mapper 中。 +4. 新增数据库实现时复用 `ports` 和 `domain`,不复制业务实体定义。 + +事务由 service 决定边界,通过 `TransactionRunner.withTransaction` 创建。Repository 接收可选的 `TransactionContext`;Mongo adapter 在内部将它映射为 `ClientSession`,SQL adapter 将它映射为 transaction connection。业务层不依赖具体数据库事务类型。 + +## 后续 TODO + +- 增加 SQL-like User adapter,并验证两种 adapter 的行为契约一致。 +- 统一 DAL 的事务、分页、唯一约束错误和并发更新抽象。 +- 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 diff --git a/packages/dal/domain/index.ts b/packages/dal/domain/index.ts new file mode 100644 index 000000000000..bea03b6ca348 --- /dev/null +++ b/packages/dal/domain/index.ts @@ -0,0 +1,4 @@ +export { EntityIdSchema } from './types'; +export type { EntityId } from './types'; +export { CreateUserSchema, UpdateUserSchema, UserSchema } from './user'; +export type { CreateUser, UpdateUser, User } from './user'; diff --git a/packages/dal/domain/types.ts b/packages/dal/domain/types.ts new file mode 100644 index 000000000000..9e6a73ce3292 --- /dev/null +++ b/packages/dal/domain/types.ts @@ -0,0 +1,14 @@ +import z from 'zod'; + +export const EntityIdSchema = z.string().min(1); +export type EntityId = z.infer; + +export const tables = { + user: 'users', + + team: 'teams', + teamDiscountCoupon: 'team_discount_coupons', + teamEnterpriseAuth: 'team_enterprise_auths', + teamEnterpriseAuthTask: 'team_enterprise_auth_tasks' +} as const; +export type TableNames = (typeof tables)[keyof typeof tables]; diff --git a/packages/dal/domain/user.ts b/packages/dal/domain/user.ts new file mode 100644 index 000000000000..777b824c7345 --- /dev/null +++ b/packages/dal/domain/user.ts @@ -0,0 +1,22 @@ +import z from 'zod'; +import { EntityIdSchema } from './types'; + +export const UserSchema = z.object({ + id: EntityIdSchema, + status: z.string(), + username: z.string(), + password: z.string(), + promotionRate: z.number(), + timezone: z.string(), + language: z.string(), + tags: z.string().array(), + createTime: z.date(), + lastLoginTmbId: EntityIdSchema.nullable() +}); +export type User = z.infer; + +export const CreateUserSchema = UserSchema.omit({ id: true, createTime: true }); +export type CreateUser = z.infer; + +export const UpdateUserSchema = CreateUserSchema.partial(); +export type UpdateUser = z.infer; diff --git a/packages/dal/index.ts b/packages/dal/index.ts new file mode 100644 index 000000000000..6b4c1c7d93f1 --- /dev/null +++ b/packages/dal/index.ts @@ -0,0 +1,4 @@ +export * from './domain'; +export type * from './ports'; +export * from './mongodb'; +export type * from './transaction'; diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts new file mode 100644 index 000000000000..8de60a2e841c --- /dev/null +++ b/packages/dal/mongodb/index.ts @@ -0,0 +1,3 @@ +export { MongoUserRepository } from './repositories/user'; +export { toUser } from './mappers/user'; +export { MongoTransactionRunner, mtxr as mongoTx } from './transaction'; diff --git a/packages/dal/mongodb/mappers/user.ts b/packages/dal/mongodb/mappers/user.ts new file mode 100644 index 000000000000..44dd4c7650b1 --- /dev/null +++ b/packages/dal/mongodb/mappers/user.ts @@ -0,0 +1,9 @@ +import type { User } from '../../domain/user'; +import type { UserDocument } from '../models/user'; +import { toEntityId } from '../utils'; + +export const toUser = (document: UserDocument): User => ({ + ...document, + id: toEntityId(document._id), + lastLoginTmbId: document.lastLoginTmbId ? toEntityId(document.lastLoginTmbId) : null +}); diff --git a/packages/dal/mongodb/models/user.ts b/packages/dal/mongodb/models/user.ts new file mode 100644 index 000000000000..236ea54fb085 --- /dev/null +++ b/packages/dal/mongodb/models/user.ts @@ -0,0 +1,20 @@ +import { model, Schema, type InferSchemaType } from 'mongoose'; +import { tables } from '../../domain/types'; +import type { WithId__v } from '../types'; + +export const UserDocumentSchema = new Schema({ + status: { type: String, required: true }, + username: { type: String, required: true }, + password: { type: String, required: true }, + promotionRate: { type: Number, required: true }, + timezone: { type: String, required: true }, + language: { type: String, required: true }, + tags: { type: [String], required: true }, + createTime: { type: Date, required: true }, + lastLoginTmbId: { type: Schema.Types.ObjectId, default: null } +}); + +export type UserMongooseSchemaType = InferSchemaType; +export type UserDocument = WithId__v; + +export const UserModel = model('User', UserDocumentSchema, tables.user); diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts new file mode 100644 index 000000000000..f176b47f73e2 --- /dev/null +++ b/packages/dal/mongodb/repositories/user.ts @@ -0,0 +1,56 @@ +import type { Model } from 'mongoose'; +import type { CreateUser, UpdateUser, User } from '../../domain/user'; +import type { EntityId } from '../../domain/types'; +import type { UserRepository } from '../../ports/user.repository'; +import type { TransactionContext } from '../../transaction'; +import { toUser } from '../mappers/user'; +import { UserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; +import { getSession, mtxr } from '../transaction'; + +export class MongoUserRepository implements UserRepository { + constructor(private readonly model: Model = UserModel) {} + + async findById(id: EntityId, context?: TransactionContext): Promise { + const query = this.model.findById(id); + const session = getSession(context); + if (session) query.session(session); + const document = await query.lean(); + + mtxr.withTransaction(async (ctx) => { + this.updateById('1', {}, ctx); + this.updateById('1', {}, ctx); + this.updateById('1', {}, ctx); + }); + + return document ? toUser(document) : null; + } + + async findByUsername(username: string, context?: TransactionContext): Promise { + const query = this.model.findOne({ username }); + const session = getSession(context); + if (session) query.session(session); + const document = await query.lean(); + return document ? toUser(document) : null; + } + + async create(input: CreateUser, context?: TransactionContext): Promise { + const session = getSession(context); + const [document] = await this.model.create( + [{ ...input, createTime: new Date() }], + session ? { session } : undefined + ); + return toUser(document.toObject() as UserDocument); + } + + async updateById( + id: EntityId, + patch: UpdateUser, + context?: TransactionContext + ): Promise { + const query = this.model.findByIdAndUpdate(id, { $set: patch }, { new: true }); + const session = getSession(context); + if (session) query.session(session); + const document = await query.lean(); + return document ? toUser(document) : null; + } +} diff --git a/packages/dal/mongodb/transaction.ts b/packages/dal/mongodb/transaction.ts new file mode 100644 index 000000000000..0c7cb05a2dd8 --- /dev/null +++ b/packages/dal/mongodb/transaction.ts @@ -0,0 +1,27 @@ +import type { ClientSession, Connection } from 'mongoose'; +import mongoose from 'mongoose'; +import type { TransactionContext, ITransactionRunner } from '../transaction'; + +const sessions = new WeakMap(); + +export const getSession = (context?: TransactionContext) => { + return context ? sessions.get(context) : undefined; +}; + +export class TransactionRunner implements ITransactionRunner { + constructor(private readonly connection: Connection = mongoose.connection) {} + + async withTransaction(handler: (context: TransactionContext) => Promise): Promise { + const session = await this.connection.startSession(); + const context = Symbol('tx'); + sessions.set(context, session); + try { + return await session.withTransaction(() => handler(context)); + } finally { + sessions.delete(context); + await session.endSession(); + } + } +} + +export const mtxr = new TransactionRunner(); diff --git a/packages/dal/mongodb/types.ts b/packages/dal/mongodb/types.ts new file mode 100644 index 000000000000..4aaddb87afa7 --- /dev/null +++ b/packages/dal/mongodb/types.ts @@ -0,0 +1,3 @@ +import type * as mongoose from 'mongoose'; + +export type WithId__v = mongoose.Default__v>; diff --git a/packages/dal/mongodb/utils.ts b/packages/dal/mongodb/utils.ts new file mode 100644 index 000000000000..a2d86e6be50d --- /dev/null +++ b/packages/dal/mongodb/utils.ts @@ -0,0 +1,8 @@ +import { Types } from 'mongoose'; + +export function toEntityId(value: unknown) { + if (value instanceof Types.ObjectId) return value.toHexString(); + if (typeof value === 'string') return value; + + throw new Error("mapping id value error: invalid 'id' value type"); +} diff --git a/packages/dal/package.json b/packages/dal/package.json index 9fff50550cac..15b2b6d0c866 100644 --- a/packages/dal/package.json +++ b/packages/dal/package.json @@ -3,6 +3,11 @@ "version": "1.0.0", "type": "module", "exports": { + ".": "./index.ts", + "./domain": "./domain/index.ts", + "./ports": "./ports/index.ts", + "./mongodb": "./mongodb/index.ts", + "./transaction": "./transaction/index.ts", "./redis": "./redis/index.ts", "./redis/adapter": "./redis/adapter.ts", "./redis/bullmq": "./redis/bullmq/index.ts", @@ -24,6 +29,7 @@ "dependencies": { "@fastgpt/global": "workspace:*", "bullmq": "5.52.2", + "mongoose": "catalog:", "ioredis": "^5.11.1", "zod": "catalog:" }, diff --git a/packages/dal/ports/index.ts b/packages/dal/ports/index.ts new file mode 100644 index 000000000000..8c317b53af25 --- /dev/null +++ b/packages/dal/ports/index.ts @@ -0,0 +1 @@ +export type { UserRepository } from './user.repository'; diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/ports/user.repository.ts new file mode 100644 index 000000000000..7e0c6bcd2bb5 --- /dev/null +++ b/packages/dal/ports/user.repository.ts @@ -0,0 +1,10 @@ +import type { CreateUser, UpdateUser, User } from '../domain/user'; +import type { EntityId } from '../domain/types'; +import type { TransactionContext } from '../transaction'; + +export type UserRepository = { + findById(id: EntityId, context?: TransactionContext): Promise; + findByUsername(username: string, context?: TransactionContext): Promise; + create(input: CreateUser, context?: TransactionContext): Promise; + updateById(id: EntityId, patch: UpdateUser, context?: TransactionContext): Promise; +}; diff --git a/packages/dal/test/mongodb-user-mapper.test.ts b/packages/dal/test/mongodb-user-mapper.test.ts new file mode 100644 index 000000000000..c9b8e66149f0 --- /dev/null +++ b/packages/dal/test/mongodb-user-mapper.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { Types } from 'mongoose'; +import { toUser } from '../mongodb/mappers/user'; + +describe('Mongo user mapper', () => { + it('maps Mongo _id and ObjectId reference fields to string ids', () => { + const user = toUser({ + _id: new Types.ObjectId('507f1f77bcf86cd799439011'), + status: 'active', + username: 'user@example.com', + password: 'hashed-password', + promotionRate: 0, + timezone: 'Asia/Shanghai', + language: 'zh-CN', + tags: [], + createTime: new Date('2026-01-01T00:00:00.000Z'), + lastLoginTmbId: new Types.ObjectId('507f1f77bcf86cd799439012'), + __v: 0 + }); + + expect(user.id).toBe('507f1f77bcf86cd799439011'); + expect(user.lastLoginTmbId).toBe('507f1f77bcf86cd799439012'); + }); + + it('maps a missing reference to null', () => { + const user = toUser({ + _id: new Types.ObjectId('507f1f77bcf86cd799439013'), + status: 'active', + username: 'user@example.com', + password: 'hashed-password', + promotionRate: 0, + timezone: 'Asia/Shanghai', + language: 'zh-CN', + tags: [], + createTime: new Date(), + lastLoginTmbId: null, + __v: 0 + }); + + expect(user.lastLoginTmbId).toBeNull(); + }); +}); diff --git a/packages/dal/transaction/index.ts b/packages/dal/transaction/index.ts new file mode 100644 index 000000000000..31221730bbfa --- /dev/null +++ b/packages/dal/transaction/index.ts @@ -0,0 +1 @@ +export type { TransactionContext, ITransactionRunner } from './types'; diff --git a/packages/dal/transaction/types.ts b/packages/dal/transaction/types.ts new file mode 100644 index 000000000000..17db75902e1f --- /dev/null +++ b/packages/dal/transaction/types.ts @@ -0,0 +1,5 @@ +export type TransactionContext = symbol; + +export interface ITransactionRunner { + withTransaction(handler: (context: TransactionContext) => Promise): Promise; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b08e0f4935db..52a6d984f8c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -432,6 +432,9 @@ importers: ioredis: specifier: ^5.11.1 version: 5.11.1 + mongoose: + specifier: 'catalog:' + version: 8.24.2(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) zod: specifier: 'catalog:' version: 4.4.3 From 42135dab316d5805cc557b2d06f612bbb2b4486a Mon Sep 17 00:00:00 2001 From: Xianquan Date: Mon, 10 Aug 2026 14:01:57 +0800 Subject: [PATCH 04/14] refactor dal mongodb user adapter --- .agents/design/dal-basic-architecture.md | 36 +++- packages/dal/db/adapter.ts | 9 + packages/dal/db/error-adapter.ts | 6 + packages/dal/db/errors.ts | 93 ++++++++ packages/dal/db/index.ts | 15 ++ packages/dal/db/tables.ts | 11 + packages/dal/domain/index.ts | 11 +- packages/dal/domain/types.ts | 10 - packages/dal/domain/user.ts | 47 +++- packages/dal/index.ts | 1 + packages/dal/mongodb/adapter.ts | 27 +++ packages/dal/mongodb/errors.ts | 80 +++++++ packages/dal/mongodb/index.ts | 7 +- packages/dal/mongodb/indexes.ts | 67 ++++++ packages/dal/mongodb/mappers/user.ts | 28 ++- packages/dal/mongodb/models/user.ts | 85 ++++++-- packages/dal/mongodb/repositories/user.ts | 95 +++++--- packages/dal/mongodb/transaction.ts | 66 ++++-- packages/dal/mongodb/utils.ts | 8 + packages/dal/package.json | 2 + packages/dal/ports/user.repository.ts | 6 +- packages/dal/test/db/errors.test.ts | 47 ++++ packages/dal/test/mongodb-user-mapper.test.ts | 42 ---- packages/dal/test/mongodb/adapter.test.ts | 16 ++ packages/dal/test/mongodb/errors.test.ts | 82 +++++++ packages/dal/test/mongodb/indexes.test.ts | 56 +++++ .../dal/test/mongodb/mappers/user.test.ts | 58 +++++ packages/dal/test/mongodb/models/user.test.ts | 35 +++ .../test/mongodb/repositories/user.test.ts | 204 ++++++++++++++++++ packages/dal/test/mongodb/transaction.test.ts | 123 +++++++++++ packages/dal/test/mongodb/utils.test.ts | 30 +++ packages/dal/transaction/index.ts | 2 +- packages/dal/transaction/types.ts | 4 +- packages/dal/vitest.config.ts | 2 +- 34 files changed, 1273 insertions(+), 138 deletions(-) create mode 100644 packages/dal/db/adapter.ts create mode 100644 packages/dal/db/error-adapter.ts create mode 100644 packages/dal/db/errors.ts create mode 100644 packages/dal/db/index.ts create mode 100644 packages/dal/db/tables.ts create mode 100644 packages/dal/mongodb/adapter.ts create mode 100644 packages/dal/mongodb/errors.ts create mode 100644 packages/dal/mongodb/indexes.ts create mode 100644 packages/dal/test/db/errors.test.ts delete mode 100644 packages/dal/test/mongodb-user-mapper.test.ts create mode 100644 packages/dal/test/mongodb/adapter.test.ts create mode 100644 packages/dal/test/mongodb/errors.test.ts create mode 100644 packages/dal/test/mongodb/indexes.test.ts create mode 100644 packages/dal/test/mongodb/mappers/user.test.ts create mode 100644 packages/dal/test/mongodb/models/user.test.ts create mode 100644 packages/dal/test/mongodb/repositories/user.test.ts create mode 100644 packages/dal/test/mongodb/transaction.test.ts create mode 100644 packages/dal/test/mongodb/utils.test.ts diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index 22f3cbec446d..d5ab4ab939ed 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -8,6 +8,7 @@ ```text domain/ 数据库无关的实体、ID、输入输出 schema +db/ Mongo 与 SQL-like adapter 共用的物理表名 ports/ Repository 接口(业务可依赖的最小能力) mongodb/ Mongo Schema、ObjectId 转换、Repository 实现 sql/ 后续增加 SQL 表结构、字段映射和 Repository 实现 @@ -18,11 +19,13 @@ transaction/ 数据库无关的事务上下文和事务入口 ## User 当前实现 -- `domain/user.ts`:`User`、`CreateUser`、`UpdateUser` -- `ports/user.repository.ts`:查询、创建、更新的最小 Repository 合约 -- `mongodb/models/user.ts`:Mongo 文档 schema,使用 `_id` -- `mongodb/mappers/user.ts`:Mongo 文档到领域对象的转换 -- `mongodb/repositories/user.ts`:通过注入 Mongoose Model 实现 Repository,便于测试 +- `domain/user.ts`:`User`、`CreateUser`、`UpdateUser`、`UserCredentials` +- `ports/user.repository.ts`:ID/用户名/凭据查询、创建和更新合约 +- `mongodb/models/user.ts`:与生产 User 字段、默认值、密码处理和索引一致的 Mongo Schema +- `mongodb/mappers/user.ts`:显式转换 Mongo 文档,不泄漏 `_id`、`__v` 和 password +- `mongodb/repositories/user.ts`:通过注入 Model 实现 Repository,支持事务 context +- `db/adapter.ts`:数据库无关的 Adapter 合约 +- `mongodb/adapter.ts`:Mongo Adapter,从同一个 Mongoose client 组装 Repository 和事务执行器 ## 约束 @@ -30,11 +33,32 @@ transaction/ 数据库无关的事务上下文和事务入口 2. `EntityId` 暂不限制为 24 位,以保留 UUID、SQL sequence 等后续实现空间。 3. 数据库字段命名、ID 类型、查询参数转换必须集中在 adapter/mapper 中。 4. 新增数据库实现时复用 `ports` 和 `domain`,不复制业务实体定义。 +5. 密码只出现在创建、更新和凭据校验输入中,不属于默认返回的 `User` 实体。 +6. Mongo adapter 收到非法 ObjectId 或非本 adapter 创建的事务 context 时必须失败,不能静默降级。 事务由 service 决定边界,通过 `TransactionRunner.withTransaction` 创建。Repository 接收可选的 `TransactionContext`;Mongo adapter 在内部将它映射为 `ClientSession`,SQL adapter 将它映射为 transaction connection。业务层不依赖具体数据库事务类型。 +## 错误 Adapter + +`DatabaseErrorAdapter` 在 Repository 和事务边界把驱动错误转换为稳定合同。调用方只使用 `execute(handler)`,不维护 operation、mode、outcome 或 retryable 等执行元数据: + +| 错误代码 | 语义 | +| --- | --- | +| `DB_INVALID_ARGUMENT` | 参数无法被目标数据库表示,操作未开始 | +| `DB_UNIQUE_CONSTRAINT` | 唯一约束冲突,只暴露冲突字段名 | +| `DB_CONFLICT` | 写冲突或可重试事务冲突 | +| `DB_TIMEOUT` | 操作超时;写操作结果可能未知 | +| `DB_UNAVAILABLE` | 连接、拓扑或节点不可用 | +| `DB_OPERATION_FAILED` | 无法进一步分类的数据库失败 | + +`null/false` 表示资源不存在,不使用异常。DAL 不决定 HTTP 状态码、用户文案或业务重试策略;原始驱动错误只保留在 `cause` 中供服务端日志使用。Mongo 事务会把已适配错误的原始 cause 交回 driver,由 `withTransaction` 在内部自行判断是否自动重试,该策略不进入公共错误合同。 + ## 后续 TODO +- [x] 对齐生产 User Schema、默认值、密码行为与当前索引。 +- [x] 补齐 User 基础 Repository 操作和 Mongo 事务支持。 +- [x] 补齐 mapper、Schema、Repository、事务和索引局部测试。 +- [x] 增加数据库错误 Adapter 与 Mongo 错误映射。 - 增加 SQL-like User adapter,并验证两种 adapter 的行为契约一致。 -- 统一 DAL 的事务、分页、唯一约束错误和并发更新抽象。 +- 统一 DAL 的分页、唯一约束错误和并发更新抽象。 - 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 diff --git a/packages/dal/db/adapter.ts b/packages/dal/db/adapter.ts new file mode 100644 index 000000000000..10f898f86044 --- /dev/null +++ b/packages/dal/db/adapter.ts @@ -0,0 +1,9 @@ +import type { UserRepository } from '../ports'; +import type { TransactionRunner } from '../transaction'; +import type { DatabaseErrorAdapter } from './error-adapter'; + +export type DatabaseAdapter = { + userRepository: UserRepository; + transactionRunner: TransactionRunner; + errorAdapter: DatabaseErrorAdapter; +}; diff --git a/packages/dal/db/error-adapter.ts b/packages/dal/db/error-adapter.ts new file mode 100644 index 000000000000..e6df3d25e260 --- /dev/null +++ b/packages/dal/db/error-adapter.ts @@ -0,0 +1,6 @@ +import type { DatabaseOperationError } from './errors'; + +export type DatabaseErrorAdapter = { + adapt(error: unknown): DatabaseOperationError; + execute(handler: () => Promise): Promise; +}; diff --git a/packages/dal/db/errors.ts b/packages/dal/db/errors.ts new file mode 100644 index 000000000000..60427b5a0573 --- /dev/null +++ b/packages/dal/db/errors.ts @@ -0,0 +1,93 @@ +export type DatabaseErrorCode = + | 'DB_INVALID_ARGUMENT' + | 'DB_UNIQUE_CONSTRAINT' + | 'DB_CONFLICT' + | 'DB_TIMEOUT' + | 'DB_UNAVAILABLE' + | 'DB_OPERATION_FAILED'; + +export class DatabaseOperationError extends Error { + readonly code: DatabaseErrorCode; + override readonly cause?: unknown; + + constructor({ + message, + code, + cause + }: { + message: string; + code: DatabaseErrorCode; + cause?: unknown; + }) { + super(message); + this.name = new.target.name; + this.code = code; + this.cause = cause; + } +} + +export class DatabaseInvalidArgumentError extends DatabaseOperationError { + constructor(cause?: unknown) { + super({ + message: 'Invalid database operation argument', + code: 'DB_INVALID_ARGUMENT', + cause + }); + } +} + +export class DatabaseUniqueConstraintError extends DatabaseOperationError { + readonly fields: readonly string[]; + + constructor({ fields, cause }: { fields?: readonly string[]; cause?: unknown } = {}) { + super({ + message: 'Database unique constraint violated', + code: 'DB_UNIQUE_CONSTRAINT', + cause + }); + this.fields = fields ?? []; + } +} + +export class DatabaseConflictError extends DatabaseOperationError { + constructor(cause?: unknown) { + super({ + message: 'Database operation conflict', + code: 'DB_CONFLICT', + cause + }); + } +} + +export class DatabaseTimeoutError extends DatabaseOperationError { + constructor(cause?: unknown) { + super({ + message: 'Database operation timed out', + code: 'DB_TIMEOUT', + cause + }); + } +} + +export class DatabaseUnavailableError extends DatabaseOperationError { + constructor(cause?: unknown) { + super({ + message: 'Database is unavailable', + code: 'DB_UNAVAILABLE', + cause + }); + } +} + +export class DatabaseExecutionError extends DatabaseOperationError { + constructor(cause?: unknown) { + super({ + message: 'Database operation failed', + code: 'DB_OPERATION_FAILED', + cause + }); + } +} + +export const isDatabaseOperationError = (error: unknown): error is DatabaseOperationError => + error instanceof DatabaseOperationError; diff --git a/packages/dal/db/index.ts b/packages/dal/db/index.ts new file mode 100644 index 000000000000..e968cc97b4a4 --- /dev/null +++ b/packages/dal/db/index.ts @@ -0,0 +1,15 @@ +export { tables } from './tables'; +export type { TableName } from './tables'; +export type { DatabaseAdapter } from './adapter'; +export type { DatabaseErrorAdapter } from './error-adapter'; +export { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseOperationError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError, + isDatabaseOperationError +} from './errors'; +export type { DatabaseErrorCode } from './errors'; diff --git a/packages/dal/db/tables.ts b/packages/dal/db/tables.ts new file mode 100644 index 000000000000..15f249176bdb --- /dev/null +++ b/packages/dal/db/tables.ts @@ -0,0 +1,11 @@ +/** Mongo collection 与 SQL-like table 共用的物理名称。 */ +export const tables = { + user: 'users', + team: 'teams', + teamMember: 'team_members', + teamDiscountCoupon: 'team_discount_coupons', + teamEnterpriseAuth: 'team_enterprise_auths', + teamEnterpriseAuthTask: 'team_enterprise_auth_tasks' +} as const; + +export type TableName = (typeof tables)[keyof typeof tables]; diff --git a/packages/dal/domain/index.ts b/packages/dal/domain/index.ts index bea03b6ca348..db78862637e0 100644 --- a/packages/dal/domain/index.ts +++ b/packages/dal/domain/index.ts @@ -1,4 +1,11 @@ export { EntityIdSchema } from './types'; export type { EntityId } from './types'; -export { CreateUserSchema, UpdateUserSchema, UserSchema } from './user'; -export type { CreateUser, UpdateUser, User } from './user'; +export { + CreateUserSchema, + UpdateUserSchema, + UserCredentialsSchema, + UserMetaSchema, + UserOpenaiAccountSchema, + UserSchema +} from './user'; +export type { CreateUser, UpdateUser, User, UserCredentials } from './user'; diff --git a/packages/dal/domain/types.ts b/packages/dal/domain/types.ts index 9e6a73ce3292..a7805cdfa942 100644 --- a/packages/dal/domain/types.ts +++ b/packages/dal/domain/types.ts @@ -2,13 +2,3 @@ import z from 'zod'; export const EntityIdSchema = z.string().min(1); export type EntityId = z.infer; - -export const tables = { - user: 'users', - - team: 'teams', - teamDiscountCoupon: 'team_discount_coupons', - teamEnterpriseAuth: 'team_enterprise_auths', - teamEnterpriseAuthTask: 'team_enterprise_auth_tasks' -} as const; -export type TableNames = (typeof tables)[keyof typeof tables]; diff --git a/packages/dal/domain/user.ts b/packages/dal/domain/user.ts index 777b824c7345..0be1e71d538c 100644 --- a/packages/dal/domain/user.ts +++ b/packages/dal/domain/user.ts @@ -1,22 +1,53 @@ import z from 'zod'; +import { LanguageSchema } from '@fastgpt/global/common/i18n/type'; +import { FastGPT_SEM_Schema } from '@fastgpt/global/support/marketing/type'; +import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { UserTagsSchema } from '@fastgpt/global/support/user/type'; import { EntityIdSchema } from './types'; +export const UserOpenaiAccountSchema = z.object({ + key: z.string().optional(), + baseUrl: z.string().optional() +}); + +export const UserMetaSchema = z.object({ + isActivatedWecomLicense: z.boolean().optional() +}); + export const UserSchema = z.object({ id: EntityIdSchema, - status: z.string(), + status: z.enum(UserStatusEnum), username: z.string(), - password: z.string(), + passwordUpdateTime: z.date().optional(), + createTime: z.date(), promotionRate: z.number(), + openaiAccount: UserOpenaiAccountSchema.optional(), timezone: z.string(), - language: z.string(), - tags: z.string().array(), - createTime: z.date(), - lastLoginTmbId: EntityIdSchema.nullable() + language: LanguageSchema, + lastLoginTmbId: EntityIdSchema.optional(), + inviterId: EntityIdSchema.optional(), + fastgpt_sem: FastGPT_SEM_Schema.optional(), + phonePrefix: z.number().optional(), + contact: z.string().optional(), + tags: z.array(UserTagsSchema), + meta: UserMetaSchema.optional(), + avatar: z.string().optional() }); export type User = z.infer; -export const CreateUserSchema = UserSchema.omit({ id: true, createTime: true }); +export const CreateUserSchema = UserSchema.omit({ id: true, createTime: true }) + .partial() + .extend({ + username: z.string().min(1), + password: z.string().min(1) + }); export type CreateUser = z.infer; -export const UpdateUserSchema = CreateUserSchema.partial(); +export const UpdateUserSchema = CreateUserSchema.partial().omit({ username: true }); export type UpdateUser = z.infer; + +export const UserCredentialsSchema = z.object({ + username: z.string().min(1), + password: z.string().min(1) +}); +export type UserCredentials = z.infer; diff --git a/packages/dal/index.ts b/packages/dal/index.ts index 6b4c1c7d93f1..c38ebb74bc7d 100644 --- a/packages/dal/index.ts +++ b/packages/dal/index.ts @@ -1,4 +1,5 @@ export * from './domain'; +export * from './db'; export type * from './ports'; export * from './mongodb'; export type * from './transaction'; diff --git a/packages/dal/mongodb/adapter.ts b/packages/dal/mongodb/adapter.ts new file mode 100644 index 000000000000..4fa025be5ea8 --- /dev/null +++ b/packages/dal/mongodb/adapter.ts @@ -0,0 +1,27 @@ +import mongoose, { type Mongoose } from 'mongoose'; +import type { DatabaseAdapter } from '../db'; +import type { DatabaseErrorAdapter } from '../db'; +import { MongoErrorAdapter } from './errors'; +import { createMongoUserRepository } from './repositories/user'; +import { MongoTransactionRunner } from './transaction'; + +export type MongoAdapterDependencies = { + client?: Mongoose; + errorAdapter?: DatabaseErrorAdapter; +}; + +/** MongoDB adapter,保证所有仓储和事务共享同一个 Mongoose client。 */ +export class MongoAdapter implements DatabaseAdapter { + readonly userRepository; + readonly transactionRunner; + readonly errorAdapter; + + constructor({ + client = mongoose, + errorAdapter = new MongoErrorAdapter() + }: MongoAdapterDependencies = {}) { + this.errorAdapter = errorAdapter; + this.userRepository = createMongoUserRepository(client, errorAdapter); + this.transactionRunner = new MongoTransactionRunner(client, errorAdapter); + } +} diff --git a/packages/dal/mongodb/errors.ts b/packages/dal/mongodb/errors.ts new file mode 100644 index 000000000000..5c4a3eff92e1 --- /dev/null +++ b/packages/dal/mongodb/errors.ts @@ -0,0 +1,80 @@ +import { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError, + isDatabaseOperationError, + type DatabaseErrorAdapter +} from '../db'; + +export class MongoInvalidArgumentError extends Error {} + +type ErrorRecord = { + name?: unknown; + code?: unknown; + codeName?: unknown; + keyPattern?: unknown; + hasErrorLabel?: unknown; +}; + +const toErrorRecord = (error: unknown): ErrorRecord => + error !== null && typeof error === 'object' ? error : {}; + +const hasErrorLabel = (error: ErrorRecord, label: string) => + typeof error.hasErrorLabel === 'function' && error.hasErrorLabel(label) === true; + +const getUniqueFields = (error: ErrorRecord) => { + if (!error.keyPattern || typeof error.keyPattern !== 'object') return []; + return Object.keys(error.keyPattern).sort(); +}; + +const conflictCodes = new Set([112, 244, 251]); +const timeoutCodes = new Set([50, 262]); +const unavailableCodes = new Set([6, 7, 89, 91, 189, 9001, 11600, 11602]); +const timeoutNames = new Set(['MongoNetworkTimeoutError', 'MongoOperationTimeoutError']); +const unavailableNames = new Set(['MongoNetworkError', 'MongoServerSelectionError']); + +export class MongoErrorAdapter implements DatabaseErrorAdapter { + adapt(error: unknown) { + if (isDatabaseOperationError(error)) return error; + if (error instanceof MongoInvalidArgumentError) { + return new DatabaseInvalidArgumentError(error); + } + + const record = toErrorRecord(error); + if (record.code === 11000 || record.code === 11001) { + return new DatabaseUniqueConstraintError({ + fields: getUniqueFields(record), + cause: error + }); + } + if (record.name === 'ValidationError' || record.name === 'CastError') { + return new DatabaseInvalidArgumentError(error); + } + if ( + conflictCodes.has(record.code as number) || + record.codeName === 'WriteConflict' || + hasErrorLabel(record, 'TransientTransactionError') + ) { + return new DatabaseConflictError(error); + } + if (timeoutCodes.has(record.code as number) || timeoutNames.has(String(record.name))) { + return new DatabaseTimeoutError(error); + } + if (unavailableCodes.has(record.code as number) || unavailableNames.has(String(record.name))) { + return new DatabaseUnavailableError(error); + } + + return new DatabaseExecutionError(error); + } + + async execute(handler: () => Promise): Promise { + try { + return await handler(); + } catch (error) { + throw this.adapt(error); + } + } +} diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts index 8de60a2e841c..fff8e7b8d0cf 100644 --- a/packages/dal/mongodb/index.ts +++ b/packages/dal/mongodb/index.ts @@ -1,3 +1,6 @@ -export { MongoUserRepository } from './repositories/user'; +export { createMongoUserRepository, MongoUserRepository } from './repositories/user'; export { toUser } from './mappers/user'; -export { MongoTransactionRunner, mtxr as mongoTx } from './transaction'; +export { MongoAdapter } from './adapter'; +export type { MongoAdapterDependencies } from './adapter'; +export { MongoErrorAdapter } from './errors'; +export { MongoTransactionRunner } from './transaction'; diff --git a/packages/dal/mongodb/indexes.ts b/packages/dal/mongodb/indexes.ts new file mode 100644 index 000000000000..4af1faf64d7d --- /dev/null +++ b/packages/dal/mongodb/indexes.ts @@ -0,0 +1,67 @@ +import type { IndexDefinition, IndexOptions, Schema } from 'mongoose'; + +const deprecatedMongoIndexesKey = Symbol.for('fastgpt.mongo.deprecatedIndexes'); + +export type DeprecatedMongoIndexOptions = Pick< + IndexOptions, + 'unique' | 'sparse' | 'expireAfterSeconds' | 'partialFilterExpression' | 'collation' +>; + +export type DeprecatedMongoIndexDefinition = { + indexName: string; + key: IndexDefinition; + options?: DeprecatedMongoIndexOptions; +}; + +export type DefineMongoIndexOptions = { + key: IndexDefinition; + options?: IndexOptions; + deprecated?: true; +}; + +/** 统一声明当前索引;废弃索引只登记元数据,供索引管理器精确清理。 */ +export const defineIndex = ( + schema: Schema, + { key, options, deprecated }: DefineMongoIndexOptions +) => { + if (deprecated !== true) { + schema.index(key, options); + return; + } + + const registeredIndexes = getDeprecatedIndexes(schema); + const indexName = + options?.name ?? + Object.entries(key) + .map(([field, order]) => `${field}_${order}`) + .join('_'); + + if (registeredIndexes.some((index) => index.indexName === indexName)) { + throw new Error(`Duplicate deprecated MongoDB index declaration: ${indexName}`); + } + + const deprecatedOptions: DeprecatedMongoIndexOptions = { + unique: options?.unique, + sparse: options?.sparse, + expireAfterSeconds: options?.expireAfterSeconds, + partialFilterExpression: options?.partialFilterExpression, + collation: options?.collation + }; + const hasDeprecatedOptions = Object.values(deprecatedOptions).some( + (value) => value !== undefined + ); + + Reflect.set(schema, deprecatedMongoIndexesKey, [ + ...registeredIndexes, + { + indexName, + key, + options: hasDeprecatedOptions ? deprecatedOptions : undefined + } + ] satisfies DeprecatedMongoIndexDefinition[]); +}; + +export const getDeprecatedIndexes = (schema: Schema): readonly DeprecatedMongoIndexDefinition[] => { + const indexes: unknown = Reflect.get(schema, deprecatedMongoIndexesKey); + return Array.isArray(indexes) ? indexes : []; +}; diff --git a/packages/dal/mongodb/mappers/user.ts b/packages/dal/mongodb/mappers/user.ts index 44dd4c7650b1..2413b0fcd959 100644 --- a/packages/dal/mongodb/mappers/user.ts +++ b/packages/dal/mongodb/mappers/user.ts @@ -1,9 +1,25 @@ -import type { User } from '../../domain/user'; +import { UserSchema, type User } from '../../domain/user'; import type { UserDocument } from '../models/user'; import { toEntityId } from '../utils'; -export const toUser = (document: UserDocument): User => ({ - ...document, - id: toEntityId(document._id), - lastLoginTmbId: document.lastLoginTmbId ? toEntityId(document.lastLoginTmbId) : null -}); +/** 将 Mongo User 文档显式映射为领域实体,避免泄漏 `_id`、`__v` 和 password。 */ +export const toUser = (document: UserDocument): User => + UserSchema.parse({ + id: toEntityId(document._id), + status: document.status, + username: document.username, + passwordUpdateTime: document.passwordUpdateTime, + createTime: document.createTime, + promotionRate: document.promotionRate, + openaiAccount: document.openaiAccount, + timezone: document.timezone, + language: document.language, + lastLoginTmbId: document.lastLoginTmbId ? toEntityId(document.lastLoginTmbId) : undefined, + inviterId: document.inviterId ? toEntityId(document.inviterId) : undefined, + fastgpt_sem: document.fastgpt_sem, + phonePrefix: document.phonePrefix, + contact: document.contact, + tags: document.tags, + meta: document.meta, + avatar: document.avatar + }); diff --git a/packages/dal/mongodb/models/user.ts b/packages/dal/mongodb/models/user.ts index 236ea54fb085..a8c07e010de5 100644 --- a/packages/dal/mongodb/models/user.ts +++ b/packages/dal/mongodb/models/user.ts @@ -1,20 +1,81 @@ -import { model, Schema, type InferSchemaType } from 'mongoose'; -import { tables } from '../../domain/types'; +import { hashStr } from '@fastgpt/global/common/string/tools'; +import { LangEnum } from '@fastgpt/global/common/i18n/type'; +import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; +import { UserTagsSchema } from '@fastgpt/global/support/user/type'; +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; import type { WithId__v } from '../types'; export const UserDocumentSchema = new Schema({ - status: { type: String, required: true }, - username: { type: String, required: true }, - password: { type: String, required: true }, - promotionRate: { type: Number, required: true }, - timezone: { type: String, required: true }, - language: { type: String, required: true }, - tags: { type: [String], required: true }, - createTime: { type: Date, required: true }, - lastLoginTmbId: { type: Schema.Types.ObjectId, default: null } + status: { + type: String, + enum: Object.keys(userStatusMap), + default: UserStatusEnum.active + }, + username: { + type: String, + required: true + }, + password: { + type: String, + required: true, + set: (value: string) => hashStr(value), + get: (value: string) => hashStr(value), + select: false + }, + passwordUpdateTime: Date, + createTime: { + type: Date, + default: () => new Date() + }, + promotionRate: { + type: Number, + default: 0 + }, + openaiAccount: { + type: { + key: String, + baseUrl: String + } + }, + timezone: { + type: String, + default: 'Asia/Shanghai' + }, + language: { + type: String, + default: LangEnum.zh_CN + }, + lastLoginTmbId: { + type: Schema.Types.ObjectId, + ref: tables.teamMember + }, + inviterId: { + type: Schema.Types.ObjectId, + ref: tables.user + }, + fastgpt_sem: Object, + phonePrefix: Number, + contact: String, + tags: { + type: [String], + enum: UserTagsSchema.enum + }, + meta: Object, + avatar: String }); +defineIndex(UserDocumentSchema, { + key: { username: 1 }, + options: { unique: true } +}); +defineIndex(UserDocumentSchema, { key: { createTime: -1 } }); + export type UserMongooseSchemaType = InferSchemaType; export type UserDocument = WithId__v; -export const UserModel = model('User', UserDocumentSchema, tables.user); +export const getUserModel = (client: Mongoose = mongoose): Model => { + const model = client.models.User as Model | undefined; + return model ?? client.model('User', UserDocumentSchema, tables.user); +}; diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts index f176b47f73e2..10d5add51ee3 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/repositories/user.ts @@ -1,45 +1,69 @@ -import type { Model } from 'mongoose'; -import type { CreateUser, UpdateUser, User } from '../../domain/user'; +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import type { DatabaseErrorAdapter } from '../../db'; +import type { CreateUser, UpdateUser, User, UserCredentials } from '../../domain/user'; import type { EntityId } from '../../domain/types'; import type { UserRepository } from '../../ports/user.repository'; import type { TransactionContext } from '../../transaction'; import { toUser } from '../mappers/user'; -import { UserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; -import { getSession, mtxr } from '../transaction'; +import { MongoErrorAdapter } from '../errors'; +import { getUserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; +import { getMongoSession } from '../transaction'; +import { toMongoObjectId } from '../utils'; export class MongoUserRepository implements UserRepository { - constructor(private readonly model: Model = UserModel) {} + constructor( + private readonly model: Model = getUserModel(mongoose), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } async findById(id: EntityId, context?: TransactionContext): Promise { - const query = this.model.findById(id); - const session = getSession(context); - if (session) query.session(session); - const document = await query.lean(); - - mtxr.withTransaction(async (ctx) => { - this.updateById('1', {}, ctx); - this.updateById('1', {}, ctx); - this.updateById('1', {}, ctx); + return this.execute(async () => { + const document = await this.withSession( + this.model.findById(toMongoObjectId(id)), + context + ).lean(); + return document ? toUser(document) : null; }); - - return document ? toUser(document) : null; } async findByUsername(username: string, context?: TransactionContext): Promise { - const query = this.model.findOne({ username }); - const session = getSession(context); - if (session) query.session(session); - const document = await query.lean(); - return document ? toUser(document) : null; + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne({ username }), + context + ).lean(); + return document ? toUser(document) : null; + }); + } + + async findByCredentials( + { username, password }: UserCredentials, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne({ username, password }), + context + ).lean(); + return document ? toUser(document) : null; + }); } async create(input: CreateUser, context?: TransactionContext): Promise { - const session = getSession(context); - const [document] = await this.model.create( - [{ ...input, createTime: new Date() }], - session ? { session } : undefined - ); - return toUser(document.toObject() as UserDocument); + return this.execute(async () => { + const session = getMongoSession(context); + const [document] = await this.model.create([input], session ? { session } : undefined); + return toUser(document.toObject() as UserDocument); + }); } async updateById( @@ -47,10 +71,17 @@ export class MongoUserRepository implements UserRepository { patch: UpdateUser, context?: TransactionContext ): Promise { - const query = this.model.findByIdAndUpdate(id, { $set: patch }, { new: true }); - const session = getSession(context); - if (session) query.session(session); - const document = await query.lean(); - return document ? toUser(document) : null; + return this.execute(async () => { + const document = await this.withSession( + this.model.findByIdAndUpdate(toMongoObjectId(id), { $set: patch }, { new: true }), + context + ).lean(); + return document ? toUser(document) : null; + }); } } + +export const createMongoUserRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => new MongoUserRepository(getUserModel(client), errorAdapter); diff --git a/packages/dal/mongodb/transaction.ts b/packages/dal/mongodb/transaction.ts index 0c7cb05a2dd8..332bfdb75709 100644 --- a/packages/dal/mongodb/transaction.ts +++ b/packages/dal/mongodb/transaction.ts @@ -1,27 +1,67 @@ -import type { ClientSession, Connection } from 'mongoose'; -import mongoose from 'mongoose'; -import type { TransactionContext, ITransactionRunner } from '../transaction'; +import mongoose, { type ClientSession, type Mongoose } from 'mongoose'; +import type { TransactionContext, TransactionRunner } from '../transaction'; +import { isDatabaseOperationError, type DatabaseErrorAdapter } from '../db'; +import { MongoErrorAdapter, MongoInvalidArgumentError } from './errors'; const sessions = new WeakMap(); -export const getSession = (context?: TransactionContext) => { - return context ? sessions.get(context) : undefined; +class TransactionHandlerError extends Error { + constructor(readonly originalError: unknown) { + super('Transaction handler failed'); + } +} + +export const getMongoSession = (context?: TransactionContext) => { + if (!context) return undefined; + + const session = sessions.get(context); + if (!session) throw new MongoInvalidArgumentError('Invalid MongoDB transaction context'); + return session; }; -export class TransactionRunner implements ITransactionRunner { - constructor(private readonly connection: Connection = mongoose.connection) {} +export class MongoTransactionRunner implements TransactionRunner { + constructor( + private readonly client: Mongoose = mongoose, + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} async withTransaction(handler: (context: TransactionContext) => Promise): Promise { - const session = await this.connection.startSession(); - const context = Symbol('tx'); + const session = await this.errorAdapter.execute(() => this.client.startSession()); + const context = Symbol('mongo-transaction'); sessions.set(context, session); + let operationError: unknown; + try { - return await session.withTransaction(() => handler(context)); + try { + return await session.withTransaction( + async () => { + try { + return await handler(context); + } catch (error) { + if (isDatabaseOperationError(error) && error.cause !== undefined) { + throw error.cause; + } + throw new TransactionHandlerError(error); + } + }, + { maxCommitTimeMS: 60000 } + ); + } catch (error) { + operationError = + error instanceof TransactionHandlerError + ? error.originalError + : this.errorAdapter.adapt(error); + throw operationError; + } } finally { sessions.delete(context); - await session.endSession(); + try { + await session.endSession(); + } catch (error) { + if (operationError === undefined) { + throw this.errorAdapter.adapt(error); + } + } } } } - -export const mtxr = new TransactionRunner(); diff --git a/packages/dal/mongodb/utils.ts b/packages/dal/mongodb/utils.ts index a2d86e6be50d..fcf4db4aab82 100644 --- a/packages/dal/mongodb/utils.ts +++ b/packages/dal/mongodb/utils.ts @@ -1,4 +1,6 @@ import { Types } from 'mongoose'; +import type { EntityId } from '../domain'; +import { MongoInvalidArgumentError } from './errors'; export function toEntityId(value: unknown) { if (value instanceof Types.ObjectId) return value.toHexString(); @@ -6,3 +8,9 @@ export function toEntityId(value: unknown) { throw new Error("mapping id value error: invalid 'id' value type"); } + +/** 将通用实体 ID 转换为 Mongo ObjectId,并在 adapter 边界拒绝非法值。 */ +export const toMongoObjectId = (id: EntityId) => { + if (Types.ObjectId.isValid(id)) return new Types.ObjectId(id); + throw new MongoInvalidArgumentError('Invalid MongoDB entity id'); +}; diff --git a/packages/dal/package.json b/packages/dal/package.json index 15b2b6d0c866..f31e43d41a64 100644 --- a/packages/dal/package.json +++ b/packages/dal/package.json @@ -4,9 +4,11 @@ "type": "module", "exports": { ".": "./index.ts", + "./db": "./db/index.ts", "./domain": "./domain/index.ts", "./ports": "./ports/index.ts", "./mongodb": "./mongodb/index.ts", + "./mongodb/adapter": "./mongodb/adapter.ts", "./transaction": "./transaction/index.ts", "./redis": "./redis/index.ts", "./redis/adapter": "./redis/adapter.ts", diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/ports/user.repository.ts index 7e0c6bcd2bb5..c912d1af533a 100644 --- a/packages/dal/ports/user.repository.ts +++ b/packages/dal/ports/user.repository.ts @@ -1,10 +1,14 @@ -import type { CreateUser, UpdateUser, User } from '../domain/user'; +import type { CreateUser, UpdateUser, User, UserCredentials } from '../domain/user'; import type { EntityId } from '../domain/types'; import type { TransactionContext } from '../transaction'; export type UserRepository = { findById(id: EntityId, context?: TransactionContext): Promise; findByUsername(username: string, context?: TransactionContext): Promise; + findByCredentials( + credentials: UserCredentials, + context?: TransactionContext + ): Promise; create(input: CreateUser, context?: TransactionContext): Promise; updateById(id: EntityId, patch: UpdateUser, context?: TransactionContext): Promise; }; diff --git a/packages/dal/test/db/errors.test.ts b/packages/dal/test/db/errors.test.ts new file mode 100644 index 000000000000..985b95a206e0 --- /dev/null +++ b/packages/dal/test/db/errors.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseOperationError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError, + isDatabaseOperationError +} from '../../db'; + +describe('database error classes', () => { + it('defines invalid argument and unique constraint semantics', () => { + const cause = new Error('raw'); + const invalid = new DatabaseInvalidArgumentError(cause); + const unique = new DatabaseUniqueConstraintError(); + + expect(invalid).toMatchObject({ code: 'DB_INVALID_ARGUMENT', cause }); + expect(unique).toMatchObject({ code: 'DB_UNIQUE_CONSTRAINT', fields: [] }); + expect(invalid).not.toHaveProperty('operation'); + expect(invalid).not.toHaveProperty('mode'); + expect(invalid).not.toHaveProperty('outcome'); + expect(invalid).not.toHaveProperty('retryable'); + }); + + it('defines conflict, timeout and unavailable errors', () => { + expect(new DatabaseConflictError()).toMatchObject({ code: 'DB_CONFLICT' }); + expect(new DatabaseTimeoutError()).toMatchObject({ code: 'DB_TIMEOUT' }); + expect(new DatabaseUnavailableError()).toMatchObject({ code: 'DB_UNAVAILABLE' }); + }); + + it('preserves execution error causes', () => { + expect(new DatabaseExecutionError('raw')).toMatchObject({ + code: 'DB_OPERATION_FAILED', + cause: 'raw' + }); + }); +}); + +describe('isDatabaseOperationError', () => { + it('identifies stable database errors', () => { + expect(isDatabaseOperationError(new DatabaseInvalidArgumentError())).toBe(true); + expect(isDatabaseOperationError(new Error('raw'))).toBe(false); + expect(new DatabaseInvalidArgumentError()).toBeInstanceOf(DatabaseOperationError); + }); +}); diff --git a/packages/dal/test/mongodb-user-mapper.test.ts b/packages/dal/test/mongodb-user-mapper.test.ts deleted file mode 100644 index c9b8e66149f0..000000000000 --- a/packages/dal/test/mongodb-user-mapper.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { Types } from 'mongoose'; -import { toUser } from '../mongodb/mappers/user'; - -describe('Mongo user mapper', () => { - it('maps Mongo _id and ObjectId reference fields to string ids', () => { - const user = toUser({ - _id: new Types.ObjectId('507f1f77bcf86cd799439011'), - status: 'active', - username: 'user@example.com', - password: 'hashed-password', - promotionRate: 0, - timezone: 'Asia/Shanghai', - language: 'zh-CN', - tags: [], - createTime: new Date('2026-01-01T00:00:00.000Z'), - lastLoginTmbId: new Types.ObjectId('507f1f77bcf86cd799439012'), - __v: 0 - }); - - expect(user.id).toBe('507f1f77bcf86cd799439011'); - expect(user.lastLoginTmbId).toBe('507f1f77bcf86cd799439012'); - }); - - it('maps a missing reference to null', () => { - const user = toUser({ - _id: new Types.ObjectId('507f1f77bcf86cd799439013'), - status: 'active', - username: 'user@example.com', - password: 'hashed-password', - promotionRate: 0, - timezone: 'Asia/Shanghai', - language: 'zh-CN', - tags: [], - createTime: new Date(), - lastLoginTmbId: null, - __v: 0 - }); - - expect(user.lastLoginTmbId).toBeNull(); - }); -}); diff --git a/packages/dal/test/mongodb/adapter.test.ts b/packages/dal/test/mongodb/adapter.test.ts new file mode 100644 index 000000000000..4bcf23f4928e --- /dev/null +++ b/packages/dal/test/mongodb/adapter.test.ts @@ -0,0 +1,16 @@ +import { Mongoose } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { MongoAdapter } from '../../mongodb/adapter'; +import { MongoUserRepository } from '../../mongodb/repositories/user'; +import { MongoTransactionRunner } from '../../mongodb/transaction'; +import { MongoErrorAdapter } from '../../mongodb/errors'; + +describe('MongoAdapter', () => { + it('assembles repositories and transaction runner from one client', () => { + const adapter = new MongoAdapter({ client: new Mongoose() }); + + expect(adapter.userRepository).toBeInstanceOf(MongoUserRepository); + expect(adapter.transactionRunner).toBeInstanceOf(MongoTransactionRunner); + expect(adapter.errorAdapter).toBeInstanceOf(MongoErrorAdapter); + }); +}); diff --git a/packages/dal/test/mongodb/errors.test.ts b/packages/dal/test/mongodb/errors.test.ts new file mode 100644 index 000000000000..799f16f823e5 --- /dev/null +++ b/packages/dal/test/mongodb/errors.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; +import { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError +} from '../../db'; +import { MongoErrorAdapter, MongoInvalidArgumentError } from '../../mongodb/errors'; + +const adapter = new MongoErrorAdapter(); + +describe('MongoErrorAdapter.adapt', () => { + it('preserves an already adapted database error', () => { + const error = new DatabaseInvalidArgumentError(); + expect(adapter.adapt(error)).toBe(error); + }); + + it('maps internal invalid arguments and Mongoose validation errors', () => { + expect(adapter.adapt(new MongoInvalidArgumentError())).toBeInstanceOf( + DatabaseInvalidArgumentError + ); + expect(adapter.adapt({ name: 'ValidationError' })).toBeInstanceOf(DatabaseInvalidArgumentError); + expect(adapter.adapt({ name: 'CastError' })).toBeInstanceOf(DatabaseInvalidArgumentError); + }); + + it('maps duplicate keys and only exposes sorted field names', () => { + const error = adapter.adapt({ + code: 11000, + keyPattern: { username: 1, teamId: 1 }, + keyValue: { username: 'secret' } + }); + + expect(error).toBeInstanceOf(DatabaseUniqueConstraintError); + expect(error).toMatchObject({ fields: ['teamId', 'username'] }); + expect(error.message).not.toContain('secret'); + expect(adapter.adapt({ code: 11001 })).toMatchObject({ fields: [] }); + }); + + it('maps conflict codes, names and transaction labels', () => { + expect(adapter.adapt({ code: 112 })).toBeInstanceOf(DatabaseConflictError); + expect(adapter.adapt({ codeName: 'WriteConflict' })).toBeInstanceOf(DatabaseConflictError); + expect( + adapter.adapt({ + hasErrorLabel: (label: string) => label === 'TransientTransactionError' + }) + ).toBeInstanceOf(DatabaseConflictError); + }); + + it('maps timeout codes and names', () => { + expect(adapter.adapt({ code: 50 })).toBeInstanceOf(DatabaseTimeoutError); + expect(adapter.adapt({ name: 'MongoNetworkTimeoutError' })).toBeInstanceOf( + DatabaseTimeoutError + ); + }); + + it('maps unavailable codes and names', () => { + expect(adapter.adapt({ code: 91 })).toBeInstanceOf(DatabaseUnavailableError); + expect(adapter.adapt({ name: 'MongoServerSelectionError' })).toBeInstanceOf( + DatabaseUnavailableError + ); + }); + + it('falls back to an execution error', () => { + expect(adapter.adapt('unknown')).toBeInstanceOf(DatabaseExecutionError); + }); +}); + +describe('MongoErrorAdapter.execute', () => { + it('returns successful results', async () => { + await expect(adapter.execute(async () => 'result')).resolves.toBe('result'); + }); + + it('adapts rejected operations', async () => { + await expect( + adapter.execute(async () => { + throw { code: 11000 }; + }) + ).rejects.toBeInstanceOf(DatabaseUniqueConstraintError); + }); +}); diff --git a/packages/dal/test/mongodb/indexes.test.ts b/packages/dal/test/mongodb/indexes.test.ts new file mode 100644 index 000000000000..9724edcaf5e6 --- /dev/null +++ b/packages/dal/test/mongodb/indexes.test.ts @@ -0,0 +1,56 @@ +import { Schema } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { defineIndex, getDeprecatedIndexes } from '../../mongodb/indexes'; + +describe('defineIndex', () => { + it('adds an active index to the schema', () => { + const schema = new Schema(); + defineIndex(schema, { key: { username: 1 }, options: { unique: true } }); + + expect(schema.indexes()).toEqual([[{ username: 1 }, { unique: true, background: true }]]); + }); + + it('registers a deprecated index without adding it to the schema', () => { + const schema = new Schema(); + defineIndex(schema, { + key: { legacy: 1 }, + options: { unique: true, background: true }, + deprecated: true + }); + + expect(schema.indexes()).toEqual([]); + expect(getDeprecatedIndexes(schema)).toEqual([ + { + indexName: 'legacy_1', + key: { legacy: 1 }, + options: { + unique: true, + sparse: undefined, + expireAfterSeconds: undefined, + partialFilterExpression: undefined, + collation: undefined + } + } + ]); + }); + + it('supports explicit names and rejects duplicate deprecated indexes', () => { + const schema = new Schema(); + const definition = { + key: { legacy: -1 }, + options: { name: 'legacy_custom' }, + deprecated: true + } as const; + defineIndex(schema, definition); + + expect(() => defineIndex(schema, definition)).toThrow( + 'Duplicate deprecated MongoDB index declaration: legacy_custom' + ); + }); +}); + +describe('getDeprecatedIndexes', () => { + it('returns an empty list for schemas without deprecated declarations', () => { + expect(getDeprecatedIndexes(new Schema())).toEqual([]); + }); +}); diff --git a/packages/dal/test/mongodb/mappers/user.test.ts b/packages/dal/test/mongodb/mappers/user.test.ts new file mode 100644 index 000000000000..d398a9b1fddc --- /dev/null +++ b/packages/dal/test/mongodb/mappers/user.test.ts @@ -0,0 +1,58 @@ +import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { Types } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { toUser } from '../../../mongodb/mappers/user'; +import type { UserDocument } from '../../../mongodb/models/user'; + +const createDocument = (overrides: Partial = {}): UserDocument => ({ + _id: new Types.ObjectId('507f1f77bcf86cd799439011'), + __v: 3, + status: UserStatusEnum.active, + username: 'user@example.com', + password: 'hashed-password', + passwordUpdateTime: new Date('2026-02-01T00:00:00.000Z'), + createTime: new Date('2026-01-01T00:00:00.000Z'), + promotionRate: 0.1, + openaiAccount: { key: 'key', baseUrl: 'https://example.com' }, + timezone: 'Asia/Shanghai', + language: 'zh-CN', + lastLoginTmbId: new Types.ObjectId('507f1f77bcf86cd799439012'), + inviterId: new Types.ObjectId('507f1f77bcf86cd799439013'), + fastgpt_sem: { keyword: 'FastGPT' }, + phonePrefix: 86, + contact: '13800000000', + tags: ['wecom'], + meta: { isActivatedWecomLicense: true }, + avatar: 'avatar.png', + ...overrides +}); + +describe('toUser', () => { + it('maps all production fields and converts ObjectIds to entity ids', () => { + const user = toUser(createDocument()); + + expect(user.id).toBe('507f1f77bcf86cd799439011'); + expect(user.lastLoginTmbId).toBe('507f1f77bcf86cd799439012'); + expect(user.inviterId).toBe('507f1f77bcf86cd799439013'); + expect(user.meta).toEqual({ isActivatedWecomLicense: true }); + }); + + it('does not leak Mongo metadata or password', () => { + const user = toUser(createDocument()); + + expect(user).not.toHaveProperty('_id'); + expect(user).not.toHaveProperty('__v'); + expect(user).not.toHaveProperty('password'); + }); + + it('keeps missing optional references undefined', () => { + const user = toUser(createDocument({ lastLoginTmbId: undefined, inviterId: undefined })); + + expect(user.lastLoginTmbId).toBeUndefined(); + expect(user.inviterId).toBeUndefined(); + }); + + it('rejects invalid persisted domain values', () => { + expect(() => toUser(createDocument({ language: 'invalid' }))).toThrow(); + }); +}); diff --git a/packages/dal/test/mongodb/models/user.test.ts b/packages/dal/test/mongodb/models/user.test.ts new file mode 100644 index 000000000000..65ff4da8130a --- /dev/null +++ b/packages/dal/test/mongodb/models/user.test.ts @@ -0,0 +1,35 @@ +import { hashStr } from '@fastgpt/global/common/string/tools'; +import { LangEnum } from '@fastgpt/global/common/i18n/type'; +import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { Mongoose } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { getUserModel, UserDocumentSchema } from '../../../mongodb/models/user'; + +describe('UserDocumentSchema', () => { + it('matches production defaults and hashes password writes', () => { + const UserModel = getUserModel(new Mongoose()); + const document = new UserModel({ username: 'user@example.com', password: 'password' }); + + expect(document.status).toBe(UserStatusEnum.active); + expect(document.createTime).toBeInstanceOf(Date); + expect(document.promotionRate).toBe(0); + expect(document.timezone).toBe('Asia/Shanghai'); + expect(document.language).toBe(LangEnum.zh_CN); + expect(document.tags).toEqual([]); + expect(document.get('password', null, { getters: false })).toBe(hashStr('password')); + expect(document.password).toBe(hashStr(hashStr('password'))); + expect(UserDocumentSchema.path('password').options.select).toBe(false); + }); + + it('declares the production username and createTime indexes', () => { + expect(UserDocumentSchema.indexes()).toEqual([ + [{ username: 1 }, { unique: true, background: true }], + [{ createTime: -1 }, { background: true }] + ]); + }); + + it('reuses the model within the same Mongoose client', () => { + const client = new Mongoose(); + expect(getUserModel(client)).toBe(getUserModel(client)); + }); +}); diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/repositories/user.test.ts new file mode 100644 index 000000000000..7031b0ff68e3 --- /dev/null +++ b/packages/dal/test/mongodb/repositories/user.test.ts @@ -0,0 +1,204 @@ +import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { Types, type ClientSession, type Model, type Mongoose } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import type { UserDocument, UserMongooseSchemaType } from '../../../mongodb/models/user'; +import { MongoUserRepository } from '../../../mongodb/repositories/user'; +import { MongoTransactionRunner } from '../../../mongodb/transaction'; +import { + DatabaseInvalidArgumentError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError +} from '../../../db'; + +const userId = '507f1f77bcf86cd799439011'; +const document: UserDocument = { + _id: new Types.ObjectId(userId), + __v: 0, + status: UserStatusEnum.active, + username: 'user@example.com', + password: 'hashed-password', + createTime: new Date('2026-01-01T00:00:00.000Z'), + promotionRate: 0, + timezone: 'Asia/Shanghai', + language: 'zh-CN', + tags: [] +}; + +const createQuery = (value: T) => ({ + session: vi.fn().mockReturnThis(), + lean: vi.fn(async () => value) +}); + +const createRepository = () => { + const queries = { + findById: createQuery(document), + findOne: createQuery(document), + update: createQuery(document) + }; + const createdDocument = { toObject: vi.fn(() => document) }; + const model = { + findById: vi.fn(() => queries.findById), + findOne: vi.fn(() => queries.findOne), + create: vi.fn(async () => [createdDocument]), + findByIdAndUpdate: vi.fn(() => queries.update) + } as unknown as Model; + + return { repository: new MongoUserRepository(model), model, queries, createdDocument }; +}; + +describe('MongoUserRepository.findById', () => { + it('queries by ObjectId and maps the document', async () => { + const { repository, model } = createRepository(); + + const user = await repository.findById(userId); + + expect(model.findById).toHaveBeenCalledWith(new Types.ObjectId(userId)); + expect(user?.id).toBe(userId); + }); + + it('rejects ids that cannot be represented by MongoDB', async () => { + const { repository, model } = createRepository(); + + const error = await repository.findById('sql-id').catch((error) => error); + + expect(error).toBeInstanceOf(DatabaseInvalidArgumentError); + expect(error).toMatchObject({ code: 'DB_INVALID_ARGUMENT' }); + expect(error.message).not.toContain('sql-id'); + expect(model.findById).not.toHaveBeenCalled(); + }); + + it('returns null when the id does not exist', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce(null); + + await expect(repository.findById(userId)).resolves.toBeNull(); + }); +}); + +describe('MongoUserRepository.findByUsername', () => { + it('returns null when no user exists', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce(null); + + await expect(repository.findByUsername('missing@example.com')).resolves.toBeNull(); + }); +}); + +describe('MongoUserRepository.findByCredentials', () => { + it('passes raw credentials to the Mongoose schema setter', async () => { + const { repository, model } = createRepository(); + + await repository.findByCredentials({ + username: 'user@example.com', + password: 'plain-password' + }); + + expect(model.findOne).toHaveBeenCalledWith({ + username: 'user@example.com', + password: 'plain-password' + }); + }); + + it('returns null for invalid credentials', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce(null); + + await expect( + repository.findByCredentials({ + username: 'user@example.com', + password: 'wrong-password' + }) + ).resolves.toBeNull(); + }); +}); + +describe('MongoUserRepository.create', () => { + it('creates through an array write and maps the result', async () => { + const { repository, model, createdDocument } = createRepository(); + const input = { username: 'user@example.com', password: 'plain-password' }; + + const user = await repository.create(input); + + expect(model.create).toHaveBeenCalledWith([input], undefined); + expect(createdDocument.toObject).toHaveBeenCalledOnce(); + expect(user).not.toHaveProperty('password'); + }); +}); + +describe('MongoUserRepository.updateById', () => { + it('updates by ObjectId and returns the updated user', async () => { + const { repository, model } = createRepository(); + + const user = await repository.updateById(userId, { timezone: 'UTC' }); + + expect(model.findByIdAndUpdate).toHaveBeenCalledWith( + new Types.ObjectId(userId), + { $set: { timezone: 'UTC' } }, + { new: true } + ); + expect(user?.timezone).toBe('Asia/Shanghai'); + }); + + it('returns null when the user no longer exists', async () => { + const { repository, queries } = createRepository(); + queries.update.lean.mockResolvedValueOnce(null); + + await expect(repository.updateById(userId, { timezone: 'UTC' })).resolves.toBeNull(); + }); +}); + +describe('MongoUserRepository transaction context', () => { + it('attaches the same session to queries and creates', async () => { + const { repository, model, queries } = createRepository(); + const session = { + withTransaction: vi.fn(async (handler: () => Promise) => handler()), + endSession: vi.fn(async () => undefined) + } as unknown as ClientSession; + const client = { + startSession: vi.fn(async () => session) + } as unknown as Mongoose; + + await new MongoTransactionRunner(client).withTransaction(async (context) => { + await repository.findByUsername('user@example.com', context); + await repository.create( + { username: 'user@example.com', password: 'plain-password' }, + context + ); + }); + + expect(queries.findOne.session).toHaveBeenCalledWith(session); + expect(model.create).toHaveBeenLastCalledWith( + [{ username: 'user@example.com', password: 'plain-password' }], + { session } + ); + }); +}); + +describe('MongoUserRepository error adapter', () => { + it('maps driver availability errors on reads', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockRejectedValueOnce({ name: 'MongoServerSelectionError' }); + + const error = await repository.findByUsername('user@example.com').catch((error) => error); + + expect(error).toBeInstanceOf(DatabaseUnavailableError); + expect(error).toMatchObject({ code: 'DB_UNAVAILABLE' }); + }); + + it('maps duplicate writes without exposing duplicate values', async () => { + const { repository, model } = createRepository(); + vi.mocked(model.create).mockRejectedValueOnce({ + code: 11000, + keyPattern: { username: 1 }, + keyValue: { username: 'secret@example.com' } + }); + + const error = await repository + .create({ username: 'secret@example.com', password: 'password' }) + .catch((error) => error); + + expect(error).toBeInstanceOf(DatabaseUniqueConstraintError); + expect(error).toMatchObject({ fields: ['username'] }); + expect(error.message).not.toContain('secret@example.com'); + }); +}); diff --git a/packages/dal/test/mongodb/transaction.test.ts b/packages/dal/test/mongodb/transaction.test.ts new file mode 100644 index 000000000000..304f7e3b0a15 --- /dev/null +++ b/packages/dal/test/mongodb/transaction.test.ts @@ -0,0 +1,123 @@ +import type { ClientSession, Mongoose } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import { getMongoSession, MongoTransactionRunner } from '../../mongodb/transaction'; +import { + DatabaseConflictError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError +} from '../../db'; + +const createRuntime = () => { + const session = { + withTransaction: vi.fn(async (handler: () => Promise) => handler()), + endSession: vi.fn(async () => undefined) + } as unknown as ClientSession; + const client = { + startSession: vi.fn(async () => session) + } as unknown as Mongoose; + return { client, session }; +}; + +describe('getMongoSession', () => { + it('returns undefined without a transaction context', () => { + expect(getMongoSession()).toBeUndefined(); + }); + + it('rejects a context owned by another adapter', () => { + expect(() => getMongoSession(Symbol('sql-transaction'))).toThrow( + 'Invalid MongoDB transaction context' + ); + }); +}); + +describe('MongoTransactionRunner', () => { + it('exposes the active session and cleans it after completion', async () => { + const { client, session } = createRuntime(); + const runner = new MongoTransactionRunner(client); + let context: symbol | undefined; + + const result = await runner.withTransaction(async (transactionContext) => { + context = transactionContext; + expect(getMongoSession(transactionContext)).toBe(session); + return 'done'; + }); + + expect(result).toBe('done'); + expect(session.withTransaction).toHaveBeenCalledWith(expect.any(Function), { + maxCommitTimeMS: 60000 + }); + expect(session.endSession).toHaveBeenCalledOnce(); + expect(() => getMongoSession(context)).toThrow('Invalid MongoDB transaction context'); + }); + + it('ends the session when the handler fails', async () => { + const { client, session } = createRuntime(); + const runner = new MongoTransactionRunner(client); + + await expect( + runner.withTransaction(async () => { + throw new Error('failed'); + }) + ).rejects.toThrow('failed'); + expect(session.endSession).toHaveBeenCalledOnce(); + }); + + it('maps start, commit and end driver errors', async () => { + const startClient = { + startSession: vi.fn(async () => { + throw { name: 'MongoServerSelectionError' }; + }) + } as unknown as Mongoose; + await expect( + new MongoTransactionRunner(startClient).withTransaction(async () => undefined) + ).rejects.toBeInstanceOf(DatabaseUnavailableError); + + const { client: commitClient, session: commitSession } = createRuntime(); + vi.mocked(commitSession.withTransaction).mockRejectedValueOnce({ code: 50 }); + await expect( + new MongoTransactionRunner(commitClient).withTransaction(async () => undefined) + ).rejects.toBeInstanceOf(DatabaseTimeoutError); + + const { client: endClient, session: endSession } = createRuntime(); + vi.mocked(endSession.endSession).mockRejectedValueOnce({ name: 'MongoNetworkError' }); + await expect( + new MongoTransactionRunner(endClient).withTransaction(async () => undefined) + ).rejects.toBeInstanceOf(DatabaseUnavailableError); + }); + + it('preserves the handler error when session cleanup also fails', async () => { + const { client, session } = createRuntime(); + vi.mocked(session.endSession).mockRejectedValueOnce(new Error('cleanup failed')); + + await expect( + new MongoTransactionRunner(client).withTransaction(async () => { + throw new Error('business failed'); + }) + ).rejects.toThrow('business failed'); + }); + + it('returns driver causes to withTransaction and adapts the final error', async () => { + const { client } = createRuntime(); + const rawError = { + hasErrorLabel: (label: string) => label === 'TransientTransactionError' + }; + + await expect( + new MongoTransactionRunner(client).withTransaction(async () => { + throw new DatabaseConflictError(rawError); + }) + ).rejects.toBeInstanceOf(DatabaseConflictError); + }); + + it('preserves non-retryable adapted errors from the handler', async () => { + const { client } = createRuntime(); + const error = new DatabaseUniqueConstraintError(); + + await expect( + new MongoTransactionRunner(client).withTransaction(async () => { + throw error; + }) + ).rejects.toBe(error); + }); +}); diff --git a/packages/dal/test/mongodb/utils.test.ts b/packages/dal/test/mongodb/utils.test.ts new file mode 100644 index 000000000000..a50f00dad1ca --- /dev/null +++ b/packages/dal/test/mongodb/utils.test.ts @@ -0,0 +1,30 @@ +import { Types } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { toEntityId, toMongoObjectId } from '../../mongodb/utils'; +import { MongoInvalidArgumentError } from '../../mongodb/errors'; + +describe('toEntityId', () => { + it('converts ObjectId and preserves string ids', () => { + expect(toEntityId(new Types.ObjectId('507f1f77bcf86cd799439011'))).toBe( + '507f1f77bcf86cd799439011' + ); + expect(toEntityId('sql-id')).toBe('sql-id'); + }); + + it('rejects unsupported values', () => { + expect(() => toEntityId(1)).toThrow("mapping id value error: invalid 'id' value type"); + }); +}); + +describe('toMongoObjectId', () => { + it('converts a valid entity id', () => { + expect(toMongoObjectId('507f1f77bcf86cd799439011')).toEqual( + new Types.ObjectId('507f1f77bcf86cd799439011') + ); + }); + + it('rejects a non-Mongo entity id', () => { + expect(() => toMongoObjectId('sql-id')).toThrow(MongoInvalidArgumentError); + expect(() => toMongoObjectId('sql-id')).toThrow('Invalid MongoDB entity id'); + }); +}); diff --git a/packages/dal/transaction/index.ts b/packages/dal/transaction/index.ts index 31221730bbfa..262bc67635b8 100644 --- a/packages/dal/transaction/index.ts +++ b/packages/dal/transaction/index.ts @@ -1 +1 @@ -export type { TransactionContext, ITransactionRunner } from './types'; +export type { TransactionContext, TransactionRunner } from './types'; diff --git a/packages/dal/transaction/types.ts b/packages/dal/transaction/types.ts index 17db75902e1f..3d272c2000b5 100644 --- a/packages/dal/transaction/types.ts +++ b/packages/dal/transaction/types.ts @@ -1,5 +1,5 @@ export type TransactionContext = symbol; -export interface ITransactionRunner { +export type TransactionRunner = { withTransaction(handler: (context: TransactionContext) => Promise): Promise; -} +}; diff --git a/packages/dal/vitest.config.ts b/packages/dal/vitest.config.ts index 41fd8428a000..4921a29e60cc 100644 --- a/packages/dal/vitest.config.ts +++ b/packages/dal/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ enabled: true, reporter: ['text', 'text-summary', 'html', 'json-summary', 'json'], reportOnFailure: true, - include: ['redis/**/*.ts'], + include: ['redis/**/*.ts', 'mongodb/**/*.ts'], exclude: ['**/test/**', '**/*.test.ts', '**/index.ts'], cleanOnRerun: false }, From 33408bd8b4d7cfb9d100d0c6a2b63474d80087cb Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 11:34:01 +0800 Subject: [PATCH 05/14] feat: wire dal mongo connection into service layer --- .agents/design/dal-basic-architecture.md | 53 +++++++- packages/dal/domain/user.ts | 14 ++- packages/dal/mongodb/mappers/user.ts | 15 +-- packages/dal/mongodb/repositories/user.ts | 8 +- packages/dal/mongodb/utils.ts | 3 +- .../dal/test/mongodb/mappers/user.test.ts | 21 ++++ .../test/mongodb/repositories/user.test.ts | 11 ++ .../support/user/account/password/api.ts | 4 +- packages/service/common/dal/index.ts | 40 +++++++ packages/service/common/dal/mongo/config.ts | 35 ++++++ .../service/common/dal/mongo/connection.ts | 52 ++++++++ packages/service/common/dal/mongo/index.ts | 8 ++ packages/service/env.const.ts | 3 + packages/service/env.ts | 5 + .../service/support/permission/user/auth.ts | 6 +- packages/service/support/user/controller.ts | 29 ++++- projects/app/src/instrumentation-node.ts | 12 ++ .../support/user/account/checkPswExpired.ts | 4 +- .../support/user/account/resetExpiredPsw.ts | 17 +-- .../user/account/updatePasswordByOld.ts | 11 +- .../support/permission/auth/chatCompletion.ts | 6 +- .../user/account/resetExpiredPsw.test.ts | 113 +++++++++++------- .../user/account/updatePasswordByOld.test.ts | 59 +++++---- test/mocks/common/mongo.ts | 19 +++ test/setup.ts | 3 + 25 files changed, 437 insertions(+), 114 deletions(-) create mode 100644 packages/service/common/dal/index.ts create mode 100644 packages/service/common/dal/mongo/config.ts create mode 100644 packages/service/common/dal/mongo/connection.ts create mode 100644 packages/service/common/dal/mongo/index.ts diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index d5ab4ab939ed..ab97f7b9062b 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -53,12 +53,63 @@ transaction/ 数据库无关的事务上下文和事务入口 `null/false` 表示资源不存在,不使用异常。DAL 不决定 HTTP 状态码、用户文案或业务重试策略;原始驱动错误只保留在 `cause` 中供服务端日志使用。Mongo 事务会把已适配错误的原始 cause 交回 driver,由 `withTransaction` 在内部自行判断是否自动重试,该策略不进入公共错误合同。 +## 业务侧试接入 + +`packages/service/common/dal/` 是业务侧的新 DAL 组装目录,不修改原有 `common/mongo`。Mongo DAL 在 service 层拥有**独立连接**,不复用 `connectionMongo`,连接生命周期由 dal/mongo 自己管理。 + +目录结构: + +```text +packages/service/common/dal/ + index.ts # 数据库无关出口:createRepositories() 按 DAL_DB_TYPE 组装 userRepository,导出 transactionRunner 及接口类型(DatabaseAdapter/UserRepository/TransactionRunner);后续实体增加 team.ts、app.ts + mongo/ + index.ts # Mongo composition root:createMongoDal(client?)、mongoDal、transactionRunner,唯一接触 MongoAdapter 具体实现的模块 + connection.ts # MongooseConnection 类 + connection 单例(client/connect/disconnect) + config.ts # 连接参数配置(池大小、超时等)+ 连接事件监听注册 registerMongooseListeners +``` + +连接设计: + +- `connection.ts` 定义 `MongooseConnection` 类:`client` getter 暴露底层 `Mongoose` 实例(作为 adapter 的 client),`connect(url?)` 负责连接与失败重试,`disconnect()` 断开连接;连接事件监听由 `config.ts` 的 `registerMongooseListeners` 注册;导出单例 `connection`,不使用挂载到 `global` 的缓存模式。 +- dal/mongo 是自包含的 service 层连接模块,不依赖 `common/mongo` 与 `common/mongo/init.ts`:连接参数与监听注册由 `config.ts` 提供,连接与失败重试由 `connection.ts` 实现。 +- 生产接线在 `projects/app/src/instrumentation-node.ts` 增加 `connect-dal-mongo` 初始化步骤(调用 `connection.connect()`);测试接线在 `test/setup.ts` 连接内存 MongoDB,`afterAll` 断开。 +- 测试 mock(`test/mocks/common/mongo.ts`)mock `connection.connect`,沿用按测试文件/实例隔离的数据库分配,并让 DAL 连接实例与 `connectionMongo` 共享同一测试库,保证迁移期旧 Model 写入的数据可被 DAL Repository 交叉读取;生产环境两者本就指向同一数据库。 +- 业务侧从 `@fastgpt/service/common/dal` 导入:`import { userRepository } from '@fastgpt/service/common/dal'`,调用 `userRepository.findById(...)`;事务入口从同一入口获取 `transactionRunner`,保证 repository 与事务 context 来自同一个 adapter。 + +解耦约束: + +- 业务侧导入路径不带 `mongo`,`userRepository`/`transactionRunner` 的类型一律是 `@fastgpt/dal` 的接口(`UserRepository`/`TransactionRunner`/`DatabaseAdapter`),业务代码不感知具体数据库实现。 +- `common/dal/mongo` 是 service 层 composition root,只负责用独立连接组装 `MongoAdapter` 并导出单例;新增 SQL adapter 时仅替换 `mongo/index.ts` 内部的组装,`common/dal` 的出口和业务导入路径不变。 +- 数据库 adapter 选择由 `DAL_DB_TYPE`(`mongo`/`sql`,默认 `mongo`)决定,声明在 `packages/service/env.ts`;`common/dal/index.ts` 的 `createRepositories()` 按该变量组装 Repository,`sql` 接入前显式失败。 + +独立连接的代价与边界: + +- 应用会存在第二个 Mongo 连接池(大小由 `DB_MAX_LINK` 控制),部署时需确认 Mongo 连接数上限。 +- DAL Model 与旧 Model 指向同一 `users` 集合,索引自动创建幂等;DAL Model 尚未接入业务侧 `addCommonMiddleware` 慢查询日志和 `MongoIndexManager` 统一管理,删除旧 Model 前必须补齐。 + +首批只迁移不依赖旧 `ClientSession`、Mongoose Document 或字段投影的调用: + +- 系统管理员鉴权通过 `userRepository.findById` 查询 root 用户。 +- Chat Completion auth proxy 通过 `userRepository.findByUsername` 获取领域对象的 `id: string`。 +- `authUserExist` 以及不带 session 的 `getUserDetail` 已使用 DAL;`getUserDetail` 对旧 `ClientSession` 保留显式兼容分支。 +- 密码过期检查、过期密码重置、旧密码校验与密码更新已切换到 `UserRepository`。 + +仍接收旧 `ClientSession` 的业务暂不迁移;这类调用需要先把事务边界整体切换到 DAL 的 `TransactionContext`,不能把两种事务上下文混用。当前明确延后的边界包括: + +- 验证码登录会在旧事务中返回并修改 Mongoose Document。 +- 用户资料更新同时修改 TeamMember 和头像资源。 +- root 用户初始化同时创建默认团队。 +- 日志导出中的 `users` `$lookup` 属于跨集合读模型,未下沉到 UserRepository。 + +迁移期间旧 User Model 仍负责索引同步。DAL Model 尚未接入业务侧 `addCommonMiddleware` 的慢查询日志,也未被 `MongoIndexManager` 管理;在删除旧 Model 前必须补齐这两个生命周期能力。 + ## 后续 TODO - [x] 对齐生产 User Schema、默认值、密码行为与当前索引。 - [x] 补齐 User 基础 Repository 操作和 Mongo 事务支持。 - [x] 补齐 mapper、Schema、Repository、事务和索引局部测试。 - [x] 增加数据库错误 Adapter 与 Mongo 错误映射。 -- 增加 SQL-like User adapter,并验证两种 adapter 的行为契约一致。 +- 增加 SQL-like User adapter(接入 `DAL_DB_TYPE=sql` 分支),并验证两种 adapter 的行为契约一致。 - 统一 DAL 的分页、唯一约束错误和并发更新抽象。 - 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 +- 接入独立 DAL 连接后,验证与主连接的索引同步、慢查询日志和生命周期管理。 diff --git a/packages/dal/domain/user.ts b/packages/dal/domain/user.ts index 0be1e71d538c..90032b8c7b6a 100644 --- a/packages/dal/domain/user.ts +++ b/packages/dal/domain/user.ts @@ -46,8 +46,14 @@ export type CreateUser = z.infer; export const UpdateUserSchema = CreateUserSchema.partial().omit({ username: true }); export type UpdateUser = z.infer; -export const UserCredentialsSchema = z.object({ - username: z.string().min(1), - password: z.string().min(1) -}); +export const UserCredentialsSchema = z.union([ + z.object({ + username: z.string().min(1), + password: z.string().min(1) + }), + z.object({ + id: EntityIdSchema, + password: z.string().min(1) + }) +]); export type UserCredentials = z.infer; diff --git a/packages/dal/mongodb/mappers/user.ts b/packages/dal/mongodb/mappers/user.ts index 2413b0fcd959..8c5a701fb0c2 100644 --- a/packages/dal/mongodb/mappers/user.ts +++ b/packages/dal/mongodb/mappers/user.ts @@ -1,25 +1,26 @@ +import { LangEnum } from '@fastgpt/global/common/i18n/type'; +import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { UserSchema, type User } from '../../domain/user'; import type { UserDocument } from '../models/user'; import { toEntityId } from '../utils'; -/** 将 Mongo User 文档显式映射为领域实体,避免泄漏 `_id`、`__v` 和 password。 */ export const toUser = (document: UserDocument): User => UserSchema.parse({ id: toEntityId(document._id), - status: document.status, + status: document.status ?? UserStatusEnum.active, username: document.username, passwordUpdateTime: document.passwordUpdateTime, - createTime: document.createTime, - promotionRate: document.promotionRate, + createTime: document.createTime ?? document._id.getTimestamp(), + promotionRate: document.promotionRate ?? 0, openaiAccount: document.openaiAccount, - timezone: document.timezone, - language: document.language, + timezone: document.timezone ?? 'Asia/Shanghai', + language: document.language ?? LangEnum.zh_CN, lastLoginTmbId: document.lastLoginTmbId ? toEntityId(document.lastLoginTmbId) : undefined, inviterId: document.inviterId ? toEntityId(document.inviterId) : undefined, fastgpt_sem: document.fastgpt_sem, phonePrefix: document.phonePrefix, contact: document.contact, - tags: document.tags, + tags: document.tags ?? [], meta: document.meta, avatar: document.avatar }); diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts index 10d5add51ee3..45a8966fc24c 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/repositories/user.ts @@ -46,12 +46,16 @@ export class MongoUserRepository implements UserRepository { } async findByCredentials( - { username, password }: UserCredentials, + credentials: UserCredentials, context?: TransactionContext ): Promise { return this.execute(async () => { const document = await this.withSession( - this.model.findOne({ username, password }), + this.model.findOne( + 'id' in credentials + ? { _id: toMongoObjectId(credentials.id), password: credentials.password } + : { username: credentials.username, password: credentials.password } + ), context ).lean(); return document ? toUser(document) : null; diff --git a/packages/dal/mongodb/utils.ts b/packages/dal/mongodb/utils.ts index fcf4db4aab82..99242e82bb2a 100644 --- a/packages/dal/mongodb/utils.ts +++ b/packages/dal/mongodb/utils.ts @@ -6,10 +6,9 @@ export function toEntityId(value: unknown) { if (value instanceof Types.ObjectId) return value.toHexString(); if (typeof value === 'string') return value; - throw new Error("mapping id value error: invalid 'id' value type"); + throw new MongoInvalidArgumentError("mapping id value error: invalid 'id' value type"); } -/** 将通用实体 ID 转换为 Mongo ObjectId,并在 adapter 边界拒绝非法值。 */ export const toMongoObjectId = (id: EntityId) => { if (Types.ObjectId.isValid(id)) return new Types.ObjectId(id); throw new MongoInvalidArgumentError('Invalid MongoDB entity id'); diff --git a/packages/dal/test/mongodb/mappers/user.test.ts b/packages/dal/test/mongodb/mappers/user.test.ts index d398a9b1fddc..65664bbcb777 100644 --- a/packages/dal/test/mongodb/mappers/user.test.ts +++ b/packages/dal/test/mongodb/mappers/user.test.ts @@ -1,4 +1,5 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { Types } from 'mongoose'; import { describe, expect, it } from 'vitest'; import { toUser } from '../../../mongodb/mappers/user'; @@ -52,6 +53,26 @@ describe('toUser', () => { expect(user.inviterId).toBeUndefined(); }); + it('applies production defaults to historical documents with missing fields', () => { + const document = createDocument({ + status: undefined, + createTime: undefined, + promotionRate: undefined, + timezone: undefined, + language: undefined, + tags: undefined + }); + + expect(toUser(document)).toMatchObject({ + status: UserStatusEnum.active, + createTime: document._id.getTimestamp(), + promotionRate: 0, + timezone: 'Asia/Shanghai', + language: LangEnum.zh_CN, + tags: [] + }); + }); + it('rejects invalid persisted domain values', () => { expect(() => toUser(createDocument({ language: 'invalid' }))).toThrow(); }); diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/repositories/user.test.ts index 7031b0ff68e3..cd5bf9d154fb 100644 --- a/packages/dal/test/mongodb/repositories/user.test.ts +++ b/packages/dal/test/mongodb/repositories/user.test.ts @@ -99,6 +99,17 @@ describe('MongoUserRepository.findByCredentials', () => { }); }); + it('supports credential lookup by domain id', async () => { + const { repository, model } = createRepository(); + + await repository.findByCredentials({ id: userId, password: 'plain-password' }); + + expect(model.findOne).toHaveBeenCalledWith({ + _id: new Types.ObjectId(userId), + password: 'plain-password' + }); + }); + it('returns null for invalid credentials', async () => { const { repository, queries } = createRepository(); queries.findOne.lean.mockResolvedValueOnce(null); diff --git a/packages/global/openapi/support/user/account/password/api.ts b/packages/global/openapi/support/user/account/password/api.ts index 373b133ea5a0..bfdf62cc9755 100644 --- a/packages/global/openapi/support/user/account/password/api.ts +++ b/packages/global/openapi/support/user/account/password/api.ts @@ -25,8 +25,8 @@ export const UpdatePasswordByOldBodySchema = z } }); export type UpdatePasswordByOldBodyType = z.infer; -export const UpdatePasswordByOldResponseSchema = z.any().meta({ - description: '用户信息' +export const UpdatePasswordByOldResponseSchema = z.undefined().meta({ + description: '密码更新成功' }); export type UpdatePasswordByOldResponseType = z.infer; diff --git a/packages/service/common/dal/index.ts b/packages/service/common/dal/index.ts new file mode 100644 index 000000000000..69859d1c9251 --- /dev/null +++ b/packages/service/common/dal/index.ts @@ -0,0 +1,40 @@ +import type { DatabaseAdapter } from '@fastgpt/dal/db'; +import { serviceEnv } from '../../env'; +import { createMongoDal } from './mongo'; + +export type { DatabaseAdapter } from '@fastgpt/dal/db'; +export type { UserRepository } from '@fastgpt/dal/ports'; +export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/transaction'; + +function selectDal() { + switch (serviceEnv.DAL_DB_TYPE) { + case 'mongo': { + return createMongoDal(); + } + + default: { + throw new Error(`DAL_DB_TYPE=${serviceEnv.DAL_DB_TYPE} is not implemented yet`); + } + } +} + +function createRepositories() { + const { errorAdapter: _e, transactionRunner: _t, ...repositories }: DatabaseAdapter = selectDal(); + + return { + ...repositories + }; +} + +function createTransactionRunner() { + const { transactionRunner }: DatabaseAdapter = selectDal(); + + return { + transactionRunner + }; +} + +export const { transactionRunner, userRepository } = { + ...createRepositories(), + ...createTransactionRunner() +}; diff --git a/packages/service/common/dal/mongo/config.ts b/packages/service/common/dal/mongo/config.ts new file mode 100644 index 000000000000..f67699092505 --- /dev/null +++ b/packages/service/common/dal/mongo/config.ts @@ -0,0 +1,35 @@ +import { serviceEnv } from '../../../env'; +import type { ConnectOptions, Mongoose } from 'mongoose'; +import { getLogger, LogCategories } from '../../logger'; + +const logger = getLogger(LogCategories.INFRA.MONGO); + +export const createDefaultMongooseConnectOptions = (): ConnectOptions => { + const maxConnecting = Math.max(5, serviceEnv.DB_MAX_LINK); + + return { + bufferCommands: true, + maxConnecting, + maxPoolSize: maxConnecting, + minPoolSize: 1, + connectTimeoutMS: 60_000, + waitQueueTimeoutMS: 60_000, + socketTimeoutMS: 60_000, + maxIdleTimeMS: 300_000, + retryWrites: true, + retryReads: true, + serverSelectionTimeoutMS: 10_000, + heartbeatFrequencyMS: 5_000 + }; +}; + +export const registerMongooseListeners = (db: Mongoose) => { + db.connection.removeAllListeners('error'); + db.connection.removeAllListeners('disconnected'); + + db.connection.on('error', (error) => { + logger.error('DAL MongoDB connection error', { error, readyState: db.connection.readyState }); + }); + db.connection.on('connected', () => logger.info('DAL MongoDB connected successfully')); + db.connection.on('disconnected', () => logger.warn('DAL MongoDB disconnected')); +}; diff --git a/packages/service/common/dal/mongo/connection.ts b/packages/service/common/dal/mongo/connection.ts new file mode 100644 index 000000000000..d7ca5bb10a35 --- /dev/null +++ b/packages/service/common/dal/mongo/connection.ts @@ -0,0 +1,52 @@ +import { Mongoose } from 'mongoose'; +import { delay } from '@fastgpt/global/common/system/utils'; +import { getLogger, LogCategories } from '../../logger'; +import { serviceEnv } from '../../../env'; +import { createDefaultMongooseConnectOptions, registerMongooseListeners } from './config'; + +const logger = getLogger(LogCategories.INFRA.MONGO); + +export class MongooseConnection { + private readonly mongoose: Mongoose; + + constructor() { + this.mongoose = new Mongoose(); + } + + get client(): Mongoose { + return this.mongoose; + } + + async connect(url: string = serviceEnv.MONGODB_URI): Promise { + const db = this.mongoose; + + if (db.connection.readyState !== 0) { + return db; + } + + db.set('strictQuery', 'throw'); + + registerMongooseListeners(db); + + try { + const options = createDefaultMongooseConnectOptions(); + await db.connect(url, { + ...options + }); + return db; + } catch (error) { + logger.error('DAL MongoDB connection failed, will retry', { error }); + await db.disconnect(); + await delay(1000); + return this.connect(url); + } + } + + async disconnect(): Promise { + if (this.mongoose.connection.readyState !== 0) { + await this.mongoose.disconnect(); + } + } +} + +export const connection = new MongooseConnection(); diff --git a/packages/service/common/dal/mongo/index.ts b/packages/service/common/dal/mongo/index.ts new file mode 100644 index 000000000000..9d776851cef5 --- /dev/null +++ b/packages/service/common/dal/mongo/index.ts @@ -0,0 +1,8 @@ +import { MongoAdapter } from '@fastgpt/dal/mongodb'; +import type { DatabaseAdapter } from '@fastgpt/dal/db'; +import type { Mongoose } from 'mongoose'; +import { connection } from './connection'; + +export const createMongoDal = (client: Mongoose = connection.client): DatabaseAdapter => { + return new MongoAdapter({ client }); +}; diff --git a/packages/service/env.const.ts b/packages/service/env.const.ts index 7cf4f9d578aa..f6cf27112a65 100644 --- a/packages/service/env.const.ts +++ b/packages/service/env.const.ts @@ -9,3 +9,6 @@ export const LogLevelSchema = z.enum(['trace', 'debug', 'info', 'warning', 'erro // S3 export const StorageVendorSchema = z.enum(['minio', 'aws-s3', 'r2', 'cos', 'oss']); export const StorageCosProtocolSchema = z.enum(['https:', 'http:']); + +// DAL 数据库类型(决定业务侧 Repository 使用哪个数据库 adapter) +export const DalDbTypeSchema = z.enum(['mongo', 'mysql', 'oceanbase', 'postgres']); diff --git a/packages/service/env.ts b/packages/service/env.ts index 85b3196ff696..e355f35eeda3 100644 --- a/packages/service/env.ts +++ b/packages/service/env.ts @@ -14,6 +14,7 @@ import { validateS3Env } from './env.util'; import { + DalDbTypeSchema, LogLevelSchema, StorageVendorSchema, StorageCosProtocolSchema, @@ -32,6 +33,10 @@ export const serviceEnv = createEnv({ // ==================== 基础配置 ==================== DB_MAX_LINK: IntSchema.min(1).default(5), + // ==================== 数据访问层(DAL) ==================== + // 决定业务侧 Repository 使用哪个数据库 adapter;当前仅实现 mongo,sql 待接入 + DAL_DB_TYPE: DalDbTypeSchema.default('mongo'), + // ==================== 密钥 ==================== ROOT_KEY: z .string() diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index c4e01362f026..ea0f0179fc79 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -5,9 +5,9 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { NullPermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { authCert, parseHeaderCert } from '../auth/common'; -import { MongoUser } from '../../user/schema'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import type { NodeHttpRequest } from '../../../types/http'; +import { userRepository } from '../../../common/dal'; /* auth user role */ export async function authUserPer(props: AuthModeType): Promise< @@ -41,9 +41,7 @@ export async function authUserPer(props: AuthModeType): Promise< export const authSystemAdmin = async ({ req }: { req: NodeHttpRequest }) => { try { const result = await authCert({ req, authToken: true }); - const user = await MongoUser.findOne({ - _id: result.userId - }); + const user = await userRepository.findById(result.userId); if (!user || user.username !== 'root') { return Promise.reject(ERROR_ENUM.unAuthorization); diff --git a/packages/service/support/user/controller.ts b/packages/service/support/user/controller.ts index b17bd6f6fe7a..710f0e9a17c5 100644 --- a/packages/service/support/user/controller.ts +++ b/packages/service/support/user/controller.ts @@ -1,16 +1,18 @@ import { type UserType } from '@fastgpt/global/support/user/type'; import { MongoUser } from './schema'; +import { userRepository } from '../../common/dal'; import { getTmbInfoByTmbId, getUserDefaultTeam } from './team/controller'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import type { ClientSession } from '../../common/mongo'; +import { LangEnum } from '@fastgpt/global/common/i18n/type'; export async function authUserExist({ userId, username }: { userId?: string; username?: string }) { if (userId) { - return MongoUser.findOne({ _id: userId }); + return userRepository.findById(userId); } if (username) { - return MongoUser.findOne({ username }); + return userRepository.findByUsername(username); } return null; } @@ -38,9 +40,24 @@ export async function getUserDetail({ } return Promise.reject(ERROR_ENUM.unAuthorization); })(); - const query = MongoUser.findById(tmb.userId); - if (session) query.session(session); - const user = await query; + const user = await (async () => { + if (!session) { + return userRepository.findById(String(tmb.userId)); + } + + // 登录事务仍使用旧 ClientSession;相关集合全部迁入 DAL 后再删除该兼容分支。 + const document = await MongoUser.findById(tmb.userId).session(session); + if (!document) return null; + return { + id: String(document._id), + username: document.username, + timezone: document.timezone ?? 'Asia/Shanghai', + promotionRate: document.promotionRate ?? 0, + contact: document.contact, + language: document.language ?? LangEnum.zh_CN, + tags: document.tags ?? [] + }; + })(); if (!user) { return Promise.reject(ERROR_ENUM.unAuthorization); @@ -53,7 +70,7 @@ export async function getUserDetail({ }; return { - _id: user._id, + _id: user.id, username: user.username, avatar: tmb.avatar, timezone: user.timezone, diff --git a/projects/app/src/instrumentation-node.ts b/projects/app/src/instrumentation-node.ts index 7006c0d43073..b9143fb11aba 100644 --- a/projects/app/src/instrumentation-node.ts +++ b/projects/app/src/instrumentation-node.ts @@ -14,6 +14,7 @@ export async function registerNodeInstrumentation() { const [ { connectMongo }, + { connection }, { connectionMongo, connectionLogMongo, MONGO_URL, MONGO_LOG_URL }, { systemStartCb }, { initGlobalVariables, getInitConfig, initSystemPluginTags, initAppTemplateTypes }, @@ -39,6 +40,7 @@ export async function registerNodeInstrumentation() { { validateAgentSandboxProxyEnv } ] = await Promise.all([ import('@fastgpt/service/common/mongo/init'), + import('@fastgpt/service/common/dal/mongo/connection'), import('@fastgpt/service/common/mongo/index'), import('@fastgpt/service/common/system/tools'), import('@/service/common/system'), @@ -144,6 +146,16 @@ export async function registerNodeInstrumentation() { mongoLogUrl: MONGO_LOG_URL } }), + runInitializationStep({ + step: 'connect-dal-mongo', + stage: InitialErrorEnum.MONGO_ERROR, + action: () => connection.connect(), + logger, + getErrText, + meta: { + mongoUrl: MONGO_URL + } + }), runInitializationStep({ step: 'init-bullmq-workers', stage: InitialErrorEnum.REDIS_ERROR, diff --git a/projects/app/src/pages/api/support/user/account/checkPswExpired.ts b/projects/app/src/pages/api/support/user/account/checkPswExpired.ts index ce423581a083..b2ef9d70cea5 100644 --- a/projects/app/src/pages/api/support/user/account/checkPswExpired.ts +++ b/projects/app/src/pages/api/support/user/account/checkPswExpired.ts @@ -2,7 +2,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/next/type'; import { NextAPI } from '@/service/middleware/entry'; import { checkPswExpired } from '@/service/support/user/account/password'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; -import { MongoUser } from '@fastgpt/service/support/user/schema'; +import { userRepository } from '@fastgpt/service/common/dal'; import type { CheckPswExpiredResponseType } from '@fastgpt/global/openapi/support/user/account/password/api'; async function handler( @@ -11,7 +11,7 @@ async function handler( ): Promise { const { userId } = await authCert({ req, authToken: true }); - const user = await MongoUser.findById(userId, 'passwordUpdateTime'); + const user = await userRepository.findById(userId); if (!user) { return false; diff --git a/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts b/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts index d13dff5b8107..9f3bfea57045 100644 --- a/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts +++ b/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts @@ -1,6 +1,6 @@ import type { ApiRequestProps } from '@fastgpt/next/type'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; -import { MongoUser } from '@fastgpt/service/support/user/schema'; +import { userRepository } from '@fastgpt/service/common/dal'; import { NextAPI } from '@/service/middleware/entry'; import { i18nT } from '@fastgpt/global/common/i18n/utils'; import { checkPswExpired } from '@/service/support/user/account/password'; @@ -18,7 +18,7 @@ async function resetExpiredPswHandler(req: ApiRequestProps): Promise { - const user = await MongoUser.findOne({ username }).select('_id').lean(); + const user = await userRepository.findByUsername(username); if (!user) return null; return MongoTeamMember.findOne({ teamId, - userId: user._id, + userId: user.id, status: notLeaveStatus }) .select('_id userId teamId') diff --git a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts index b400ed15410f..356661d805ee 100644 --- a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts +++ b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts @@ -54,16 +54,19 @@ describe('resetExpiredPsw API', () => { passwordUpdateTime: twoMonthsAgo }); - const res = await Call(resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, - auth: { - userId: String(testUser._id), - teamId: String(testTeam._id), - tmbId: String(testTmb._id), - isRoot: false, - sessionId: 'session123' - } as any - }); + const res = await Call, any>( + resetExpiredPswApi.default, + { + body: { newPsw: 'newhashedpassword' }, + auth: { + userId: String(testUser._id), + teamId: String(testTeam._id), + tmbId: String(testTmb._id), + isRoot: false, + sessionId: 'session123' + } as any + } + ); expect(res.code).toBe(200); @@ -75,6 +78,12 @@ describe('resetExpiredPsw API', () => { expect(updatedUser?.passwordUpdateTime).toBeDefined(); const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime(); expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime()); + await expect( + MongoUser.findOne({ _id: testUser._id, password: 'newhashedpassword' }) + ).resolves.not.toBeNull(); + await expect( + MongoUser.findOne({ _id: testUser._id, password: 'oldpassword' }) + ).resolves.toBeNull(); }); it('should reject when password is not expired (PASSWORD_EXPIRED_MONTH not set)', async () => { @@ -85,16 +94,19 @@ describe('resetExpiredPsw API', () => { passwordUpdateTime: new Date() }); - const res = await Call(resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, - auth: { - userId: String(testUser._id), - teamId: String(testTeam._id), - tmbId: String(testTmb._id), - isRoot: false, - sessionId: 'session123' - } as any - }); + const res = await Call, any>( + resetExpiredPswApi.default, + { + body: { newPsw: 'newhashedpassword' }, + auth: { + userId: String(testUser._id), + teamId: String(testTeam._id), + tmbId: String(testTmb._id), + isRoot: false, + sessionId: 'session123' + } as any + } + ); expect(res.code).toBe(500); expect(res.error).toBeDefined(); @@ -109,23 +121,28 @@ describe('resetExpiredPsw API', () => { passwordUpdateTime: new Date() }); - const res = await Call(resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, - auth: { - userId: String(testUser._id), - teamId: String(testTeam._id), - tmbId: String(testTmb._id), - isRoot: false, - sessionId: 'session123' - } as any - }); + const res = await Call, any>( + resetExpiredPswApi.default, + { + body: { newPsw: 'newhashedpassword' }, + auth: { + userId: String(testUser._id), + teamId: String(testTeam._id), + tmbId: String(testTmb._id), + isRoot: false, + sessionId: 'session123' + } as any + } + ); expect(res.code).toBe(500); }); it('should reject when newPsw is missing', async () => { vi.stubEnv('PASSWORD_EXPIRED_MONTH', '1'); - const res = await Call(resetExpiredPswHandler, { + const resetExpiredPswApi = await loadResetExpiredPswApi(); + + const res = await Call, any>(resetExpiredPswApi.default, { body: {}, auth: { userId: String(testUser._id), @@ -146,16 +163,19 @@ describe('resetExpiredPsw API', () => { const nonExistentId = '000000000000000000000001'; - const res = await Call(resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, - auth: { - userId: nonExistentId, - teamId: String(testTeam._id), - tmbId: String(testTmb._id), - isRoot: false, - sessionId: 'session123' - } as any - }); + const res = await Call, any>( + resetExpiredPswApi.default, + { + body: { newPsw: 'newhashedpassword' }, + auth: { + userId: nonExistentId, + teamId: String(testTeam._id), + tmbId: String(testTmb._id), + isRoot: false, + sessionId: 'session123' + } as any + } + ); expect(res.code).toBe(500); expect(res.error).toBe('The password has not expired'); @@ -163,9 +183,12 @@ describe('resetExpiredPsw API', () => { it('should reject request without authentication', async () => { const resetExpiredPswApi = await loadResetExpiredPswApi(); - const res = await Call(resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' } - }); + const res = await Call, any>( + resetExpiredPswApi.default, + { + body: { newPsw: 'newhashedpassword' } + } + ); expect(res.code).toBe(500); }); @@ -179,7 +202,7 @@ describe('resetExpiredPsw API', () => { passwordUpdateTime: twoMonthsAgo }); - const res = await Call(resetExpiredPswHandler, { + const res = await Call, any>(resetExpiredPswApi.default, { body: { newPsw: { $ne: '' } }, auth: { userId: String(testUser._id), diff --git a/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts b/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts index 0abf78f8f2a4..969d2e4c541b 100644 --- a/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts +++ b/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts @@ -43,23 +43,36 @@ describe('updatePasswordByOld API', () => { }); it('should update password successfully with correct old password', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }, - auth: makeAuth(testUser, testTeam, testTmb) as any - }); + const res = await Call, any>( + updatePasswordApi.default, + { + body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }, + auth: makeAuth(testUser, testTeam, testTmb) as any + } + ); expect(res.code).toBe(200); + expect(res.data).toBeUndefined(); const updatedUser = await MongoUser.findById(testUser._id).select('+password'); expect(updatedUser?.password).toBeDefined(); expect(updatedUser?.passwordUpdateTime).toBeDefined(); + await expect( + MongoUser.findOne({ _id: testUser._id, password: 'newhashpassword' }) + ).resolves.not.toBeNull(); + await expect( + MongoUser.findOne({ _id: testUser._id, password: 'oldhashpassword' }) + ).resolves.toBeNull(); }); it('should reject when old password is incorrect', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' }, - auth: makeAuth(testUser, testTeam, testTmb) as any - }); + const res = await Call, any>( + updatePasswordApi.default, + { + body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' }, + auth: makeAuth(testUser, testTeam, testTmb) as any + } + ); expect(res.code).toBe(500); @@ -69,16 +82,19 @@ describe('updatePasswordByOld API', () => { }); it('should reject when old and new passwords are the same', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' }, - auth: makeAuth(testUser, testTeam, testTmb) as any - }); + const res = await Call, any>( + updatePasswordApi.default, + { + body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' }, + auth: makeAuth(testUser, testTeam, testTmb) as any + } + ); expect(res.code).toBe(500); }); it('should reject when oldPsw is missing', async () => { - const res = await Call(updatePasswordApi.default, { + const res = await Call, any>(updatePasswordApi.default, { body: { newPsw: 'newhashpassword' }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -88,7 +104,7 @@ describe('updatePasswordByOld API', () => { }); it('should reject when newPsw is missing', async () => { - const res = await Call(updatePasswordApi.default, { + const res = await Call, any>(updatePasswordApi.default, { body: { oldPsw: 'oldhashpassword' }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -108,9 +124,12 @@ describe('updatePasswordByOld API', () => { }); it('should reject request without authentication', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' } - }); + const res = await Call, any>( + updatePasswordApi.default, + { + body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' } + } + ); expect(res.code).toBe(500); }); @@ -119,7 +138,7 @@ describe('updatePasswordByOld API', () => { it('should reject oldPsw as MongoDB operator object ($ne injection)', async () => { // GHSA-jxvr-h2vx-p73r Step 3: oldPsw: {"$ne": ""} bypasses old password check - const res = await Call(updatePasswordApi.default, { + const res = await Call, any>(updatePasswordApi.default, { body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -134,7 +153,7 @@ describe('updatePasswordByOld API', () => { }); it('should reject oldPsw with $regex injection', async () => { - const res = await Call(updatePasswordApi.default, { + const res = await Call, any>(updatePasswordApi.default, { body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -144,7 +163,7 @@ describe('updatePasswordByOld API', () => { }); it('should reject newPsw as non-string type', async () => { - const res = await Call(updatePasswordApi.default, { + const res = await Call, any>(updatePasswordApi.default, { body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } }, auth: makeAuth(testUser, testTeam, testTmb) as any }); diff --git a/test/mocks/common/mongo.ts b/test/mocks/common/mongo.ts index a69b2bfd232a..a63f09887c1e 100644 --- a/test/mocks/common/mongo.ts +++ b/test/mocks/common/mongo.ts @@ -1,6 +1,8 @@ import { vi } from 'vitest'; import { randomUUID } from 'crypto'; import type { Mongoose } from '@fastgpt/service/common/mongo'; +import { connectionMongo } from '@fastgpt/service/common/mongo'; +import { connection } from '@fastgpt/service/common/dal/mongo/connection'; const fileDbPrefix = `fastgpt_test_${process.env.VITEST_WORKER_ID ?? '0'}_${randomUUID().replaceAll( '-', @@ -11,6 +13,12 @@ const connectPromises = new WeakMap>(); let dbSeq = 0; const getDbName = (db: Mongoose) => { + // 主业务库:DAL 连接实例与 connectionMongo 共享同一测试库, + // 保证迁移期旧 Model 写入的数据可以被 DAL Repository 读到(生产环境两者指向同一数据库)。 + if (db === connection.client) { + return getDbName(connectionMongo); + } + const existing = dbNames.get(db); if (existing) return existing; @@ -66,6 +74,17 @@ vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any }; }); +/** + * Mock DAL 独立连接:复用同一套按测试文件/实例隔离的数据库分配, + * 并让 DAL 连接实例与 connectionMongo 共享主业务库。 + */ +vi.mock(import('@fastgpt/service/common/dal/mongo/connection'), async (importOriginal: any) => { + const mod = await importOriginal(); + mod.connection.connect = async (url: string) => + connectTestMongo({ db: mod.connection.client, url }); + return mod; +}); + /** * Mock mongoSessionRun to avoid transaction issues in tests * MongoDB transactions require replica set, which may not be available in test environment diff --git a/test/setup.ts b/test/setup.ts index 355d4f23a357..2238ece1a38d 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -7,6 +7,7 @@ import { afterAll, beforeAll, beforeEach, inject, onTestFinished, vi } from 'vit import setupModels from './setupModels'; import { clean } from './datas/users'; import { connectionLogMongo, connectionMongo } from '@fastgpt/service/common/mongo'; +import { connection } from '@fastgpt/service/common/dal/mongo/connection'; import { loadVectorDBEnv } from './utils/env'; import type { Mongoose } from '@fastgpt/service/common/mongo'; @@ -31,6 +32,7 @@ beforeAll(async () => { vi.stubEnv('MONGODB_URI', inject('MONGODB_URI')); await connectMongo({ db: connectionMongo, url: inject('MONGODB_URI') }); await connectMongo({ db: connectionLogMongo, url: inject('MONGODB_URI') }); + await connection.connect(inject('MONGODB_URI')); initGlobalVariables(); global.systemEnv = {} as any; @@ -60,6 +62,7 @@ afterAll(async () => { await connectionLogMongo?.connection.db?.dropDatabase(); await connectionMongo?.disconnect(); await connectionLogMongo?.disconnect(); + await connection.disconnect(); }); beforeEach(async () => { From 28d9f9acb4bc59e2c0943417c9f77f38b2b7e611 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 14:54:24 +0800 Subject: [PATCH 06/14] fix: apply dal review fixes and align docker node to 24.19.0 --- document/Dockerfile | 2 +- packages/dal/domain/user.ts | 20 +++--- packages/dal/mongodb/errors.ts | 16 +++-- packages/dal/mongodb/index.ts | 2 +- packages/dal/mongodb/indexes.ts | 7 +- packages/dal/mongodb/mappers/user.ts | 36 +++++++--- packages/dal/mongodb/models/user.ts | 3 +- packages/dal/mongodb/repositories/user.ts | 33 ++++++++- packages/dal/mongodb/transaction.ts | 10 +-- packages/dal/mongodb/utils.ts | 3 +- packages/dal/ports/user.repository.ts | 5 ++ .../dal/test/mongodb/mappers/user.test.ts | 22 ++++++ packages/dal/test/mongodb/models/user.test.ts | 2 +- .../test/mongodb/repositories/user.test.ts | 69 +++++++++++++++++++ packages/dal/test/mongodb/transaction.test.ts | 5 +- packages/dal/test/mongodb/utils.test.ts | 4 ++ packages/service/common/dal/index.ts | 27 ++------ packages/service/common/dal/mongo/config.ts | 1 + .../service/common/dal/mongo/connection.ts | 6 +- packages/service/support/user/controller.ts | 12 ++-- projects/app/Dockerfile | 6 +- projects/app/src/instrumentation-node.ts | 2 +- .../support/user/account/checkPswExpired.ts | 6 +- .../support/user/account/resetExpiredPsw.ts | 6 +- .../support/permission/auth/chatCompletion.ts | 6 +- projects/marketplace/Dockerfile | 6 +- projects/mcp_server/Dockerfile | 6 +- projects/volume-manager/Dockerfile | 4 +- test/mocks/common/mongo.ts | 3 +- 29 files changed, 239 insertions(+), 91 deletions(-) diff --git a/document/Dockerfile b/document/Dockerfile index e9d6f57634ae..8c1bcd0a2422 100644 --- a/document/Dockerfile +++ b/document/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.16.0-alpine AS base +FROM node:24.19.0-alpine AS base FROM base AS builder RUN apk add --no-cache \ diff --git a/packages/dal/domain/user.ts b/packages/dal/domain/user.ts index 90032b8c7b6a..69f68c40d5f8 100644 --- a/packages/dal/domain/user.ts +++ b/packages/dal/domain/user.ts @@ -47,13 +47,17 @@ export const UpdateUserSchema = CreateUserSchema.partial().omit({ username: true export type UpdateUser = z.infer; export const UserCredentialsSchema = z.union([ - z.object({ - username: z.string().min(1), - password: z.string().min(1) - }), - z.object({ - id: EntityIdSchema, - password: z.string().min(1) - }) + z + .object({ + username: z.string().min(1), + password: z.string().min(1) + }) + .strict(), + z + .object({ + id: EntityIdSchema, + password: z.string().min(1) + }) + .strict() ]); export type UserCredentials = z.infer; diff --git a/packages/dal/mongodb/errors.ts b/packages/dal/mongodb/errors.ts index 5c4a3eff92e1..0faa113acabb 100644 --- a/packages/dal/mongodb/errors.ts +++ b/packages/dal/mongodb/errors.ts @@ -5,6 +5,7 @@ import { DatabaseTimeoutError, DatabaseUnavailableError, DatabaseUniqueConstraintError, + type DatabaseOperationError, isDatabaseOperationError, type DatabaseErrorAdapter } from '../db'; @@ -30,21 +31,24 @@ const getUniqueFields = (error: ErrorRecord) => { return Object.keys(error.keyPattern).sort(); }; -const conflictCodes = new Set([112, 244, 251]); +// 112 是 WriteConflict;244(NamespaceNotFound)/251(NoSuchTransaction) 不属于写冲突语义。 +const conflictCodes = new Set([112]); const timeoutCodes = new Set([50, 262]); const unavailableCodes = new Set([6, 7, 89, 91, 189, 9001, 11600, 11602]); const timeoutNames = new Set(['MongoNetworkTimeoutError', 'MongoOperationTimeoutError']); const unavailableNames = new Set(['MongoNetworkError', 'MongoServerSelectionError']); export class MongoErrorAdapter implements DatabaseErrorAdapter { - adapt(error: unknown) { + adapt(error: unknown): DatabaseOperationError { if (isDatabaseOperationError(error)) return error; if (error instanceof MongoInvalidArgumentError) { return new DatabaseInvalidArgumentError(error); } const record = toErrorRecord(error); - if (record.code === 11000 || record.code === 11001) { + // 序列化/包装后的驱动错误可能携带字符串 code,统一归一化为数字再比较。 + const code = typeof record.code === 'number' ? record.code : Number(record.code); + if (code === 11000 || code === 11001) { return new DatabaseUniqueConstraintError({ fields: getUniqueFields(record), cause: error @@ -54,16 +58,16 @@ export class MongoErrorAdapter implements DatabaseErrorAdapter { return new DatabaseInvalidArgumentError(error); } if ( - conflictCodes.has(record.code as number) || + conflictCodes.has(code) || record.codeName === 'WriteConflict' || hasErrorLabel(record, 'TransientTransactionError') ) { return new DatabaseConflictError(error); } - if (timeoutCodes.has(record.code as number) || timeoutNames.has(String(record.name))) { + if (timeoutCodes.has(code) || timeoutNames.has(String(record.name))) { return new DatabaseTimeoutError(error); } - if (unavailableCodes.has(record.code as number) || unavailableNames.has(String(record.name))) { + if (unavailableCodes.has(code) || unavailableNames.has(String(record.name))) { return new DatabaseUnavailableError(error); } diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts index fff8e7b8d0cf..748f5a05c843 100644 --- a/packages/dal/mongodb/index.ts +++ b/packages/dal/mongodb/index.ts @@ -1,5 +1,5 @@ export { createMongoUserRepository, MongoUserRepository } from './repositories/user'; -export { toUser } from './mappers/user'; +export { toUser, userDefaultFieldValues } from './mappers/user'; export { MongoAdapter } from './adapter'; export type { MongoAdapterDependencies } from './adapter'; export { MongoErrorAdapter } from './errors'; diff --git a/packages/dal/mongodb/indexes.ts b/packages/dal/mongodb/indexes.ts index 4af1faf64d7d..a29557222899 100644 --- a/packages/dal/mongodb/indexes.ts +++ b/packages/dal/mongodb/indexes.ts @@ -24,6 +24,10 @@ export const defineIndex = ( schema: Schema, { key, options, deprecated }: DefineMongoIndexOptions ) => { + if (Object.keys(key).length === 0) { + throw new Error('defineIndex: key must not be empty'); + } + if (deprecated !== true) { schema.index(key, options); return; @@ -63,5 +67,6 @@ export const defineIndex = ( export const getDeprecatedIndexes = (schema: Schema): readonly DeprecatedMongoIndexDefinition[] => { const indexes: unknown = Reflect.get(schema, deprecatedMongoIndexesKey); - return Array.isArray(indexes) ? indexes : []; + // 返回副本,防止外部直接修改 schema 上登记的内部元数据。 + return Array.isArray(indexes) ? [...indexes] : []; }; diff --git a/packages/dal/mongodb/mappers/user.ts b/packages/dal/mongodb/mappers/user.ts index 8c5a701fb0c2..4304fdfca727 100644 --- a/packages/dal/mongodb/mappers/user.ts +++ b/packages/dal/mongodb/mappers/user.ts @@ -1,26 +1,40 @@ import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import type { UserTagsType } from '@fastgpt/global/support/user/type'; import { UserSchema, type User } from '../../domain/user'; import type { UserDocument } from '../models/user'; import { toEntityId } from '../utils'; +/** toUser 与旧 ClientSession 兼容分支共用的用户字段默认值,避免两处默认值漂移。 */ +export const userDefaultFieldValues: { + timezone: string; + promotionRate: number; + language: string; + tags: UserTagsType[]; +} = { + timezone: 'Asia/Shanghai', + promotionRate: 0, + language: LangEnum.zh_CN, + tags: [] +}; + export const toUser = (document: UserDocument): User => UserSchema.parse({ id: toEntityId(document._id), status: document.status ?? UserStatusEnum.active, username: document.username, - passwordUpdateTime: document.passwordUpdateTime, + passwordUpdateTime: document.passwordUpdateTime ?? undefined, createTime: document.createTime ?? document._id.getTimestamp(), - promotionRate: document.promotionRate ?? 0, - openaiAccount: document.openaiAccount, - timezone: document.timezone ?? 'Asia/Shanghai', - language: document.language ?? LangEnum.zh_CN, + promotionRate: document.promotionRate ?? userDefaultFieldValues.promotionRate, + openaiAccount: document.openaiAccount ?? undefined, + timezone: document.timezone ?? userDefaultFieldValues.timezone, + language: document.language ?? userDefaultFieldValues.language, lastLoginTmbId: document.lastLoginTmbId ? toEntityId(document.lastLoginTmbId) : undefined, inviterId: document.inviterId ? toEntityId(document.inviterId) : undefined, - fastgpt_sem: document.fastgpt_sem, - phonePrefix: document.phonePrefix, - contact: document.contact, - tags: document.tags ?? [], - meta: document.meta, - avatar: document.avatar + fastgpt_sem: document.fastgpt_sem ?? undefined, + phonePrefix: document.phonePrefix ?? undefined, + contact: document.contact ?? undefined, + tags: document.tags ?? userDefaultFieldValues.tags, + meta: document.meta ?? undefined, + avatar: document.avatar ?? undefined }); diff --git a/packages/dal/mongodb/models/user.ts b/packages/dal/mongodb/models/user.ts index a8c07e010de5..41c3a6ae93ad 100644 --- a/packages/dal/mongodb/models/user.ts +++ b/packages/dal/mongodb/models/user.ts @@ -21,7 +21,6 @@ export const UserDocumentSchema = new Schema({ type: String, required: true, set: (value: string) => hashStr(value), - get: (value: string) => hashStr(value), select: false }, passwordUpdateTime: Date, @@ -60,7 +59,7 @@ export const UserDocumentSchema = new Schema({ contact: String, tags: { type: [String], - enum: UserTagsSchema.enum + enum: UserTagsSchema.options }, meta: Object, avatar: String diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts index 45a8966fc24c..e5282daf231c 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/repositories/user.ts @@ -8,7 +8,7 @@ import { toUser } from '../mappers/user'; import { MongoErrorAdapter } from '../errors'; import { getUserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; import { getMongoSession } from '../transaction'; -import { toMongoObjectId } from '../utils'; +import { toEntityId, toMongoObjectId } from '../utils'; export class MongoUserRepository implements UserRepository { constructor( @@ -45,6 +45,31 @@ export class MongoUserRepository implements UserRepository { }); } + /** 仅投影 _id,用于鉴权热路径,避免全文档读取与严格映射。 */ + async findIdByUsername(username: string, context?: TransactionContext): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne({ username }).select('_id'), + context + ).lean<{ _id: unknown }>(); + return document ? toEntityId(document._id) : null; + }); + } + + /** 仅投影 passwordUpdateTime,供密码过期检查使用;null 归一化为 undefined。 */ + async findPasswordUpdateTimeById( + id: EntityId, + context?: TransactionContext + ): Promise<{ passwordUpdateTime?: Date } | null> { + return this.execute(async () => { + const document = await this.withSession( + this.model.findById(toMongoObjectId(id)).select('passwordUpdateTime'), + context + ).lean<{ passwordUpdateTime?: Date | null }>(); + return document ? { passwordUpdateTime: document.passwordUpdateTime ?? undefined } : null; + }); + } + async findByCredentials( credentials: UserCredentials, context?: TransactionContext @@ -76,8 +101,12 @@ export class MongoUserRepository implements UserRepository { context?: TransactionContext ): Promise { return this.execute(async () => { + // 只更新显式提供的字段,避免 partial patch 中的 undefined 覆盖已存值。 + const $set = Object.fromEntries( + Object.entries(patch).filter(([, value]) => value !== undefined) + ); const document = await this.withSession( - this.model.findByIdAndUpdate(toMongoObjectId(id), { $set: patch }, { new: true }), + this.model.findByIdAndUpdate(toMongoObjectId(id), { $set }, { new: true }), context ).lean(); return document ? toUser(document) : null; diff --git a/packages/dal/mongodb/transaction.ts b/packages/dal/mongodb/transaction.ts index 332bfdb75709..a3f83e7c4638 100644 --- a/packages/dal/mongodb/transaction.ts +++ b/packages/dal/mongodb/transaction.ts @@ -22,7 +22,8 @@ export const getMongoSession = (context?: TransactionContext) => { export class MongoTransactionRunner implements TransactionRunner { constructor( private readonly client: Mongoose = mongoose, - private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter(), + private readonly commitTimeoutMS = 60000 ) {} async withTransaction(handler: (context: TransactionContext) => Promise): Promise { @@ -44,7 +45,7 @@ export class MongoTransactionRunner implements TransactionRunner { throw new TransactionHandlerError(error); } }, - { maxCommitTimeMS: 60000 } + { maxCommitTimeMS: this.commitTimeoutMS } ); } catch (error) { operationError = @@ -58,9 +59,8 @@ export class MongoTransactionRunner implements TransactionRunner { try { await session.endSession(); } catch (error) { - if (operationError === undefined) { - throw this.errorAdapter.adapt(error); - } + // 事务已提交或已失败,session 清理失败不应覆盖原结果,记录日志避免静默丢失。 + console.warn('MongoTransactionRunner: failed to end MongoDB session', error); } } } diff --git a/packages/dal/mongodb/utils.ts b/packages/dal/mongodb/utils.ts index 99242e82bb2a..3b37e82b8bb9 100644 --- a/packages/dal/mongodb/utils.ts +++ b/packages/dal/mongodb/utils.ts @@ -10,6 +10,7 @@ export function toEntityId(value: unknown) { } export const toMongoObjectId = (id: EntityId) => { - if (Types.ObjectId.isValid(id)) return new Types.ObjectId(id); + // ObjectId.isValid 对任意 12 字符字符串也返回 true,这里用严格 24 位十六进制校验。 + if (/^[0-9a-fA-F]{24}$/.test(id)) return new Types.ObjectId(id); throw new MongoInvalidArgumentError('Invalid MongoDB entity id'); }; diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/ports/user.repository.ts index c912d1af533a..57f940720303 100644 --- a/packages/dal/ports/user.repository.ts +++ b/packages/dal/ports/user.repository.ts @@ -5,6 +5,11 @@ import type { TransactionContext } from '../transaction'; export type UserRepository = { findById(id: EntityId, context?: TransactionContext): Promise; findByUsername(username: string, context?: TransactionContext): Promise; + findIdByUsername(username: string, context?: TransactionContext): Promise; + findPasswordUpdateTimeById( + id: EntityId, + context?: TransactionContext + ): Promise<{ passwordUpdateTime?: Date } | null>; findByCredentials( credentials: UserCredentials, context?: TransactionContext diff --git a/packages/dal/test/mongodb/mappers/user.test.ts b/packages/dal/test/mongodb/mappers/user.test.ts index 65664bbcb777..8357528a6570 100644 --- a/packages/dal/test/mongodb/mappers/user.test.ts +++ b/packages/dal/test/mongodb/mappers/user.test.ts @@ -53,6 +53,28 @@ describe('toUser', () => { expect(user.inviterId).toBeUndefined(); }); + it('normalizes legacy explicit null values to undefined', () => { + const user = toUser( + createDocument({ + passwordUpdateTime: null, + openaiAccount: null, + fastgpt_sem: null, + phonePrefix: null, + contact: null, + meta: null, + avatar: null + } as unknown as Partial) + ); + + expect(user.passwordUpdateTime).toBeUndefined(); + expect(user.openaiAccount).toBeUndefined(); + expect(user.fastgpt_sem).toBeUndefined(); + expect(user.phonePrefix).toBeUndefined(); + expect(user.contact).toBeUndefined(); + expect(user.meta).toBeUndefined(); + expect(user.avatar).toBeUndefined(); + }); + it('applies production defaults to historical documents with missing fields', () => { const document = createDocument({ status: undefined, diff --git a/packages/dal/test/mongodb/models/user.test.ts b/packages/dal/test/mongodb/models/user.test.ts index 65ff4da8130a..1e5e1a6ce3df 100644 --- a/packages/dal/test/mongodb/models/user.test.ts +++ b/packages/dal/test/mongodb/models/user.test.ts @@ -17,7 +17,7 @@ describe('UserDocumentSchema', () => { expect(document.language).toBe(LangEnum.zh_CN); expect(document.tags).toEqual([]); expect(document.get('password', null, { getters: false })).toBe(hashStr('password')); - expect(document.password).toBe(hashStr(hashStr('password'))); + expect(document.password).toBe(hashStr('password')); expect(UserDocumentSchema.path('password').options.select).toBe(false); }); diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/repositories/user.test.ts index cd5bf9d154fb..57acf2d7d219 100644 --- a/packages/dal/test/mongodb/repositories/user.test.ts +++ b/packages/dal/test/mongodb/repositories/user.test.ts @@ -26,6 +26,7 @@ const document: UserDocument = { const createQuery = (value: T) => ({ session: vi.fn().mockReturnThis(), + select: vi.fn().mockReturnThis(), lean: vi.fn(async () => value) }); @@ -84,6 +85,62 @@ describe('MongoUserRepository.findByUsername', () => { }); }); +describe('MongoUserRepository.findIdByUsername', () => { + it('projects only the entity id', async () => { + const { repository, model, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce({ + _id: new Types.ObjectId(userId) + } as unknown as UserDocument); + + const id = await repository.findIdByUsername('user@example.com'); + + expect(model.findOne).toHaveBeenCalledWith({ username: 'user@example.com' }); + expect(queries.findOne.select).toHaveBeenCalledWith('_id'); + expect(id).toBe(userId); + }); + + it('returns null when no user exists', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce(null); + + await expect(repository.findIdByUsername('missing@example.com')).resolves.toBeNull(); + }); +}); + +describe('MongoUserRepository.findPasswordUpdateTimeById', () => { + it('projects only the password update time', async () => { + const { repository, model, queries } = createRepository(); + const updateTime = new Date('2026-02-01T00:00:00.000Z'); + queries.findById.lean.mockResolvedValueOnce({ + passwordUpdateTime: updateTime + } as unknown as UserDocument); + + const result = await repository.findPasswordUpdateTimeById(userId); + + expect(model.findById).toHaveBeenCalledWith(new Types.ObjectId(userId)); + expect(queries.findById.select).toHaveBeenCalledWith('passwordUpdateTime'); + expect(result).toEqual({ passwordUpdateTime: updateTime }); + }); + + it('normalizes an explicit null update time to undefined', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce({ + passwordUpdateTime: null + } as unknown as UserDocument); + + await expect(repository.findPasswordUpdateTimeById(userId)).resolves.toEqual({ + passwordUpdateTime: undefined + }); + }); + + it('returns null when the user does not exist', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce(null); + + await expect(repository.findPasswordUpdateTimeById(userId)).resolves.toBeNull(); + }); +}); + describe('MongoUserRepository.findByCredentials', () => { it('passes raw credentials to the Mongoose schema setter', async () => { const { repository, model } = createRepository(); @@ -156,6 +213,18 @@ describe('MongoUserRepository.updateById', () => { await expect(repository.updateById(userId, { timezone: 'UTC' })).resolves.toBeNull(); }); + + it('drops undefined patch fields before updating', async () => { + const { repository, model } = createRepository(); + + await repository.updateById(userId, { timezone: 'UTC', contact: undefined }); + + expect(model.findByIdAndUpdate).toHaveBeenCalledWith( + new Types.ObjectId(userId), + { $set: { timezone: 'UTC' } }, + { new: true } + ); + }); }); describe('MongoUserRepository transaction context', () => { diff --git a/packages/dal/test/mongodb/transaction.test.ts b/packages/dal/test/mongodb/transaction.test.ts index 304f7e3b0a15..8d1508791421 100644 --- a/packages/dal/test/mongodb/transaction.test.ts +++ b/packages/dal/test/mongodb/transaction.test.ts @@ -63,7 +63,7 @@ describe('MongoTransactionRunner', () => { expect(session.endSession).toHaveBeenCalledOnce(); }); - it('maps start, commit and end driver errors', async () => { + it('maps start and commit driver errors and keeps session cleanup best-effort', async () => { const startClient = { startSession: vi.fn(async () => { throw { name: 'MongoServerSelectionError' }; @@ -79,11 +79,12 @@ describe('MongoTransactionRunner', () => { new MongoTransactionRunner(commitClient).withTransaction(async () => undefined) ).rejects.toBeInstanceOf(DatabaseTimeoutError); + // 事务已提交后 endSession 失败不应让调用方误以为事务失败(记录日志即可)。 const { client: endClient, session: endSession } = createRuntime(); vi.mocked(endSession.endSession).mockRejectedValueOnce({ name: 'MongoNetworkError' }); await expect( new MongoTransactionRunner(endClient).withTransaction(async () => undefined) - ).rejects.toBeInstanceOf(DatabaseUnavailableError); + ).resolves.toBeUndefined(); }); it('preserves the handler error when session cleanup also fails', async () => { diff --git a/packages/dal/test/mongodb/utils.test.ts b/packages/dal/test/mongodb/utils.test.ts index a50f00dad1ca..a9fd1d1a1ebe 100644 --- a/packages/dal/test/mongodb/utils.test.ts +++ b/packages/dal/test/mongodb/utils.test.ts @@ -27,4 +27,8 @@ describe('toMongoObjectId', () => { expect(() => toMongoObjectId('sql-id')).toThrow(MongoInvalidArgumentError); expect(() => toMongoObjectId('sql-id')).toThrow('Invalid MongoDB entity id'); }); + + it('rejects short hex-like strings that ObjectId.isValid accepts', () => { + expect(() => toMongoObjectId('abcdefghijkl')).toThrow(MongoInvalidArgumentError); + }); }); diff --git a/packages/service/common/dal/index.ts b/packages/service/common/dal/index.ts index 69859d1c9251..c68776472148 100644 --- a/packages/service/common/dal/index.ts +++ b/packages/service/common/dal/index.ts @@ -6,8 +6,9 @@ export type { DatabaseAdapter } from '@fastgpt/dal/db'; export type { UserRepository } from '@fastgpt/dal/ports'; export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/transaction'; -function selectDal() { - switch (serviceEnv.DAL_DB_TYPE) { +function selectDal(): DatabaseAdapter { + // env schema 默认 mongo;被 mock 的 serviceEnv 可能缺少该字段,这里保持同样的默认语义。 + switch (serviceEnv.DAL_DB_TYPE ?? 'mongo') { case 'mongo': { return createMongoDal(); } @@ -18,23 +19,7 @@ function selectDal() { } } -function createRepositories() { - const { errorAdapter: _e, transactionRunner: _t, ...repositories }: DatabaseAdapter = selectDal(); +// 只实例化一次 adapter,保证 userRepository 与 transactionRunner 来自同一个 adapter 实例。 +const dal = selectDal(); - return { - ...repositories - }; -} - -function createTransactionRunner() { - const { transactionRunner }: DatabaseAdapter = selectDal(); - - return { - transactionRunner - }; -} - -export const { transactionRunner, userRepository } = { - ...createRepositories(), - ...createTransactionRunner() -}; +export const { transactionRunner, userRepository } = dal; diff --git a/packages/service/common/dal/mongo/config.ts b/packages/service/common/dal/mongo/config.ts index f67699092505..93d52414f1dd 100644 --- a/packages/service/common/dal/mongo/config.ts +++ b/packages/service/common/dal/mongo/config.ts @@ -25,6 +25,7 @@ export const createDefaultMongooseConnectOptions = (): ConnectOptions => { export const registerMongooseListeners = (db: Mongoose) => { db.connection.removeAllListeners('error'); + db.connection.removeAllListeners('connected'); db.connection.removeAllListeners('disconnected'); db.connection.on('error', (error) => { diff --git a/packages/service/common/dal/mongo/connection.ts b/packages/service/common/dal/mongo/connection.ts index d7ca5bb10a35..bdc49320653f 100644 --- a/packages/service/common/dal/mongo/connection.ts +++ b/packages/service/common/dal/mongo/connection.ts @@ -36,7 +36,11 @@ export class MongooseConnection { return db; } catch (error) { logger.error('DAL MongoDB connection failed, will retry', { error }); - await db.disconnect(); + try { + await db.disconnect(); + } catch (disconnectError) { + logger.warn('DAL MongoDB disconnect failed during retry', { error: disconnectError }); + } await delay(1000); return this.connect(url); } diff --git a/packages/service/support/user/controller.ts b/packages/service/support/user/controller.ts index 710f0e9a17c5..e8c5584e9ea7 100644 --- a/packages/service/support/user/controller.ts +++ b/packages/service/support/user/controller.ts @@ -1,11 +1,11 @@ import { type UserType } from '@fastgpt/global/support/user/type'; +import { userDefaultFieldValues } from '@fastgpt/dal'; import { MongoUser } from './schema'; import { userRepository } from '../../common/dal'; import { getTmbInfoByTmbId, getUserDefaultTeam } from './team/controller'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import type { ClientSession } from '../../common/mongo'; -import { LangEnum } from '@fastgpt/global/common/i18n/type'; export async function authUserExist({ userId, username }: { userId?: string; username?: string }) { if (userId) { @@ -51,11 +51,11 @@ export async function getUserDetail({ return { id: String(document._id), username: document.username, - timezone: document.timezone ?? 'Asia/Shanghai', - promotionRate: document.promotionRate ?? 0, - contact: document.contact, - language: document.language ?? LangEnum.zh_CN, - tags: document.tags ?? [] + timezone: document.timezone ?? userDefaultFieldValues.timezone, + promotionRate: document.promotionRate ?? userDefaultFieldValues.promotionRate, + contact: document.contact ?? undefined, + language: document.language ?? userDefaultFieldValues.language, + tags: document.tags ?? userDefaultFieldValues.tags }; })(); diff --git a/projects/app/Dockerfile b/projects/app/Dockerfile index f331d056f29a..a2009a7fac3d 100644 --- a/projects/app/Dockerfile +++ b/projects/app/Dockerfile @@ -1,5 +1,5 @@ # --------- install dependence ----------- -FROM node:24.16.0-alpine AS maindeps +FROM node:24.19.0-alpine AS maindeps WORKDIR /app ARG proxy @@ -23,7 +23,7 @@ RUN if [ -z "$proxy" ]; then \ fi # --------- builder ----------- -FROM node:24.16.0-alpine AS builder +FROM node:24.19.0-alpine AS builder WORKDIR /app ARG proxy @@ -58,7 +58,7 @@ RUN rm -rf projects/app/.next/standalone/node_modules/.pnpm/@next+rspack-binding projects/app/.next/standalone/node_modules/.pnpm/@img+sharp-libvips-linux-x64@*/ # --------- runner ----------- -FROM node:24.16.0-alpine AS runner +FROM node:24.19.0-alpine AS runner WORKDIR /app ARG proxy diff --git a/projects/app/src/instrumentation-node.ts b/projects/app/src/instrumentation-node.ts index b9143fb11aba..bf3188516694 100644 --- a/projects/app/src/instrumentation-node.ts +++ b/projects/app/src/instrumentation-node.ts @@ -149,7 +149,7 @@ export async function registerNodeInstrumentation() { runInitializationStep({ step: 'connect-dal-mongo', stage: InitialErrorEnum.MONGO_ERROR, - action: () => connection.connect(), + action: () => connection.connect(MONGO_URL), logger, getErrText, meta: { diff --git a/projects/app/src/pages/api/support/user/account/checkPswExpired.ts b/projects/app/src/pages/api/support/user/account/checkPswExpired.ts index b2ef9d70cea5..535f8ed0fce3 100644 --- a/projects/app/src/pages/api/support/user/account/checkPswExpired.ts +++ b/projects/app/src/pages/api/support/user/account/checkPswExpired.ts @@ -11,13 +11,13 @@ async function handler( ): Promise { const { userId } = await authCert({ req, authToken: true }); - const user = await userRepository.findById(userId); + const updateTime = await userRepository.findPasswordUpdateTimeById(userId); - if (!user) { + if (!updateTime) { return false; } - return checkPswExpired({ updateTime: user.passwordUpdateTime }); + return checkPswExpired({ updateTime: updateTime.passwordUpdateTime }); } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts b/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts index 9f3bfea57045..cb52359389bd 100644 --- a/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts +++ b/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts @@ -18,14 +18,14 @@ async function resetExpiredPswHandler(req: ApiRequestProps): Promise { - const user = await userRepository.findByUsername(username); - if (!user) return null; + const userId = await userRepository.findIdByUsername(username); + if (!userId) return null; return MongoTeamMember.findOne({ teamId, - userId: user.id, + userId, status: notLeaveStatus }) .select('_id userId teamId') diff --git a/projects/marketplace/Dockerfile b/projects/marketplace/Dockerfile index 41decac9d7ab..73d63ce5a233 100644 --- a/projects/marketplace/Dockerfile +++ b/projects/marketplace/Dockerfile @@ -1,5 +1,5 @@ # --------- install dependence ----------- -FROM node:24.16.0-alpine AS maindeps +FROM node:24.19.0-alpine AS maindeps WORKDIR /app ARG proxy @@ -23,7 +23,7 @@ RUN if [ -z "$proxy" ]; then \ fi # --------- builder ----------- -FROM node:24.16.0-alpine AS builder +FROM node:24.19.0-alpine AS builder WORKDIR /app ARG proxy @@ -47,7 +47,7 @@ RUN pnpm --filter @fastgpt-sdk/otel --filter @fastgpt-sdk/storage --filter @fast RUN pnpm --filter=marketplace build # --------- runner ----------- -FROM node:24.16.0-alpine AS runner +FROM node:24.19.0-alpine AS runner WORKDIR /app ARG proxy diff --git a/projects/mcp_server/Dockerfile b/projects/mcp_server/Dockerfile index af9a056cec50..295017ff0285 100644 --- a/projects/mcp_server/Dockerfile +++ b/projects/mcp_server/Dockerfile @@ -1,5 +1,5 @@ # --------- Install ----------- -FROM node:24.16.0-alpine AS install +FROM node:24.19.0-alpine AS install WORKDIR /app ARG proxy @@ -30,7 +30,7 @@ RUN if [ -z "$proxy" ]; then \ fi # --------- builder ----------- -FROM node:24.16.0-alpine AS builder +FROM node:24.19.0-alpine AS builder WORKDIR /app ARG proxy @@ -53,7 +53,7 @@ RUN pnpm --filter @fastgpt-sdk/otel build RUN pnpm --filter=@fastgpt/mcp_server build # runner -FROM node:24.16.0-alpine AS runner +FROM node:24.19.0-alpine AS runner WORKDIR /app ARG proxy diff --git a/projects/volume-manager/Dockerfile b/projects/volume-manager/Dockerfile index 29bf93a4e457..a75b77e374bf 100644 --- a/projects/volume-manager/Dockerfile +++ b/projects/volume-manager/Dockerfile @@ -1,7 +1,7 @@ # Build from repository root: # docker build -f projects/volume-manager/Dockerfile -t fastgpt-volume-manager:latest . -FROM node:24.16.0-alpine AS builder +FROM node:24.19.0-alpine AS builder WORKDIR /app @@ -22,7 +22,7 @@ RUN if [ -z "$proxy" ]; then \ RUN cd /app/projects/volume-manager && pnpm build -FROM node:24.16.0-alpine AS runner +FROM node:24.19.0-alpine AS runner WORKDIR /app diff --git a/test/mocks/common/mongo.ts b/test/mocks/common/mongo.ts index a63f09887c1e..978e97e571b8 100644 --- a/test/mocks/common/mongo.ts +++ b/test/mocks/common/mongo.ts @@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'; import type { Mongoose } from '@fastgpt/service/common/mongo'; import { connectionMongo } from '@fastgpt/service/common/mongo'; import { connection } from '@fastgpt/service/common/dal/mongo/connection'; +import { serviceEnv } from '@fastgpt/service/env'; const fileDbPrefix = `fastgpt_test_${process.env.VITEST_WORKER_ID ?? '0'}_${randomUUID().replaceAll( '-', @@ -80,7 +81,7 @@ vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any */ vi.mock(import('@fastgpt/service/common/dal/mongo/connection'), async (importOriginal: any) => { const mod = await importOriginal(); - mod.connection.connect = async (url: string) => + mod.connection.connect = async (url: string = serviceEnv.MONGODB_URI) => connectTestMongo({ db: mod.connection.client, url }); return mod; }); From fb694d20b02447afd775d9631263313dc536648b Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 16:36:17 +0800 Subject: [PATCH 07/14] docs: defer sql adapter and add m9 migration scope --- .agents/design/dal-basic-architecture.md | 2 +- .agents/design/dal-m9-user-call-migration.md | 44 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .agents/design/dal-m9-user-call-migration.md diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index ab97f7b9062b..cffcd5b6fcab 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -109,7 +109,7 @@ packages/service/common/dal/ - [x] 补齐 User 基础 Repository 操作和 Mongo 事务支持。 - [x] 补齐 mapper、Schema、Repository、事务和索引局部测试。 - [x] 增加数据库错误 Adapter 与 Mongo 错误映射。 -- 增加 SQL-like User adapter(接入 `DAL_DB_TYPE=sql` 分支),并验证两种 adapter 的行为契约一致。 +- [ ] ~~增加 SQL-like User adapter(接入 `DAL_DB_TYPE=sql` 分支)~~ 已延后:整个 MongoDB 重构完成后再考虑,另起项目跟进。 - 统一 DAL 的分页、唯一约束错误和并发更新抽象。 - 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 - 接入独立 DAL 连接后,验证与主连接的索引同步、慢查询日志和生命周期管理。 diff --git a/.agents/design/dal-m9-user-call-migration.md b/.agents/design/dal-m9-user-call-migration.md new file mode 100644 index 000000000000..b21079a78672 --- /dev/null +++ b/.agents/design/dal-m9-user-call-migration.md @@ -0,0 +1,44 @@ +# M9:迁移 support/user 剩余调用到 UserRepository + +关联任务:Kaneo M#9「迁移 support/user 剩余调用到 UserRepository」。 +前置设计:[dal-basic-architecture.md](./dal-basic-architecture.md)。 + +## 目标 + +把 `packages/service/support/user` 及周边对 `MongoUser` 的剩余直接调用迁移到 `@fastgpt/service/common/dal` 的 `userRepository`,并清理 `getUserDetail` 的旧 `ClientSession` 兼容分支。迁移必须遵守「不能混用两种事务上下文」的约束。 + +## 现状盘点(直接依赖 MongoUser 的剩余调用) + +| 调用点 | 用途 | 是否可直接迁移 | +| --- | --- | --- | +| `support/marketing/interface/crm.ts` | 按 ownerId 投影读取 `fastgpt_sem` | ✅ 纯 User 读取,无事务 | +| `support/user/account/verification/password/service.ts` | `findUserByCredentials`(登录事务内) | ⛔ 绑定旧 `consumeInTransaction`(ClientSession)+ 修改 Document 后 `save` | +| `support/user/controller.ts` `getUserDetail` | 登录事务内的用户读取(兼容分支) | ⛔ 依赖登录事务的旧 session | +| `pages/api/support/user/account/update.ts` | 资料更新(user + teamMember + 头像) | ⛔ 同一旧事务内改 TeamMember/头像资源 | +| `service/mongo.ts` `initRootUser` | root 初始化(user + 默认团队) | ⛔ 同一旧事务内 `createDefaultTeam`(Team/TeamMember/MemberGroup) | +| `pages/api/core/app/logs/exportLogs.ts` | 成员导出 `users` `$lookup` | ➖ 设计上明确暂不沉底的跨集合读模型 | + +## 阻塞点 + +前三个 ⛔ 边界都在同一个旧 `ClientSession` 事务里同时操作 `users` 与 `teams/teamMembers/memberGroups`。 +DAL 事务上下文是内部 symbol,旧 Model 无法参与 DAL 事务;Team/TeamMember 尚未有 DAL Repository。 +因此按设计约束,必须先补齐 Team/TeamMember 垂直切片,才能迁移这三个流程并删除 `getUserDetail` 兼容分支。 + +## 候选方案 + +### 方案 A:完整做 M9(推荐) +1. 新增 Team / TeamMember(含 MemberGroup 最小能力)DAL:`domain`、`ports`、`mongodb`(models/mappers/repositories)+ 局部测试。 +2. `verification` 提供基于 `transactionRunner` 的消费入口(DAL 事务变体),`consumeInTransaction` 内部使用 DAL 事务。 +3. 迁移 `loginByPassword`(凭据校验 → `findByCredentials`,`lastLoginTmbId/language/fastgpt_sem` → `updateById`)。 +4. 迁移 `update.ts`(user 字段走 `userRepository.updateById`,TeamMember 头像走新 TeamMember DAL,头像刷新保留)。 +5. 迁移 `initRootUser`(root 用户走 `userRepository`,默认团队走 Team DAL)。 +6. 删除 `getUserDetail` 兼容分支;`crm.ts` 投影读取迁移到 `userRepository`(新增投影方法)。 +7. `exportLogs.ts` `$lookup` 维持现状(设计决策)。 + +### 方案 B:只做纯 User 部分 +仅迁移 `crm.ts` 投影读取 + 新增对应 Repository 方法;三个事务边界与 `getUserDetail` 兼容分支保持现状,Team/TeamMember 切片另开任务。 + +## 待确认 + +- 选 A(完整迁移,改动较大,需新增 Team/TeamMember DAL 切片)还是 B(先只迁移纯 User 调用)? +- 若选 A:Team/TeamMember DAL 是否只覆盖当前三个流程所需的最小能力(findByIds/按 userId 查默认 tmb/创建默认团队/更新头像),不做完整 CRUD? From fef20fdf51646cf387d1a0870686cae21f4cb262 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 16:51:12 +0800 Subject: [PATCH 08/14] feat(dal): unified pagination and optimistic update abstractions (M8) --- .agents/design/dal-unified-abstractions.md | 50 ++++++++++ packages/dal/db/concurrency.ts | 19 ++++ packages/dal/db/index.ts | 3 + packages/dal/db/pagination.ts | 39 ++++++++ packages/dal/mongodb/concurrency.ts | 30 ++++++ packages/dal/mongodb/index.ts | 4 + packages/dal/mongodb/pagination.ts | 38 +++++++ packages/dal/mongodb/repositories/user.ts | 32 +++++- packages/dal/ports/user.repository.ts | 11 +++ packages/dal/test/db/pagination.test.ts | 36 +++++++ packages/dal/test/mongodb/concurrency.test.ts | 98 +++++++++++++++++++ packages/dal/test/mongodb/pagination.test.ts | 75 ++++++++++++++ .../test/mongodb/repositories/user.test.ts | 74 +++++++++++++- 13 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 .agents/design/dal-unified-abstractions.md create mode 100644 packages/dal/db/concurrency.ts create mode 100644 packages/dal/db/pagination.ts create mode 100644 packages/dal/mongodb/concurrency.ts create mode 100644 packages/dal/mongodb/pagination.ts create mode 100644 packages/dal/test/db/pagination.test.ts create mode 100644 packages/dal/test/mongodb/concurrency.test.ts create mode 100644 packages/dal/test/mongodb/pagination.test.ts diff --git a/.agents/design/dal-unified-abstractions.md b/.agents/design/dal-unified-abstractions.md new file mode 100644 index 000000000000..d276dae32408 --- /dev/null +++ b/.agents/design/dal-unified-abstractions.md @@ -0,0 +1,50 @@ +# M8:统一 DAL 的分页、唯一约束错误和并发更新抽象 + +关联任务:Kaneo M#8。前置设计:[dal-basic-architecture.md](./dal-basic-architecture.md)。 + +## 目标 + +为后续 Repository 提供统一的三个通用能力,先以 Mongo adapter 为基准落地并补契约测试;SQL 契约一致性验证随延后的 SQL 项目另行进行。 + +## 现状 + +- 唯一约束冲突字段暴露已实现:`MongoErrorAdapter` 从 `keyPattern` 提取字段(排序后暴露),不泄漏 `keyValue`;已有单元测试(`mongodb/errors.test.ts`)与仓储测试(`repositories/user.test.ts`:duplicate write 断言 `fields: ['username']` 且 message 不含值)。**本任务不重复实现,只保持契约。** +- 分页、并发更新无 DAL 抽象。 + +## 1. 分页统一结构 + +- `packages/dal/db/pagination.ts`(数据库无关契约): + - `PageParams = { page: number; pageSize: number }`(1-based) + - `PageResult = { total: number; list: T[] }`(与业务侧 `PaginationResponseType` 对齐) + - `normalizePageParams(params, opts?)` → `{ skip, limit }`:缺省 `page=1`、`pageSize=10`,`pageSize` 上限可配(默认 100),非法输入取默认值。 +- `packages/dal/mongodb/pagination.ts`(Mongo 实现): + - `paginate({ model, filter, page, pageSize, sort, session })`:`countDocuments` 与 `find().sort().skip().limit().lean()` 并行执行,返回 `PageResult<文档>`;支持 `ClientSession`(供事务内分页)。 + +## 2. 并发更新/乐观锁语义 + +语义约定(与「`null/false` 表示资源不存在」区分): + +- Repository 提供 `updateByIdIfState(id, expected, patch, context?)`:用「期望状态」做原子条件更新(CAS)。 +- 期望状态匹配:正常更新,返回更新后的实体。 +- 文档不存在:返回 `null`(保持既有语义)。 +- 文档存在但期望状态不匹配:抛 `DatabaseConflictError`(`DB_CONFLICT`),语义是「并发写冲突」,与「不存在」区分开。 + +实现: + +- `packages/dal/db/concurrency.ts`:契约类型 `ExpectedState`、`CasUpdate` 接口(文档说明 mismatch → `DB_CONFLICT`)。 +- `packages/dal/mongodb/concurrency.ts`:`casUpdateById({ model, id, expected, patch, session })`,基于 `findOneAndUpdate({ _id, ...expected }, { $set: patch }, { new: true })`;文档存在但未命中返回 `null` 供 Repository 映射为 `DB_CONFLICT`。 +- `UserRepository` port 增加 `updateByIdIfState`;`MongoUserRepository` 实现:id 经 `toMongoObjectId` 转换,expected/patch 为领域字段(当前 User 领域字段与文档字段同名,字段映射由各 Repository 的 mapper 集中负责,本实现直接透传)。 +- 设计决策记录:CAS 不匹配抛 `DB_CONFLICT` 而非返回 `null`,避免业务无法区分「不存在」与「并发冲突」;原始驱动错误只保留在 `cause`。 + +## 测试 + +- `test/db/pagination.test.ts`:`normalizePageParams` 边界(缺省、上限、非法值)。 +- `test/mongodb/pagination.test.ts`:`paginate` 的 total/list、skip/limit、sort、session 透传(mock model)。 +- `test/mongodb/concurrency.test.ts`:`casUpdateById` 匹配更新、不匹配返回 null、非法 id 报错(mock model)。 +- `test/mongodb/repositories/user.test.ts`:新增 `updateByIdIfState` 用例(匹配→新实体;不匹配→`DB_CONFLICT`;不存在→null;非法 id→`DB_INVALID_ARGUMENT`)。 + +## 不做的内容 + +- 不新增未被业务调用的 Repository 分页方法(User 尚无分页业务需求,抽象先落在 db/mongodb 层)。 +- 不修改 `DatabaseUniqueConstraintError` 现有语义与字段暴露逻辑。 +- 不在本次引入 `version` 字段(生产 User Schema 无该字段;乐观锁以期望状态 CAS 表达,字段可由业务按需选用)。 diff --git a/packages/dal/db/concurrency.ts b/packages/dal/db/concurrency.ts new file mode 100644 index 000000000000..7e90852db789 --- /dev/null +++ b/packages/dal/db/concurrency.ts @@ -0,0 +1,19 @@ +import type { EntityId } from '../domain'; +import type { TransactionContext } from '../transaction'; + +/** 期望状态:并发更新(CAS)时用于匹配的当前值子集,字段为领域字段。 */ +export type ExpectedState = Partial; + +/** + * 并发更新契约:按期望状态做原子条件更新。 + * 文档不存在返回 null;文档存在但期望状态不匹配时抛 DatabaseConflictError(DB_CONFLICT), + * 与「资源不存在」的 null 语义区分开。 + */ +export type CasUpdate = { + updateByIdIfState( + id: EntityId, + expected: ExpectedState, + patch: P, + context?: TransactionContext + ): Promise; +}; diff --git a/packages/dal/db/index.ts b/packages/dal/db/index.ts index e968cc97b4a4..be3fab7afd2d 100644 --- a/packages/dal/db/index.ts +++ b/packages/dal/db/index.ts @@ -13,3 +13,6 @@ export { isDatabaseOperationError } from './errors'; export type { DatabaseErrorCode } from './errors'; +export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, normalizePageParams } from './pagination'; +export type { NormalizedPageParams, PageParams, PageResult } from './pagination'; +export type { CasUpdate, ExpectedState } from './concurrency'; diff --git a/packages/dal/db/pagination.ts b/packages/dal/db/pagination.ts new file mode 100644 index 000000000000..8d207c7cd273 --- /dev/null +++ b/packages/dal/db/pagination.ts @@ -0,0 +1,39 @@ +export type PageParams = { + page: number; + pageSize: number; +}; + +export type PageResult = { + total: number; + list: T[]; +}; + +export type NormalizedPageParams = { + skip: number; + limit: number; +}; + +export const DEFAULT_PAGE_SIZE = 10; +export const MAX_PAGE_SIZE = 100; + +/** + * 将 1-based 分页参数归一化为 skip/limit。 + * 非法或缺失的 page/pageSize 回退默认值,pageSize 超过 maxPageSize 时截断。 + */ +export function normalizePageParams( + params: PageParams, + opts: { defaultPageSize?: number; maxPageSize?: number } = {} +): NormalizedPageParams { + const defaultPageSize = opts.defaultPageSize ?? DEFAULT_PAGE_SIZE; + const maxPageSize = opts.maxPageSize ?? MAX_PAGE_SIZE; + + const page = Number.isInteger(params.page) && params.page > 0 ? params.page : 1; + const pageSize = + Number.isInteger(params.pageSize) && params.pageSize > 0 ? params.pageSize : defaultPageSize; + const limit = Math.min(pageSize, maxPageSize); + + return { + skip: (page - 1) * limit, + limit + }; +} diff --git a/packages/dal/mongodb/concurrency.ts b/packages/dal/mongodb/concurrency.ts new file mode 100644 index 000000000000..e78ace042e7e --- /dev/null +++ b/packages/dal/mongodb/concurrency.ts @@ -0,0 +1,30 @@ +import type { ClientSession, FilterQuery, Model, UpdateQuery } from 'mongoose'; +import type { EntityId } from '../domain'; +import { toMongoObjectId } from './utils'; + +export type CasUpdateByIdParams = { + model: Model; + id: EntityId; + expected: Record; + patch: Record; + session?: ClientSession; +}; + +/** + * 基于 findOneAndUpdate 的原子条件更新:filter 为 `{ _id, ...expected }`, + * 期望状态不匹配时返回 null 且不执行更新;patch 中的 undefined 字段会被过滤。 + */ +export async function casUpdateById(params: CasUpdateByIdParams): Promise { + const { model, id, expected, patch, session } = params; + + const $set = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)); + const query = model.findOneAndUpdate( + { _id: toMongoObjectId(id), ...expected } as FilterQuery, + { $set } as UpdateQuery, + { new: true } + ); + if (session) query.session(session); + + // 低层 helper:lean 返回 FlattenMaps 形状,由调用方映射为领域类型。 + return query.lean() as unknown as Promise; +} diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts index 748f5a05c843..96eafa90cfa4 100644 --- a/packages/dal/mongodb/index.ts +++ b/packages/dal/mongodb/index.ts @@ -4,3 +4,7 @@ export { MongoAdapter } from './adapter'; export type { MongoAdapterDependencies } from './adapter'; export { MongoErrorAdapter } from './errors'; export { MongoTransactionRunner } from './transaction'; +export { paginate } from './pagination'; +export type { PaginateParams } from './pagination'; +export { casUpdateById } from './concurrency'; +export type { CasUpdateByIdParams } from './concurrency'; diff --git a/packages/dal/mongodb/pagination.ts b/packages/dal/mongodb/pagination.ts new file mode 100644 index 000000000000..16daa548fc07 --- /dev/null +++ b/packages/dal/mongodb/pagination.ts @@ -0,0 +1,38 @@ +import type { ClientSession, FilterQuery, Model } from 'mongoose'; +import { normalizePageParams, type PageParams, type PageResult } from '../db'; + +export type PaginateParams = { + model: Model; + filter: FilterQuery; + page: PageParams['page']; + pageSize: PageParams['pageSize']; + sort?: Record; + session?: ClientSession; +}; + +/** + * Mongo 分页实现:countDocuments 与 find().sort().skip().limit().lean() 并行执行, + * 返回统一的 PageResult。session 存在时两处查询都绑定到同一事务。 + */ +export async function paginate(params: PaginateParams): Promise> { + const { model, filter, page, pageSize, sort, session } = params; + const { skip, limit } = normalizePageParams({ page, pageSize }); + + const countQuery = model.countDocuments(filter); + const findQuery = model + .find(filter) + .sort(sort ?? {}) + .skip(skip) + .limit(limit); + if (session) { + countQuery.session(session); + findQuery.session(session); + } + + // 低层 helper:lean 返回 FlattenMaps 形状,由调用方映射为领域类型。 + const [total, list] = await Promise.all([ + countQuery, + findQuery.lean() as unknown as Promise + ]); + return { total, list }; +} diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts index e5282daf231c..9bf23ceb0d09 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/repositories/user.ts @@ -1,5 +1,5 @@ import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; -import type { DatabaseErrorAdapter } from '../../db'; +import { DatabaseConflictError, type DatabaseErrorAdapter } from '../../db'; import type { CreateUser, UpdateUser, User, UserCredentials } from '../../domain/user'; import type { EntityId } from '../../domain/types'; import type { UserRepository } from '../../ports/user.repository'; @@ -7,6 +7,7 @@ import type { TransactionContext } from '../../transaction'; import { toUser } from '../mappers/user'; import { MongoErrorAdapter } from '../errors'; import { getUserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; +import { casUpdateById } from '../concurrency'; import { getMongoSession } from '../transaction'; import { toEntityId, toMongoObjectId } from '../utils'; @@ -112,6 +113,35 @@ export class MongoUserRepository implements UserRepository { return document ? toUser(document) : null; }); } + + async updateByIdIfState( + id: EntityId, + expected: Partial, + patch: UpdateUser, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + // 低层 helper 返回文档形状,此处映射为带 _id/__v 的 UserDocument 再走领域转换。 + const document = (await casUpdateById({ + model: this.model, + id, + expected, + patch, + session: getMongoSession(context) + })) as UserDocument | null; + if (document) return toUser(document); + + // CAS 未命中时区分「文档不存在」与「并发冲突」:先确认文档是否仍存在。 + const exists = await this.withSession( + this.model.exists({ _id: toMongoObjectId(id) }), + context + ); + if (exists) { + throw new DatabaseConflictError(); + } + return null; + }); + } } export const createMongoUserRepository = ( diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/ports/user.repository.ts index 57f940720303..abb0d91d40fb 100644 --- a/packages/dal/ports/user.repository.ts +++ b/packages/dal/ports/user.repository.ts @@ -1,3 +1,4 @@ +import type { ExpectedState } from '../db'; import type { CreateUser, UpdateUser, User, UserCredentials } from '../domain/user'; import type { EntityId } from '../domain/types'; import type { TransactionContext } from '../transaction'; @@ -16,4 +17,14 @@ export type UserRepository = { ): Promise; create(input: CreateUser, context?: TransactionContext): Promise; updateById(id: EntityId, patch: UpdateUser, context?: TransactionContext): Promise; + /** + * 按期望状态做原子条件更新:期望状态不匹配时抛 DatabaseConflictError(DB_CONFLICT), + * 文档不存在返回 null。 + */ + updateByIdIfState( + id: EntityId, + expected: ExpectedState, + patch: UpdateUser, + context?: TransactionContext + ): Promise; }; diff --git a/packages/dal/test/db/pagination.test.ts b/packages/dal/test/db/pagination.test.ts new file mode 100644 index 000000000000..053124a1a06a --- /dev/null +++ b/packages/dal/test/db/pagination.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, normalizePageParams } from '../../db'; + +describe('normalizePageParams', () => { + it('keeps valid 1-based params and computes skip', () => { + expect(normalizePageParams({ page: 3, pageSize: 20 })).toEqual({ skip: 40, limit: 20 }); + }); + + it('falls back to defaults for missing or invalid params', () => { + expect(normalizePageParams({ page: 0, pageSize: 0 })).toEqual({ + skip: 0, + limit: DEFAULT_PAGE_SIZE + }); + expect(normalizePageParams({ page: 1.5, pageSize: -1 })).toEqual({ + skip: 0, + limit: DEFAULT_PAGE_SIZE + }); + expect(normalizePageParams({ page: NaN, pageSize: NaN })).toEqual({ + skip: 0, + limit: DEFAULT_PAGE_SIZE + }); + }); + + it('clamps pageSize to the max limit', () => { + expect(normalizePageParams({ page: 1, pageSize: 1000 })).toEqual({ + skip: 0, + limit: MAX_PAGE_SIZE + }); + }); + + it('honors custom defaults and limits', () => { + expect( + normalizePageParams({ page: 2, pageSize: 300 }, { defaultPageSize: 25, maxPageSize: 200 }) + ).toEqual({ skip: 200, limit: 200 }); + }); +}); diff --git a/packages/dal/test/mongodb/concurrency.test.ts b/packages/dal/test/mongodb/concurrency.test.ts new file mode 100644 index 000000000000..14c0afe0c456 --- /dev/null +++ b/packages/dal/test/mongodb/concurrency.test.ts @@ -0,0 +1,98 @@ +import type { Model, Types } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import { casUpdateById } from '../../mongodb/concurrency'; +import { MongoInvalidArgumentError } from '../../mongodb/errors'; + +type Doc = { _id: Types.ObjectId; username: string; timezone: string; contact?: string }; + +const userId = '507f1f77bcf86cd799439011'; +const doc: Doc = { + _id: userId as unknown as Types.ObjectId, + username: 'user@example.com', + timezone: 'UTC' +}; + +const createQuery = (value: T) => ({ + session: vi.fn().mockReturnThis(), + lean: vi.fn(async () => value) +}); + +const createModel = (value: Doc | null) => { + const findOneAndUpdate = vi.fn(() => createQuery(value)); + const model = { findOneAndUpdate } as unknown as Model; + return { model, findOneAndUpdate }; +}; + +describe('casUpdateById', () => { + it('matches expected state and returns the updated document', async () => { + const { model, findOneAndUpdate } = createModel(doc); + + const result = await casUpdateById({ + model, + id: userId, + expected: { timezone: 'Asia/Shanghai' }, + patch: { timezone: 'UTC' } + }); + + expect(findOneAndUpdate).toHaveBeenCalledWith( + { _id: expect.anything(), timezone: 'Asia/Shanghai' }, + { $set: { timezone: 'UTC' } }, + { new: true } + ); + expect(result).toEqual(doc); + }); + + it('returns null when expected state does not match', async () => { + const { model } = createModel(null); + + await expect( + casUpdateById({ + model, + id: userId, + expected: { timezone: 'UTC' }, + patch: { timezone: 'Asia/Shanghai' } + }) + ).resolves.toBeNull(); + }); + + it('filters undefined fields from the patch', async () => { + const { model, findOneAndUpdate } = createModel(doc); + + await casUpdateById({ + model, + id: userId, + expected: {}, + patch: { timezone: 'UTC', contact: undefined } + }); + + expect(findOneAndUpdate).toHaveBeenCalledWith( + { _id: expect.anything() }, + { $set: { timezone: 'UTC' } }, + { new: true } + ); + }); + + it('rejects ids that cannot be represented by MongoDB', async () => { + const { model, findOneAndUpdate } = createModel(doc); + + await expect( + casUpdateById({ model, id: 'sql-id', expected: {}, patch: {} }) + ).rejects.toBeInstanceOf(MongoInvalidArgumentError); + expect(findOneAndUpdate).not.toHaveBeenCalled(); + }); + + it('binds the query to the transaction session', async () => { + const session = {} as never; + const { model, findOneAndUpdate } = createModel(doc); + + await casUpdateById({ + model, + id: userId, + expected: {}, + patch: { timezone: 'UTC' }, + session + }); + + expect(findOneAndUpdate.mock.results[0].value.session).toHaveBeenCalledWith(session); + }); +}); diff --git a/packages/dal/test/mongodb/pagination.test.ts b/packages/dal/test/mongodb/pagination.test.ts new file mode 100644 index 000000000000..12ed1b636af9 --- /dev/null +++ b/packages/dal/test/mongodb/pagination.test.ts @@ -0,0 +1,75 @@ +import type { Model, Types } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import { paginate } from '../../mongodb/pagination'; + +type Doc = { _id: Types.ObjectId; username: string }; + +const createQuery = (value: T) => ({ + sort: vi.fn().mockReturnThis(), + skip: vi.fn().mockReturnThis(), + limit: vi.fn().mockReturnThis(), + session: vi.fn().mockReturnThis(), + lean: vi.fn(async () => value) +}); + +const createCountQuery = (value: number) => ({ + session: vi.fn().mockReturnThis(), + then: (onFulfilled: (v: number) => void) => Promise.resolve(onFulfilled(value)) +}); + +const createModel = (total: number, list: Doc[]) => { + const countDocuments = vi.fn(() => createCountQuery(total)); + const find = vi.fn(() => createQuery(list)); + const model = { + countDocuments, + find + } as unknown as Model; + return { model, countDocuments, find }; +}; + +describe('paginate', () => { + const list: Doc[] = [ + { _id: '507f1f77bcf86cd799439011' as unknown as Types.ObjectId, username: 'a@example.com' } + ]; + + it('returns total and list with normalized skip/limit', async () => { + const { model, countDocuments, find } = createModel(42, list); + + const result = await paginate({ + model, + filter: { status: 'active' }, + page: 2, + pageSize: 20, + sort: { createTime: -1 } + }); + + expect(countDocuments).toHaveBeenCalledWith({ status: 'active' }); + const query = find.mock.results[0].value; + expect(query.sort).toHaveBeenCalledWith({ createTime: -1 }); + expect(query.skip).toHaveBeenCalledWith(20); + expect(query.limit).toHaveBeenCalledWith(20); + expect(result).toEqual({ total: 42, list }); + }); + + it('uses defaults for missing page params', async () => { + const { model } = createModel(1, []); + const find = vi.spyOn(model, 'find'); + + await paginate({ model, filter: {}, page: 0, pageSize: 0 }); + + const query = find.mock.results[0].value; + expect(query.skip).toHaveBeenCalledWith(0); + expect(query.limit).toHaveBeenCalledWith(10); + }); + + it('binds both queries to the transaction session', async () => { + const session = {} as never; + const { model, countDocuments } = createModel(3, []); + const find = vi.spyOn(model, 'find'); + + await paginate({ model, filter: {}, page: 1, pageSize: 10, session }); + + expect(countDocuments.mock.results[0].value.session).toHaveBeenCalledWith(session); + expect(find.mock.results[0].value.session).toHaveBeenCalledWith(session); + }); +}); diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/repositories/user.test.ts index 57acf2d7d219..35085b85ef76 100644 --- a/packages/dal/test/mongodb/repositories/user.test.ts +++ b/packages/dal/test/mongodb/repositories/user.test.ts @@ -5,6 +5,7 @@ import type { UserDocument, UserMongooseSchemaType } from '../../../mongodb/mode import { MongoUserRepository } from '../../../mongodb/repositories/user'; import { MongoTransactionRunner } from '../../../mongodb/transaction'; import { + DatabaseConflictError, DatabaseInvalidArgumentError, DatabaseUnavailableError, DatabaseUniqueConstraintError @@ -30,21 +31,30 @@ const createQuery = (value: T) => ({ lean: vi.fn(async () => value) }); +const createExistsQuery = (value: unknown) => ({ + session: vi.fn().mockReturnThis(), + then: (onFulfilled: (v: unknown) => void) => Promise.resolve(onFulfilled(value)) +}); + const createRepository = () => { const queries = { findById: createQuery(document), findOne: createQuery(document), - update: createQuery(document) + update: createQuery(document), + casUpdate: createQuery(document) }; + const exists = vi.fn(() => createExistsQuery(true)); const createdDocument = { toObject: vi.fn(() => document) }; const model = { findById: vi.fn(() => queries.findById), findOne: vi.fn(() => queries.findOne), create: vi.fn(async () => [createdDocument]), - findByIdAndUpdate: vi.fn(() => queries.update) + findByIdAndUpdate: vi.fn(() => queries.update), + findOneAndUpdate: vi.fn(() => queries.casUpdate), + exists } as unknown as Model; - return { repository: new MongoUserRepository(model), model, queries, createdDocument }; + return { repository: new MongoUserRepository(model), model, queries, createdDocument, exists }; }; describe('MongoUserRepository.findById', () => { @@ -282,3 +292,61 @@ describe('MongoUserRepository error adapter', () => { expect(error.message).not.toContain('secret@example.com'); }); }); + +describe('MongoUserRepository.updateByIdIfState', () => { + it('updates atomically when the expected state matches', async () => { + const { repository, model, queries } = createRepository(); + queries.casUpdate.lean.mockResolvedValueOnce({ + ...document, + timezone: 'UTC' + } as UserDocument); + + const user = await repository.updateByIdIfState( + userId, + { timezone: 'Asia/Shanghai' }, + { timezone: 'UTC' } + ); + + expect(model.findOneAndUpdate).toHaveBeenCalledWith( + { _id: new Types.ObjectId(userId), timezone: 'Asia/Shanghai' }, + { $set: { timezone: 'UTC' } }, + { new: true } + ); + expect(queries.casUpdate.lean).toHaveBeenCalledOnce(); + expect(user?.timezone).toBe('UTC'); + }); + + it('throws DB_CONFLICT when the document exists but the expected state mismatches', async () => { + const { repository, queries } = createRepository(); + queries.casUpdate.lean.mockResolvedValueOnce(null); + + const error = await repository + .updateByIdIfState(userId, { timezone: 'UTC' }, { timezone: 'Asia/Shanghai' }) + .catch((err) => err); + + expect(error).toBeInstanceOf(DatabaseConflictError); + expect(error).toMatchObject({ code: 'DB_CONFLICT' }); + }); + + it('returns null when the document does not exist', async () => { + const { repository, queries, exists } = createRepository(); + queries.casUpdate.lean.mockResolvedValueOnce(null); + exists.mockReturnValueOnce(createExistsQuery(null)); + + await expect( + repository.updateByIdIfState(userId, { timezone: 'UTC' }, { timezone: 'Asia/Shanghai' }) + ).resolves.toBeNull(); + }); + + it('rejects ids that cannot be represented by MongoDB', async () => { + const { repository, model } = createRepository(); + + const error = await repository + .updateByIdIfState('sql-id', {}, { timezone: 'UTC' }) + .catch((err) => err); + + expect(error).toBeInstanceOf(DatabaseInvalidArgumentError); + expect(error).toMatchObject({ code: 'DB_INVALID_ARGUMENT' }); + expect(model.findOneAndUpdate).not.toHaveBeenCalled(); + }); +}); From 2c1bcc2305b208420ea7746fac3f41f46e8f0635 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 16:54:21 +0800 Subject: [PATCH 09/14] docs: mark M8 dal abstractions todo as done --- .agents/design/dal-basic-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index cffcd5b6fcab..116130466c54 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -110,6 +110,6 @@ packages/service/common/dal/ - [x] 补齐 mapper、Schema、Repository、事务和索引局部测试。 - [x] 增加数据库错误 Adapter 与 Mongo 错误映射。 - [ ] ~~增加 SQL-like User adapter(接入 `DAL_DB_TYPE=sql` 分支)~~ 已延后:整个 MongoDB 重构完成后再考虑,另起项目跟进。 -- 统一 DAL 的分页、唯一约束错误和并发更新抽象。 +- [x] 统一 DAL 的分页、唯一约束错误和并发更新抽象。 - 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 - 接入独立 DAL 连接后,验证与主连接的索引同步、慢查询日志和生命周期管理。 From c08d2fa219b220066fe36e404786f35355a9faea Mon Sep 17 00:00:00 2001 From: Xianquan Date: Tue, 11 Aug 2026 18:25:14 +0800 Subject: [PATCH 10/14] feat(dal): migrate support/user to DAL repositories (M9) and add model lifecycle (M10) --- .agents/design/dal-basic-architecture.md | 6 +- .agents/design/dal-m10-model-lifecycle.md | 41 ++++ .agents/design/dal-m9-user-call-migration.md | 45 ++++ packages/dal/db/adapter.ts | 4 +- packages/dal/db/tables.ts | 3 + packages/dal/domain/index.ts | 16 ++ packages/dal/domain/team.ts | 50 +++++ packages/dal/domain/tmpData.ts | 16 ++ packages/dal/mongodb/adapter.ts | 6 + packages/dal/mongodb/index.ts | 7 + packages/dal/mongodb/logger.ts | 18 ++ packages/dal/mongodb/mappers/team.ts | 14 ++ packages/dal/mongodb/middleware.ts | 64 ++++++ packages/dal/mongodb/models/helper.ts | 18 ++ packages/dal/mongodb/models/memberGroup.ts | 48 +++++ packages/dal/mongodb/models/org.ts | 55 +++++ packages/dal/mongodb/models/team.ts | 56 +++++ packages/dal/mongodb/models/teamMember.ts | 64 ++++++ packages/dal/mongodb/models/tmpData.ts | 39 ++++ packages/dal/mongodb/models/user.ts | 4 +- packages/dal/mongodb/repositories/team.ts | 159 ++++++++++++++ packages/dal/mongodb/repositories/tmpData.ts | 73 +++++++ packages/dal/mongodb/repositories/user.ts | 18 ++ packages/dal/package.json | 6 + packages/dal/ports/index.ts | 2 + packages/dal/ports/team.repository.ts | 28 +++ packages/dal/ports/tmpData.repository.ts | 11 + packages/dal/ports/user.repository.ts | 6 + packages/dal/test/mongodb/middleware.test.ts | 50 +++++ packages/dal/test/mongodb/models/team.test.ts | 83 ++++++++ .../test/mongodb/repositories/team.test.ts | 194 ++++++++++++++++++ .../test/mongodb/repositories/tmpData.test.ts | 84 ++++++++ .../test/mongodb/repositories/user.test.ts | 33 +++ packages/service/common/dal/index.ts | 4 +- .../service/common/dal/mongo/connection.ts | 5 + packages/service/common/dal/mongo/index.ts | 8 + .../service/common/dal/mongo/lifecycle.ts | 49 +++++ .../support/marketing/interface/crm.ts | 4 +- .../service/support/tmpData/verification.ts | 43 ++++ .../account/verification/password/service.ts | 24 +-- .../account/verification/password/type.ts | 14 +- packages/service/support/user/controller.ts | 30 +-- .../support/marketing/crmInterface.test.ts | 10 +- .../verification/password/service.test.ts | 20 +- .../support/user/account/loginByPassword.ts | 29 +-- .../pages/api/support/user/account/update.ts | 46 +++-- projects/app/src/service/mongo.ts | 37 ++-- .../user/account/loginByPassword.test.ts | 2 +- 48 files changed, 1523 insertions(+), 123 deletions(-) create mode 100644 .agents/design/dal-m10-model-lifecycle.md create mode 100644 packages/dal/domain/team.ts create mode 100644 packages/dal/domain/tmpData.ts create mode 100644 packages/dal/mongodb/logger.ts create mode 100644 packages/dal/mongodb/mappers/team.ts create mode 100644 packages/dal/mongodb/middleware.ts create mode 100644 packages/dal/mongodb/models/helper.ts create mode 100644 packages/dal/mongodb/models/memberGroup.ts create mode 100644 packages/dal/mongodb/models/org.ts create mode 100644 packages/dal/mongodb/models/team.ts create mode 100644 packages/dal/mongodb/models/teamMember.ts create mode 100644 packages/dal/mongodb/models/tmpData.ts create mode 100644 packages/dal/mongodb/repositories/team.ts create mode 100644 packages/dal/mongodb/repositories/tmpData.ts create mode 100644 packages/dal/ports/team.repository.ts create mode 100644 packages/dal/ports/tmpData.repository.ts create mode 100644 packages/dal/test/mongodb/middleware.test.ts create mode 100644 packages/dal/test/mongodb/models/team.test.ts create mode 100644 packages/dal/test/mongodb/repositories/team.test.ts create mode 100644 packages/dal/test/mongodb/repositories/tmpData.test.ts create mode 100644 packages/service/common/dal/mongo/lifecycle.ts diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index 116130466c54..f8f86b0aa08b 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -111,5 +111,9 @@ packages/service/common/dal/ - [x] 增加数据库错误 Adapter 与 Mongo 错误映射。 - [ ] ~~增加 SQL-like User adapter(接入 `DAL_DB_TYPE=sql` 分支)~~ 已延后:整个 MongoDB 重构完成后再考虑,另起项目跟进。 - [x] 统一 DAL 的分页、唯一约束错误和并发更新抽象。 -- 在确认领域字段后,将 `packages/service/support/user` 的调用逐步迁移到 Repository。 +- [x] 将 `packages/service/support/user` 的调用迁移到 Repository(M9:loginByPassword / + update.ts / initRootUser / crm.ts / getUserDetail 兼容分支,详见 + [dal-m9-user-call-migration.md](./dal-m9-user-call-migration.md))。 - 接入独立 DAL 连接后,验证与主连接的索引同步、慢查询日志和生命周期管理。 + (M10 代码接入完成:慢查询中间件 + MongoIndexManager 同步;运行时验证依赖 M11 + 连接池部署观测,详见 [dal-m10-model-lifecycle.md](./dal-m10-model-lifecycle.md)。) diff --git a/.agents/design/dal-m10-model-lifecycle.md b/.agents/design/dal-m10-model-lifecycle.md new file mode 100644 index 000000000000..7ed09bcf30e3 --- /dev/null +++ b/.agents/design/dal-m10-model-lifecycle.md @@ -0,0 +1,41 @@ +# M10:DAL Model 生命周期(慢查询日志 + MongoIndexManager 索引管理) + +关联任务:Kaneo M#10「DAL 独立连接生命周期:索引同步、慢查询日志、MongoIndexManager 管理」。 +前置设计:[dal-basic-architecture.md](./dal-basic-architecture.md)、 +[dal-m9-user-call-migration.md](./dal-m9-user-call-migration.md)。 + +## 目标 + +在删除旧 User Model 前,补齐 DAL Model 的两个生命周期能力: +接入慢查询日志(对齐业务侧 `addCommonMiddleware`)与纳入 `MongoIndexManager` +统一索引管理。部署侧的第二连接池观测与连接数上限验证(M11)不在本任务内。 + +## 慢查询日志 + +- 新增 `packages/dal/mongodb/middleware.ts` 的 `addDalCommonMiddleware(schema)`: + 与业务侧 `addCommonMiddleware` 相同的慢查询计时(>2s / >500ms 两档), + **不做 post-find 的 `_id` → string 转换**——DAL mapper 依赖 ObjectId 做 + `toEntityId`/`getTimestamp`,转换会破坏映射层。 +- 新增 `packages/dal/mongodb/logger.ts`:`setDalLogger` / `getDalLogger`。 + service 侧 composition root(`common/dal/mongo/index.ts`)在模块加载时注入业务 + logger(pino),避免 DAL 包反向依赖 service 日志实现;未注入时退回 console。 +- 新增 `packages/dal/mongodb/models/helper.ts` 的 `getDalModel`:DAL Model 统一注册 + 入口,注册前挂慢查询中间件;User Model 重构为同一入口。 + +## 索引管理 + +- 新增 `packages/service/common/dal/mongo/lifecycle.ts` 的 + `syncDalModelIndexes(client)`:对 DAL 全部 Model(User/Team/TeamMember/ + MemberGroup/Org/TmpData)调用 `MongoIndexManager.syncModelIndexes`,与旧 Model 的 + `syncMongoIndex` 同一套语义(补建当前索引 + 清理登记的废弃索引);测试与构建阶段跳过。 +- `connection.connect()` 成功后异步触发 `syncDalModelIndexes`(失败只记录日志, + 与旧 Model 的异步补偿一致)。 +- DAL Model 与旧 Model 指向同一集合,索引创建幂等;`defineIndex` 两侧共享 + `Symbol.for('fastgpt.mongo.deprecatedIndexes')`,清理语义一致。 + +## 验证边界 + +- 本任务验证到「代码接入 + 单测」:middleware 计时钩子、模型注册复用、索引声明与生产 + 一致(models 测试断言 indexes())。 +- 「独立 DAL 连接与主连接索引同步」的运行时验证依赖 M11 的部署观测(同一副本集下 + diffIndexes 幂等),不在本任务。 diff --git a/.agents/design/dal-m9-user-call-migration.md b/.agents/design/dal-m9-user-call-migration.md index b21079a78672..901564c91481 100644 --- a/.agents/design/dal-m9-user-call-migration.md +++ b/.agents/design/dal-m9-user-call-migration.md @@ -42,3 +42,48 @@ DAL 事务上下文是内部 symbol,旧 Model 无法参与 DAL 事务;Team/T - 选 A(完整迁移,改动较大,需新增 Team/TeamMember DAL 切片)还是 B(先只迁移纯 User 调用)? - 若选 A:Team/TeamMember DAL 是否只覆盖当前三个流程所需的最小能力(findByIds/按 userId 查默认 tmb/创建默认团队/更新头像),不做完整 CRUD? + +## 决策记录(2026-08-11,用户确认做 M9+M10) + +选 **方案 A**,按最小能力落地: + +### Team/TeamMember DAL 切片(最小能力) + +- 新增 `TeamRepository` port,只覆盖三个流程所需能力: + - `findMemberById(id, ctx?)`:按 tmbId 读 `{ userId, avatar }`(update.ts 用)。 + - `updateMemberAvatar(id, avatar, ctx?)`:更新 tmb 头像(update.ts 用)。 + - `createDefaultTeam({ userId, teamName?, avatar?, ctx })`:在 DAL 事务内创建默认 + 团队(teams + team_members + team_member_groups + team_orgs 四条写入),返回新 tmb; + 已存在默认 tmb 时返回 `null`(沿用旧语义:已存在则跳过)。 +- 新增 `TmpDataRepository`(最小能力):`findActiveMaterial` / `deleteActiveMaterial`, + 供 verification 在 DAL 事务内读取和消费验证码材料。 +- 权限合成(`getTmbPermission`,涉及 resource_permissions/memberGroups/orgs 读取) + **留在 service 层、事务外**:登录/资料更新流程对 tmb/team/权限只读,读一致性由流程 + 语义保证(tmb/team 在登录期间不变),不把 resource_permissions 切片拖进本次范围。 +- `getUserDetail` 的 tmb/team 读取继续走旧 `getTmbInfoByTmbId` / `getUserDefaultTeam` + (**不再传 session**),只把 `MongoUser` 读取换成 `userRepository.findById`,并删除 + 旧 `ClientSession` 兼容分支。 + +### 事务边界(不能混用两种事务上下文) + +- `loginByPassword`:验证码读取/消费 + 凭据校验 + `lastLoginTmbId/language/fastgpt_sem` + 更新全部在 **DAL 事务**内(`transactionRunner.withTransaction`);tmb/team/权限读取在 + 事务外(纯读,无 session)。 +- `update.ts`:user 字段走 `userRepository.updateById`、tmb 头像走 + `teamRepository.updateMemberAvatar`,都在 DAL 事务内;S3 头像刷新(含 MongoS3TTL + 清理)改为 **事务提交后**执行、best-effort(try/catch + 日志),避免把 s3_ttl 集合 + 拖进 DAL 事务。 +- `initRootUser`:root 用户(`findByUsername`/`create`/`updateById`)与默认团队 + (`teamRepository.createDefaultTeam`)在同一个 DAL 事务内。 + +### 密码处理 + +- DAL User Model 的 `password` 已有 `set: hashStr`;迁移后统一传**明文**,由 DAL 负责 + 哈希(与 `updatePasswordByOld` 既有用法一致),不再像旧 initRootUser 那样手工预哈希。 + +### 其他 + +- `crm.ts`:`MongoUser.findById(ownerId, 'fastgpt_sem')` 迁移为 `userRepository` 新增 + 投影方法 `findSemById(id)`。 +- `exportLogs.ts` 的 `users $lookup`:维持现状(跨集合读模型,设计上不沉底),只在本 + 文档登记决策。 diff --git a/packages/dal/db/adapter.ts b/packages/dal/db/adapter.ts index 10f898f86044..9e82de03f4bd 100644 --- a/packages/dal/db/adapter.ts +++ b/packages/dal/db/adapter.ts @@ -1,9 +1,11 @@ -import type { UserRepository } from '../ports'; +import type { TeamRepository, TmpDataRepository, UserRepository } from '../ports'; import type { TransactionRunner } from '../transaction'; import type { DatabaseErrorAdapter } from './error-adapter'; export type DatabaseAdapter = { userRepository: UserRepository; + teamRepository: TeamRepository; + tmpDataRepository: TmpDataRepository; transactionRunner: TransactionRunner; errorAdapter: DatabaseErrorAdapter; }; diff --git a/packages/dal/db/tables.ts b/packages/dal/db/tables.ts index 15f249176bdb..9f4b78137fcf 100644 --- a/packages/dal/db/tables.ts +++ b/packages/dal/db/tables.ts @@ -3,6 +3,9 @@ export const tables = { user: 'users', team: 'teams', teamMember: 'team_members', + teamMemberGroup: 'team_member_groups', + teamOrg: 'team_orgs', + tmpData: 'tmp_datas', teamDiscountCoupon: 'team_discount_coupons', teamEnterpriseAuth: 'team_enterprise_auths', teamEnterpriseAuthTask: 'team_enterprise_auth_tasks' diff --git a/packages/dal/domain/index.ts b/packages/dal/domain/index.ts index db78862637e0..e46ddf19b9dc 100644 --- a/packages/dal/domain/index.ts +++ b/packages/dal/domain/index.ts @@ -9,3 +9,19 @@ export { UserSchema } from './user'; export type { CreateUser, UpdateUser, User, UserCredentials } from './user'; +export { + CreateMemberGroupSchema, + CreateOrgSchema, + CreateTeamMemberSchema, + CreateTeamSchema, + TeamMemberDetailSchema +} from './team'; +export type { + CreateMemberGroup, + CreateOrg, + CreateTeam, + CreateTeamMember, + TeamMemberDetail +} from './team'; +export { ActiveTmpDataFilterSchema, TmpDataMaterialSchema } from './tmpData'; +export type { ActiveTmpDataFilter, TmpDataMaterial } from './tmpData'; diff --git a/packages/dal/domain/team.ts b/packages/dal/domain/team.ts new file mode 100644 index 000000000000..bfdb8790a8b5 --- /dev/null +++ b/packages/dal/domain/team.ts @@ -0,0 +1,50 @@ +import z from 'zod'; +import { + TeamMemberRoleEnum, + TeamMemberStatusEnum +} from '@fastgpt/global/support/user/team/constant'; +import { EntityIdSchema } from './types'; + +/** 默认团队/头像更新的最小 tmb 形状,只暴露当前三个流程需要的字段。 */ +export const TeamMemberDetailSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + userId: EntityIdSchema, + avatar: z.string().optional(), + name: z.string(), + role: z.string().optional(), + status: z.string().optional() +}); +export type TeamMemberDetail = z.infer; + +export const CreateTeamSchema = z.object({ + ownerId: EntityIdSchema, + name: z.string(), + avatar: z.string(), + createTime: z.date().optional() +}); +export type CreateTeam = z.infer; + +export const CreateTeamMemberSchema = z.object({ + teamId: EntityIdSchema, + userId: EntityIdSchema, + name: z.string(), + role: z.enum(TeamMemberRoleEnum), + status: z.enum(TeamMemberStatusEnum), + createTime: z.date().optional() +}); +export type CreateTeamMember = z.infer; + +export const CreateMemberGroupSchema = z.object({ + teamId: EntityIdSchema, + name: z.string(), + avatar: z.string() +}); +export type CreateMemberGroup = z.infer; + +export const CreateOrgSchema = z.object({ + teamId: EntityIdSchema, + name: z.string(), + path: z.string() +}); +export type CreateOrg = z.infer; diff --git a/packages/dal/domain/tmpData.ts b/packages/dal/domain/tmpData.ts new file mode 100644 index 000000000000..fe6d349cdb72 --- /dev/null +++ b/packages/dal/domain/tmpData.ts @@ -0,0 +1,16 @@ +import z from 'zod'; + +/** 身份验证材料在 DAL 侧的最小形状,字段语义与旧 tmp_datas 集合一致。 */ +export const TmpDataMaterialSchema = z.object({ + dataId: z.string().min(1), + data: z.unknown(), + expireAt: z.date() +}); +export type TmpDataMaterial = z.infer; + +/** 查询有效材料的最小过滤条件;match 按材料字段直接映射到 `data.`。 */ +export const ActiveTmpDataFilterSchema = z.object({ + dataId: z.string().min(1), + match: z.record(z.string(), z.unknown()).optional() +}); +export type ActiveTmpDataFilter = z.infer; diff --git a/packages/dal/mongodb/adapter.ts b/packages/dal/mongodb/adapter.ts index 4fa025be5ea8..9769eaca87f0 100644 --- a/packages/dal/mongodb/adapter.ts +++ b/packages/dal/mongodb/adapter.ts @@ -3,6 +3,8 @@ import type { DatabaseAdapter } from '../db'; import type { DatabaseErrorAdapter } from '../db'; import { MongoErrorAdapter } from './errors'; import { createMongoUserRepository } from './repositories/user'; +import { createMongoTeamRepository } from './repositories/team'; +import { createMongoTmpDataRepository } from './repositories/tmpData'; import { MongoTransactionRunner } from './transaction'; export type MongoAdapterDependencies = { @@ -13,6 +15,8 @@ export type MongoAdapterDependencies = { /** MongoDB adapter,保证所有仓储和事务共享同一个 Mongoose client。 */ export class MongoAdapter implements DatabaseAdapter { readonly userRepository; + readonly teamRepository; + readonly tmpDataRepository; readonly transactionRunner; readonly errorAdapter; @@ -22,6 +26,8 @@ export class MongoAdapter implements DatabaseAdapter { }: MongoAdapterDependencies = {}) { this.errorAdapter = errorAdapter; this.userRepository = createMongoUserRepository(client, errorAdapter); + this.teamRepository = createMongoTeamRepository(client, errorAdapter); + this.tmpDataRepository = createMongoTmpDataRepository(client, errorAdapter); this.transactionRunner = new MongoTransactionRunner(client, errorAdapter); } } diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts index 96eafa90cfa4..b1a3a65cd617 100644 --- a/packages/dal/mongodb/index.ts +++ b/packages/dal/mongodb/index.ts @@ -1,5 +1,8 @@ export { createMongoUserRepository, MongoUserRepository } from './repositories/user'; +export { createMongoTeamRepository, MongoTeamRepository } from './repositories/team'; +export { createMongoTmpDataRepository, MongoTmpDataRepository } from './repositories/tmpData'; export { toUser, userDefaultFieldValues } from './mappers/user'; +export { toTeamMemberDetail } from './mappers/team'; export { MongoAdapter } from './adapter'; export type { MongoAdapterDependencies } from './adapter'; export { MongoErrorAdapter } from './errors'; @@ -8,3 +11,7 @@ export { paginate } from './pagination'; export type { PaginateParams } from './pagination'; export { casUpdateById } from './concurrency'; export type { CasUpdateByIdParams } from './concurrency'; +export { setDalLogger, getDalLogger } from './logger'; +export type { DalLogger } from './logger'; +export { createSlowQueryMiddleware as addDalCommonMiddleware } from './middleware'; +export { getDalModel } from './models/helper'; diff --git a/packages/dal/mongodb/logger.ts b/packages/dal/mongodb/logger.ts new file mode 100644 index 000000000000..5ac952fe69b9 --- /dev/null +++ b/packages/dal/mongodb/logger.ts @@ -0,0 +1,18 @@ +export type DalLogger = { + warn: (message: string, data?: Record) => void; +}; + +let injectedLogger: DalLogger | undefined; + +/** + * 注入 DAL 慢查询日志器。service 侧 composition root 启动时把业务 logger 注入进来, + * 避免 DAL 包反向依赖 service 的日志实现;未注入时退回 console。 + */ +export const setDalLogger = (logger: DalLogger) => { + injectedLogger = logger; +}; + +export const getDalLogger = (): DalLogger => + injectedLogger ?? { + warn: (message, data) => console.warn(message, data ?? {}) + }; diff --git a/packages/dal/mongodb/mappers/team.ts b/packages/dal/mongodb/mappers/team.ts new file mode 100644 index 000000000000..03832a4aff34 --- /dev/null +++ b/packages/dal/mongodb/mappers/team.ts @@ -0,0 +1,14 @@ +import { TeamMemberDetailSchema, type TeamMemberDetail } from '../../domain/team'; +import type { TeamMemberDocument } from '../models/teamMember'; +import { toEntityId } from '../utils'; + +export const toTeamMemberDetail = (document: TeamMemberDocument): TeamMemberDetail => + TeamMemberDetailSchema.parse({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId), + userId: toEntityId(document.userId), + avatar: document.avatar ?? undefined, + name: document.name, + role: document.role ?? undefined, + status: document.status ?? undefined + }); diff --git a/packages/dal/mongodb/middleware.ts b/packages/dal/mongodb/middleware.ts new file mode 100644 index 000000000000..170fe065cd1a --- /dev/null +++ b/packages/dal/mongodb/middleware.ts @@ -0,0 +1,64 @@ +import type { Schema } from 'mongoose'; +import { getDalLogger } from './logger'; + +const operations = [ + /^find/, + 'save', + 'create', + /^update/, + /^delete/, + 'aggregate', + 'count', + 'countDocuments', + 'estimatedDocumentCount', + 'distinct', + 'insertMany' +] as const; + +/** + * DAL Model 的慢查询中间件:只做慢查询计时日志。 + */ +export const createSlowQueryMiddleware = (schema: Schema) => { + operations.forEach((op: any) => { + schema.pre(op, function (this: any, next) { + this._startTime = Date.now(); + this._query = this.getQuery ? this.getQuery() : null; + next(); + }); + + schema.post(op, function (this: any, _result, next) { + if (this._startTime) { + const logger = getDalLogger(); + const duration = Date.now() - this._startTime; + + const getLogData = () => { + const collectionName = this.model?.collection?.name || this._model?.collection?.name; + const operation = (() => { + if (this.op) return this.op; + if (this._pipeline) return 'aggregate'; + if (this.constructor?.name === 'model') return 'save/create'; + return this.constructor?.name || 'unknown'; + })(); + return { + duration, + collectionName, + op: operation, + ...(this._query && { query: this._query }), + ...(this._pipeline && { pipeline: this._pipeline }), + ...(this._update && { update: this._update }), + ...(this._delete && { delete: this._delete }) + }; + }; + + if (duration > 2000) { + logger.warn('MongoDB slow query (>2s)', getLogData()); + } else if (duration > 500) { + logger.warn('MongoDB slow query (>500ms)', getLogData()); + } + } + next(); + }); + }); + + return schema; +}; diff --git a/packages/dal/mongodb/models/helper.ts b/packages/dal/mongodb/models/helper.ts new file mode 100644 index 000000000000..7f8fc4937a98 --- /dev/null +++ b/packages/dal/mongodb/models/helper.ts @@ -0,0 +1,18 @@ +import type { Model, Mongoose, Schema } from 'mongoose'; +import { createSlowQueryMiddleware } from '../middleware'; + +/** + * DAL Model 统一注册入口:同一 client 内按模型名复用,注册前挂慢查询中间件。 + */ +export const getDalModel = ( + client: Mongoose, + name: string, + schema: Schema, + collectionName: string +): Model => { + const existing = client.models[name] as Model | undefined; + if (existing) return existing; + + createSlowQueryMiddleware(schema); + return client.model(name, schema, collectionName) as unknown as Model; +}; diff --git a/packages/dal/mongodb/models/memberGroup.ts b/packages/dal/mongodb/models/memberGroup.ts new file mode 100644 index 000000000000..71dbe8548512 --- /dev/null +++ b/packages/dal/mongodb/models/memberGroup.ts @@ -0,0 +1,48 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; +import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; + +export const MemberGroupDocumentSchema = new Schema( + { + teamId: { + type: Schema.Types.ObjectId, + ref: tables.team, + required: true + }, + name: { + type: String, + required: true + }, + avatar: String, + updateTime: { + type: Date, + default: () => new Date() + } + }, + { + timestamps: { + updatedAt: 'updateTime' + } + } +); + +defineIndex(MemberGroupDocumentSchema, { + key: { teamId: 1, name: 1 }, + options: { unique: true } +}); + +export type MemberGroupMongooseSchemaType = InferSchemaType; +export type MemberGroupDocument = WithId__v; + +export const getMemberGroupModel = ( + client: Mongoose = mongoose +): Model => { + return getDalModel( + client, + 'MemberGroup', + MemberGroupDocumentSchema, + tables.teamMemberGroup + ); +}; diff --git a/packages/dal/mongodb/models/org.ts b/packages/dal/mongodb/models/org.ts new file mode 100644 index 000000000000..fed7c2b50f04 --- /dev/null +++ b/packages/dal/mongodb/models/org.ts @@ -0,0 +1,55 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; +import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; + +export const OrgDocumentSchema = new Schema( + { + teamId: { + type: Schema.Types.ObjectId, + ref: tables.team, + required: true + }, + pathId: { + type: String, + required: true, + default: () => getNanoid() + }, + path: { + type: String, + required: true + }, + name: { + type: String, + required: true + }, + avatar: String, + description: String, + updateTime: { + type: Date, + default: () => new Date() + } + }, + { + timestamps: { + updatedAt: 'updateTime' + } + } +); + +defineIndex(OrgDocumentSchema, { + key: { teamId: 1, path: 1 } +}); +defineIndex(OrgDocumentSchema, { + key: { teamId: 1, pathId: 1 }, + options: { unique: true } +}); + +export type OrgMongooseSchemaType = InferSchemaType; +export type OrgDocument = WithId__v; + +export const getOrgModel = (client: Mongoose = mongoose): Model => { + return getDalModel(client, 'Org', OrgDocumentSchema, tables.teamOrg); +}; diff --git a/packages/dal/mongodb/models/team.ts b/packages/dal/mongodb/models/team.ts new file mode 100644 index 000000000000..2c4a399869ca --- /dev/null +++ b/packages/dal/mongodb/models/team.ts @@ -0,0 +1,56 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; +import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; + +export const TeamDocumentSchema = new Schema({ + name: { + type: String, + required: true + }, + ownerId: { + type: Schema.Types.ObjectId, + ref: tables.user + }, + avatar: { + type: String, + default: '/icon/logo.svg' + }, + createTime: { + type: Date, + default: () => Date.now() + }, + balance: Number, + limit: { + lastExportDatasetTime: Date, + lastWebsiteSyncTime: Date + }, + openaiAccount: { + type: { + key: String, + baseUrl: String + } + }, + externalWorkflowVariables: { + type: Object, + default: {} + }, + notificationAccount: String, + meta: Object, + deleteTime: Date +}); + +defineIndex(TeamDocumentSchema, { key: { name: 1 } }); +defineIndex(TeamDocumentSchema, { key: { ownerId: 1 } }); +defineIndex(TeamDocumentSchema, { + key: { 'meta.wecom.corpId': 1 }, + options: { sparse: true, unique: true } +}); + +export type TeamMongooseSchemaType = InferSchemaType; +export type TeamDocument = WithId__v; + +export const getTeamModel = (client: Mongoose = mongoose): Model => { + return getDalModel(client, 'Team', TeamDocumentSchema, tables.team); +}; diff --git a/packages/dal/mongodb/models/teamMember.ts b/packages/dal/mongodb/models/teamMember.ts new file mode 100644 index 000000000000..48561720e44e --- /dev/null +++ b/packages/dal/mongodb/models/teamMember.ts @@ -0,0 +1,64 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { TeamMemberStatusMap } from '@fastgpt/global/support/user/team/constant'; +import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; +import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; + +export const TeamMemberDocumentSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: tables.team, + required: true + }, + userId: { + type: Schema.Types.ObjectId, + ref: tables.user, + required: true + }, + avatar: { + type: String, + default: () => getRandomUserAvatar() + }, + name: { + type: String, + default: 'Member' + }, + status: { + type: String, + enum: Object.keys(TeamMemberStatusMap) + }, + createTime: { + type: Date, + default: () => new Date() + }, + updateTime: Date, + /** @deprecated 部分旧代码仍用 role 判断 owner,保留字段与生产一致。 */ + role: String, + /** @deprecated */ + defaultTeam: Boolean +}); + +defineIndex(TeamMemberDocumentSchema, { + key: { teamId: 1 }, + options: { background: true } +}); +defineIndex(TeamMemberDocumentSchema, { + key: { userId: 1 }, + options: { background: true } +}); + +export type TeamMemberMongooseSchemaType = InferSchemaType; +export type TeamMemberDocument = WithId__v; + +export const getTeamMemberModel = ( + client: Mongoose = mongoose +): Model => { + return getDalModel( + client, + 'TeamMember', + TeamMemberDocumentSchema, + tables.teamMember + ); +}; diff --git a/packages/dal/mongodb/models/tmpData.ts b/packages/dal/mongodb/models/tmpData.ts new file mode 100644 index 000000000000..531ba17eb5da --- /dev/null +++ b/packages/dal/mongodb/models/tmpData.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { tables } from '../../db'; +import { defineIndex } from '../indexes'; +import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; + +export const TmpDataDocumentSchema = new Schema({ + dataId: { + type: String, + required: true + }, + data: Object, + expireAt: { + type: Date, + required: true + } +}); + +defineIndex(TmpDataDocumentSchema, { + key: { dataId: 1 }, + options: { unique: true } +}); +defineIndex(TmpDataDocumentSchema, { key: { dataId: -1 } }); +defineIndex(TmpDataDocumentSchema, { + key: { expireAt: -1 }, + options: { expireAfterSeconds: 5 } +}); + +export type TmpDataMongooseSchemaType = InferSchemaType; +export type TmpDataDocument = WithId__v; + +export const getTmpDataModel = (client: Mongoose = mongoose): Model => { + return getDalModel( + client, + 'TmpData', + TmpDataDocumentSchema, + tables.tmpData + ); +}; diff --git a/packages/dal/mongodb/models/user.ts b/packages/dal/mongodb/models/user.ts index 41c3a6ae93ad..fa7ea77a28f2 100644 --- a/packages/dal/mongodb/models/user.ts +++ b/packages/dal/mongodb/models/user.ts @@ -6,6 +6,7 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } fro import { tables } from '../../db'; import { defineIndex } from '../indexes'; import type { WithId__v } from '../types'; +import { getDalModel } from './helper'; export const UserDocumentSchema = new Schema({ status: { @@ -75,6 +76,5 @@ export type UserMongooseSchemaType = InferSchemaType; export type UserDocument = WithId__v; export const getUserModel = (client: Mongoose = mongoose): Model => { - const model = client.models.User as Model | undefined; - return model ?? client.model('User', UserDocumentSchema, tables.user); + return getDalModel(client, 'User', UserDocumentSchema, tables.user); }; diff --git a/packages/dal/mongodb/repositories/team.ts b/packages/dal/mongodb/repositories/team.ts new file mode 100644 index 000000000000..746e85dbe321 --- /dev/null +++ b/packages/dal/mongodb/repositories/team.ts @@ -0,0 +1,159 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { + TeamMemberRoleEnum, + TeamMemberStatusEnum +} from '@fastgpt/global/support/user/team/constant'; +import type { DatabaseErrorAdapter } from '../../db'; +import type { TeamMemberDetail } from '../../domain/team'; +import type { EntityId } from '../../domain/types'; +import type { CreateDefaultTeamParams, TeamRepository } from '../../ports/team.repository'; +import type { TransactionContext } from '../../transaction'; +import { toTeamMemberDetail } from '../mappers/team'; +import { MongoErrorAdapter } from '../errors'; +import { getMemberGroupModel, type MemberGroupMongooseSchemaType } from '../models/memberGroup'; +import { getOrgModel, type OrgMongooseSchemaType } from '../models/org'; +import { getTeamModel, type TeamMongooseSchemaType } from '../models/team'; +import { + getTeamMemberModel, + type TeamMemberDocument, + type TeamMemberMongooseSchemaType +} from '../models/teamMember'; +import { getMongoSession } from '../transaction'; +import { toMongoObjectId } from '../utils'; + +export class MongoTeamRepository implements TeamRepository { + constructor( + private readonly teamModel: Model = getTeamModel(mongoose), + private readonly teamMemberModel: Model = getTeamMemberModel( + mongoose + ), + private readonly memberGroupModel: Model = getMemberGroupModel( + mongoose + ), + private readonly orgModel: Model = getOrgModel(mongoose), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession( + query: Query, + context?: TransactionContext + ) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + async findMemberById( + id: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findById(toMongoObjectId(id)), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async updateMemberAvatar( + id: EntityId, + avatar: string, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findByIdAndUpdate( + toMongoObjectId(id), + { $set: { avatar } }, + { new: true } + ), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async createDefaultTeam({ + userId, + teamName = 'My Team', + avatar = '/icon/logo.svg', + context + }: CreateDefaultTeamParams): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const sessionOptions = session ? { session, ordered: true } : undefined; + + // 已存在默认 tmb 时沿用旧语义:跳过创建。 + const existing = await ( + session + ? this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }).session(session) + : this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }) + ).lean(); + if (existing) return null; + + const [teamDocument] = await this.teamModel.create( + [ + { + ownerId: userId, + name: teamName, + avatar, + createTime: new Date() + } + ], + sessionOptions + ); + const [tmbDocument] = await this.teamMemberModel.create( + [ + { + teamId: teamDocument._id, + userId, + name: 'Owner', + role: TeamMemberRoleEnum.owner, + status: TeamMemberStatusEnum.active, + createTime: new Date() + } + ], + sessionOptions + ); + await this.memberGroupModel.create( + [ + { + teamId: tmbDocument.teamId, + name: DefaultGroupName, + avatar + } + ], + sessionOptions + ); + await this.orgModel.create( + [ + { + teamId: tmbDocument.teamId, + name: 'ROOT', + path: '' + } + ], + sessionOptions + ); + + return toTeamMemberDetail(tmbDocument.toObject() as TeamMemberDocument); + }); + } +} + +export const createMongoTeamRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => + new MongoTeamRepository( + getTeamModel(client), + getTeamMemberModel(client), + getMemberGroupModel(client), + getOrgModel(client), + errorAdapter + ); diff --git a/packages/dal/mongodb/repositories/tmpData.ts b/packages/dal/mongodb/repositories/tmpData.ts new file mode 100644 index 000000000000..ca0db473f867 --- /dev/null +++ b/packages/dal/mongodb/repositories/tmpData.ts @@ -0,0 +1,73 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import type { DatabaseErrorAdapter } from '../../db'; +import type { ActiveTmpDataFilter, TmpDataMaterial } from '../../domain/tmpData'; +import type { TmpDataRepository } from '../../ports/tmpData.repository'; +import type { TransactionContext } from '../../transaction'; +import { MongoErrorAdapter } from '../errors'; +import { + getTmpDataModel, + type TmpDataDocument, + type TmpDataMongooseSchemaType +} from '../models/tmpData'; +import { getMongoSession } from '../transaction'; + +/** 将材料匹配字段映射为 `data.` 查询条件,语义与旧 verification 模块一致。 */ +const getDataMatch = (match?: Record) => + Object.fromEntries(Object.entries(match ?? {}).map(([field, value]) => [`data.${field}`, value])); + +const getActiveFilter = (filter: ActiveTmpDataFilter) => ({ + dataId: filter.dataId, + expireAt: { $gt: new Date() }, + ...getDataMatch(filter.match) +}); + +export class MongoTmpDataRepository implements TmpDataRepository { + constructor( + private readonly model: Model = getTmpDataModel(mongoose), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + async findActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne(getActiveFilter(filter)), + context + ).lean(); + return document + ? { + dataId: document.dataId, + data: document.data, + expireAt: document.expireAt + } + : null; + }); + } + + async deleteActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const query = this.model.deleteOne(getActiveFilter(filter)); + const result = await this.withSession(query, context); + return result.deletedCount === 1; + }); + } +} + +export const createMongoTmpDataRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => new MongoTmpDataRepository(getTmpDataModel(client), errorAdapter); diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/repositories/user.ts index 9bf23ceb0d09..10109c346c4f 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/repositories/user.ts @@ -10,6 +10,7 @@ import { getUserModel, type UserDocument, type UserMongooseSchemaType } from '.. import { casUpdateById } from '../concurrency'; import { getMongoSession } from '../transaction'; import { toEntityId, toMongoObjectId } from '../utils'; +import { FastGPT_SEM_Schema, type FastGPTSemType } from '@fastgpt/global/support/marketing/type'; export class MongoUserRepository implements UserRepository { constructor( @@ -88,6 +89,23 @@ export class MongoUserRepository implements UserRepository { }); } + /** 仅投影 fastgpt_sem;非法形状归一化为 undefined,避免污染 CRM 归因。 */ + async findSemById( + id: EntityId, + context?: TransactionContext + ): Promise<{ fastgpt_sem?: FastGPTSemType } | null> { + return this.execute(async () => { + const document = await this.withSession( + this.model.findById(toMongoObjectId(id)).select('fastgpt_sem'), + context + ).lean<{ fastgpt_sem?: unknown }>(); + if (!document) return null; + + const parsed = FastGPT_SEM_Schema.safeParse(document.fastgpt_sem); + return { fastgpt_sem: parsed.success ? parsed.data : undefined }; + }); + } + async create(input: CreateUser, context?: TransactionContext): Promise { return this.execute(async () => { const session = getMongoSession(context); diff --git a/packages/dal/package.json b/packages/dal/package.json index f31e43d41a64..50fa909d958c 100644 --- a/packages/dal/package.json +++ b/packages/dal/package.json @@ -9,6 +9,12 @@ "./ports": "./ports/index.ts", "./mongodb": "./mongodb/index.ts", "./mongodb/adapter": "./mongodb/adapter.ts", + "./mongodb/models/user": "./mongodb/models/user.ts", + "./mongodb/models/team": "./mongodb/models/team.ts", + "./mongodb/models/teamMember": "./mongodb/models/teamMember.ts", + "./mongodb/models/memberGroup": "./mongodb/models/memberGroup.ts", + "./mongodb/models/org": "./mongodb/models/org.ts", + "./mongodb/models/tmpData": "./mongodb/models/tmpData.ts", "./transaction": "./transaction/index.ts", "./redis": "./redis/index.ts", "./redis/adapter": "./redis/adapter.ts", diff --git a/packages/dal/ports/index.ts b/packages/dal/ports/index.ts index 8c317b53af25..04c61d148772 100644 --- a/packages/dal/ports/index.ts +++ b/packages/dal/ports/index.ts @@ -1 +1,3 @@ export type { UserRepository } from './user.repository'; +export type { CreateDefaultTeamParams, TeamRepository } from './team.repository'; +export type { TmpDataRepository } from './tmpData.repository'; diff --git a/packages/dal/ports/team.repository.ts b/packages/dal/ports/team.repository.ts new file mode 100644 index 000000000000..e6ea72256533 --- /dev/null +++ b/packages/dal/ports/team.repository.ts @@ -0,0 +1,28 @@ +import type { TeamMemberDetail } from '../domain/team'; +import type { EntityId } from '../domain/types'; +import type { TransactionContext } from '../transaction'; + +export type CreateDefaultTeamParams = { + userId: EntityId; + teamName?: string; + avatar?: string; + context?: TransactionContext; +}; + +/** + * Team/TeamMember 最小 DAL 能力,只覆盖 M9 三个迁移流程需要的操作。 + * 权限、组织等复杂读模型仍留在 service 层(事务外读取)。 + */ +export type TeamRepository = { + findMemberById(id: EntityId, context?: TransactionContext): Promise; + updateMemberAvatar( + id: EntityId, + avatar: string, + context?: TransactionContext + ): Promise; + /** + * 在事务内创建默认团队(teams + team_members + team_member_groups + team_orgs)。 + * 已存在默认 tmb 时返回 null(沿用旧语义:跳过创建)。 + */ + createDefaultTeam(params: CreateDefaultTeamParams): Promise; +}; diff --git a/packages/dal/ports/tmpData.repository.ts b/packages/dal/ports/tmpData.repository.ts new file mode 100644 index 000000000000..6d1bcb2b0601 --- /dev/null +++ b/packages/dal/ports/tmpData.repository.ts @@ -0,0 +1,11 @@ +import type { ActiveTmpDataFilter, TmpDataMaterial } from '../domain/tmpData'; +import type { TransactionContext } from '../transaction'; + +/** 验证码材料的最小 DAL 能力,供 verification 在 DAL 事务内读取与消费。 */ +export type TmpDataRepository = { + findActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise; + deleteActiveMaterial(filter: ActiveTmpDataFilter, context?: TransactionContext): Promise; +}; diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/ports/user.repository.ts index abb0d91d40fb..77a9ae7e6b2a 100644 --- a/packages/dal/ports/user.repository.ts +++ b/packages/dal/ports/user.repository.ts @@ -2,6 +2,7 @@ import type { ExpectedState } from '../db'; import type { CreateUser, UpdateUser, User, UserCredentials } from '../domain/user'; import type { EntityId } from '../domain/types'; import type { TransactionContext } from '../transaction'; +import type { FastGPTSemType } from '@fastgpt/global/support/marketing/type'; export type UserRepository = { findById(id: EntityId, context?: TransactionContext): Promise; @@ -11,6 +12,11 @@ export type UserRepository = { id: EntityId, context?: TransactionContext ): Promise<{ passwordUpdateTime?: Date } | null>; + /** 仅投影 fastgpt_sem,供 CRM 营销归因读取;不返回完整文档。 */ + findSemById( + id: EntityId, + context?: TransactionContext + ): Promise<{ fastgpt_sem?: FastGPTSemType } | null>; findByCredentials( credentials: UserCredentials, context?: TransactionContext diff --git a/packages/dal/test/mongodb/middleware.test.ts b/packages/dal/test/mongodb/middleware.test.ts new file mode 100644 index 000000000000..0c20c3260c51 --- /dev/null +++ b/packages/dal/test/mongodb/middleware.test.ts @@ -0,0 +1,50 @@ +import { Schema } from 'mongoose'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getDalLogger, setDalLogger } from '../../mongodb/logger'; +import { createSlowQueryMiddleware } from '../../mongodb/middleware'; + +afterEach(() => { + setDalLogger(undefined); +}); + +const runSaveHooks = async (schema: Schema, startTime: number) => { + const context: Record = { name: 'item' }; + await schema.s.hooks.execPre('save', context, (error: unknown) => { + if (error) throw error; + }); + context._startTime = startTime; + await schema.s.hooks.execPost('save', context, [context], (error: unknown) => { + if (error) throw error; + }); +}; + +describe('addDalCommonMiddleware', () => { + it('logs slow queries through the injected DAL logger', async () => { + const warn = vi.fn(); + setDalLogger({ warn }); + const schema = new Schema({ name: String }); + createSlowQueryMiddleware(schema); + + await runSaveHooks(schema, Date.now() - 3000); + + expect(warn).toHaveBeenCalledWith( + 'MongoDB slow query (>2s)', + expect.objectContaining({ duration: expect.any(Number), op: expect.any(String) }) + ); + }); + + it('does not log fast operations', async () => { + const warn = vi.fn(); + setDalLogger({ warn }); + const schema = new Schema({ name: String }); + createSlowQueryMiddleware(schema); + + await runSaveHooks(schema, Date.now()); + + expect(warn).not.toHaveBeenCalled(); + }); + + it('falls back to the console logger when none is injected', () => { + expect(getDalLogger()).toBeDefined(); + }); +}); diff --git a/packages/dal/test/mongodb/models/team.test.ts b/packages/dal/test/mongodb/models/team.test.ts new file mode 100644 index 000000000000..d24abb707f91 --- /dev/null +++ b/packages/dal/test/mongodb/models/team.test.ts @@ -0,0 +1,83 @@ +import { Mongoose } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { + MemberGroupDocumentSchema, + getMemberGroupModel +} from '../../../mongodb/models/memberGroup'; +import { OrgDocumentSchema, getOrgModel } from '../../../mongodb/models/org'; +import { TeamDocumentSchema, getTeamModel } from '../../../mongodb/models/team'; +import { TeamMemberDocumentSchema, getTeamMemberModel } from '../../../mongodb/models/teamMember'; +import { TmpDataDocumentSchema, getTmpDataModel } from '../../../mongodb/models/tmpData'; + +describe('TeamDocumentSchema', () => { + it('matches production defaults and declares production indexes', () => { + const TeamModel = getTeamModel(new Mongoose()); + const document = new TeamModel({ name: 'My Team', ownerId: '507f1f77bcf86cd799439011' }); + + expect(document.avatar).toBe('/icon/logo.svg'); + expect(document.externalWorkflowVariables).toEqual({}); + expect(TeamDocumentSchema.indexes()).toEqual([ + [{ name: 1 }, { background: true }], + [{ ownerId: 1 }, { background: true }], + [{ 'meta.wecom.corpId': 1 }, { sparse: true, unique: true, background: true }] + ]); + }); +}); + +describe('TeamMemberDocumentSchema', () => { + it('matches production defaults and declares production indexes', () => { + const TeamMemberModel = getTeamMemberModel(new Mongoose()); + const document = new TeamMemberModel({ + teamId: '507f1f77bcf86cd799439012', + userId: '507f1f77bcf86cd799439011' + }); + + expect(document.name).toBe('Member'); + expect(typeof document.avatar).toBe('string'); + expect(document.createTime).toBeInstanceOf(Date); + expect(TeamMemberDocumentSchema.indexes()).toEqual([ + [{ teamId: 1 }, { background: true }], + [{ userId: 1 }, { background: true }] + ]); + }); +}); + +describe('MemberGroupDocumentSchema', () => { + it('declares the production unique team+name index', () => { + expect(MemberGroupDocumentSchema.indexes()).toEqual([ + [ + { teamId: 1, name: 1 }, + { unique: true, background: true } + ] + ]); + }); +}); + +describe('OrgDocumentSchema', () => { + it('declares production indexes', () => { + expect(OrgDocumentSchema.indexes()).toEqual([ + [{ teamId: 1, path: 1 }, { background: true }], + [ + { teamId: 1, pathId: 1 }, + { unique: true, background: true } + ] + ]); + }); +}); + +describe('TmpDataDocumentSchema', () => { + it('matches production shape and declares TTL indexes', () => { + const TmpDataModel = getTmpDataModel(new Mongoose()); + const document = new TmpDataModel({ + dataId: 'verification:v1:login:password:user@example.com', + expireAt: new Date() + }); + + expect(document.data).toBeUndefined(); + expect(TmpDataDocumentSchema.indexes()).toEqual([ + [{ dataId: 1 }, { unique: true, background: true }], + [{ dataId: -1 }, { background: true }], + [{ expireAt: -1 }, { expireAfterSeconds: 5, background: true }] + ]); + }); +}); diff --git a/packages/dal/test/mongodb/repositories/team.test.ts b/packages/dal/test/mongodb/repositories/team.test.ts new file mode 100644 index 000000000000..ba0c3e2afd37 --- /dev/null +++ b/packages/dal/test/mongodb/repositories/team.test.ts @@ -0,0 +1,194 @@ +import { Types, type Model, type Mongoose } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import { DatabaseInvalidArgumentError } from '../../../db'; +import type { TeamMemberDetail } from '../../../domain/team'; +import type { MemberGroupMongooseSchemaType } from '../../../mongodb/models/memberGroup'; +import type { OrgMongooseSchemaType } from '../../../mongodb/models/org'; +import type { TeamMongooseSchemaType } from '../../../mongodb/models/team'; +import type { + TeamMemberDocument, + TeamMemberMongooseSchemaType +} from '../../../mongodb/models/teamMember'; +import { MongoTeamRepository } from '../../../mongodb/repositories/team'; + +const tmbId = '507f1f77bcf86cd799439011'; +const teamId = '507f1f77bcf86cd799439012'; +const userId = '507f1f77bcf86cd799439013'; + +const tmbDocument: TeamMemberDocument = { + _id: new Types.ObjectId(tmbId), + __v: 0, + teamId: new Types.ObjectId(teamId), + userId: new Types.ObjectId(userId), + avatar: 'avatar.png', + name: 'Owner', + role: 'owner', + status: 'active', + createTime: new Date('2026-01-01T00:00:00.000Z') +}; + +const createQuery = (value: T) => ({ + session: vi.fn().mockReturnThis(), + lean: vi.fn(async () => value) +}); + +const createRepository = () => { + const queries = { + findById: createQuery(tmbDocument), + updateAvatar: createQuery(tmbDocument), + existingTmb: createQuery(null) + }; + const teamCreate = vi.fn(async () => [{ _id: new Types.ObjectId(teamId) }]); + const tmbCreate = vi.fn(async () => [{ ...tmbDocument, toObject: vi.fn(() => tmbDocument) }]); + const groupCreate = vi.fn(async () => [{}]); + const orgCreate = vi.fn(async () => [{}]); + const teamMemberModel = { + findById: vi.fn(() => queries.findById), + findByIdAndUpdate: vi.fn(() => queries.updateAvatar), + findOne: vi.fn(() => queries.existingTmb), + create: tmbCreate + } as unknown as Model; + const model = { + teamModel: { + create: teamCreate + } as unknown as Model, + teamMemberModel, + memberGroupModel: { + create: groupCreate + } as unknown as Model, + orgModel: { + create: orgCreate + } as unknown as Model + }; + + const repository = new MongoTeamRepository( + model.teamModel, + model.teamMemberModel, + model.memberGroupModel, + model.orgModel + ); + + return { repository, model, queries, teamCreate, tmbCreate, groupCreate, orgCreate }; +}; + +describe('MongoTeamRepository.findMemberById', () => { + it('queries by ObjectId and maps the member detail', async () => { + const { repository, model } = createRepository(); + + const tmb = await repository.findMemberById(tmbId); + + expect(model.teamMemberModel.findById).toHaveBeenCalledWith(new Types.ObjectId(tmbId)); + expect(tmb).toMatchObject({ + id: tmbId, + teamId, + userId, + avatar: 'avatar.png', + name: 'Owner', + role: 'owner', + status: 'active' + }); + }); + + it('returns null when the member does not exist', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce(null); + + await expect(repository.findMemberById(tmbId)).resolves.toBeNull(); + }); + + it('rejects ids that cannot be represented by MongoDB', async () => { + const { repository, model } = createRepository(); + + const error = await repository.findMemberById('sql-id').catch((error) => error); + + expect(error).toBeInstanceOf(DatabaseInvalidArgumentError); + expect(error).toMatchObject({ code: 'DB_INVALID_ARGUMENT' }); + expect(model.teamMemberModel.findById).not.toHaveBeenCalled(); + }); +}); + +describe('MongoTeamRepository.updateMemberAvatar', () => { + it('updates the avatar and returns the updated member', async () => { + const { repository, model } = createRepository(); + + const tmb = await repository.updateMemberAvatar(tmbId, 'new-avatar.png'); + + expect(model.teamMemberModel.findByIdAndUpdate).toHaveBeenCalledWith( + new Types.ObjectId(tmbId), + { $set: { avatar: 'new-avatar.png' } }, + { new: true } + ); + expect(tmb?.avatar).toBe('avatar.png'); + }); + + it('returns null when the member does not exist', async () => { + const { repository, queries } = createRepository(); + queries.updateAvatar.lean.mockResolvedValueOnce(null); + + await expect(repository.updateMemberAvatar(tmbId, 'new-avatar.png')).resolves.toBeNull(); + }); +}); + +describe('MongoTeamRepository.createDefaultTeam', () => { + it('creates team, member, default group and root org in order', async () => { + const { repository, teamCreate, tmbCreate, groupCreate, orgCreate } = createRepository(); + + const tmb = await repository.createDefaultTeam({ userId }); + + expect(teamCreate).toHaveBeenCalledWith( + [ + { + ownerId: userId, + name: 'My Team', + avatar: '/icon/logo.svg', + createTime: expect.any(Date) + } + ], + undefined + ); + expect(tmbCreate).toHaveBeenCalledWith( + [ + { + teamId: new Types.ObjectId(teamId), + userId, + name: 'Owner', + role: 'owner', + status: 'active', + createTime: expect.any(Date) + } + ], + undefined + ); + expect(groupCreate).toHaveBeenCalledWith( + [ + { + teamId: new Types.ObjectId(teamId), + name: 'DEFAULT_GROUP', + avatar: '/icon/logo.svg' + } + ], + undefined + ); + expect(orgCreate).toHaveBeenCalledWith( + [ + { + teamId: new Types.ObjectId(teamId), + name: 'ROOT', + path: '' + } + ], + undefined + ); + expect(tmb?.id).toBe(tmbId); + }); + + it('skips creation when a default member already exists', async () => { + const { repository, queries, teamCreate, tmbCreate } = createRepository(); + queries.existingTmb.lean.mockResolvedValueOnce(tmbDocument); + + await expect(repository.createDefaultTeam({ userId })).resolves.toBeNull(); + + expect(teamCreate).not.toHaveBeenCalled(); + expect(tmbCreate).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/dal/test/mongodb/repositories/tmpData.test.ts b/packages/dal/test/mongodb/repositories/tmpData.test.ts new file mode 100644 index 000000000000..bcb46b095ddf --- /dev/null +++ b/packages/dal/test/mongodb/repositories/tmpData.test.ts @@ -0,0 +1,84 @@ +import type { Model } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +import type { TmpDataDocument, TmpDataMongooseSchemaType } from '../../../mongodb/models/tmpData'; +import { MongoTmpDataRepository } from '../../../mongodb/repositories/tmpData'; + +const document: TmpDataDocument = { + dataId: 'verification:v1:login:password:user@example.com', + data: { preLoginCode: 'ABC123' }, + expireAt: new Date('2026-01-02T00:00:00.000Z') +} as unknown as TmpDataDocument; + +const createQuery = (value: T) => ({ + session: vi.fn().mockReturnThis(), + lean: vi.fn(async () => value) +}); + +const createRepository = () => { + const queries = { + findOne: createQuery(document), + deletedCount: 1 + }; + const model = { + findOne: vi.fn(() => queries.findOne), + deleteOne: vi.fn((filter) => ({ + session: vi.fn().mockReturnThis(), + then: (onFulfilled: (v: { deletedCount: number }) => void) => + Promise.resolve(onFulfilled({ deletedCount: queries.deletedCount })), + filter + })) + } as unknown as Model; + + return { repository: new MongoTmpDataRepository(model), model, queries }; +}; + +const filter = { + dataId: 'verification:v1:login:password:user@example.com', + match: { preLoginCode: 'ABC123' } +}; + +describe('MongoTmpDataRepository.findActiveMaterial', () => { + it('filters by dataId, active expiry and mapped material fields', async () => { + const { repository, model } = createRepository(); + + const material = await repository.findActiveMaterial(filter); + + expect(model.findOne).toHaveBeenCalledWith({ + dataId: filter.dataId, + expireAt: { $gt: expect.any(Date) }, + 'data.preLoginCode': 'ABC123' + }); + expect(material).toMatchObject({ + dataId: document.dataId, + data: { preLoginCode: 'ABC123' } + }); + }); + + it('returns null when no active material matches', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce(null); + + await expect(repository.findActiveMaterial(filter)).resolves.toBeNull(); + }); +}); + +describe('MongoTmpDataRepository.deleteActiveMaterial', () => { + it('deletes the active material and reports success', async () => { + const { repository, queries, model } = createRepository(); + + await expect(repository.deleteActiveMaterial(filter)).resolves.toBe(true); + + expect(model.deleteOne).toHaveBeenCalledWith({ + dataId: filter.dataId, + expireAt: { $gt: expect.any(Date) }, + 'data.preLoginCode': 'ABC123' + }); + }); + + it('reports failure when no active material was deleted', async () => { + const { repository, queries } = createRepository(); + queries.deletedCount = 0; + + await expect(repository.deleteActiveMaterial(filter)).resolves.toBe(false); + }); +}); diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/repositories/user.test.ts index 35085b85ef76..1e2fdbbd1983 100644 --- a/packages/dal/test/mongodb/repositories/user.test.ts +++ b/packages/dal/test/mongodb/repositories/user.test.ts @@ -151,6 +151,39 @@ describe('MongoUserRepository.findPasswordUpdateTimeById', () => { }); }); +describe('MongoUserRepository.findSemById', () => { + it('projects only the fastgpt_sem field and returns the parsed value', async () => { + const { repository, model, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce({ + fastgpt_sem: { keyword: 'FastGPT' } + } as unknown as UserDocument); + + const result = await repository.findSemById(userId); + + expect(model.findById).toHaveBeenCalledWith(new Types.ObjectId(userId)); + expect(queries.findById.select).toHaveBeenCalledWith('fastgpt_sem'); + expect(result).toEqual({ fastgpt_sem: { keyword: 'FastGPT' } }); + }); + + it('normalizes an invalid fastgpt_sem value to undefined', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce({ + fastgpt_sem: 'not-an-object' + } as unknown as UserDocument); + + await expect(repository.findSemById(userId)).resolves.toEqual({ + fastgpt_sem: undefined + }); + }); + + it('returns null when the user does not exist', async () => { + const { repository, queries } = createRepository(); + queries.findById.lean.mockResolvedValueOnce(null); + + await expect(repository.findSemById(userId)).resolves.toBeNull(); + }); +}); + describe('MongoUserRepository.findByCredentials', () => { it('passes raw credentials to the Mongoose schema setter', async () => { const { repository, model } = createRepository(); diff --git a/packages/service/common/dal/index.ts b/packages/service/common/dal/index.ts index c68776472148..8f86285e72fc 100644 --- a/packages/service/common/dal/index.ts +++ b/packages/service/common/dal/index.ts @@ -4,6 +4,8 @@ import { createMongoDal } from './mongo'; export type { DatabaseAdapter } from '@fastgpt/dal/db'; export type { UserRepository } from '@fastgpt/dal/ports'; +export type { TeamRepository } from '@fastgpt/dal/ports'; +export type { TmpDataRepository } from '@fastgpt/dal/ports'; export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/transaction'; function selectDal(): DatabaseAdapter { @@ -22,4 +24,4 @@ function selectDal(): DatabaseAdapter { // 只实例化一次 adapter,保证 userRepository 与 transactionRunner 来自同一个 adapter 实例。 const dal = selectDal(); -export const { transactionRunner, userRepository } = dal; +export const { transactionRunner, userRepository, teamRepository, tmpDataRepository } = dal; diff --git a/packages/service/common/dal/mongo/connection.ts b/packages/service/common/dal/mongo/connection.ts index bdc49320653f..f11a82461812 100644 --- a/packages/service/common/dal/mongo/connection.ts +++ b/packages/service/common/dal/mongo/connection.ts @@ -3,6 +3,7 @@ import { delay } from '@fastgpt/global/common/system/utils'; import { getLogger, LogCategories } from '../../logger'; import { serviceEnv } from '../../../env'; import { createDefaultMongooseConnectOptions, registerMongooseListeners } from './config'; +import { syncDalModelIndexes } from './lifecycle'; const logger = getLogger(LogCategories.INFRA.MONGO); @@ -33,6 +34,10 @@ export class MongooseConnection { await db.connect(url, { ...options }); + // DAL Model 索引同步与旧 Model 同样为异步补偿:失败只记录日志,不阻塞启动。 + void syncDalModelIndexes(db).catch((error) => { + logger.warn('DAL MongoDB index sync failed during connect', { error }); + }); return db; } catch (error) { logger.error('DAL MongoDB connection failed, will retry', { error }); diff --git a/packages/service/common/dal/mongo/index.ts b/packages/service/common/dal/mongo/index.ts index 9d776851cef5..2e1d3d75b4f1 100644 --- a/packages/service/common/dal/mongo/index.ts +++ b/packages/service/common/dal/mongo/index.ts @@ -1,7 +1,15 @@ import { MongoAdapter } from '@fastgpt/dal/mongodb'; +import { setDalLogger } from '@fastgpt/dal/mongodb'; import type { DatabaseAdapter } from '@fastgpt/dal/db'; import type { Mongoose } from 'mongoose'; import { connection } from './connection'; +import { getLogger, LogCategories } from '../../logger'; + +// 把业务 logger 注入 DAL 慢查询中间件,避免 DAL 包反向依赖 service 日志实现。 +// 测试环境可能只 mock 部分 LogCategories,这里做存在性防护,未注入时 DAL 退回 console。 +if (LogCategories.INFRA?.MONGO) { + setDalLogger(getLogger(LogCategories.INFRA.MONGO)); +} export const createMongoDal = (client: Mongoose = connection.client): DatabaseAdapter => { return new MongoAdapter({ client }); diff --git a/packages/service/common/dal/mongo/lifecycle.ts b/packages/service/common/dal/mongo/lifecycle.ts new file mode 100644 index 000000000000..c6a3dcae4b34 --- /dev/null +++ b/packages/service/common/dal/mongo/lifecycle.ts @@ -0,0 +1,49 @@ +import type { Mongoose } from 'mongoose'; +import { getLogger, LogCategories } from '../../logger'; +import { MongoIndexManager } from '../../mongo'; +import { getMemberGroupModel } from '@fastgpt/dal/mongodb/models/memberGroup'; +import { getOrgModel } from '@fastgpt/dal/mongodb/models/org'; +import { getTeamModel } from '@fastgpt/dal/mongodb/models/team'; +import { getTeamMemberModel } from '@fastgpt/dal/mongodb/models/teamMember'; +import { getTmpDataModel } from '@fastgpt/dal/mongodb/models/tmpData'; +import { getUserModel } from '@fastgpt/dal/mongodb/models/user'; +import { serviceEnv } from '../../../env'; + +const logger = getLogger(LogCategories.INFRA.MONGO); + +/** + * 同步 DAL Model 索引:补建当前 Schema 索引,并清理该 Schema 明确登记的历史索引。 + * + * 与旧 Model 的 syncMongoIndex 保持同一套 MongoIndexManager 语义;测试与构建阶段跳过。 + * DAL Model 与旧 Model 指向同一集合,索引创建幂等,同步顺序不影响正确性。 + */ +export const syncDalModelIndexes = async (client: Mongoose) => { + if ( + process.env.NODE_ENV === 'test' || + process.env.NEXT_PHASE === 'phase-production-build' || + !serviceEnv.SYNC_INDEX + ) { + return; + } + + const models = [ + getUserModel(client), + getTeamModel(client), + getTeamMemberModel(client), + getMemberGroupModel(client), + getOrgModel(client), + getTmpDataModel(client) + ]; + + await Promise.all( + models.map((model) => + MongoIndexManager.syncModelIndexes({ model, logger }).catch((error) => { + logger.error('Failed to ensure DAL MongoDB indexes', { + modelName: model.modelName, + collectionName: model.collection.collectionName, + error + }); + }) + ) + ); +}; diff --git a/packages/service/support/marketing/interface/crm.ts b/packages/service/support/marketing/interface/crm.ts index 679c4e1a59b9..c70c1210cf86 100644 --- a/packages/service/support/marketing/interface/crm.ts +++ b/packages/service/support/marketing/interface/crm.ts @@ -1,7 +1,7 @@ import { successMarkerCache, type SuccessMarkerParams } from '@fastgpt/dal/redis/caches'; import { FastGPT_SEM_Schema } from '@fastgpt/global/support/marketing/type'; import { getLogger, LogCategories } from '../../../common/logger'; -import { MongoUser } from '../../user/schema'; +import { userRepository } from '../../../common/dal'; import { MongoTeam } from '../../user/team/teamSchema'; import { isCRMReportingConfigured, @@ -84,7 +84,7 @@ const reportCRMTeamEventOnce = async ({ const team = await MongoTeam.findById(teamId, 'ownerId name').lean(); if (!team?.ownerId) return; - const user = await MongoUser.findById(team.ownerId, 'fastgpt_sem').lean(); + const user = await userRepository.findSemById(String(team.ownerId)); const visitorId = getVisitorId(user?.fastgpt_sem); const success = await report({ cloudUserId: String(team.ownerId), diff --git a/packages/service/support/tmpData/verification.ts b/packages/service/support/tmpData/verification.ts index d4f380069cbb..f93a210d287a 100644 --- a/packages/service/support/tmpData/verification.ts +++ b/packages/service/support/tmpData/verification.ts @@ -11,6 +11,8 @@ import { type VerificationTtlPreset } from '@fastgpt/global/support/user/account/verification/type'; import { hashStr } from '@fastgpt/global/common/string/tools'; +import { transactionRunner, tmpDataRepository } from '../../common/dal'; +import type { TransactionContext } from '../../common/dal'; export type Scene = AccountVerificationPurpose; export type Type = VerificationType; @@ -58,6 +60,11 @@ export type VerificationConsumeContext = { session: ClientSession; }; +export type VerificationConsumeDalContext = { + material: VerificationMaterial; + dalContext: TransactionContext; +}; + export class VerificationMaterialError extends Error { constructor() { super('Verification material is invalid or already consumed'); @@ -244,3 +251,39 @@ export const verification = { }); } }; + +const getActiveFilterParams = (params: VerificationConsumeParams) => ({ + dataId: getDataId(params), + match: params.match as Record | undefined +}); + +/** + * 在同一 DAL 事务内读取材料、执行业务回调,并在回调成功后消费材料。 + * + * 与 consumeInTransaction 的差异:事务上下文是 DAL TransactionContext, + * 回调内只能使用 DAL Repository 访问数据库(旧 Model 无法参与 DAL 事务)。 + */ +export const consumeInTransactionWithDal = async ( + params: VerificationConsumeParams, + handler: (context: VerificationConsumeDalContext) => Promise +): Promise => { + return transactionRunner.withTransaction(async (dalContext) => { + const filter = getActiveFilterParams(params); + const record = await tmpDataRepository.findActiveMaterial(filter, dalContext); + if (!record) { + throw new VerificationMaterialError(); + } + + const result = await handler({ + material: record.data as VerificationMaterial, + dalContext + }); + + const deleted = await tmpDataRepository.deleteActiveMaterial(filter, dalContext); + if (!deleted) { + throw new VerificationMaterialError(); + } + + return result; + }); +}; diff --git a/packages/service/support/user/account/verification/password/service.ts b/packages/service/support/user/account/verification/password/service.ts index e7c82e61211e..3a78ef111348 100644 --- a/packages/service/support/user/account/verification/password/service.ts +++ b/packages/service/support/user/account/verification/password/service.ts @@ -1,8 +1,12 @@ import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; import { UserError } from '@fastgpt/global/common/error/utils'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { verification, VerificationMaterialError } from '../../../../tmpData/verification'; -import { MongoUser } from '../../../schema'; +import { + verification, + VerificationMaterialError, + consumeInTransactionWithDal +} from '../../../../tmpData/verification'; +import { userRepository } from '../../../../../common/dal'; import { serviceEnv } from '../../../../../env'; import { assertPasswordVerificationConsumeRateLimit, @@ -38,12 +42,9 @@ const defaultDependencies: PasswordVerificationDependencies = { data: { preLoginCode: code }, ttlPreset }), - findUserByCredentials: ({ username, password, session }) => { - const query = MongoUser.findOne({ username, password }); - if (session) query.session(session); - return query; - }, - consumeInTransaction: verification.consumeInTransaction + findUserByCredentials: ({ username, password }) => + userRepository.findByCredentials({ username, password }), + consumeInTransaction: consumeInTransactionWithDal }; /** @@ -96,17 +97,16 @@ export class PasswordVerificationService { key: params.username, match: { preLoginCode: params.code } }, - async ({ session }) => { + async ({ dalContext }) => { const user = await this.dependencies.findUserByCredentials({ username: params.username, - password: params.password, - session + password: params.password }); if (!user) { return Promise.reject(UserErrEnum.account_psw_error); } - return handler({ user, session }); + return handler({ user, dalContext }); } ); } catch (error) { diff --git a/packages/service/support/user/account/verification/password/type.ts b/packages/service/support/user/account/verification/password/type.ts index 9fdccd46249e..ae26d4f862eb 100644 --- a/packages/service/support/user/account/verification/password/type.ts +++ b/packages/service/support/user/account/verification/password/type.ts @@ -1,17 +1,16 @@ -import type { HydratedDocument } from 'mongoose'; -import type { UserModelSchema } from '@fastgpt/global/support/user/type'; +import type { User } from '@fastgpt/dal'; +import type { TransactionContext } from '../../../../../common/dal'; import type { PasswordVerificationPurpose, VerificationTtlPreset, VerificationType } from '@fastgpt/global/support/user/account/verification/type'; -import type { ClientSession } from '../../../../../common/mongo'; import type { - VerificationConsumeContext, + VerificationConsumeDalContext, VerificationConsumeParams } from '../../../../tmpData/verification'; -export type PasswordVerificationUser = HydratedDocument; +export type PasswordVerificationUser = User; export type { PasswordVerificationPurpose } from '@fastgpt/global/support/user/account/verification/type'; @@ -33,7 +32,7 @@ export type VerifyPasswordCredentialsParams = { export type PasswordVerificationHandler = (params: { user: PasswordVerificationUser; - session: ClientSession; + dalContext: TransactionContext; }) => Promise; export type PasswordVerificationDependencies = { @@ -55,10 +54,9 @@ export type PasswordVerificationDependencies = { findUserByCredentials: (params: { username: string; password: string; - session?: ClientSession; }) => Promise; consumeInTransaction: ( params: VerificationConsumeParams, - handler: (context: VerificationConsumeContext) => Promise + handler: (context: VerificationConsumeDalContext) => Promise ) => Promise; }; diff --git a/packages/service/support/user/controller.ts b/packages/service/support/user/controller.ts index e8c5584e9ea7..738d3a6c1a56 100644 --- a/packages/service/support/user/controller.ts +++ b/packages/service/support/user/controller.ts @@ -1,11 +1,8 @@ import { type UserType } from '@fastgpt/global/support/user/type'; -import { userDefaultFieldValues } from '@fastgpt/dal'; -import { MongoUser } from './schema'; import { userRepository } from '../../common/dal'; import { getTmbInfoByTmbId, getUserDefaultTeam } from './team/controller'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; -import type { ClientSession } from '../../common/mongo'; export async function authUserExist({ userId, username }: { userId?: string; username?: string }) { if (userId) { @@ -20,44 +17,25 @@ export async function authUserExist({ userId, username }: { userId?: string; use export async function getUserDetail({ tmbId, userId, - isRoot = false, - session + isRoot = false }: { tmbId?: string; userId?: string; isRoot?: boolean; - session?: ClientSession; }): Promise { const tmb = await (async () => { if (tmbId) { try { - const result = await getTmbInfoByTmbId({ tmbId, session }); + const result = await getTmbInfoByTmbId({ tmbId }); return result; } catch (error) {} } if (userId) { - return getUserDefaultTeam({ userId, session }); + return getUserDefaultTeam({ userId }); } return Promise.reject(ERROR_ENUM.unAuthorization); })(); - const user = await (async () => { - if (!session) { - return userRepository.findById(String(tmb.userId)); - } - - // 登录事务仍使用旧 ClientSession;相关集合全部迁入 DAL 后再删除该兼容分支。 - const document = await MongoUser.findById(tmb.userId).session(session); - if (!document) return null; - return { - id: String(document._id), - username: document.username, - timezone: document.timezone ?? userDefaultFieldValues.timezone, - promotionRate: document.promotionRate ?? userDefaultFieldValues.promotionRate, - contact: document.contact ?? undefined, - language: document.language ?? userDefaultFieldValues.language, - tags: document.tags ?? userDefaultFieldValues.tags - }; - })(); + const user = await userRepository.findById(String(tmb.userId)); if (!user) { return Promise.reject(ERROR_ENUM.unAuthorization); diff --git a/packages/service/test/support/marketing/crmInterface.test.ts b/packages/service/test/support/marketing/crmInterface.test.ts index 906148ee38ff..b5cf6f6d8d9b 100644 --- a/packages/service/test/support/marketing/crmInterface.test.ts +++ b/packages/service/test/support/marketing/crmInterface.test.ts @@ -23,9 +23,9 @@ vi.mock('@fastgpt/service/common/logger', () => ({ LogCategories: { MODULE: { USER: { ACCOUNT: ['user', 'account'] } } } })); -vi.mock('@fastgpt/service/support/user/schema', () => ({ - MongoUser: { - findById: (...args: unknown[]) => ({ lean: () => mocks.findUser(...args) }) +vi.mock('@fastgpt/service/common/dal', () => ({ + userRepository: { + findSemById: (...args: unknown[]) => mocks.findUser(...args) } })); @@ -81,8 +81,8 @@ describe('CRM team lifecycle interface', () => { it('resolves the team owner visitor and marks the team only after CRM succeeds', async () => { await reportCRMTeamRechargeOnce({ teamId: 'team-1' }); - expect(mocks.findTeam).toHaveBeenCalledWith('team-1', 'ownerId name'); - expect(mocks.findUser).toHaveBeenCalledWith('user-1', 'fastgpt_sem'); + expect(mocks.findTeam).toHaveBeenCalledWith('team-1', 'ownerId'); + expect(mocks.findUser).toHaveBeenCalledWith('user-1'); expect(mocks.report).toHaveBeenCalledWith({ visitorId: 'stored-visitor', event: 'recharge', diff --git a/packages/service/test/support/user/account/verification/password/service.test.ts b/packages/service/test/support/user/account/verification/password/service.test.ts index 8c15e21d18ef..72dd5d25ab7a 100644 --- a/packages/service/test/support/user/account/verification/password/service.test.ts +++ b/packages/service/test/support/user/account/verification/password/service.test.ts @@ -9,7 +9,7 @@ import { getDataId, VerificationMaterialError } from '@fastgpt/service/support/tmpData/verification'; -import type { ClientSession } from '@fastgpt/service/common/mongo'; +import type { TransactionContext } from '@fastgpt/service/common/dal'; import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; import { describe, expect, it, vi } from 'vitest'; @@ -24,7 +24,7 @@ const createDependencies = ( consumeInTransaction: vi.fn(async (_params, handler) => handler({ material: { preLoginCode: 'ABC123' }, - session: undefined as unknown as ClientSession + dalContext: Symbol('test-transaction') as TransactionContext }) ), ...overrides @@ -98,7 +98,7 @@ describe('PasswordVerificationService default adapters', () => { dataId: getDataId({ scene: 'login', type: 'password', key: username }) }) ).resolves.toBeNull(); - expect(String(result._id)).toBe(String(storedUser._id)); + expect(result.id).toBe(String(storedUser._id)); }); }); @@ -162,23 +162,23 @@ describe('PasswordVerificationService.withVerifiedCredentials', () => { expect(dependencies.findUserByCredentials).not.toHaveBeenCalled(); }); - it('passes the matched user and transaction session to the business callback', async () => { - const user = { _id: 'user-id' } as unknown as PasswordVerificationUser; - const session = {} as ClientSession; + it('passes the matched user and DAL transaction context to the business callback', async () => { + const user = { id: 'user-id' } as unknown as PasswordVerificationUser; + const dalContext = Symbol('test-transaction') as TransactionContext; const consumeInTransaction = vi.fn(async (_params, handler) => - handler({ material: { preLoginCode: 'ABC123' }, session }) + handler({ material: { preLoginCode: 'ABC123' }, dalContext }) ); const dependencies = createDependencies({ findUserByCredentials: vi.fn(async (params) => { - expect(params.session).toBe(session); + expect(params).not.toHaveProperty('session'); return user; }), consumeInTransaction }); const service = new PasswordVerificationService(dependencies); - const handler = vi.fn(async ({ user: matchedUser, session: callbackSession }) => { + const handler = vi.fn(async ({ user: matchedUser, dalContext: callbackContext }) => { expect(matchedUser).toBe(user); - expect(callbackSession).toBe(session); + expect(callbackContext).toBe(dalContext); return 'completed'; }); diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index b23007d13154..82ee857ad44b 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -1,4 +1,5 @@ import { getUserDetail } from '@fastgpt/service/support/user/controller'; +import { userRepository } from '@fastgpt/service/common/dal'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { NextAPI } from '@/service/middleware/entry'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -38,7 +39,7 @@ async function handler( code, purpose: 'login' }, - async ({ user, session }) => { + async ({ user, dalContext }) => { if (user.status === UserStatusEnum.forbidden) { return Promise.reject('Invalid account!'); } @@ -49,28 +50,30 @@ async function handler( const userDetail = await getUserDetail({ tmbId: user?.lastLoginTmbId, - userId: user._id, - isRoot: username === 'root', - session + userId: user.id, + isRoot: username === 'root' }); - user.lastLoginTmbId = userDetail.team.tmbId; - user.language = language; const visitorIdentity = resolveCRMVisitorId({ storedFastgptSem: user.fastgpt_sem, incomingVisitorId: fastgpt_sem?.visitor_id }); - if (visitorIdentity.shouldPersist) { - user.fastgpt_sem = visitorIdentity.fastgptSem; - } - await user.save({ session }); + await userRepository.updateById( + user.id, + { + lastLoginTmbId: userDetail.team.tmbId, + language, + ...(visitorIdentity.shouldPersist ? { fastgpt_sem: visitorIdentity.fastgptSem } : {}) + }, + dalContext + ); return { user, userDetail, visitorIdentity }; } ); const token = await createUserSession({ - userId: user._id, + userId: user.id, teamId: userDetail.team.teamId, tmbId: userDetail.team.tmbId, isRoot: username === 'root', @@ -81,14 +84,14 @@ async function handler( void reportCRMVisitorIdentity({ visitorId: visitorIdentity.visitorId, - userId: String(user._id), + userId: user.id, username: user.username, contact: user.contact }); pushTrack.login({ type: 'password', - uid: user._id, + uid: user.id, teamId: userDetail.team.teamId, tmbId: userDetail.team.tmbId }); diff --git a/projects/app/src/pages/api/support/user/account/update.ts b/projects/app/src/pages/api/support/user/account/update.ts index cc3c8fa19563..9d50108672ab 100644 --- a/projects/app/src/pages/api/support/user/account/update.ts +++ b/projects/app/src/pages/api/support/user/account/update.ts @@ -1,20 +1,20 @@ -import { MongoUser } from '@fastgpt/service/support/user/schema'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { transactionRunner, userRepository, teamRepository } from '@fastgpt/service/common/dal'; +import { getLogger, LogCategories } from '@fastgpt/service/common/logger'; /* update user info */ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/next/type'; import { NextAPI } from '@/service/middleware/entry'; -import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { UpdateUserAccountBodySchema, - UpdateUserAccountResponseSchema, type UpdateUserAccountBody, type UpdateUserAccountResponse } from '@fastgpt/global/openapi/support/user/account/update/api'; +const logger = getLogger(LogCategories.MODULE.USER.ACCOUNT); + export type UserAccountUpdateQuery = Record; async function handler( @@ -27,30 +27,40 @@ async function handler( }).body; const { tmbId } = await authCert({ req, authToken: true }); - // const user = await getUserDetail({ tmbId }); - // 更新对应的记录 - await mongoSessionRun(async (session) => { - const tmb = await MongoTeamMember.findById(tmbId).session(session); + const tmb = await teamRepository.findMemberById(tmbId); + if (!tmb) { + return Promise.reject('can not find it'); + } + + // user 字段与 tmb 头像在同一个 DAL 事务内更新,避免混用两种事务上下文。 + await transactionRunner.withTransaction(async (context) => { if (timezone || language) { - await MongoUser.updateOne( - { - _id: tmb?.userId - }, + await userRepository.updateById( + tmb.userId, { ...(timezone && { timezone }), ...(language && { language }) - } - ).session(session); + }, + context + ); } // if avatar, update team member avatar if (avatar) { - await MongoTeamMember.updateOne({ _id: tmbId }, { avatar }).session(session); - - await getS3AvatarSource().refreshAvatar(avatar, tmb?.avatar, session); + await teamRepository.updateMemberAvatar(tmbId, avatar, context); } }); - return UpdateUserAccountResponseSchema.parse({}); + // S3 头像刷新(含 MongoS3TTL 清理)在事务提交后执行:best-effort, + // 避免把 s3_ttl 集合拖进 DAL 事务;失败只记录日志,不影响更新结果。 + if (avatar && avatar !== tmb.avatar) { + try { + await getS3AvatarSource().refreshAvatar(avatar, tmb.avatar); + } catch (error) { + logger.warn('Avatar refresh failed after account update', { error, tmbId }); + } + } + + return {}; } export default NextAPI(handler); diff --git a/projects/app/src/service/mongo.ts b/projects/app/src/service/mongo.ts index 64d50d4935d6..fbad8505ae9e 100644 --- a/projects/app/src/service/mongo.ts +++ b/projects/app/src/service/mongo.ts @@ -1,8 +1,5 @@ -import { MongoUser } from '@fastgpt/service/support/user/schema'; -import { hashStr } from '@fastgpt/global/common/string/tools'; -import { createDefaultTeam } from '@fastgpt/service/support/user/team/controller'; import { exit } from 'process'; -import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; +import { transactionRunner, userRepository, teamRepository } from '@fastgpt/service/common/dal'; import { getLogger, LogCategories } from '@fastgpt/service/common/logger'; import { appEnv } from '@/env'; @@ -10,33 +7,27 @@ const logger = getLogger(LogCategories.SYSTEM); export async function initRootUser(retry = 3): Promise { try { - const rootUser = await MongoUser.findOne({ - username: 'root' - }); const psw = appEnv.DEFAULT_ROOT_PSW; - let rootId = rootUser?._id || ''; - - await mongoSessionRun(async (session) => { + // root 用户与默认团队在同一个 DAL 事务内初始化,避免混用两种事务上下文。 + await transactionRunner.withTransaction(async (context) => { + const rootUser = await userRepository.findByUsername('root'); // init root user if (rootUser) { - await rootUser.updateOne({ - password: hashStr(psw) - }); + await userRepository.updateById(rootUser.id, { password: psw }, context); } else { - const [{ _id }] = await MongoUser.create( - [ - { - username: 'root', - password: hashStr(psw) - } - ], - { session, ordered: true } + const created = await userRepository.create( + { + username: 'root', + password: psw + }, + context ); - rootId = _id; + await teamRepository.createDefaultTeam({ userId: created.id, context }); + return; } // init root team - await createDefaultTeam({ userId: rootId, session }); + await teamRepository.createDefaultTeam({ userId: rootUser.id, context }); }); logger.info('Root user initialized', { diff --git a/projects/app/test/api/support/user/account/loginByPassword.test.ts b/projects/app/test/api/support/user/account/loginByPassword.test.ts index dfe76f8883bd..a3fd5cae1da0 100644 --- a/projects/app/test/api/support/user/account/loginByPassword.test.ts +++ b/projects/app/test/api/support/user/account/loginByPassword.test.ts @@ -92,7 +92,7 @@ describe('loginByPassword API', () => { expect(setCookie).toHaveBeenCalled(); expect(pushTrack.login).toHaveBeenCalledWith({ type: 'password', - uid: testUser._id, + uid: String(testUser._id), teamId: String(testTeam._id), tmbId: String(testTmb._id) }); From da678ba04b323bb29d4f228bb46cab0213292957 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Thu, 20 Aug 2026 16:50:25 +0800 Subject: [PATCH 11/14] refactor(dal): reorganize database access layers --- .agents/design/dal-basic-architecture.md | 88 ++- .agents/design/dal-m10-model-lifecycle.md | 2 +- AGENTS.md | 1 + packages/dal/AGENTS.override.md | 172 ++++++ packages/dal/business/index.ts | 1 + packages/dal/business/support/user/dto.ts | 30 + .../support/user/entity.ts} | 30 +- packages/dal/business/support/user/index.ts | 5 + .../support/user/repository.ts} | 14 +- .../dal/business/support/user/team/dto.ts | 51 ++ .../dal/business/support/user/team/entity.ts | 69 +++ .../business/support/user/team/group/dto.ts | 9 + .../support/user/team/group/entity.ts | 20 + .../business/support/user/team/group/index.ts | 3 + .../support/user/team/group/repository.ts | 43 ++ .../dal/business/support/user/team/index.ts | 5 + .../dal/business/support/user/team/org/dto.ts | 9 + .../business/support/user/team/org/entity.ts | 22 + .../business/support/user/team/org/index.ts | 3 + .../support/user/team/org/repository.ts | 31 ++ .../business/support/user/team/repository.ts | 97 ++++ .../support/user/verification/dto.ts} | 8 - .../support/user/verification/entity.ts | 11 + .../support/user/verification/index.ts | 3 + .../support/user/verification/repository.ts | 17 + packages/dal/db/adapter.ts | 12 +- packages/dal/db/concurrency.ts | 4 +- packages/dal/db/index.ts | 3 + packages/dal/db/tables.ts | 2 + .../types.ts => db/transaction.ts} | 0 packages/dal/{domain => db}/types.ts | 1 + packages/dal/domain/index.ts | 27 - packages/dal/domain/team.ts | 50 -- packages/dal/index.ts | 4 +- packages/dal/mongodb/adapter.ts | 17 +- packages/dal/mongodb/business/index.ts | 1 + .../support/user/entity.ts} | 10 +- .../mongodb/business/support/user/index.ts | 10 + .../support/user/repository.ts} | 44 +- .../support/user/schema.ts} | 11 +- .../business/support/user/team/entity.ts | 67 +++ .../support/user/team/group/entity.ts | 26 + .../business/support/user/team/group/index.ts | 9 + .../support/user/team/group/member/index.ts | 6 + .../support/user/team/group/member/schema.ts | 48 ++ .../support/user/team/group/repository.ts | 207 +++++++ .../support/user/team/group/schema.ts} | 8 +- .../business/support/user/team/index.ts | 28 + .../support/user/team/member/index.ts | 6 + .../support/user/team/member/schema.ts} | 21 +- .../business/support/user/team/org/entity.ts | 29 + .../business/support/user/team/org/index.ts | 9 + .../support/user/team/org/member/index.ts | 6 + .../support/user/team/org/member/schema.ts | 49 ++ .../support/user/team/org/repository.ts | 170 ++++++ .../support/user/team/org/schema.ts} | 8 +- .../business/support/user/team/repository.ts | 521 ++++++++++++++++++ .../support/user/team/schema.ts} | 8 +- .../support/user/verification/entity.ts | 13 + .../support/user/verification/index.ts | 8 + .../support/user/verification/repository.ts | 142 +++++ .../support/user/verification/schema.ts} | 8 +- packages/dal/mongodb/concurrency.ts | 2 +- packages/dal/mongodb/index.ts | 8 +- packages/dal/mongodb/logger.ts | 2 +- packages/dal/mongodb/mappers/team.ts | 14 - .../mongodb/{models/helper.ts => model.ts} | 2 +- packages/dal/mongodb/repositories/team.ts | 159 ------ packages/dal/mongodb/repositories/tmpData.ts | 73 --- packages/dal/mongodb/transaction.ts | 2 +- packages/dal/mongodb/utils.ts | 2 +- packages/dal/package.json | 15 +- packages/dal/ports/index.ts | 3 - packages/dal/ports/team.repository.ts | 28 - packages/dal/ports/tmpData.repository.ts | 11 - .../dal/test/integrations/mongodb/client.ts | 17 + .../test/integrations/mongodb/global-setup.ts | 32 ++ .../mongodb/smoke.integration.test.ts | 19 + packages/dal/test/mongodb/adapter.test.ts | 2 +- .../user/entity.test.ts} | 4 +- .../user/repository.test.ts} | 42 +- .../user/schema.test.ts} | 9 +- .../user/team/repository.test.ts} | 62 ++- .../user/team/schema.test.ts} | 18 +- .../user/verification/repository.test.ts} | 7 +- packages/dal/test/mongodb/middleware.test.ts | 14 +- .../dal/test/redis/bullmq-services.test.ts | 14 +- packages/dal/transaction/index.ts | 1 - packages/dal/vitest.integration.config.ts | 18 + packages/service/common/dal/index.ts | 29 +- packages/service/common/dal/mongo/config.ts | 36 -- .../service/common/dal/mongo/connection.ts | 61 -- packages/service/common/dal/mongo/index.ts | 4 +- .../service/common/dal/mongo/lifecycle.ts | 18 +- packages/service/support/invoke/invoke.ts | 19 +- .../support/marketing/interface/crm.ts | 5 +- .../service/support/permission/controller.ts | 19 +- .../dataClean/danglingPermission.ts | 27 +- .../permission/memberGroup/controllers.ts | 93 ++-- .../support/permission/org/controllers.ts | 84 +-- .../service/support/permission/teamLimit.ts | 8 +- .../service/support/tmpData/controller.ts | 26 +- .../service/support/tmpData/verification.ts | 95 ++-- .../service/support/user/team/controller.ts | 245 +++----- .../support/user/team/delete/processor.ts | 38 +- packages/service/support/user/team/utils.ts | 159 ++++-- packages/service/support/user/utils.ts | 95 +--- .../support/marketing/crmInterface.test.ts | 11 +- .../test/support/permission/teamLimit.test.ts | 18 +- .../verification/password/service.test.ts | 6 +- .../service/test/support/user/utils.test.ts | 20 +- pnpm-lock.yaml | 3 + projects/app/AGENTS.md | 9 + projects/app/CLAUDE.md | 1 + projects/app/src/instrumentation-node.ts | 25 +- projects/app/src/pages/api/admin/initv4141.ts | 6 +- projects/app/src/pages/api/core/app/create.ts | 12 +- .../src/pages/api/core/app/logs/exportLogs.ts | 27 +- .../src/pages/api/core/app/logs/getUsers.ts | 12 +- .../user/account/updatePasswordByOld.ts | 5 +- .../user/team/plan/getTeamPlanStatus.ts | 10 +- projects/app/src/service/mongo.ts | 8 +- .../support/permission/auth/chatCompletion.ts | 30 +- .../user/account/loginByPassword.test.ts | 22 +- .../user/account/resetExpiredPsw.test.ts | 18 +- .../user/account/updatePasswordByOld.test.ts | 29 +- test/mocks/common/mongo.ts | 19 - test/setup.ts | 3 - 128 files changed, 2888 insertions(+), 1384 deletions(-) create mode 100644 packages/dal/AGENTS.override.md create mode 100644 packages/dal/business/index.ts create mode 100644 packages/dal/business/support/user/dto.ts rename packages/dal/{domain/user.ts => business/support/user/entity.ts} (61%) create mode 100644 packages/dal/business/support/user/index.ts rename packages/dal/{ports/user.repository.ts => business/support/user/repository.ts} (74%) create mode 100644 packages/dal/business/support/user/team/dto.ts create mode 100644 packages/dal/business/support/user/team/entity.ts create mode 100644 packages/dal/business/support/user/team/group/dto.ts create mode 100644 packages/dal/business/support/user/team/group/entity.ts create mode 100644 packages/dal/business/support/user/team/group/index.ts create mode 100644 packages/dal/business/support/user/team/group/repository.ts create mode 100644 packages/dal/business/support/user/team/index.ts create mode 100644 packages/dal/business/support/user/team/org/dto.ts create mode 100644 packages/dal/business/support/user/team/org/entity.ts create mode 100644 packages/dal/business/support/user/team/org/index.ts create mode 100644 packages/dal/business/support/user/team/org/repository.ts create mode 100644 packages/dal/business/support/user/team/repository.ts rename packages/dal/{domain/tmpData.ts => business/support/user/verification/dto.ts} (53%) create mode 100644 packages/dal/business/support/user/verification/entity.ts create mode 100644 packages/dal/business/support/user/verification/index.ts create mode 100644 packages/dal/business/support/user/verification/repository.ts rename packages/dal/{transaction/types.ts => db/transaction.ts} (100%) rename packages/dal/{domain => db}/types.ts (51%) delete mode 100644 packages/dal/domain/index.ts delete mode 100644 packages/dal/domain/team.ts create mode 100644 packages/dal/mongodb/business/index.ts rename packages/dal/mongodb/{mappers/user.ts => business/support/user/entity.ts} (78%) create mode 100644 packages/dal/mongodb/business/support/user/index.ts rename packages/dal/mongodb/{repositories/user.ts => business/support/user/repository.ts} (80%) rename packages/dal/mongodb/{models/user.ts => business/support/user/schema.ts} (86%) create mode 100644 packages/dal/mongodb/business/support/user/team/entity.ts create mode 100644 packages/dal/mongodb/business/support/user/team/group/entity.ts create mode 100644 packages/dal/mongodb/business/support/user/team/group/index.ts create mode 100644 packages/dal/mongodb/business/support/user/team/group/member/index.ts create mode 100644 packages/dal/mongodb/business/support/user/team/group/member/schema.ts create mode 100644 packages/dal/mongodb/business/support/user/team/group/repository.ts rename packages/dal/mongodb/{models/memberGroup.ts => business/support/user/team/group/schema.ts} (83%) create mode 100644 packages/dal/mongodb/business/support/user/team/index.ts create mode 100644 packages/dal/mongodb/business/support/user/team/member/index.ts rename packages/dal/mongodb/{models/teamMember.ts => business/support/user/team/member/schema.ts} (77%) create mode 100644 packages/dal/mongodb/business/support/user/team/org/entity.ts create mode 100644 packages/dal/mongodb/business/support/user/team/org/index.ts create mode 100644 packages/dal/mongodb/business/support/user/team/org/member/index.ts create mode 100644 packages/dal/mongodb/business/support/user/team/org/member/schema.ts create mode 100644 packages/dal/mongodb/business/support/user/team/org/repository.ts rename packages/dal/mongodb/{models/org.ts => business/support/user/team/org/schema.ts} (85%) create mode 100644 packages/dal/mongodb/business/support/user/team/repository.ts rename packages/dal/mongodb/{models/team.ts => business/support/user/team/schema.ts} (86%) create mode 100644 packages/dal/mongodb/business/support/user/verification/entity.ts create mode 100644 packages/dal/mongodb/business/support/user/verification/index.ts create mode 100644 packages/dal/mongodb/business/support/user/verification/repository.ts rename packages/dal/mongodb/{models/tmpData.ts => business/support/user/verification/schema.ts} (82%) delete mode 100644 packages/dal/mongodb/mappers/team.ts rename packages/dal/mongodb/{models/helper.ts => model.ts} (89%) delete mode 100644 packages/dal/mongodb/repositories/team.ts delete mode 100644 packages/dal/mongodb/repositories/tmpData.ts delete mode 100644 packages/dal/ports/index.ts delete mode 100644 packages/dal/ports/team.repository.ts delete mode 100644 packages/dal/ports/tmpData.repository.ts create mode 100644 packages/dal/test/integrations/mongodb/client.ts create mode 100644 packages/dal/test/integrations/mongodb/global-setup.ts create mode 100644 packages/dal/test/integrations/mongodb/smoke.integration.test.ts rename packages/dal/test/mongodb/{mappers/user.test.ts => business/user/entity.test.ts} (95%) rename packages/dal/test/mongodb/{repositories/user.test.ts => business/user/repository.test.ts} (92%) rename packages/dal/test/mongodb/{models/user.test.ts => business/user/schema.test.ts} (80%) rename packages/dal/test/mongodb/{repositories/team.test.ts => business/user/team/repository.test.ts} (72%) rename packages/dal/test/mongodb/{models/team.test.ts => business/user/team/schema.test.ts} (85%) rename packages/dal/test/mongodb/{repositories/tmpData.test.ts => business/user/verification/repository.test.ts} (91%) delete mode 100644 packages/dal/transaction/index.ts create mode 100644 packages/dal/vitest.integration.config.ts delete mode 100644 packages/service/common/dal/mongo/config.ts delete mode 100644 packages/service/common/dal/mongo/connection.ts create mode 100644 projects/app/AGENTS.md create mode 100644 projects/app/CLAUDE.md diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md index f8f86b0aa08b..da02d9c1ba1b 100644 --- a/.agents/design/dal-basic-architecture.md +++ b/.agents/design/dal-basic-architecture.md @@ -7,23 +7,81 @@ ## 分层 ```text -domain/ 数据库无关的实体、ID、输入输出 schema -db/ Mongo 与 SQL-like adapter 共用的物理表名 -ports/ Repository 接口(业务可依赖的最小能力) -mongodb/ Mongo Schema、ObjectId 转换、Repository 实现 -sql/ 后续增加 SQL 表结构、字段映射和 Repository 实现 -transaction/ 数据库无关的事务上下文和事务入口 +business/support/ 按业务域组织数据库无关 DTO、实体和 Repository 合同 +db/ Mongo 与 SQL-like adapter 共用的物理表名、错误、并发和事务抽象 +mongodb/ Mongo 通用工具、索引、生命周期能力,以及 business 下的 Mongo 业务实现 +sql/ 后续增加 SQL-like 的 business 实现,复用 business/support 中的合同 ``` -领域对象统一使用 `id: string`。MongoDB 的 `_id: ObjectId` 只在 `mongodb` 层出现,外键也在映射边界转换为字符串。 +对外 DTO 统一使用 `id: string`。MongoDB 的 `_id: ObjectId` 只在业务目录的 Mongo Entity/Repository 边界和 `mongodb/` 通用基础设施中出现,外键也在映射边界转换为字符串。 + +业务目录约定: + +```text +business/support/user/ + entity.ts # 数据库无关 User 实体和实体校验 schema + dto.ts # CreateUser、UpdateUser、UserCredentials + repository.ts # UserRepository 合同 + team/ + entity.ts # 数据库无关 TeamMemberDetail 实体 + dto.ts # 团队/成员创建 DTO + repository.ts # TeamRepository 合同 + group/ + entity.ts # 数据库无关 MemberGroup、GroupMember 实体 + dto.ts # 群组 DTO + repository.ts # GroupRepository 合同 + org/ + entity.ts # 数据库无关 Org、OrgMember 实体 + dto.ts # 组织 DTO + repository.ts # OrgRepository 合同 + verification/ + entity.ts # 数据库无关 TmpDataMaterial 实体 + dto.ts # ActiveTmpDataFilter + repository.ts # TmpDataRepository 合同 + +mongodb/business/support/user/ + entity.ts # Mongo 文档到共享实体的映射(toUser) + schema.ts # User Mongoose Schema 和 Model + repository.ts # MongoUserRepository + team/ + entity.ts # Mongo 文档到 TeamMemberDetail 的映射 + schema.ts # Team Mongoose Schema 和 Model + repository.ts # MongoTeamRepository + group/ + entity.ts # MemberGroup、GroupMember 的 Mongo 映射 + schema.ts # MemberGroup Mongoose Schema 和 Model + repository.ts # MongoGroupRepository + member/schema.ts # TeamMember Mongoose Schema 和 Model + org/ + entity.ts # Org、OrgMember 的 Mongo 映射 + schema.ts # Org Mongoose Schema 和 Model + repository.ts # MongoOrgRepository + verification/ + entity.ts # TmpData Mongo 文档映射 + schema.ts # TmpData Mongoose Schema 和 Model + repository.ts # MongoTmpDataRepository +``` + +`business/support/` 只承载数据库无关的实体、DTO 和 Repository 合同,不引入 Mongoose。 +`mongodb/` 只承载 Mongo 通用基础设施;业务专属的 Mongo Schema、字段映射和 Repository +放在 `mongodb/business/support/` 下。未来 SQL-like 数据库可以在自己的 adapter +目录中实现同一组 `business/support` 合同,不需要复制对外实体类型。 ## User 当前实现 -- `domain/user.ts`:`User`、`CreateUser`、`UpdateUser`、`UserCredentials` -- `ports/user.repository.ts`:ID/用户名/凭据查询、创建和更新合约 -- `mongodb/models/user.ts`:与生产 User 字段、默认值、密码处理和索引一致的 Mongo Schema -- `mongodb/mappers/user.ts`:显式转换 Mongo 文档,不泄漏 `_id`、`__v` 和 password -- `mongodb/repositories/user.ts`:通过注入 Model 实现 Repository,支持事务 context +- `business/support/user/entity.ts`:数据库无关 User 实体 +- `business/support/user/dto.ts`:`CreateUser`、`UpdateUser`、`UserCredentials` +- `business/support/user/repository.ts`:UserRepository 合同 +- `business/support/user/team/`:Team、TeamMember 实体、DTO 和 Repository 合同 +- `business/support/user/team/group/`:MemberGroup、GroupMember 实体、DTO 和 Repository 合同 +- `business/support/user/team/org/`:Org、OrgMember 实体、DTO 和 Repository 合同 +- `business/support/user/verification/`:TmpDataMaterial 实体、过滤 DTO 和 Repository 合同 +- `mongodb/business/support/user/entity.ts`:`toUser` 和 Mongo 默认值归一化 +- `mongodb/business/support/user/schema.ts`:与生产 User 字段、默认值、密码处理和索引一致的 Mongo Schema +- `mongodb/business/support/user/team/`:Team、TeamMember 的 Mongo Schema、映射和 Repository +- `mongodb/business/support/user/team/group/`:MemberGroup、GroupMember 的 Mongo Schema、映射和 Repository +- `mongodb/business/support/user/team/org/`:Org、OrgMember 的 Mongo Schema、映射和 Repository +- `mongodb/business/support/user/verification/`:TmpData Mongo Schema、映射和 Repository - `db/adapter.ts`:数据库无关的 Adapter 合约 - `mongodb/adapter.ts`:Mongo Adapter,从同一个 Mongoose client 组装 Repository 和事务执行器 @@ -32,7 +90,7 @@ transaction/ 数据库无关的事务上下文和事务入口 1. 上层不得依赖 Mongo Model、Mongoose Query 或 Mongo 查询对象。 2. `EntityId` 暂不限制为 24 位,以保留 UUID、SQL sequence 等后续实现空间。 3. 数据库字段命名、ID 类型、查询参数转换必须集中在 adapter/mapper 中。 -4. 新增数据库实现时复用 `ports` 和 `domain`,不复制业务实体定义。 +4. 新增数据库实现时复用 `business/support/` 中的实体、DTO 和 Repository 合同,不复制业务实体定义。 5. 密码只出现在创建、更新和凭据校验输入中,不属于默认返回的 `User` 实体。 6. Mongo adapter 收到非法 ObjectId 或非本 adapter 创建的事务 context 时必须失败,不能静默降级。 @@ -61,9 +119,9 @@ transaction/ 数据库无关的事务上下文和事务入口 ```text packages/service/common/dal/ - index.ts # 数据库无关出口:createRepositories() 按 DAL_DB_TYPE 组装 userRepository,导出 transactionRunner 及接口类型(DatabaseAdapter/UserRepository/TransactionRunner);后续实体增加 team.ts、app.ts + index.ts # 数据库无关出口:按 DAL_DB_TYPE 组装各业务 Repository,导出 transactionRunner 及接口类型 mongo/ - index.ts # Mongo composition root:createMongoDal(client?)、mongoDal、transactionRunner,唯一接触 MongoAdapter 具体实现的模块 + index.ts # Mongo composition root:createMongoAdapter(client?),唯一接触 MongoAdapter 具体实现的模块 connection.ts # MongooseConnection 类 + connection 单例(client/connect/disconnect) config.ts # 连接参数配置(池大小、超时等)+ 连接事件监听注册 registerMongooseListeners ``` diff --git a/.agents/design/dal-m10-model-lifecycle.md b/.agents/design/dal-m10-model-lifecycle.md index 7ed09bcf30e3..22da4c52e4e9 100644 --- a/.agents/design/dal-m10-model-lifecycle.md +++ b/.agents/design/dal-m10-model-lifecycle.md @@ -19,7 +19,7 @@ - 新增 `packages/dal/mongodb/logger.ts`:`setDalLogger` / `getDalLogger`。 service 侧 composition root(`common/dal/mongo/index.ts`)在模块加载时注入业务 logger(pino),避免 DAL 包反向依赖 service 日志实现;未注入时退回 console。 -- 新增 `packages/dal/mongodb/models/helper.ts` 的 `getDalModel`:DAL Model 统一注册 +- 新增 `packages/dal/mongodb/model.ts` 的 `getDalModel`:DAL Model 统一注册 入口,注册前挂慢查询中间件;User Model 重构为同一入口。 ## 索引管理 diff --git a/AGENTS.md b/AGENTS.md index 7c6dd8e150dc..6464c585d93e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ## 代码规范 - 所有代码编写、修改、重构和测试调整都必须遵守 [FastGPT 代码规范](./.agents/code/syntax.md)。开始改动前先查看相关规范;如果规范与当前实现习惯冲突,优先按规范执行,并只在有明确业务或兼容性理由时说明例外。 +- 修改 `packages/dal/**` 前,必须额外阅读并遵守 [DAL 局部开发规范](./packages/dal/AGENTS.override.md)。该文件补充 DAL 的业务分层、数据库实现边界、目录结构、Mongo Schema/Entity/Repository 职责和测试要求;与 DAL 相关的具体目录规则以该文件为准。 ### MongoDB Schema 与索引维护 diff --git a/packages/dal/AGENTS.override.md b/packages/dal/AGENTS.override.md new file mode 100644 index 000000000000..5d0ff6881166 --- /dev/null +++ b/packages/dal/AGENTS.override.md @@ -0,0 +1,172 @@ +# DAL 局部开发规范 + +本文件适用于 `packages/dal/**`。它补充根目录 `AGENTS.md` 和 +[FastGPT 代码规范](../../.agents/code/syntax.md),修改 DAL 前必须先阅读并遵守。 + +## 总体目标 + +DAL 按业务组织数据访问能力,同时隔离数据库实现: + +```text +packages/dal/ +├── business/ +│ └── support/ # 数据库无关的业务实体、DTO、Repository 合同 +├── mongodb/ +│ ├── business/ +│ │ └── support/ # MongoDB 业务实现 +│ └── ... # MongoDB 通用基础设施 +├── db/ # 数据库通用类型、错误、并发、事务和表名抽象 +└── redis/ # Redis adapter、cache 和 runtime +``` + +当前 `business/support` 目录承担 DAL 外层业务定义的职责。它与 +`mongodb/business/support` 的职责不同: + +- `business/support/` 定义 MongoDB、SQL-like 数据库都能复用的实体、DTO 和 Repository 合同。 +- `mongodb/business/support/` 实现 MongoDB 的 Schema、字段映射和 Repository。 +- 未来 SQL-like 实现应复用 `business/support`,在自己的 adapter/business 目录实现,不复制公共实体。 +- 真正的跨 Repository 业务编排、权限判断和业务规则属于 `packages/service`,不放入 DAL。 + +## 目录组织 + +必须以业务域作为主目录,不以 `entity/user`、`repository/user`、`model/user` 等技术类型作为一级分类。 + +User 垂直切片的标准结构: + +```text +business/support/user/ + entity.ts # 数据库无关 User 实体 + dto.ts # CreateUser、UpdateUser、UserCredentials 等输入结构 + repository.ts # UserRepository 合同 + team/ + entity.ts # 数据库无关 TeamMemberDetail 等实体 + dto.ts # 团队相关 DTO + repository.ts # TeamRepository 合同 + verification/ + entity.ts # 数据库无关 TmpDataMaterial 等实体 + dto.ts # 查询过滤 DTO + repository.ts # TmpDataRepository 合同 + +mongodb/business/support/user/ + entity.ts # Mongo 文档到公共实体的映射,例如 toUser + schema.ts # User Mongoose Schema、Model 和 SchemaType + repository.ts # MongoUserRepository + team/ + entity.ts # Mongo team member 到公共实体的映射 + schema.ts # Team Mongoose Schema、Model 和 SchemaType + repository.ts # MongoTeamRepository + group/ + schema.ts # MemberGroup Mongoose Schema 和 Model + member/ + schema.ts # TeamMember Mongoose Schema 和 Model + org/ + schema.ts # Org Mongoose Schema 和 Model + verification/ + schema.ts # TmpData Mongoose Schema 和 Model + repository.ts # MongoTmpDataRepository +``` + +`group`、`member`、`org` 是独立业务子目录。不要把它们重新合并成 +`group.ts`、`member.ts` 或 `org.ts`。 + +## 文件职责 + +### `business/support` 公共业务层 + +#### `entity.ts` + +- 定义对外稳定的实体结构、Zod schema 和 `z.infer` 类型。 +- 实体 ID 统一使用 `EntityId`/`string`,不能暴露 Mongo `ObjectId`。 +- 可以依赖 `@fastgpt/global`、`db/types` 和数据库无关的 transaction 类型。 +- 禁止引入 `mongoose`、Mongo Model、Mongo Query、`ClientSession`、集合名、`defineIndex` 或 Mongo 工具。 +- 不负责查询数据库,也不放 `findById`、`create` 等持久化方法。 + +#### `dto.ts` + +- 定义创建、更新、查询和凭据等输入 DTO。 +- 可复用 `entity.ts` 的公共 schema,避免重复定义同一结构。 +- DTO 不得包含 Mongo 专用字段和数据库驱动类型。 + +#### `repository.ts` + +- 只定义数据库无关的 Repository 合同和参数类型。 +- 方法参数、返回值和事务上下文必须使用公共实体、DTO、`EntityId`、`TransactionContext`。 +- 不得实现数据库查询,不得导入 Mongoose 或 Mongo adapter。 + +### `mongodb/business/support` Mongo 实现层 + +#### `schema.ts` + +- 定义 Mongoose `Schema`、Model 工厂、Mongoose SchemaType 和 Mongo 文档类型。 +- 所有 FastGPT 管理的索引必须使用 `defineIndex` 声明,禁止直接调用 `schema.index()`。 +- 字段默认值、密码 setter、引用关系、TTL、唯一约束等 Mongo 持久化细节放在这里。 +- Schema 文件可以依赖 `mongodb` 通用基础设施和 `db/tables`,但不能反向修改公共实体契约。 + +#### `entity.ts` + +- 负责 Mongo 文档与公共实体之间的字段映射和归一化。 +- `toUser`、`toTeamMemberDetail` 等函数必须保留在对应 `mongodb/business/support` 业务目录的 `entity.ts`。 +- 负责 `ObjectId -> string`、历史字段默认值、Mongo 元数据隐藏等边界转换。 +- 不定义 Mongoose Schema,不把公共实体改成 Mongo 文档类型。 + +#### `repository.ts` + +- 实现 `business/support` 中对应的 Repository 合同。 +- 可以使用 Mongoose Model、Query、ClientSession、Mongo 错误适配和 Mongo 事务工具。 +- 数据库错误必须经过 `DatabaseErrorAdapter`,事务必须使用公共 `TransactionContext` 映射到 Mongo session。 +- 查询、写入、投影、CAS 更新等持久化细节放在这里;返回结果必须经过 Mongo `entity.ts` 映射。 +- 不在 Repository 中编排跨业务服务、处理权限或决定 HTTP 错误。 + +#### `index.ts` + +- 作为目录的统一对外出口,只做实际实现的聚合导出。 +- 不创建旧路径兼容转发文件。 +- 对外导出 Mongo Schema、Model 工厂、映射函数和 Repository 实现时,必须从对应目录入口导出。 + +## 依赖方向 + +允许的依赖关系: + +```text +packages/service + │ + ▼ +business/support/ ◄── mongodb/business/support/ + ▲ │ + │ ▼ + └────────────── mongodb 通用基础设施 / db / transaction +``` + +具体要求: + +- `business/support` 不得依赖 `mongodb/business`。 +- `business/support` 不得依赖 `mongodb` 目录中的实现文件。 +- `mongodb/business/support` 可以依赖 `business/support` 合同和 `mongodb` 通用基础设施。 +- `packages/service` 优先依赖 `business/support` 合同或 `common/dal` 出口,不直接依赖 Mongo Model。 +- 新增 SQL-like adapter 时,复用 `business/support` 合同,不修改公共实体以适配某一种数据库。 + +## 命名和边界规则 + +- `entity.ts` 是公共实体或数据库边界映射;`schema.ts` 在 Mongo business 中专指 Mongoose Schema。 +- 不要把 Mongoose Schema 放到公共 `business/support` 的 `entity.ts`。 +- 不要把 `toUser`、`toTeamMemberDetail` 移入 Repository;映射职责属于 Mongo `entity.ts`。 +- 不要在公共层暴露 `_id`、`__v`、password 或其他 Mongo 文档元数据。 +- 不要在 DAL 中复制 `packages/service` 已有的业务 Schema;只定义 DAL 所需的最小实体和合同。 +- 使用 `type`,不使用 `interface`;Zod schema 应通过 `z.infer` 推导类型。 +- 目录需要对外导出时使用 `index.ts`,不要创建只为兼容旧路径的转发文件。 + +## Mongo Schema 与索引 + +- 新增或修改字段、索引、唯一约束、TTL、partial filter、collation 时,必须检查历史索引。 +- 明确由 FastGPT 创建且已废弃的历史索引,使用 `defineIndex(schema, { ..., deprecated: true })` 登记。 +- 不登记客户自建索引、来源不明的索引,也不能仅凭当前 Schema 未声明就推断其废弃。 +- DAL Mongo Model 必须通过 `getDalModel` 注册,以确保公共中间件和 Model 复用逻辑一致。 +- 数据库无关事务合同统一放在 `db/transaction.ts`,Mongo session 适配放在 `mongodb/transaction.ts`。 + +## 测试要求 + +- Mongo business 的测试放在 `packages/dal/test/mongodb/business/`,目录结构应尽量对应实现目录。 +- `schema` 测试覆盖默认值、字段行为和索引声明。 +- `entity` 测试覆盖文档映射、历史数据默认值、Mongo 元数据隐藏和非法值处理。 +- `repository` 测试覆盖查询条件、事务 session、错误适配、资源不存在和并发行为。 +- 修改 DAL 后至少运行 `pnpm --filter @fastgpt/dal typecheck` 和相关局部测试;不要在开发过程中用全量测试替代局部验证。 diff --git a/packages/dal/business/index.ts b/packages/dal/business/index.ts new file mode 100644 index 000000000000..c10cd5863017 --- /dev/null +++ b/packages/dal/business/index.ts @@ -0,0 +1 @@ +export * from './support/user'; diff --git a/packages/dal/business/support/user/dto.ts b/packages/dal/business/support/user/dto.ts new file mode 100644 index 000000000000..bceb49cb6794 --- /dev/null +++ b/packages/dal/business/support/user/dto.ts @@ -0,0 +1,30 @@ +import z from 'zod'; +import { EntityIdSchema } from '../../../db/types'; +import { UserSchema } from './entity'; + +export const CreateUserSchema = UserSchema.omit({ id: true, createTime: true }) + .partial() + .extend({ + username: z.string().min(1), + password: z.string().min(1) + }); +export type CreateUser = z.infer; + +export const UpdateUserSchema = CreateUserSchema.partial().omit({ username: true }); +export type UpdateUser = z.infer; + +export const UserCredentialsSchema = z.union([ + z + .object({ + username: z.string().min(1), + password: z.string().min(1) + }) + .strict(), + z + .object({ + id: EntityIdSchema, + password: z.string().min(1) + }) + .strict() +]); +export type UserCredentials = z.infer; diff --git a/packages/dal/domain/user.ts b/packages/dal/business/support/user/entity.ts similarity index 61% rename from packages/dal/domain/user.ts rename to packages/dal/business/support/user/entity.ts index 69f68c40d5f8..f09e09ff7466 100644 --- a/packages/dal/domain/user.ts +++ b/packages/dal/business/support/user/entity.ts @@ -3,7 +3,7 @@ import { LanguageSchema } from '@fastgpt/global/common/i18n/type'; import { FastGPT_SEM_Schema } from '@fastgpt/global/support/marketing/type'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { UserTagsSchema } from '@fastgpt/global/support/user/type'; -import { EntityIdSchema } from './types'; +import { EntityIdSchema } from '../../../db/types'; export const UserOpenaiAccountSchema = z.object({ key: z.string().optional(), @@ -14,6 +14,7 @@ export const UserMetaSchema = z.object({ isActivatedWecomLicense: z.boolean().optional() }); +/** 用户实体是跨数据库对外暴露的稳定形状,不包含 Mongo 文档元数据或密码。 */ export const UserSchema = z.object({ id: EntityIdSchema, status: z.enum(UserStatusEnum), @@ -34,30 +35,3 @@ export const UserSchema = z.object({ avatar: z.string().optional() }); export type User = z.infer; - -export const CreateUserSchema = UserSchema.omit({ id: true, createTime: true }) - .partial() - .extend({ - username: z.string().min(1), - password: z.string().min(1) - }); -export type CreateUser = z.infer; - -export const UpdateUserSchema = CreateUserSchema.partial().omit({ username: true }); -export type UpdateUser = z.infer; - -export const UserCredentialsSchema = z.union([ - z - .object({ - username: z.string().min(1), - password: z.string().min(1) - }) - .strict(), - z - .object({ - id: EntityIdSchema, - password: z.string().min(1) - }) - .strict() -]); -export type UserCredentials = z.infer; diff --git a/packages/dal/business/support/user/index.ts b/packages/dal/business/support/user/index.ts new file mode 100644 index 000000000000..f82ee3938b93 --- /dev/null +++ b/packages/dal/business/support/user/index.ts @@ -0,0 +1,5 @@ +export * from './dto'; +export * from './entity'; +export type { UserRepository } from './repository'; +export * from './team'; +export * from './verification'; diff --git a/packages/dal/ports/user.repository.ts b/packages/dal/business/support/user/repository.ts similarity index 74% rename from packages/dal/ports/user.repository.ts rename to packages/dal/business/support/user/repository.ts index 77a9ae7e6b2a..ad149f49aef3 100644 --- a/packages/dal/ports/user.repository.ts +++ b/packages/dal/business/support/user/repository.ts @@ -1,8 +1,9 @@ -import type { ExpectedState } from '../db'; -import type { CreateUser, UpdateUser, User, UserCredentials } from '../domain/user'; -import type { EntityId } from '../domain/types'; -import type { TransactionContext } from '../transaction'; import type { FastGPTSemType } from '@fastgpt/global/support/marketing/type'; +import type { ExpectedState } from '../../../db'; +import type { EntityId } from '../../../db/types'; +import type { TransactionContext } from '../../../db/transaction'; +import type { CreateUser, UpdateUser, UserCredentials } from './dto'; +import type { User } from './entity'; export type UserRepository = { findById(id: EntityId, context?: TransactionContext): Promise; @@ -23,10 +24,7 @@ export type UserRepository = { ): Promise; create(input: CreateUser, context?: TransactionContext): Promise; updateById(id: EntityId, patch: UpdateUser, context?: TransactionContext): Promise; - /** - * 按期望状态做原子条件更新:期望状态不匹配时抛 DatabaseConflictError(DB_CONFLICT), - * 文档不存在返回 null。 - */ + /** 仅当记录仍符合 expected 时更新,记录存在但状态不匹配时抛出 DB_CONFLICT。 */ updateByIdIfState( id: EntityId, expected: ExpectedState, diff --git a/packages/dal/business/support/user/team/dto.ts b/packages/dal/business/support/user/team/dto.ts new file mode 100644 index 000000000000..7b23a5a7929f --- /dev/null +++ b/packages/dal/business/support/user/team/dto.ts @@ -0,0 +1,51 @@ +import z from 'zod'; +import { + TeamMemberRoleEnum, + TeamMemberStatusEnum +} from '@fastgpt/global/support/user/team/constant'; +import { EntityIdSchema } from '../../../../db/types'; +import { TeamOpenaiAccountSchema } from './entity'; + +export const CreateTeamSchema = z.object({ + ownerId: EntityIdSchema, + name: z.string(), + avatar: z.string(), + createTime: z.date().optional() +}); +export type CreateTeam = z.infer; + +export const CreateTeamMemberSchema = z.object({ + teamId: EntityIdSchema, + userId: EntityIdSchema, + name: z.string(), + role: z.enum(TeamMemberRoleEnum), + status: z.enum(TeamMemberStatusEnum), + createTime: z.date().optional() +}); +export type CreateTeamMember = z.infer; + +export const UpdateTeamSchema = z.object({ + name: z.string().optional(), + avatar: z.string().optional(), + openaiAccount: TeamOpenaiAccountSchema.optional(), + clearOpenaiAccount: z.boolean().optional(), + externalWorkflowVariable: z + .object({ + key: z.string().min(1), + value: z.string() + }) + .optional() +}); +export type UpdateTeam = z.infer; + +export const UpdateTeamLimitSchema = z.object({ + lastExportDatasetTime: z.date().nullable().optional(), + lastWebsiteSyncTime: z.date().nullable().optional() +}); +export type UpdateTeamLimit = z.infer; + +export const TeamMemberQuerySchema = z.object({ + teamId: EntityIdSchema.optional(), + includeLeft: z.boolean().optional() +}); +export type TeamMemberQuery = z.infer; diff --git a/packages/dal/business/support/user/team/entity.ts b/packages/dal/business/support/user/team/entity.ts new file mode 100644 index 000000000000..7c6c9a374641 --- /dev/null +++ b/packages/dal/business/support/user/team/entity.ts @@ -0,0 +1,69 @@ +import z from 'zod'; +import { EntityIdSchema } from '../../../../db/types'; + +export const TeamOpenaiAccountSchema = z.object({ + key: z.string(), + baseUrl: z.string() +}); +export type TeamOpenaiAccount = z.infer; + +export const TeamSchema = z.object({ + id: EntityIdSchema, + name: z.string(), + ownerId: EntityIdSchema, + avatar: z.string().optional(), + createTime: z.date(), + balance: z.number().optional(), + limit: z + .object({ + lastExportDatasetTime: z.date().optional(), + lastWebsiteSyncTime: z.date().optional() + }) + .optional(), + openaiAccount: TeamOpenaiAccountSchema.optional(), + externalWorkflowVariables: z.record(z.string(), z.string()).optional(), + notificationAccount: z.string().optional(), + meta: z.record(z.string(), z.unknown()).optional(), + deleteTime: z.date().optional() +}); +export type Team = z.infer; + +export const TeamMemberSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + userId: EntityIdSchema, + avatar: z.string().optional(), + name: z.string(), + role: z.string().optional(), + status: z.string().optional(), + createTime: z.date().optional(), + updateTime: z.date().optional() +}); +export type TeamMember = z.infer; + +export const TeamMemberUserSummarySchema = z.object({ + id: EntityIdSchema, + username: z.string(), + contact: z.string().optional(), + timezone: z.string() +}); +export type TeamMemberUserSummary = z.infer; + +export const TeamMemberRelationsSchema = z.object({ + member: TeamMemberSchema, + team: TeamSchema, + user: TeamMemberUserSummarySchema.optional() +}); +export type TeamMemberRelations = z.infer; + +/** 默认团队和头像流程对外使用的最小团队成员实体。 */ +export const TeamMemberDetailSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + userId: EntityIdSchema, + avatar: z.string().optional(), + name: z.string(), + role: z.string().optional(), + status: z.string().optional() +}); +export type TeamMemberDetail = z.infer; diff --git a/packages/dal/business/support/user/team/group/dto.ts b/packages/dal/business/support/user/team/group/dto.ts new file mode 100644 index 000000000000..6d9c338a3acc --- /dev/null +++ b/packages/dal/business/support/user/team/group/dto.ts @@ -0,0 +1,9 @@ +import z from 'zod'; +import { EntityIdSchema } from '../../../../../db/types'; + +export const CreateMemberGroupSchema = z.object({ + teamId: EntityIdSchema, + name: z.string(), + avatar: z.string().optional() +}); +export type CreateMemberGroup = z.infer; diff --git a/packages/dal/business/support/user/team/group/entity.ts b/packages/dal/business/support/user/team/group/entity.ts new file mode 100644 index 000000000000..2bd5a1d20fdf --- /dev/null +++ b/packages/dal/business/support/user/team/group/entity.ts @@ -0,0 +1,20 @@ +import z from 'zod'; +import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; +import { EntityIdSchema } from '../../../../../db/types'; + +export const MemberGroupSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + name: z.string(), + avatar: z.string().optional(), + updateTime: z.date().optional() +}); +export type MemberGroup = z.infer; + +export const GroupMemberSchema = z.object({ + id: EntityIdSchema, + groupId: EntityIdSchema, + tmbId: EntityIdSchema, + role: z.enum(GroupMemberRole) +}); +export type GroupMember = z.infer; diff --git a/packages/dal/business/support/user/team/group/index.ts b/packages/dal/business/support/user/team/group/index.ts new file mode 100644 index 000000000000..e3069496476d --- /dev/null +++ b/packages/dal/business/support/user/team/group/index.ts @@ -0,0 +1,3 @@ +export * from './dto'; +export * from './entity'; +export type { GroupRepository, GroupReference } from './repository'; diff --git a/packages/dal/business/support/user/team/group/repository.ts b/packages/dal/business/support/user/team/group/repository.ts new file mode 100644 index 000000000000..ca8c4307c70e --- /dev/null +++ b/packages/dal/business/support/user/team/group/repository.ts @@ -0,0 +1,43 @@ +import type { EntityId } from '../../../../../db/types'; +import type { TransactionContext } from '../../../../../db/transaction'; +import type { CreateMemberGroup } from './dto'; +import type { GroupMember, MemberGroup } from './entity'; + +export type GroupReference = { id: EntityId; teamId: EntityId }; + +/** Group 业务相关的数据库无关访问合同。 */ +export type GroupRepository = { + findMemberGroupReferencesByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise; + createMemberGroup(input: CreateMemberGroup, context?: TransactionContext): Promise; + findMemberGroupsByIds(ids: EntityId[], context?: TransactionContext): Promise; + findGroupsByTmbId( + teamId: EntityId, + tmbId: EntityId, + roles?: string[], + context?: TransactionContext + ): Promise; + findGroupMember( + groupId: EntityId, + tmbId: EntityId, + context?: TransactionContext + ): Promise; + findGroupMembersByGroupId( + groupId: EntityId, + context?: TransactionContext + ): Promise; + findMemberGroupByTeamAndName( + teamId: EntityId, + name: string, + context?: TransactionContext + ): Promise; + updateMemberGroupAvatar( + teamId: EntityId, + name: string, + avatar: string, + context?: TransactionContext + ): Promise; + deleteMemberGroupsByTeamId(teamId: EntityId, context?: TransactionContext): Promise; +}; diff --git a/packages/dal/business/support/user/team/index.ts b/packages/dal/business/support/user/team/index.ts new file mode 100644 index 000000000000..4020a991015a --- /dev/null +++ b/packages/dal/business/support/user/team/index.ts @@ -0,0 +1,5 @@ +export * from './dto'; +export * from './entity'; +export type { CreateDefaultTeamParams, TeamRepository } from './repository'; +export * from './group'; +export * from './org'; diff --git a/packages/dal/business/support/user/team/org/dto.ts b/packages/dal/business/support/user/team/org/dto.ts new file mode 100644 index 000000000000..c17053a237e5 --- /dev/null +++ b/packages/dal/business/support/user/team/org/dto.ts @@ -0,0 +1,9 @@ +import z from 'zod'; +import { EntityIdSchema } from '../../../../../db/types'; + +export const CreateOrgSchema = z.object({ + teamId: EntityIdSchema, + name: z.string(), + path: z.string() +}); +export type CreateOrg = z.infer; diff --git a/packages/dal/business/support/user/team/org/entity.ts b/packages/dal/business/support/user/team/org/entity.ts new file mode 100644 index 000000000000..d1fe3992acf7 --- /dev/null +++ b/packages/dal/business/support/user/team/org/entity.ts @@ -0,0 +1,22 @@ +import z from 'zod'; +import { EntityIdSchema } from '../../../../../db/types'; + +export const OrgSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + pathId: z.string(), + path: z.string(), + name: z.string(), + avatar: z.string().optional(), + description: z.string().optional(), + updateTime: z.date().optional() +}); +export type Org = z.infer; + +export const OrgMemberSchema = z.object({ + id: EntityIdSchema, + teamId: EntityIdSchema, + orgId: EntityIdSchema, + tmbId: EntityIdSchema +}); +export type OrgMember = z.infer; diff --git a/packages/dal/business/support/user/team/org/index.ts b/packages/dal/business/support/user/team/org/index.ts new file mode 100644 index 000000000000..6ad77d056e0e --- /dev/null +++ b/packages/dal/business/support/user/team/org/index.ts @@ -0,0 +1,3 @@ +export * from './dto'; +export * from './entity'; +export type { OrgRepository, OrgReference } from './repository'; diff --git a/packages/dal/business/support/user/team/org/repository.ts b/packages/dal/business/support/user/team/org/repository.ts new file mode 100644 index 000000000000..1db9b8bd5cb3 --- /dev/null +++ b/packages/dal/business/support/user/team/org/repository.ts @@ -0,0 +1,31 @@ +import type { EntityId } from '../../../../../db/types'; +import type { TransactionContext } from '../../../../../db/transaction'; +import type { CreateOrg } from './dto'; +import type { Org, OrgMember } from './entity'; + +export type OrgReference = { id: EntityId; teamId: EntityId }; + +/** Org 业务相关的数据库无关访问合同。 */ +export type OrgRepository = { + findOrgReferencesByIds(ids: EntityId[], context?: TransactionContext): Promise; + createOrg(input: CreateOrg, context?: TransactionContext): Promise; + findOrgsByIds(ids: EntityId[], context?: TransactionContext): Promise; + findOrgsByTeamId(teamId: EntityId, context?: TransactionContext): Promise; + findOrgsByTeamAndPathIds( + teamId: EntityId, + pathIds: string[], + context?: TransactionContext + ): Promise; + findOrgById(orgId: EntityId, teamId: EntityId, context?: TransactionContext): Promise; + findOrgChildren( + org: Pick, + teamId: EntityId, + context?: TransactionContext + ): Promise; + findOrgMembersByTmbId( + teamId: EntityId, + tmbId: EntityId, + context?: TransactionContext + ): Promise; + deleteOrgsByTeamId(teamId: EntityId, context?: TransactionContext): Promise; +}; diff --git a/packages/dal/business/support/user/team/repository.ts b/packages/dal/business/support/user/team/repository.ts new file mode 100644 index 000000000000..ebce2b30910d --- /dev/null +++ b/packages/dal/business/support/user/team/repository.ts @@ -0,0 +1,97 @@ +import type { EntityId } from '../../../../db/types'; +import type { TransactionContext } from '../../../../db/transaction'; +import type { + CreateTeam, + CreateTeamMember, + TeamMemberQuery, + UpdateTeam, + UpdateTeamLimit +} from './dto'; +import type { Team, TeamMember, TeamMemberDetail, TeamMemberRelations } from './entity'; + +export type CreateDefaultTeamParams = { + userId: EntityId; + teamName?: string; + avatar?: string; + context?: TransactionContext; +}; + +export type TeamReference = { id: EntityId }; +export type TeamScopedReference = TeamReference & { teamId: EntityId }; + +/** Team 业务相关的数据库无关访问合同。 */ +export type TeamRepository = { + findMemberById(id: EntityId, context?: TransactionContext): Promise; + findMemberByTeamAndUser( + teamId: EntityId, + userId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise; + findMemberByIdInTeam( + id: EntityId, + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise; + findMemberRelationsById( + id: EntityId, + context?: TransactionContext + ): Promise; + findMemberRelationsByUserId( + userId: EntityId, + context?: TransactionContext + ): Promise; + findMemberRelationsByTeamId( + teamId: EntityId, + context?: TransactionContext + ): Promise; + findOwnerByTeamId(teamId: EntityId, context?: TransactionContext): Promise; + findMembersByIds( + ids: EntityId[], + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise; + findMembersByTeamId( + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise; + countMembersByTeamId( + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise; + findTeamById(teamId: EntityId, context?: TransactionContext): Promise; + findTeamsByIds(teamIds: EntityId[], context?: TransactionContext): Promise; + findAllTeamIds(context?: TransactionContext): Promise; + findTeamReferencesByIds( + teamIds: EntityId[], + context?: TransactionContext + ): Promise; + findMemberReferencesByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise; + createTeam(input: CreateTeam, context?: TransactionContext): Promise; + createTeamMember(input: CreateTeamMember, context?: TransactionContext): Promise; + updateMemberAvatar( + id: EntityId, + avatar: string, + context?: TransactionContext + ): Promise; + updateTeam( + teamId: EntityId, + patch: UpdateTeam, + context?: TransactionContext + ): Promise; + updateTeamLimit( + teamId: EntityId, + patch: UpdateTeamLimit, + context?: TransactionContext + ): Promise; + deleteMembersByTeamId(teamId: EntityId, context?: TransactionContext): Promise; + clearTeamSensitiveData(teamId: EntityId, context?: TransactionContext): Promise; + /** 已存在默认成员时返回 null,创建过程中的四张表共享同一事务上下文。 */ + createDefaultTeam(params: CreateDefaultTeamParams): Promise; +}; diff --git a/packages/dal/domain/tmpData.ts b/packages/dal/business/support/user/verification/dto.ts similarity index 53% rename from packages/dal/domain/tmpData.ts rename to packages/dal/business/support/user/verification/dto.ts index fe6d349cdb72..c9077995e465 100644 --- a/packages/dal/domain/tmpData.ts +++ b/packages/dal/business/support/user/verification/dto.ts @@ -1,13 +1,5 @@ import z from 'zod'; -/** 身份验证材料在 DAL 侧的最小形状,字段语义与旧 tmp_datas 集合一致。 */ -export const TmpDataMaterialSchema = z.object({ - dataId: z.string().min(1), - data: z.unknown(), - expireAt: z.date() -}); -export type TmpDataMaterial = z.infer; - /** 查询有效材料的最小过滤条件;match 按材料字段直接映射到 `data.`。 */ export const ActiveTmpDataFilterSchema = z.object({ dataId: z.string().min(1), diff --git a/packages/dal/business/support/user/verification/entity.ts b/packages/dal/business/support/user/verification/entity.ts new file mode 100644 index 000000000000..6be307e5520c --- /dev/null +++ b/packages/dal/business/support/user/verification/entity.ts @@ -0,0 +1,11 @@ +import z from 'zod'; + +/** 身份验证材料在 DAL 侧的最小实体形状,字段语义与旧 tmp_datas 集合一致。 */ +export const TmpDataMaterialSchema = z.object({ + dataId: z.string().min(1), + data: z.unknown(), + expireAt: z.date() +}); +export type TmpDataMaterial = z.infer; + +export type TmpDataWrite = Pick; diff --git a/packages/dal/business/support/user/verification/index.ts b/packages/dal/business/support/user/verification/index.ts new file mode 100644 index 000000000000..4b99b646e255 --- /dev/null +++ b/packages/dal/business/support/user/verification/index.ts @@ -0,0 +1,3 @@ +export * from './dto'; +export * from './entity'; +export type { TmpDataRepository } from './repository'; diff --git a/packages/dal/business/support/user/verification/repository.ts b/packages/dal/business/support/user/verification/repository.ts new file mode 100644 index 000000000000..2dae165d282b --- /dev/null +++ b/packages/dal/business/support/user/verification/repository.ts @@ -0,0 +1,17 @@ +import type { TransactionContext } from '../../../../db/transaction'; +import type { ActiveTmpDataFilter } from './dto'; +import type { TmpDataMaterial, TmpDataWrite } from './entity'; + +/** 验证码材料的最小 DAL 能力,供账户验证流程在事务内读取与消费。 */ +export type TmpDataRepository = { + findByDataId(dataId: string, context?: TransactionContext): Promise; + findActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise; + createIfInactive(input: TmpDataWrite, context?: TransactionContext): Promise; + upsert(input: TmpDataWrite, context?: TransactionContext): Promise; + updateIfActive(input: TmpDataWrite, context?: TransactionContext): Promise; + findActiveDataIds(dataIds: readonly string[], context?: TransactionContext): Promise; + deleteActiveMaterial(filter: ActiveTmpDataFilter, context?: TransactionContext): Promise; +}; diff --git a/packages/dal/db/adapter.ts b/packages/dal/db/adapter.ts index 9e82de03f4bd..53dc4405563f 100644 --- a/packages/dal/db/adapter.ts +++ b/packages/dal/db/adapter.ts @@ -1,10 +1,18 @@ -import type { TeamRepository, TmpDataRepository, UserRepository } from '../ports'; -import type { TransactionRunner } from '../transaction'; +import type { + GroupRepository, + OrgRepository, + TeamRepository, + TmpDataRepository, + UserRepository +} from '../business/support/user'; +import type { TransactionRunner } from './transaction'; import type { DatabaseErrorAdapter } from './error-adapter'; export type DatabaseAdapter = { userRepository: UserRepository; teamRepository: TeamRepository; + groupRepository: GroupRepository; + orgRepository: OrgRepository; tmpDataRepository: TmpDataRepository; transactionRunner: TransactionRunner; errorAdapter: DatabaseErrorAdapter; diff --git a/packages/dal/db/concurrency.ts b/packages/dal/db/concurrency.ts index 7e90852db789..02b9efd1e792 100644 --- a/packages/dal/db/concurrency.ts +++ b/packages/dal/db/concurrency.ts @@ -1,5 +1,5 @@ -import type { EntityId } from '../domain'; -import type { TransactionContext } from '../transaction'; +import type { EntityId } from './types'; +import type { TransactionContext } from './transaction'; /** 期望状态:并发更新(CAS)时用于匹配的当前值子集,字段为领域字段。 */ export type ExpectedState = Partial; diff --git a/packages/dal/db/index.ts b/packages/dal/db/index.ts index be3fab7afd2d..b6b9b882b250 100644 --- a/packages/dal/db/index.ts +++ b/packages/dal/db/index.ts @@ -1,5 +1,7 @@ export { tables } from './tables'; export type { TableName } from './tables'; +export { EntityIdSchema } from './types'; +export type { EntityId } from './types'; export type { DatabaseAdapter } from './adapter'; export type { DatabaseErrorAdapter } from './error-adapter'; export { @@ -16,3 +18,4 @@ export type { DatabaseErrorCode } from './errors'; export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, normalizePageParams } from './pagination'; export type { NormalizedPageParams, PageParams, PageResult } from './pagination'; export type { CasUpdate, ExpectedState } from './concurrency'; +export type { TransactionContext, TransactionRunner } from './transaction'; diff --git a/packages/dal/db/tables.ts b/packages/dal/db/tables.ts index 9f4b78137fcf..5aac2df47bc0 100644 --- a/packages/dal/db/tables.ts +++ b/packages/dal/db/tables.ts @@ -4,7 +4,9 @@ export const tables = { team: 'teams', teamMember: 'team_members', teamMemberGroup: 'team_member_groups', + teamGroupMember: 'team_group_members', teamOrg: 'team_orgs', + teamOrgMember: 'team_org_members', tmpData: 'tmp_datas', teamDiscountCoupon: 'team_discount_coupons', teamEnterpriseAuth: 'team_enterprise_auths', diff --git a/packages/dal/transaction/types.ts b/packages/dal/db/transaction.ts similarity index 100% rename from packages/dal/transaction/types.ts rename to packages/dal/db/transaction.ts diff --git a/packages/dal/domain/types.ts b/packages/dal/db/types.ts similarity index 51% rename from packages/dal/domain/types.ts rename to packages/dal/db/types.ts index a7805cdfa942..56c43ec999c0 100644 --- a/packages/dal/domain/types.ts +++ b/packages/dal/db/types.ts @@ -1,4 +1,5 @@ import z from 'zod'; +/** 数据库无关的主键类型,允许 Mongo ObjectId、UUID 和 SQL sequence 在边界统一为字符串。 */ export const EntityIdSchema = z.string().min(1); export type EntityId = z.infer; diff --git a/packages/dal/domain/index.ts b/packages/dal/domain/index.ts deleted file mode 100644 index e46ddf19b9dc..000000000000 --- a/packages/dal/domain/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { EntityIdSchema } from './types'; -export type { EntityId } from './types'; -export { - CreateUserSchema, - UpdateUserSchema, - UserCredentialsSchema, - UserMetaSchema, - UserOpenaiAccountSchema, - UserSchema -} from './user'; -export type { CreateUser, UpdateUser, User, UserCredentials } from './user'; -export { - CreateMemberGroupSchema, - CreateOrgSchema, - CreateTeamMemberSchema, - CreateTeamSchema, - TeamMemberDetailSchema -} from './team'; -export type { - CreateMemberGroup, - CreateOrg, - CreateTeam, - CreateTeamMember, - TeamMemberDetail -} from './team'; -export { ActiveTmpDataFilterSchema, TmpDataMaterialSchema } from './tmpData'; -export type { ActiveTmpDataFilter, TmpDataMaterial } from './tmpData'; diff --git a/packages/dal/domain/team.ts b/packages/dal/domain/team.ts deleted file mode 100644 index bfdb8790a8b5..000000000000 --- a/packages/dal/domain/team.ts +++ /dev/null @@ -1,50 +0,0 @@ -import z from 'zod'; -import { - TeamMemberRoleEnum, - TeamMemberStatusEnum -} from '@fastgpt/global/support/user/team/constant'; -import { EntityIdSchema } from './types'; - -/** 默认团队/头像更新的最小 tmb 形状,只暴露当前三个流程需要的字段。 */ -export const TeamMemberDetailSchema = z.object({ - id: EntityIdSchema, - teamId: EntityIdSchema, - userId: EntityIdSchema, - avatar: z.string().optional(), - name: z.string(), - role: z.string().optional(), - status: z.string().optional() -}); -export type TeamMemberDetail = z.infer; - -export const CreateTeamSchema = z.object({ - ownerId: EntityIdSchema, - name: z.string(), - avatar: z.string(), - createTime: z.date().optional() -}); -export type CreateTeam = z.infer; - -export const CreateTeamMemberSchema = z.object({ - teamId: EntityIdSchema, - userId: EntityIdSchema, - name: z.string(), - role: z.enum(TeamMemberRoleEnum), - status: z.enum(TeamMemberStatusEnum), - createTime: z.date().optional() -}); -export type CreateTeamMember = z.infer; - -export const CreateMemberGroupSchema = z.object({ - teamId: EntityIdSchema, - name: z.string(), - avatar: z.string() -}); -export type CreateMemberGroup = z.infer; - -export const CreateOrgSchema = z.object({ - teamId: EntityIdSchema, - name: z.string(), - path: z.string() -}); -export type CreateOrg = z.infer; diff --git a/packages/dal/index.ts b/packages/dal/index.ts index c38ebb74bc7d..f7c359ee18c0 100644 --- a/packages/dal/index.ts +++ b/packages/dal/index.ts @@ -1,5 +1,3 @@ -export * from './domain'; +export * from './business'; export * from './db'; -export type * from './ports'; export * from './mongodb'; -export type * from './transaction'; diff --git a/packages/dal/mongodb/adapter.ts b/packages/dal/mongodb/adapter.ts index 9769eaca87f0..e97e41abcb1b 100644 --- a/packages/dal/mongodb/adapter.ts +++ b/packages/dal/mongodb/adapter.ts @@ -2,9 +2,11 @@ import mongoose, { type Mongoose } from 'mongoose'; import type { DatabaseAdapter } from '../db'; import type { DatabaseErrorAdapter } from '../db'; import { MongoErrorAdapter } from './errors'; -import { createMongoUserRepository } from './repositories/user'; -import { createMongoTeamRepository } from './repositories/team'; -import { createMongoTmpDataRepository } from './repositories/tmpData'; +import { createMongoUserRepository } from './business/support/user/repository'; +import { createMongoTeamRepository } from './business/support/user/team/repository'; +import { createMongoGroupRepository } from './business/support/user/team/group/repository'; +import { createMongoOrgRepository } from './business/support/user/team/org/repository'; +import { createMongoTmpDataRepository } from './business/support/user/verification/repository'; import { MongoTransactionRunner } from './transaction'; export type MongoAdapterDependencies = { @@ -16,6 +18,8 @@ export type MongoAdapterDependencies = { export class MongoAdapter implements DatabaseAdapter { readonly userRepository; readonly teamRepository; + readonly groupRepository; + readonly orgRepository; readonly tmpDataRepository; readonly transactionRunner; readonly errorAdapter; @@ -26,7 +30,12 @@ export class MongoAdapter implements DatabaseAdapter { }: MongoAdapterDependencies = {}) { this.errorAdapter = errorAdapter; this.userRepository = createMongoUserRepository(client, errorAdapter); - this.teamRepository = createMongoTeamRepository(client, errorAdapter); + this.groupRepository = createMongoGroupRepository(client, errorAdapter); + this.orgRepository = createMongoOrgRepository(client, errorAdapter); + this.teamRepository = createMongoTeamRepository(client, errorAdapter, { + groupRepository: this.groupRepository, + orgRepository: this.orgRepository + }); this.tmpDataRepository = createMongoTmpDataRepository(client, errorAdapter); this.transactionRunner = new MongoTransactionRunner(client, errorAdapter); } diff --git a/packages/dal/mongodb/business/index.ts b/packages/dal/mongodb/business/index.ts new file mode 100644 index 000000000000..c10cd5863017 --- /dev/null +++ b/packages/dal/mongodb/business/index.ts @@ -0,0 +1 @@ +export * from './support/user'; diff --git a/packages/dal/mongodb/mappers/user.ts b/packages/dal/mongodb/business/support/user/entity.ts similarity index 78% rename from packages/dal/mongodb/mappers/user.ts rename to packages/dal/mongodb/business/support/user/entity.ts index 4304fdfca727..2e8fbf28424e 100644 --- a/packages/dal/mongodb/mappers/user.ts +++ b/packages/dal/mongodb/business/support/user/entity.ts @@ -1,11 +1,12 @@ import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import type { UserTagsType } from '@fastgpt/global/support/user/type'; -import { UserSchema, type User } from '../../domain/user'; -import type { UserDocument } from '../models/user'; -import { toEntityId } from '../utils'; +import type { User } from '../../../../business/support/user/entity'; +import { UserSchema } from '../../../../business/support/user/entity'; +import type { UserDocument } from './schema'; +import { toEntityId } from '../../../utils'; -/** toUser 与旧 ClientSession 兼容分支共用的用户字段默认值,避免两处默认值漂移。 */ +/** Mongo 文档映射为共享 User 实体时使用的生产默认值。 */ export const userDefaultFieldValues: { timezone: string; promotionRate: number; @@ -18,6 +19,7 @@ export const userDefaultFieldValues: { tags: [] }; +/** 将 Mongo 用户文档转换为数据库无关的 User 实体,并隐藏 Mongo 元数据和密码。 */ export const toUser = (document: UserDocument): User => UserSchema.parse({ id: toEntityId(document._id), diff --git a/packages/dal/mongodb/business/support/user/index.ts b/packages/dal/mongodb/business/support/user/index.ts new file mode 100644 index 000000000000..9e895d490905 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/index.ts @@ -0,0 +1,10 @@ +export { toUser, userDefaultFieldValues } from './entity'; +export { + UserDocumentSchema, + getUserModel, + type UserDocument, + type UserMongooseSchemaType +} from './schema'; +export { MongoUserRepository, createMongoUserRepository } from './repository'; +export * from './team'; +export * from './verification'; diff --git a/packages/dal/mongodb/repositories/user.ts b/packages/dal/mongodb/business/support/user/repository.ts similarity index 80% rename from packages/dal/mongodb/repositories/user.ts rename to packages/dal/mongodb/business/support/user/repository.ts index 10109c346c4f..fbca6173f703 100644 --- a/packages/dal/mongodb/repositories/user.ts +++ b/packages/dal/mongodb/business/support/user/repository.ts @@ -1,16 +1,22 @@ import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; -import { DatabaseConflictError, type DatabaseErrorAdapter } from '../../db'; -import type { CreateUser, UpdateUser, User, UserCredentials } from '../../domain/user'; -import type { EntityId } from '../../domain/types'; -import type { UserRepository } from '../../ports/user.repository'; -import type { TransactionContext } from '../../transaction'; -import { toUser } from '../mappers/user'; -import { MongoErrorAdapter } from '../errors'; -import { getUserModel, type UserDocument, type UserMongooseSchemaType } from '../models/user'; -import { casUpdateById } from '../concurrency'; -import { getMongoSession } from '../transaction'; -import { toEntityId, toMongoObjectId } from '../utils'; import { FastGPT_SEM_Schema, type FastGPTSemType } from '@fastgpt/global/support/marketing/type'; +import { DatabaseConflictError, type DatabaseErrorAdapter } from '../../../../db'; +import type { ExpectedState } from '../../../../db'; +import type { EntityId } from '../../../../db/types'; +import type { + CreateUser, + UpdateUser, + UserCredentials +} from '../../../../business/support/user/dto'; +import type { User } from '../../../../business/support/user/entity'; +import type { UserRepository } from '../../../../business/support/user/repository'; +import type { TransactionContext } from '../../../../db/transaction'; +import { MongoErrorAdapter } from '../../../errors'; +import { casUpdateById } from '../../../concurrency'; +import { getMongoSession } from '../../../transaction'; +import { toEntityId, toMongoObjectId } from '../../../utils'; +import { getUserModel, type UserDocument, type UserMongooseSchemaType } from './schema'; +import { toUser } from './entity'; export class MongoUserRepository implements UserRepository { constructor( @@ -77,15 +83,17 @@ export class MongoUserRepository implements UserRepository { context?: TransactionContext ): Promise { return this.execute(async () => { + // 密码由 API 边界提前哈希,不能放入带有密码写入转换的查询条件中; + // 先按身份读取隐藏字段,再与已存哈希直接比较,避免查询阶段重复转换。 + const filter = + 'id' in credentials + ? { _id: toMongoObjectId(credentials.id) } + : { username: credentials.username }; const document = await this.withSession( - this.model.findOne( - 'id' in credentials - ? { _id: toMongoObjectId(credentials.id), password: credentials.password } - : { username: credentials.username, password: credentials.password } - ), + this.model.findOne(filter).select('+password'), context ).lean(); - return document ? toUser(document) : null; + return document && document.password === credentials.password ? toUser(document) : null; }); } @@ -134,7 +142,7 @@ export class MongoUserRepository implements UserRepository { async updateByIdIfState( id: EntityId, - expected: Partial, + expected: ExpectedState, patch: UpdateUser, context?: TransactionContext ): Promise { diff --git a/packages/dal/mongodb/models/user.ts b/packages/dal/mongodb/business/support/user/schema.ts similarity index 86% rename from packages/dal/mongodb/models/user.ts rename to packages/dal/mongodb/business/support/user/schema.ts index fa7ea77a28f2..a197e7c3cf51 100644 --- a/packages/dal/mongodb/models/user.ts +++ b/packages/dal/mongodb/business/support/user/schema.ts @@ -1,12 +1,11 @@ -import { hashStr } from '@fastgpt/global/common/string/tools'; import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { UserStatusEnum, userStatusMap } from '@fastgpt/global/support/user/constant'; import { UserTagsSchema } from '@fastgpt/global/support/user/type'; import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../db'; +import { defineIndex } from '../../../indexes'; +import type { WithId__v } from '../../../types'; +import { getDalModel } from '../../../model'; export const UserDocumentSchema = new Schema({ status: { @@ -21,7 +20,7 @@ export const UserDocumentSchema = new Schema({ password: { type: String, required: true, - set: (value: string) => hashStr(value), + // API 边界已经完成哈希,DAL 按不透明凭据原样保存,避免重复哈希。 select: false }, passwordUpdateTime: Date, diff --git a/packages/dal/mongodb/business/support/user/team/entity.ts b/packages/dal/mongodb/business/support/user/team/entity.ts new file mode 100644 index 000000000000..d52a54ccf095 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/entity.ts @@ -0,0 +1,67 @@ +import { + TeamMemberRelationsSchema, + TeamMemberSchema, + TeamMemberDetailSchema, + TeamOpenaiAccountSchema, + TeamSchema, + type Team, + type TeamMember, + type TeamMemberDetail, + type TeamMemberRelations +} from '../../../../../business/support/user/team/entity'; +import type { UserDocument } from '../schema'; +import type { TeamDocument } from './schema'; +import type { TeamMemberDocument } from './member/schema'; +import { toEntityId } from '../../../../utils'; + +type TeamMemberRelationsDocument = TeamMemberDocument & { + team?: TeamDocument; + user?: UserDocument; +}; + +export const toTeam = (document: TeamDocument): Team => + TeamSchema.parse({ + id: toEntityId(document._id), + name: document.name, + ownerId: toEntityId(document.ownerId), + avatar: document.avatar ?? undefined, + createTime: document.createTime, + balance: document.balance ?? undefined, + limit: document.limit ?? undefined, + openaiAccount: TeamOpenaiAccountSchema.safeParse(document.openaiAccount).data, + externalWorkflowVariables: document.externalWorkflowVariables ?? undefined, + notificationAccount: document.notificationAccount ?? undefined, + meta: document.meta ?? undefined, + deleteTime: document.deleteTime ?? undefined + }); + +export const toTeamMember = (document: TeamMemberDocument): TeamMember => + TeamMemberSchema.parse({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId), + userId: toEntityId(document.userId), + avatar: document.avatar ?? undefined, + name: document.name, + role: document.role ?? undefined, + status: document.status ?? undefined, + createTime: document.createTime ?? undefined, + updateTime: document.updateTime ?? undefined + }); + +export const toTeamMemberRelations = (document: TeamMemberRelationsDocument): TeamMemberRelations => + TeamMemberRelationsSchema.parse({ + member: toTeamMember(document), + team: document.team ? toTeam(document.team) : undefined, + user: document.user + ? { + id: toEntityId(document.user._id), + username: document.user.username, + contact: document.user.contact ?? undefined, + timezone: document.user.timezone ?? 'Asia/Shanghai' + } + : undefined + }); + +/** 将 Mongo team member 文档转换为共享 TeamMemberDetail 实体。 */ +export const toTeamMemberDetail = (document: TeamMemberDocument): TeamMemberDetail => + TeamMemberDetailSchema.parse(toTeamMember(document)); diff --git a/packages/dal/mongodb/business/support/user/team/group/entity.ts b/packages/dal/mongodb/business/support/user/team/group/entity.ts new file mode 100644 index 000000000000..bc6959d789b8 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/entity.ts @@ -0,0 +1,26 @@ +import { + GroupMemberSchema, + MemberGroupSchema, + type GroupMember, + type MemberGroup +} from '../../../../../../business/support/user/team/group'; +import type { GroupMemberDocument } from './member/schema'; +import type { MemberGroupDocument } from './schema'; +import { toEntityId } from '../../../../../utils'; + +export const toMemberGroup = (document: MemberGroupDocument): MemberGroup => + MemberGroupSchema.parse({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId), + name: document.name, + avatar: document.avatar ?? undefined, + updateTime: document.updateTime ?? undefined + }); + +export const toGroupMember = (document: GroupMemberDocument): GroupMember => + GroupMemberSchema.parse({ + id: toEntityId(document._id), + groupId: toEntityId(document.groupId), + tmbId: toEntityId(document.tmbId), + role: document.role + }); diff --git a/packages/dal/mongodb/business/support/user/team/group/index.ts b/packages/dal/mongodb/business/support/user/team/group/index.ts new file mode 100644 index 000000000000..438fa05a8c60 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/index.ts @@ -0,0 +1,9 @@ +export { + MemberGroupDocumentSchema, + getMemberGroupModel, + type MemberGroupDocument, + type MemberGroupMongooseSchemaType +} from './schema'; +export * from './member'; +export { toGroupMember, toMemberGroup } from './entity'; +export { MongoGroupRepository, createMongoGroupRepository } from './repository'; diff --git a/packages/dal/mongodb/business/support/user/team/group/member/index.ts b/packages/dal/mongodb/business/support/user/team/group/member/index.ts new file mode 100644 index 000000000000..9f4bfa715043 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/member/index.ts @@ -0,0 +1,6 @@ +export { + GroupMemberDocumentSchema, + getGroupMemberModel, + type GroupMemberDocument, + type GroupMemberMongooseSchemaType +} from './schema'; diff --git a/packages/dal/mongodb/business/support/user/team/group/member/schema.ts b/packages/dal/mongodb/business/support/user/team/group/member/schema.ts new file mode 100644 index 000000000000..5b40d52043e3 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/member/schema.ts @@ -0,0 +1,48 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; +import { tables } from '../../../../../../../db'; +import { defineIndex } from '../../../../../../indexes'; +import type { WithId__v } from '../../../../../../types'; +import { getDalModel } from '../../../../../../model'; + +export const GroupMemberDocumentSchema = new Schema({ + groupId: { + type: Schema.Types.ObjectId, + ref: 'MemberGroup', + required: true + }, + tmbId: { + type: Schema.Types.ObjectId, + ref: 'TeamMember', + required: true + }, + role: { + type: String, + enum: Object.values(GroupMemberRole), + required: true, + default: GroupMemberRole.member + } +}); + +GroupMemberDocumentSchema.virtual('group', { + ref: 'MemberGroup', + localField: 'groupId', + foreignField: '_id', + justOne: true +}); + +defineIndex(GroupMemberDocumentSchema, { key: { groupId: 1 } }); +defineIndex(GroupMemberDocumentSchema, { key: { tmbId: 1 } }); + +export type GroupMemberMongooseSchemaType = InferSchemaType; +export type GroupMemberDocument = WithId__v; + +export const getGroupMemberModel = ( + client: Mongoose = mongoose +): Model => + getDalModel( + client, + 'GroupMember', + GroupMemberDocumentSchema, + tables.teamGroupMember + ); diff --git a/packages/dal/mongodb/business/support/user/team/group/repository.ts b/packages/dal/mongodb/business/support/user/team/group/repository.ts new file mode 100644 index 000000000000..cd85e8c896a8 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/repository.ts @@ -0,0 +1,207 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import type { DatabaseErrorAdapter } from '../../../../../../db'; +import type { EntityId } from '../../../../../../db/types'; +import type { TransactionContext } from '../../../../../../db/transaction'; +import type { + CreateMemberGroup, + GroupMember, + GroupRepository, + MemberGroup +} from '../../../../../../business/support/user/team/group'; +import { MongoErrorAdapter } from '../../../../../errors'; +import { getMongoSession } from '../../../../../transaction'; +import { toEntityId, toMongoObjectId } from '../../../../../utils'; +import { + getGroupMemberModel, + type GroupMemberDocument, + type GroupMemberMongooseSchemaType +} from './member/schema'; +import { + getMemberGroupModel, + type MemberGroupDocument, + type MemberGroupMongooseSchemaType +} from './schema'; +import { toGroupMember, toMemberGroup } from './entity'; + +export class MongoGroupRepository implements GroupRepository { + constructor( + private readonly memberGroupModel: Model = getMemberGroupModel( + mongoose + ), + private readonly groupMemberModel: Model = getGroupMemberModel( + mongoose + ), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + async findMemberGroupReferencesByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise> { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.memberGroupModel.find({ _id: { $in: ids.map(toMongoObjectId) } }).select('_id teamId'), + context + ).lean>(); + return documents.map((document) => ({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId) + })); + }); + } + + async createMemberGroup( + input: CreateMemberGroup, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const [document] = await this.memberGroupModel.create( + [input], + session ? { session } : undefined + ); + return toMemberGroup(document.toObject() as MemberGroupDocument); + }); + } + + async findMemberGroupsByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.memberGroupModel.find({ _id: { $in: ids.map(toMongoObjectId) } }), + context + ).lean(); + return documents.map(toMemberGroup); + }); + } + + async findGroupsByTmbId( + teamId: EntityId, + tmbId: EntityId, + roles?: string[], + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const members = await this.withSession( + this.groupMemberModel.find({ + tmbId: toMongoObjectId(tmbId), + ...(roles?.length ? { role: { $in: roles } } : {}) + }), + context + ).lean(); + const groupIds = members.map((member) => member.groupId); + if (groupIds.length === 0) return []; + + const groups = await this.withSession( + this.memberGroupModel.find({ + _id: { $in: groupIds }, + teamId: toMongoObjectId(teamId) + }), + context + ).lean(); + return groups.map(toMemberGroup); + }); + } + + async findGroupMember( + groupId: EntityId, + tmbId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.groupMemberModel.findOne({ + groupId: toMongoObjectId(groupId), + tmbId: toMongoObjectId(tmbId) + }), + context + ).lean(); + return document ? toGroupMember(document) : null; + }); + } + + async findGroupMembersByGroupId( + groupId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const documents = await this.withSession( + this.groupMemberModel.find({ groupId: toMongoObjectId(groupId) }), + context + ).lean(); + return documents.map(toGroupMember); + }); + } + + async findMemberGroupByTeamAndName( + teamId: EntityId, + name: string, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.memberGroupModel.findOne({ teamId: toMongoObjectId(teamId), name }), + context + ).lean(); + return document ? toMemberGroup(document) : null; + }); + } + + async updateMemberGroupAvatar( + teamId: EntityId, + name: string, + avatar: string, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.memberGroupModel.findOneAndUpdate( + { teamId: toMongoObjectId(teamId), name }, + { $set: { avatar } }, + { new: true } + ), + context + ).lean(); + return document ? toMemberGroup(document) : null; + }); + } + + async deleteMemberGroupsByTeamId(teamId: EntityId, context?: TransactionContext): Promise { + await this.execute(async () => { + const groups = await this.withSession( + this.memberGroupModel.find({ teamId: toMongoObjectId(teamId) }).select('_id'), + context + ).lean>(); + const groupIds = groups.map((group) => group._id); + if (groupIds.length > 0) { + await this.withSession( + this.groupMemberModel.deleteMany({ groupId: { $in: groupIds } }), + context + ); + } + await this.withSession( + this.memberGroupModel.deleteMany({ teamId: toMongoObjectId(teamId) }), + context + ); + }); + } +} + +export const createMongoGroupRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => + new MongoGroupRepository(getMemberGroupModel(client), getGroupMemberModel(client), errorAdapter); diff --git a/packages/dal/mongodb/models/memberGroup.ts b/packages/dal/mongodb/business/support/user/team/group/schema.ts similarity index 83% rename from packages/dal/mongodb/models/memberGroup.ts rename to packages/dal/mongodb/business/support/user/team/group/schema.ts index 71dbe8548512..4f8b8d2d4609 100644 --- a/packages/dal/mongodb/models/memberGroup.ts +++ b/packages/dal/mongodb/business/support/user/team/group/schema.ts @@ -1,8 +1,8 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../../../db'; +import { defineIndex } from '../../../../../indexes'; +import type { WithId__v } from '../../../../../types'; +import { getDalModel } from '../../../../../model'; export const MemberGroupDocumentSchema = new Schema( { diff --git a/packages/dal/mongodb/business/support/user/team/index.ts b/packages/dal/mongodb/business/support/user/team/index.ts new file mode 100644 index 000000000000..e4ccb507a7d9 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/index.ts @@ -0,0 +1,28 @@ +export { toTeam, toTeamMember, toTeamMemberDetail, toTeamMemberRelations } from './entity'; +export { + TeamDocumentSchema, + getTeamModel, + type TeamDocument, + type TeamMongooseSchemaType +} from './schema'; +export { + TeamMemberDocumentSchema, + getTeamMemberModel, + type TeamMemberDocument, + type TeamMemberMongooseSchemaType +} from './member'; +export { + GroupMemberDocumentSchema, + getGroupMemberModel, + type GroupMemberDocument, + type GroupMemberMongooseSchemaType +} from './group/member'; +export { + OrgMemberDocumentSchema, + getOrgMemberModel, + type OrgMemberDocument, + type OrgMemberMongooseSchemaType +} from './org/member'; +export { MongoTeamRepository, createMongoTeamRepository } from './repository'; +export * from './group'; +export * from './org'; diff --git a/packages/dal/mongodb/business/support/user/team/member/index.ts b/packages/dal/mongodb/business/support/user/team/member/index.ts new file mode 100644 index 000000000000..04c4a0fb96c7 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/member/index.ts @@ -0,0 +1,6 @@ +export { + TeamMemberDocumentSchema, + getTeamMemberModel, + type TeamMemberDocument, + type TeamMemberMongooseSchemaType +} from './schema'; diff --git a/packages/dal/mongodb/models/teamMember.ts b/packages/dal/mongodb/business/support/user/team/member/schema.ts similarity index 77% rename from packages/dal/mongodb/models/teamMember.ts rename to packages/dal/mongodb/business/support/user/team/member/schema.ts index 48561720e44e..c6110b1ab5cc 100644 --- a/packages/dal/mongodb/models/teamMember.ts +++ b/packages/dal/mongodb/business/support/user/team/member/schema.ts @@ -1,10 +1,10 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; import { TeamMemberStatusMap } from '@fastgpt/global/support/user/team/constant'; import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../../../db'; +import { defineIndex } from '../../../../../indexes'; +import type { WithId__v } from '../../../../../types'; +import { getDalModel } from '../../../../../model'; export const TeamMemberDocumentSchema = new Schema({ teamId: { @@ -40,6 +40,19 @@ export const TeamMemberDocumentSchema = new Schema({ defaultTeam: Boolean }); +TeamMemberDocumentSchema.virtual('team', { + ref: 'Team', + localField: 'teamId', + foreignField: '_id', + justOne: true +}); +TeamMemberDocumentSchema.virtual('user', { + ref: 'User', + localField: 'userId', + foreignField: '_id', + justOne: true +}); + defineIndex(TeamMemberDocumentSchema, { key: { teamId: 1 }, options: { background: true } diff --git a/packages/dal/mongodb/business/support/user/team/org/entity.ts b/packages/dal/mongodb/business/support/user/team/org/entity.ts new file mode 100644 index 000000000000..e82721720c1d --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/entity.ts @@ -0,0 +1,29 @@ +import { + OrgMemberSchema, + OrgSchema, + type Org, + type OrgMember +} from '../../../../../../business/support/user/team/org'; +import type { OrgDocument } from './schema'; +import type { OrgMemberDocument } from './member/schema'; +import { toEntityId } from '../../../../../utils'; + +export const toOrg = (document: OrgDocument): Org => + OrgSchema.parse({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId), + pathId: document.pathId, + path: document.path, + name: document.name, + avatar: document.avatar ?? undefined, + description: document.description ?? undefined, + updateTime: document.updateTime ?? undefined + }); + +export const toOrgMember = (document: OrgMemberDocument): OrgMember => + OrgMemberSchema.parse({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId), + orgId: toEntityId(document.orgId), + tmbId: toEntityId(document.tmbId) + }); diff --git a/packages/dal/mongodb/business/support/user/team/org/index.ts b/packages/dal/mongodb/business/support/user/team/org/index.ts new file mode 100644 index 000000000000..d6f533be9e84 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/index.ts @@ -0,0 +1,9 @@ +export { + OrgDocumentSchema, + getOrgModel, + type OrgDocument, + type OrgMongooseSchemaType +} from './schema'; +export * from './member'; +export { toOrg, toOrgMember } from './entity'; +export { MongoOrgRepository, createMongoOrgRepository } from './repository'; diff --git a/packages/dal/mongodb/business/support/user/team/org/member/index.ts b/packages/dal/mongodb/business/support/user/team/org/member/index.ts new file mode 100644 index 000000000000..d91e99bc0d4f --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/member/index.ts @@ -0,0 +1,6 @@ +export { + OrgMemberDocumentSchema, + getOrgMemberModel, + type OrgMemberDocument, + type OrgMemberMongooseSchemaType +} from './schema'; diff --git a/packages/dal/mongodb/business/support/user/team/org/member/schema.ts b/packages/dal/mongodb/business/support/user/team/org/member/schema.ts new file mode 100644 index 000000000000..3ab55024c96f --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/member/schema.ts @@ -0,0 +1,49 @@ +import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; +import { tables } from '../../../../../../../db'; +import { defineIndex } from '../../../../../../indexes'; +import type { WithId__v } from '../../../../../../types'; +import { getDalModel } from '../../../../../../model'; + +export const OrgMemberDocumentSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: 'Team', + required: true + }, + orgId: { + type: Schema.Types.ObjectId, + ref: 'Org', + required: true + }, + tmbId: { + type: Schema.Types.ObjectId, + ref: 'TeamMember', + required: true + } +}); + +OrgMemberDocumentSchema.virtual('org', { + ref: 'Org', + localField: 'orgId', + foreignField: '_id', + justOne: true +}); + +defineIndex(OrgMemberDocumentSchema, { + key: { teamId: 1, orgId: 1, tmbId: 1 }, + options: { unique: true } +}); +defineIndex(OrgMemberDocumentSchema, { key: { teamId: 1, tmbId: 1 } }); + +export type OrgMemberMongooseSchemaType = InferSchemaType; +export type OrgMemberDocument = WithId__v; + +export const getOrgMemberModel = ( + client: Mongoose = mongoose +): Model => + getDalModel( + client, + 'OrgMember', + OrgMemberDocumentSchema, + tables.teamOrgMember + ); diff --git a/packages/dal/mongodb/business/support/user/team/org/repository.ts b/packages/dal/mongodb/business/support/user/team/org/repository.ts new file mode 100644 index 000000000000..9ab2ecfbef2e --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/repository.ts @@ -0,0 +1,170 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import type { DatabaseErrorAdapter } from '../../../../../../db'; +import type { EntityId } from '../../../../../../db/types'; +import type { TransactionContext } from '../../../../../../db/transaction'; +import type { + CreateOrg, + Org, + OrgMember, + OrgRepository +} from '../../../../../../business/support/user/team/org'; +import { MongoErrorAdapter } from '../../../../../errors'; +import { getMongoSession } from '../../../../../transaction'; +import { toEntityId, toMongoObjectId } from '../../../../../utils'; +import { + getOrgMemberModel, + type OrgMemberDocument, + type OrgMemberMongooseSchemaType +} from './member/schema'; +import { getOrgModel, type OrgDocument, type OrgMongooseSchemaType } from './schema'; +import { toOrg, toOrgMember } from './entity'; + +export class MongoOrgRepository implements OrgRepository { + constructor( + private readonly orgModel: Model = getOrgModel(mongoose), + private readonly orgMemberModel: Model = getOrgMemberModel( + mongoose + ), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + async findOrgReferencesByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise> { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.orgModel.find({ _id: { $in: ids.map(toMongoObjectId) } }).select('_id teamId'), + context + ).lean>(); + return documents.map((document) => ({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId) + })); + }); + } + + async createOrg(input: CreateOrg, context?: TransactionContext): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const [document] = await this.orgModel.create([input], session ? { session } : undefined); + return toOrg(document.toObject() as OrgDocument); + }); + } + + async findOrgsByIds(ids: EntityId[], context?: TransactionContext): Promise { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.orgModel.find({ _id: { $in: ids.map(toMongoObjectId) } }), + context + ).lean(); + return documents.map(toOrg); + }); + } + + async findOrgsByTeamId(teamId: EntityId, context?: TransactionContext): Promise { + return this.execute(async () => { + const documents = await this.withSession( + this.orgModel.find({ teamId: toMongoObjectId(teamId) }), + context + ).lean(); + return documents.map(toOrg); + }); + } + + async findOrgsByTeamAndPathIds( + teamId: EntityId, + pathIds: string[], + context?: TransactionContext + ): Promise { + if (pathIds.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.orgModel.find({ + teamId: toMongoObjectId(teamId), + pathId: { $in: pathIds } + }), + context + ).lean(); + return documents.map(toOrg); + }); + } + + async findOrgById( + orgId: EntityId, + teamId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.orgModel.findOne({ _id: toMongoObjectId(orgId), teamId: toMongoObjectId(teamId) }), + context + ).lean(); + return document ? toOrg(document) : null; + }); + } + + async findOrgChildren( + org: Pick, + teamId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const path = org.path === '' && org.pathId === '' ? '' : `${org.path}/${org.pathId}`; + const documents = await this.withSession( + this.orgModel.find({ + teamId: toMongoObjectId(teamId), + path: { $regex: `^${path}` } + }), + context + ).lean(); + return documents.map(toOrg); + }); + } + + async findOrgMembersByTmbId( + teamId: EntityId, + tmbId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const documents = await this.withSession( + this.orgMemberModel.find({ + teamId: toMongoObjectId(teamId), + tmbId: toMongoObjectId(tmbId) + }), + context + ).lean(); + return documents.map(toOrgMember); + }); + } + + async deleteOrgsByTeamId(teamId: EntityId, context?: TransactionContext): Promise { + await this.execute(async () => { + await this.withSession( + this.orgMemberModel.deleteMany({ teamId: toMongoObjectId(teamId) }), + context + ); + await this.withSession( + this.orgModel.deleteMany({ teamId: toMongoObjectId(teamId) }), + context + ); + }); + } +} + +export const createMongoOrgRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => new MongoOrgRepository(getOrgModel(client), getOrgMemberModel(client), errorAdapter); diff --git a/packages/dal/mongodb/models/org.ts b/packages/dal/mongodb/business/support/user/team/org/schema.ts similarity index 85% rename from packages/dal/mongodb/models/org.ts rename to packages/dal/mongodb/business/support/user/team/org/schema.ts index fed7c2b50f04..4ac814dfdff0 100644 --- a/packages/dal/mongodb/models/org.ts +++ b/packages/dal/mongodb/business/support/user/team/org/schema.ts @@ -1,9 +1,9 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../../../db'; +import { defineIndex } from '../../../../../indexes'; +import type { WithId__v } from '../../../../../types'; +import { getDalModel } from '../../../../../model'; export const OrgDocumentSchema = new Schema( { diff --git a/packages/dal/mongodb/business/support/user/team/repository.ts b/packages/dal/mongodb/business/support/user/team/repository.ts new file mode 100644 index 000000000000..7310d65bb05d --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/repository.ts @@ -0,0 +1,521 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { + TeamMemberRoleEnum, + TeamMemberStatusEnum +} from '@fastgpt/global/support/user/team/constant'; +import type { DatabaseErrorAdapter } from '../../../../../db'; +import type { EntityId } from '../../../../../db/types'; +import type { TeamMemberDetail } from '../../../../../business/support/user/team/entity'; +import type { + CreateTeam, + CreateTeamMember, + TeamMemberQuery, + UpdateTeam, + UpdateTeamLimit +} from '../../../../../business/support/user/team/dto'; +import type { + TeamRepository, + CreateDefaultTeamParams, + TeamReference, + TeamScopedReference +} from '../../../../../business/support/user/team/repository'; +import type { + Team, + TeamMember, + TeamMemberRelations +} from '../../../../../business/support/user/team/entity'; +import type { GroupRepository } from '../../../../../business/support/user/team/group'; +import type { OrgRepository } from '../../../../../business/support/user/team/org'; +import type { TransactionContext } from '../../../../../db/transaction'; +import { MongoErrorAdapter } from '../../../../errors'; +import { getTeamModel, type TeamMongooseSchemaType } from './schema'; +import { + getTeamMemberModel, + type TeamMemberDocument, + type TeamMemberMongooseSchemaType +} from './member/schema'; +import { getMongoSession } from '../../../../transaction'; +import { toEntityId, toMongoObjectId } from '../../../../utils'; +import { toTeam, toTeamMember, toTeamMemberDetail, toTeamMemberRelations } from './entity'; +import type { TeamDocument } from './schema'; +import type { UserDocument } from '../schema'; +import { createMongoGroupRepository } from './group/repository'; +import { createMongoOrgRepository } from './org/repository'; + +type TeamMemberRelationsDocument = TeamMemberDocument & { + team?: TeamDocument; + user?: UserDocument; +}; + +export class MongoTeamRepository implements TeamRepository { + constructor( + private readonly teamModel: Model = getTeamModel(mongoose), + private readonly teamMemberModel: Model = getTeamMemberModel( + mongoose + ), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter(), + private readonly groupRepository: GroupRepository = createMongoGroupRepository( + mongoose, + errorAdapter + ), + private readonly orgRepository: OrgRepository = createMongoOrgRepository(mongoose, errorAdapter) + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + private getMemberFilter(query?: TeamMemberQuery) { + return { + ...(query?.teamId ? { teamId: toMongoObjectId(query.teamId) } : {}), + ...(query?.includeLeft === false + ? { + status: { + $nin: [TeamMemberStatusEnum.leave, TeamMemberStatusEnum.forbidden] + } + } + : {}) + }; + } + + async findMemberById( + id: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findById(toMongoObjectId(id)), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async findMemberByTeamAndUser( + teamId: EntityId, + userId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findOne({ + ...this.getMemberFilter({ ...query, teamId }), + userId: toMongoObjectId(userId) + }), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async findMemberByIdInTeam( + id: EntityId, + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findOne({ + ...this.getMemberFilter({ ...query, teamId }), + _id: toMongoObjectId(id) + }), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async findMemberRelationsById( + id: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findById(toMongoObjectId(id)).populate('team').populate('user'), + context + ).lean(); + return document?.team ? toTeamMemberRelations(document) : null; + }); + } + + async findMemberRelationsByUserId( + userId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel + .findOne({ userId: toMongoObjectId(userId) }) + .populate('team') + .populate('user'), + context + ).lean(); + return document?.team ? toTeamMemberRelations(document) : null; + }); + } + + async findMemberRelationsByTeamId( + teamId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const documents = await this.withSession( + this.teamMemberModel + .find({ teamId: toMongoObjectId(teamId) }) + .populate('team') + .populate('user'), + context + ).lean(); + return documents.filter((document) => document.team).map(toTeamMemberRelations); + }); + } + + async findOwnerByTeamId( + teamId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findOne({ + teamId: toMongoObjectId(teamId), + role: TeamMemberRoleEnum.owner + }), + context + ).lean(); + return document ? toTeamMember(document) : null; + }); + } + + async findMembersByIds( + ids: EntityId[], + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.teamMemberModel.find({ + ...this.getMemberFilter(query), + _id: { $in: ids.map(toMongoObjectId) } + }), + context + ).lean(); + return documents.map(toTeamMember); + }); + } + + async findMembersByTeamId( + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const documents = await this.withSession( + this.teamMemberModel.find(this.getMemberFilter({ ...query, teamId })), + context + ).lean(); + return documents.map(toTeamMember); + }); + } + + async countMembersByTeamId( + teamId: EntityId, + query?: TeamMemberQuery, + context?: TransactionContext + ): Promise { + return this.execute(async () => + this.withSession( + this.teamMemberModel.countDocuments(this.getMemberFilter({ ...query, teamId })), + context + ) + ); + } + + async findTeamById(teamId: EntityId, context?: TransactionContext): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamModel.findById(toMongoObjectId(teamId)), + context + ).lean(); + return document ? toTeam(document) : null; + }); + } + + async findTeamsByIds(teamIds: EntityId[], context?: TransactionContext): Promise { + if (teamIds.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.teamModel.find({ _id: { $in: teamIds.map(toMongoObjectId) } }), + context + ).lean(); + return documents.map(toTeam); + }); + } + + async findAllTeamIds(context?: TransactionContext): Promise { + return this.execute(async () => { + const documents = await this.withSession(this.teamModel.find().select('_id'), context).lean< + { _id: unknown }[] + >(); + return documents.map((document) => toEntityId(document._id)); + }); + } + + async findTeamReferencesByIds( + teamIds: EntityId[], + context?: TransactionContext + ): Promise { + if (teamIds.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.teamModel.find({ _id: { $in: teamIds.map(toMongoObjectId) } }).select('_id'), + context + ).lean>(); + return documents.map((document) => ({ id: toEntityId(document._id) })); + }); + } + + async findMemberReferencesByIds( + ids: EntityId[], + context?: TransactionContext + ): Promise { + if (ids.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.teamMemberModel.find({ _id: { $in: ids.map(toMongoObjectId) } }).select('_id teamId'), + context + ).lean>(); + return documents.map((document) => ({ + id: toEntityId(document._id), + teamId: toEntityId(document.teamId) + })); + }); + } + + async createTeam(input: CreateTeam, context?: TransactionContext): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const [document] = await this.teamModel.create([input], session ? { session } : undefined); + return toTeam(document.toObject() as TeamDocument); + }); + } + + async createTeamMember( + input: CreateTeamMember, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const [document] = await this.teamMemberModel.create( + [input], + session ? { session } : undefined + ); + return toTeamMember(document.toObject() as TeamMemberDocument); + }); + } + + async updateMemberAvatar( + id: EntityId, + avatar: string, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamMemberModel.findByIdAndUpdate( + toMongoObjectId(id), + { $set: { avatar } }, + { new: true } + ), + context + ).lean(); + return document ? toTeamMemberDetail(document) : null; + }); + } + + async updateTeam( + teamId: EntityId, + patch: UpdateTeam, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const $set: Record = {}; + const $unset: Record = {}; + if (patch.name !== undefined) $set.name = patch.name; + if (patch.avatar !== undefined) $set.avatar = patch.avatar; + if (patch.openaiAccount) $set.openaiAccount = patch.openaiAccount; + if (patch.clearOpenaiAccount) $unset.openaiAccount = 1; + if (patch.externalWorkflowVariable) { + const field = `externalWorkflowVariables.${patch.externalWorkflowVariable.key}`; + if (patch.externalWorkflowVariable.value === '') { + $unset[field] = 1; + } else { + $set[field] = patch.externalWorkflowVariable.value; + } + } + + const document = await this.withSession( + this.teamModel.findByIdAndUpdate( + toMongoObjectId(teamId), + { + ...($set && Object.keys($set).length > 0 ? { $set } : {}), + ...($unset && Object.keys($unset).length > 0 ? { $unset } : {}) + }, + { new: true } + ), + context + ).lean(); + return document ? toTeam(document) : null; + }); + } + + async updateTeamLimit( + teamId: EntityId, + patch: UpdateTeamLimit, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const $set: Record = {}; + const $unset: Record = {}; + for (const [key, value] of Object.entries(patch)) { + const field = `limit.${key}`; + if (value === null) { + $unset[field] = 1; + } else if (value !== undefined) { + $set[field] = value; + } + } + const document = await this.withSession( + this.teamModel.findByIdAndUpdate( + toMongoObjectId(teamId), + { + ...($set && Object.keys($set).length > 0 ? { $set } : {}), + ...($unset && Object.keys($unset).length > 0 ? { $unset } : {}) + }, + { new: true } + ), + context + ).lean(); + return document ? toTeam(document) : null; + }); + } + + async deleteMembersByTeamId(teamId: EntityId, context?: TransactionContext): Promise { + await this.execute(async () => { + await this.withSession( + this.teamMemberModel.deleteMany({ teamId: toMongoObjectId(teamId) }), + context + ); + }); + } + + async clearTeamSensitiveData( + teamId: EntityId, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.teamModel.findByIdAndUpdate( + toMongoObjectId(teamId), + { + $set: { notificationAccount: '' }, + $unset: { + openaiAccount: 1, + externalWorkflowVariables: 1, + meta: 1 + } + }, + { new: true } + ), + context + ).lean(); + return document ? toTeam(document) : null; + }); + } + + async createDefaultTeam({ + userId, + teamName = 'My Team', + avatar = '/icon/logo.svg', + context + }: CreateDefaultTeamParams): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const sessionOptions = session ? { session, ordered: true } : undefined; + + // 已存在默认 tmb 时沿用旧语义:跳过创建。 + const existing = await ( + session + ? this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }).session(session) + : this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }) + ).lean(); + if (existing) return null; + + const [teamDocument] = await this.teamModel.create( + [ + { + ownerId: userId, + name: teamName, + avatar, + createTime: new Date() + } + ], + sessionOptions + ); + const [tmbDocument] = await this.teamMemberModel.create( + [ + { + teamId: teamDocument._id, + userId, + name: 'Owner', + role: TeamMemberRoleEnum.owner, + status: TeamMemberStatusEnum.active, + createTime: new Date() + } + ], + sessionOptions + ); + await this.groupRepository.createMemberGroup( + { + teamId: toEntityId(tmbDocument.teamId), + name: DefaultGroupName, + avatar + }, + context + ); + await this.orgRepository.createOrg( + { + teamId: toEntityId(tmbDocument.teamId), + name: 'ROOT', + path: '' + }, + context + ); + + return toTeamMemberDetail(tmbDocument.toObject() as TeamMemberDocument); + }); + } +} + +export const createMongoTeamRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter(), + repositories: { + groupRepository?: GroupRepository; + orgRepository?: OrgRepository; + } = {} +) => + new MongoTeamRepository( + getTeamModel(client), + getTeamMemberModel(client), + errorAdapter, + repositories.groupRepository ?? createMongoGroupRepository(client, errorAdapter), + repositories.orgRepository ?? createMongoOrgRepository(client, errorAdapter) + ); diff --git a/packages/dal/mongodb/models/team.ts b/packages/dal/mongodb/business/support/user/team/schema.ts similarity index 86% rename from packages/dal/mongodb/models/team.ts rename to packages/dal/mongodb/business/support/user/team/schema.ts index 2c4a399869ca..b6dbb237bfbe 100644 --- a/packages/dal/mongodb/models/team.ts +++ b/packages/dal/mongodb/business/support/user/team/schema.ts @@ -1,8 +1,8 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../../db'; +import { defineIndex } from '../../../../indexes'; +import type { WithId__v } from '../../../../types'; +import { getDalModel } from '../../../../model'; export const TeamDocumentSchema = new Schema({ name: { diff --git a/packages/dal/mongodb/business/support/user/verification/entity.ts b/packages/dal/mongodb/business/support/user/verification/entity.ts new file mode 100644 index 000000000000..90bc21acbd2f --- /dev/null +++ b/packages/dal/mongodb/business/support/user/verification/entity.ts @@ -0,0 +1,13 @@ +import { + TmpDataMaterialSchema, + type TmpDataMaterial +} from '../../../../../business/support/user/verification/entity'; +import type { TmpDataDocument } from './schema'; + +/** 将 Mongo 临时数据文档映射为数据库无关的验证材料实体。 */ +export const toTmpDataMaterial = (document: TmpDataDocument): TmpDataMaterial => + TmpDataMaterialSchema.parse({ + dataId: document.dataId, + data: document.data, + expireAt: document.expireAt + }); diff --git a/packages/dal/mongodb/business/support/user/verification/index.ts b/packages/dal/mongodb/business/support/user/verification/index.ts new file mode 100644 index 000000000000..6e66b375ba1e --- /dev/null +++ b/packages/dal/mongodb/business/support/user/verification/index.ts @@ -0,0 +1,8 @@ +export { + TmpDataDocumentSchema, + getTmpDataModel, + type TmpDataDocument, + type TmpDataMongooseSchemaType +} from './schema'; +export { toTmpDataMaterial } from './entity'; +export { MongoTmpDataRepository, createMongoTmpDataRepository } from './repository'; diff --git a/packages/dal/mongodb/business/support/user/verification/repository.ts b/packages/dal/mongodb/business/support/user/verification/repository.ts new file mode 100644 index 000000000000..d976ca1495e1 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/verification/repository.ts @@ -0,0 +1,142 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +import type { DatabaseErrorAdapter } from '../../../../../db'; +import type { ActiveTmpDataFilter } from '../../../../../business/support/user/verification/dto'; +import type { + TmpDataMaterial, + TmpDataWrite +} from '../../../../../business/support/user/verification/entity'; +import type { TmpDataRepository } from '../../../../../business/support/user/verification/repository'; +import type { TransactionContext } from '../../../../../db/transaction'; +import { MongoErrorAdapter } from '../../../../errors'; +import { getTmpDataModel, type TmpDataDocument, type TmpDataMongooseSchemaType } from './schema'; +import { getMongoSession } from '../../../../transaction'; +import { toTmpDataMaterial } from './entity'; + +/** 将材料匹配字段映射为 `data.` 查询条件,语义与旧 verification 模块一致。 */ +const getDataMatch = (match?: Record) => + Object.fromEntries(Object.entries(match ?? {}).map(([field, value]) => [`data.${field}`, value])); + +const getActiveFilter = (filter: ActiveTmpDataFilter) => ({ + dataId: filter.dataId, + expireAt: { $gt: new Date() }, + ...getDataMatch(filter.match) +}); + +const isDuplicateKeyError = (error: unknown) => + !!error && typeof error === 'object' && 'code' in error && error.code === 11000; + +export class MongoTmpDataRepository implements TmpDataRepository { + constructor( + private readonly model: Model = getTmpDataModel(mongoose), + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() + ) {} + + private execute(handler: () => Promise) { + return this.errorAdapter.execute(handler); + } + + private withSession(query: Query, context?: TransactionContext) { + const session = getMongoSession(context); + return session ? query.session(session) : query; + } + + async findByDataId( + dataId: string, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne({ dataId }), + context + ).lean(); + return document ? toTmpDataMaterial(document) : null; + }); + } + + async findActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne(getActiveFilter(filter)), + context + ).lean(); + return document ? toTmpDataMaterial(document) : null; + }); + } + + async createIfInactive(input: TmpDataWrite, context?: TransactionContext): Promise { + return this.execute(async () => { + const session = getMongoSession(context); + const options = session ? { session } : undefined; + await this.model.deleteOne({ dataId: input.dataId, expireAt: { $lte: new Date() } }, options); + + try { + await this.model.create([input], options); + return true; + } catch (error) { + if (isDuplicateKeyError(error)) return false; + throw error; + } + }); + } + + async upsert(input: TmpDataWrite, context?: TransactionContext): Promise { + await this.execute(async () => { + const query = this.model.updateOne( + { dataId: input.dataId }, + { $set: { data: input.data, expireAt: input.expireAt } }, + { upsert: true } + ); + await this.withSession(query, context); + }); + } + + async updateIfActive(input: TmpDataWrite, context?: TransactionContext): Promise { + return this.execute(async () => { + const query = this.model.updateOne( + { dataId: input.dataId, expireAt: { $gt: new Date() } }, + { $set: { data: input.data, expireAt: input.expireAt } } + ); + const result = await this.withSession(query, context); + return result.modifiedCount === 1; + }); + } + + async findActiveDataIds( + dataIds: readonly string[], + context?: TransactionContext + ): Promise { + if (dataIds.length === 0) return []; + return this.execute(async () => { + const documents = await this.withSession( + this.model + .find({ dataId: { $in: [...new Set(dataIds)] }, expireAt: { $gt: new Date() } }) + .select({ dataId: 1 }) + .limit(2), + context + ).lean>(); + return documents.map((document) => document.dataId); + }); + } + + async deleteActiveMaterial( + filter: ActiveTmpDataFilter, + context?: TransactionContext + ): Promise { + return this.execute(async () => { + const query = this.model.deleteOne(getActiveFilter(filter)); + const result = await this.withSession(query, context); + return result.deletedCount === 1; + }); + } +} + +export const createMongoTmpDataRepository = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => { + const model = getTmpDataModel(client); + return new MongoTmpDataRepository(model, errorAdapter); +}; diff --git a/packages/dal/mongodb/models/tmpData.ts b/packages/dal/mongodb/business/support/user/verification/schema.ts similarity index 82% rename from packages/dal/mongodb/models/tmpData.ts rename to packages/dal/mongodb/business/support/user/verification/schema.ts index 531ba17eb5da..0015b2063151 100644 --- a/packages/dal/mongodb/models/tmpData.ts +++ b/packages/dal/mongodb/business/support/user/verification/schema.ts @@ -1,8 +1,8 @@ import mongoose, { Schema, type InferSchemaType, type Model, type Mongoose } from 'mongoose'; -import { tables } from '../../db'; -import { defineIndex } from '../indexes'; -import type { WithId__v } from '../types'; -import { getDalModel } from './helper'; +import { tables } from '../../../../../db'; +import { defineIndex } from '../../../../indexes'; +import type { WithId__v } from '../../../../types'; +import { getDalModel } from '../../../../model'; export const TmpDataDocumentSchema = new Schema({ dataId: { diff --git a/packages/dal/mongodb/concurrency.ts b/packages/dal/mongodb/concurrency.ts index e78ace042e7e..2236cc49ef37 100644 --- a/packages/dal/mongodb/concurrency.ts +++ b/packages/dal/mongodb/concurrency.ts @@ -1,5 +1,5 @@ import type { ClientSession, FilterQuery, Model, UpdateQuery } from 'mongoose'; -import type { EntityId } from '../domain'; +import type { EntityId } from '../db/types'; import { toMongoObjectId } from './utils'; export type CasUpdateByIdParams = { diff --git a/packages/dal/mongodb/index.ts b/packages/dal/mongodb/index.ts index b1a3a65cd617..9cb94e2f6dc8 100644 --- a/packages/dal/mongodb/index.ts +++ b/packages/dal/mongodb/index.ts @@ -1,8 +1,3 @@ -export { createMongoUserRepository, MongoUserRepository } from './repositories/user'; -export { createMongoTeamRepository, MongoTeamRepository } from './repositories/team'; -export { createMongoTmpDataRepository, MongoTmpDataRepository } from './repositories/tmpData'; -export { toUser, userDefaultFieldValues } from './mappers/user'; -export { toTeamMemberDetail } from './mappers/team'; export { MongoAdapter } from './adapter'; export type { MongoAdapterDependencies } from './adapter'; export { MongoErrorAdapter } from './errors'; @@ -14,4 +9,5 @@ export type { CasUpdateByIdParams } from './concurrency'; export { setDalLogger, getDalLogger } from './logger'; export type { DalLogger } from './logger'; export { createSlowQueryMiddleware as addDalCommonMiddleware } from './middleware'; -export { getDalModel } from './models/helper'; +export { getDalModel } from './model'; +export * from './business'; diff --git a/packages/dal/mongodb/logger.ts b/packages/dal/mongodb/logger.ts index 5ac952fe69b9..5d1695b6be5f 100644 --- a/packages/dal/mongodb/logger.ts +++ b/packages/dal/mongodb/logger.ts @@ -8,7 +8,7 @@ let injectedLogger: DalLogger | undefined; * 注入 DAL 慢查询日志器。service 侧 composition root 启动时把业务 logger 注入进来, * 避免 DAL 包反向依赖 service 的日志实现;未注入时退回 console。 */ -export const setDalLogger = (logger: DalLogger) => { +export const setDalLogger = (logger?: DalLogger) => { injectedLogger = logger; }; diff --git a/packages/dal/mongodb/mappers/team.ts b/packages/dal/mongodb/mappers/team.ts deleted file mode 100644 index 03832a4aff34..000000000000 --- a/packages/dal/mongodb/mappers/team.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TeamMemberDetailSchema, type TeamMemberDetail } from '../../domain/team'; -import type { TeamMemberDocument } from '../models/teamMember'; -import { toEntityId } from '../utils'; - -export const toTeamMemberDetail = (document: TeamMemberDocument): TeamMemberDetail => - TeamMemberDetailSchema.parse({ - id: toEntityId(document._id), - teamId: toEntityId(document.teamId), - userId: toEntityId(document.userId), - avatar: document.avatar ?? undefined, - name: document.name, - role: document.role ?? undefined, - status: document.status ?? undefined - }); diff --git a/packages/dal/mongodb/models/helper.ts b/packages/dal/mongodb/model.ts similarity index 89% rename from packages/dal/mongodb/models/helper.ts rename to packages/dal/mongodb/model.ts index 7f8fc4937a98..5c8607f9ba1d 100644 --- a/packages/dal/mongodb/models/helper.ts +++ b/packages/dal/mongodb/model.ts @@ -1,5 +1,5 @@ import type { Model, Mongoose, Schema } from 'mongoose'; -import { createSlowQueryMiddleware } from '../middleware'; +import { createSlowQueryMiddleware } from './middleware'; /** * DAL Model 统一注册入口:同一 client 内按模型名复用,注册前挂慢查询中间件。 diff --git a/packages/dal/mongodb/repositories/team.ts b/packages/dal/mongodb/repositories/team.ts deleted file mode 100644 index 746e85dbe321..000000000000 --- a/packages/dal/mongodb/repositories/team.ts +++ /dev/null @@ -1,159 +0,0 @@ -import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; -import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { - TeamMemberRoleEnum, - TeamMemberStatusEnum -} from '@fastgpt/global/support/user/team/constant'; -import type { DatabaseErrorAdapter } from '../../db'; -import type { TeamMemberDetail } from '../../domain/team'; -import type { EntityId } from '../../domain/types'; -import type { CreateDefaultTeamParams, TeamRepository } from '../../ports/team.repository'; -import type { TransactionContext } from '../../transaction'; -import { toTeamMemberDetail } from '../mappers/team'; -import { MongoErrorAdapter } from '../errors'; -import { getMemberGroupModel, type MemberGroupMongooseSchemaType } from '../models/memberGroup'; -import { getOrgModel, type OrgMongooseSchemaType } from '../models/org'; -import { getTeamModel, type TeamMongooseSchemaType } from '../models/team'; -import { - getTeamMemberModel, - type TeamMemberDocument, - type TeamMemberMongooseSchemaType -} from '../models/teamMember'; -import { getMongoSession } from '../transaction'; -import { toMongoObjectId } from '../utils'; - -export class MongoTeamRepository implements TeamRepository { - constructor( - private readonly teamModel: Model = getTeamModel(mongoose), - private readonly teamMemberModel: Model = getTeamMemberModel( - mongoose - ), - private readonly memberGroupModel: Model = getMemberGroupModel( - mongoose - ), - private readonly orgModel: Model = getOrgModel(mongoose), - private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() - ) {} - - private execute(handler: () => Promise) { - return this.errorAdapter.execute(handler); - } - - private withSession( - query: Query, - context?: TransactionContext - ) { - const session = getMongoSession(context); - return session ? query.session(session) : query; - } - - async findMemberById( - id: EntityId, - context?: TransactionContext - ): Promise { - return this.execute(async () => { - const document = await this.withSession( - this.teamMemberModel.findById(toMongoObjectId(id)), - context - ).lean(); - return document ? toTeamMemberDetail(document) : null; - }); - } - - async updateMemberAvatar( - id: EntityId, - avatar: string, - context?: TransactionContext - ): Promise { - return this.execute(async () => { - const document = await this.withSession( - this.teamMemberModel.findByIdAndUpdate( - toMongoObjectId(id), - { $set: { avatar } }, - { new: true } - ), - context - ).lean(); - return document ? toTeamMemberDetail(document) : null; - }); - } - - async createDefaultTeam({ - userId, - teamName = 'My Team', - avatar = '/icon/logo.svg', - context - }: CreateDefaultTeamParams): Promise { - return this.execute(async () => { - const session = getMongoSession(context); - const sessionOptions = session ? { session, ordered: true } : undefined; - - // 已存在默认 tmb 时沿用旧语义:跳过创建。 - const existing = await ( - session - ? this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }).session(session) - : this.teamMemberModel.findOne({ userId: toMongoObjectId(userId) }) - ).lean(); - if (existing) return null; - - const [teamDocument] = await this.teamModel.create( - [ - { - ownerId: userId, - name: teamName, - avatar, - createTime: new Date() - } - ], - sessionOptions - ); - const [tmbDocument] = await this.teamMemberModel.create( - [ - { - teamId: teamDocument._id, - userId, - name: 'Owner', - role: TeamMemberRoleEnum.owner, - status: TeamMemberStatusEnum.active, - createTime: new Date() - } - ], - sessionOptions - ); - await this.memberGroupModel.create( - [ - { - teamId: tmbDocument.teamId, - name: DefaultGroupName, - avatar - } - ], - sessionOptions - ); - await this.orgModel.create( - [ - { - teamId: tmbDocument.teamId, - name: 'ROOT', - path: '' - } - ], - sessionOptions - ); - - return toTeamMemberDetail(tmbDocument.toObject() as TeamMemberDocument); - }); - } -} - -export const createMongoTeamRepository = ( - client: Mongoose, - errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() -) => - new MongoTeamRepository( - getTeamModel(client), - getTeamMemberModel(client), - getMemberGroupModel(client), - getOrgModel(client), - errorAdapter - ); diff --git a/packages/dal/mongodb/repositories/tmpData.ts b/packages/dal/mongodb/repositories/tmpData.ts deleted file mode 100644 index ca0db473f867..000000000000 --- a/packages/dal/mongodb/repositories/tmpData.ts +++ /dev/null @@ -1,73 +0,0 @@ -import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; -import type { DatabaseErrorAdapter } from '../../db'; -import type { ActiveTmpDataFilter, TmpDataMaterial } from '../../domain/tmpData'; -import type { TmpDataRepository } from '../../ports/tmpData.repository'; -import type { TransactionContext } from '../../transaction'; -import { MongoErrorAdapter } from '../errors'; -import { - getTmpDataModel, - type TmpDataDocument, - type TmpDataMongooseSchemaType -} from '../models/tmpData'; -import { getMongoSession } from '../transaction'; - -/** 将材料匹配字段映射为 `data.` 查询条件,语义与旧 verification 模块一致。 */ -const getDataMatch = (match?: Record) => - Object.fromEntries(Object.entries(match ?? {}).map(([field, value]) => [`data.${field}`, value])); - -const getActiveFilter = (filter: ActiveTmpDataFilter) => ({ - dataId: filter.dataId, - expireAt: { $gt: new Date() }, - ...getDataMatch(filter.match) -}); - -export class MongoTmpDataRepository implements TmpDataRepository { - constructor( - private readonly model: Model = getTmpDataModel(mongoose), - private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() - ) {} - - private execute(handler: () => Promise) { - return this.errorAdapter.execute(handler); - } - - private withSession(query: Query, context?: TransactionContext) { - const session = getMongoSession(context); - return session ? query.session(session) : query; - } - - async findActiveMaterial( - filter: ActiveTmpDataFilter, - context?: TransactionContext - ): Promise { - return this.execute(async () => { - const document = await this.withSession( - this.model.findOne(getActiveFilter(filter)), - context - ).lean(); - return document - ? { - dataId: document.dataId, - data: document.data, - expireAt: document.expireAt - } - : null; - }); - } - - async deleteActiveMaterial( - filter: ActiveTmpDataFilter, - context?: TransactionContext - ): Promise { - return this.execute(async () => { - const query = this.model.deleteOne(getActiveFilter(filter)); - const result = await this.withSession(query, context); - return result.deletedCount === 1; - }); - } -} - -export const createMongoTmpDataRepository = ( - client: Mongoose, - errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() -) => new MongoTmpDataRepository(getTmpDataModel(client), errorAdapter); diff --git a/packages/dal/mongodb/transaction.ts b/packages/dal/mongodb/transaction.ts index a3f83e7c4638..01871467e200 100644 --- a/packages/dal/mongodb/transaction.ts +++ b/packages/dal/mongodb/transaction.ts @@ -1,5 +1,5 @@ import mongoose, { type ClientSession, type Mongoose } from 'mongoose'; -import type { TransactionContext, TransactionRunner } from '../transaction'; +import type { TransactionContext, TransactionRunner } from '../db/transaction'; import { isDatabaseOperationError, type DatabaseErrorAdapter } from '../db'; import { MongoErrorAdapter, MongoInvalidArgumentError } from './errors'; diff --git a/packages/dal/mongodb/utils.ts b/packages/dal/mongodb/utils.ts index 3b37e82b8bb9..ed35f1fc24d3 100644 --- a/packages/dal/mongodb/utils.ts +++ b/packages/dal/mongodb/utils.ts @@ -1,5 +1,5 @@ import { Types } from 'mongoose'; -import type { EntityId } from '../domain'; +import type { EntityId } from '../db/types'; import { MongoInvalidArgumentError } from './errors'; export function toEntityId(value: unknown) { diff --git a/packages/dal/package.json b/packages/dal/package.json index 50fa909d958c..0e9997cd5e84 100644 --- a/packages/dal/package.json +++ b/packages/dal/package.json @@ -4,22 +4,13 @@ "type": "module", "exports": { ".": "./index.ts", + "./business": "./business/index.ts", "./db": "./db/index.ts", - "./domain": "./domain/index.ts", - "./ports": "./ports/index.ts", "./mongodb": "./mongodb/index.ts", - "./mongodb/adapter": "./mongodb/adapter.ts", - "./mongodb/models/user": "./mongodb/models/user.ts", - "./mongodb/models/team": "./mongodb/models/team.ts", - "./mongodb/models/teamMember": "./mongodb/models/teamMember.ts", - "./mongodb/models/memberGroup": "./mongodb/models/memberGroup.ts", - "./mongodb/models/org": "./mongodb/models/org.ts", - "./mongodb/models/tmpData": "./mongodb/models/tmpData.ts", - "./transaction": "./transaction/index.ts", + "./mongodb/business": "./mongodb/business/index.ts", "./redis": "./redis/index.ts", "./redis/adapter": "./redis/adapter.ts", "./redis/bullmq": "./redis/bullmq/index.ts", - "./redis/bullmq/services/*": "./redis/bullmq/services/*.ts", "./redis/caches": "./redis/caches/index.ts", "./redis/types": "./redis/types.ts", "./redis/runtime": "./redis/runtime/index.ts" @@ -27,6 +18,7 @@ "scripts": { "test": "vitest run -c vitest.config.ts", "test:watch": "vitest -c vitest.config.ts", + "test:integration:mongodb": "vitest run -c vitest.integration.config.ts", "test:integration:redis": "vitest run -c vitest.config.ts test/integrations/redis", "typecheck": "tsc --noEmit --pretty" }, @@ -43,6 +35,7 @@ }, "devDependencies": { "@types/node": "catalog:", + "mongodb-memory-server": "catalog:", "typescript": "catalog:", "vitest": "catalog:" } diff --git a/packages/dal/ports/index.ts b/packages/dal/ports/index.ts deleted file mode 100644 index 04c61d148772..000000000000 --- a/packages/dal/ports/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { UserRepository } from './user.repository'; -export type { CreateDefaultTeamParams, TeamRepository } from './team.repository'; -export type { TmpDataRepository } from './tmpData.repository'; diff --git a/packages/dal/ports/team.repository.ts b/packages/dal/ports/team.repository.ts deleted file mode 100644 index e6ea72256533..000000000000 --- a/packages/dal/ports/team.repository.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { TeamMemberDetail } from '../domain/team'; -import type { EntityId } from '../domain/types'; -import type { TransactionContext } from '../transaction'; - -export type CreateDefaultTeamParams = { - userId: EntityId; - teamName?: string; - avatar?: string; - context?: TransactionContext; -}; - -/** - * Team/TeamMember 最小 DAL 能力,只覆盖 M9 三个迁移流程需要的操作。 - * 权限、组织等复杂读模型仍留在 service 层(事务外读取)。 - */ -export type TeamRepository = { - findMemberById(id: EntityId, context?: TransactionContext): Promise; - updateMemberAvatar( - id: EntityId, - avatar: string, - context?: TransactionContext - ): Promise; - /** - * 在事务内创建默认团队(teams + team_members + team_member_groups + team_orgs)。 - * 已存在默认 tmb 时返回 null(沿用旧语义:跳过创建)。 - */ - createDefaultTeam(params: CreateDefaultTeamParams): Promise; -}; diff --git a/packages/dal/ports/tmpData.repository.ts b/packages/dal/ports/tmpData.repository.ts deleted file mode 100644 index 6d1bcb2b0601..000000000000 --- a/packages/dal/ports/tmpData.repository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ActiveTmpDataFilter, TmpDataMaterial } from '../domain/tmpData'; -import type { TransactionContext } from '../transaction'; - -/** 验证码材料的最小 DAL 能力,供 verification 在 DAL 事务内读取与消费。 */ -export type TmpDataRepository = { - findActiveMaterial( - filter: ActiveTmpDataFilter, - context?: TransactionContext - ): Promise; - deleteActiveMaterial(filter: ActiveTmpDataFilter, context?: TransactionContext): Promise; -}; diff --git a/packages/dal/test/integrations/mongodb/client.ts b/packages/dal/test/integrations/mongodb/client.ts new file mode 100644 index 000000000000..72f4cef0432c --- /dev/null +++ b/packages/dal/test/integrations/mongodb/client.ts @@ -0,0 +1,17 @@ +import { Mongoose } from 'mongoose'; +import { inject } from 'vitest'; + +/** + * Creates a Mongoose client connected to the Replica Set provided by global setup. + * Each test file owns and closes its client so models and connections do not leak. + */ +export const connectMongoIntegration = async (): Promise => { + const client = new Mongoose(); + await client.connect(inject('MONGODB_URI')); + return client; +}; + +/** Closes a MongoDB integration client and releases its connection resources. */ +export const disconnectMongoIntegration = async (client: Mongoose | undefined) => { + await client?.disconnect(); +}; diff --git a/packages/dal/test/integrations/mongodb/global-setup.ts b/packages/dal/test/integrations/mongodb/global-setup.ts new file mode 100644 index 000000000000..52413d3a83c4 --- /dev/null +++ b/packages/dal/test/integrations/mongodb/global-setup.ts @@ -0,0 +1,32 @@ +import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import type { TestProject } from 'vitest/node'; + +/** + * Provides one MongoDB Replica Set for the DAL integration test project. + * An externally managed URI can be supplied for CI or debugging. + */ +export default async function setup(project: TestProject) { + const sharedUri = process.env.FASTGPT_TEST_MONGODB_URI; + if (sharedUri) { + project.provide('MONGODB_URI', sharedUri); + return; + } + + const replset = await MongoMemoryReplSet.create({ + replSet: { + count: 1, + args: ['--setParameter', 'diagnosticDataCollectionEnabled=false'] + } + }); + project.provide('MONGODB_URI', replset.getUri('fastgpt_dal_integration')); + + return async () => { + await replset.stop(); + }; +} + +declare module 'vitest' { + export interface ProvidedContext { + MONGODB_URI: string; + } +} diff --git a/packages/dal/test/integrations/mongodb/smoke.integration.test.ts b/packages/dal/test/integrations/mongodb/smoke.integration.test.ts new file mode 100644 index 000000000000..71125e693051 --- /dev/null +++ b/packages/dal/test/integrations/mongodb/smoke.integration.test.ts @@ -0,0 +1,19 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { connectMongoIntegration, disconnectMongoIntegration } from './client'; +import type { Mongoose } from 'mongoose'; + +describe('MongoDB integration test harness', () => { + let client: Mongoose | undefined; + + beforeAll(async () => { + client = await connectMongoIntegration(); + }); + + afterAll(async () => { + await disconnectMongoIntegration(client); + }); + + it('connects to the configured MongoDB Replica Set', async () => { + await expect(client?.connection.db?.command({ ping: 1 })).resolves.toMatchObject({ ok: 1 }); + }); +}); diff --git a/packages/dal/test/mongodb/adapter.test.ts b/packages/dal/test/mongodb/adapter.test.ts index 4bcf23f4928e..0444431b68a3 100644 --- a/packages/dal/test/mongodb/adapter.test.ts +++ b/packages/dal/test/mongodb/adapter.test.ts @@ -1,7 +1,7 @@ import { Mongoose } from 'mongoose'; import { describe, expect, it } from 'vitest'; import { MongoAdapter } from '../../mongodb/adapter'; -import { MongoUserRepository } from '../../mongodb/repositories/user'; +import { MongoUserRepository } from '../../mongodb/business/support/user/repository'; import { MongoTransactionRunner } from '../../mongodb/transaction'; import { MongoErrorAdapter } from '../../mongodb/errors'; diff --git a/packages/dal/test/mongodb/mappers/user.test.ts b/packages/dal/test/mongodb/business/user/entity.test.ts similarity index 95% rename from packages/dal/test/mongodb/mappers/user.test.ts rename to packages/dal/test/mongodb/business/user/entity.test.ts index 8357528a6570..b5c27469fde5 100644 --- a/packages/dal/test/mongodb/mappers/user.test.ts +++ b/packages/dal/test/mongodb/business/user/entity.test.ts @@ -2,8 +2,8 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { Types } from 'mongoose'; import { describe, expect, it } from 'vitest'; -import { toUser } from '../../../mongodb/mappers/user'; -import type { UserDocument } from '../../../mongodb/models/user'; +import { toUser } from '../../../../mongodb/business/support/user/entity'; +import type { UserDocument } from '../../../../mongodb/business/support/user/schema'; const createDocument = (overrides: Partial = {}): UserDocument => ({ _id: new Types.ObjectId('507f1f77bcf86cd799439011'), diff --git a/packages/dal/test/mongodb/repositories/user.test.ts b/packages/dal/test/mongodb/business/user/repository.test.ts similarity index 92% rename from packages/dal/test/mongodb/repositories/user.test.ts rename to packages/dal/test/mongodb/business/user/repository.test.ts index 1e2fdbbd1983..5f432707eb95 100644 --- a/packages/dal/test/mongodb/repositories/user.test.ts +++ b/packages/dal/test/mongodb/business/user/repository.test.ts @@ -1,15 +1,18 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { Types, type ClientSession, type Model, type Mongoose } from 'mongoose'; import { describe, expect, it, vi } from 'vitest'; -import type { UserDocument, UserMongooseSchemaType } from '../../../mongodb/models/user'; -import { MongoUserRepository } from '../../../mongodb/repositories/user'; -import { MongoTransactionRunner } from '../../../mongodb/transaction'; +import type { + UserDocument, + UserMongooseSchemaType +} from '../../../../mongodb/business/support/user/schema'; +import { MongoUserRepository } from '../../../../mongodb/business/support/user/repository'; +import { MongoTransactionRunner } from '../../../../mongodb/transaction'; import { DatabaseConflictError, DatabaseInvalidArgumentError, DatabaseUnavailableError, DatabaseUniqueConstraintError -} from '../../../db'; +} from '../../../../db'; const userId = '507f1f77bcf86cd799439011'; const document: UserDocument = { @@ -185,39 +188,38 @@ describe('MongoUserRepository.findSemById', () => { }); describe('MongoUserRepository.findByCredentials', () => { - it('passes raw credentials to the Mongoose schema setter', async () => { - const { repository, model } = createRepository(); + it('loads the stored password hash and compares it outside the query filter', async () => { + const { repository, model, queries } = createRepository(); - await repository.findByCredentials({ + const user = await repository.findByCredentials({ username: 'user@example.com', - password: 'plain-password' + password: 'hashed-password' }); - expect(model.findOne).toHaveBeenCalledWith({ - username: 'user@example.com', - password: 'plain-password' - }); + expect(model.findOne).toHaveBeenCalledWith({ username: 'user@example.com' }); + expect(queries.findOne.select).toHaveBeenCalledWith('+password'); + expect(user?.id).toBe(userId); }); it('supports credential lookup by domain id', async () => { const { repository, model } = createRepository(); - await repository.findByCredentials({ id: userId, password: 'plain-password' }); + await repository.findByCredentials({ id: userId, password: 'hashed-password' }); - expect(model.findOne).toHaveBeenCalledWith({ - _id: new Types.ObjectId(userId), - password: 'plain-password' - }); + expect(model.findOne).toHaveBeenCalledWith({ _id: new Types.ObjectId(userId) }); }); - it('returns null for invalid credentials', async () => { + it('returns null when the stored hash does not match', async () => { const { repository, queries } = createRepository(); - queries.findOne.lean.mockResolvedValueOnce(null); + queries.findOne.lean.mockResolvedValueOnce({ + ...document, + password: 'another-hash' + }); await expect( repository.findByCredentials({ username: 'user@example.com', - password: 'wrong-password' + password: 'wrong-hash' }) ).resolves.toBeNull(); }); diff --git a/packages/dal/test/mongodb/models/user.test.ts b/packages/dal/test/mongodb/business/user/schema.test.ts similarity index 80% rename from packages/dal/test/mongodb/models/user.test.ts rename to packages/dal/test/mongodb/business/user/schema.test.ts index 1e5e1a6ce3df..822b87a85289 100644 --- a/packages/dal/test/mongodb/models/user.test.ts +++ b/packages/dal/test/mongodb/business/user/schema.test.ts @@ -1,12 +1,11 @@ -import { hashStr } from '@fastgpt/global/common/string/tools'; import { LangEnum } from '@fastgpt/global/common/i18n/type'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { Mongoose } from 'mongoose'; import { describe, expect, it } from 'vitest'; -import { getUserModel, UserDocumentSchema } from '../../../mongodb/models/user'; +import { getUserModel, UserDocumentSchema } from '../../../../mongodb/business/support/user/schema'; describe('UserDocumentSchema', () => { - it('matches production defaults and hashes password writes', () => { + it('matches production defaults and keeps the provided password hash', () => { const UserModel = getUserModel(new Mongoose()); const document = new UserModel({ username: 'user@example.com', password: 'password' }); @@ -16,8 +15,8 @@ describe('UserDocumentSchema', () => { expect(document.timezone).toBe('Asia/Shanghai'); expect(document.language).toBe(LangEnum.zh_CN); expect(document.tags).toEqual([]); - expect(document.get('password', null, { getters: false })).toBe(hashStr('password')); - expect(document.password).toBe(hashStr('password')); + expect(document.get('password', null, { getters: false })).toBe('password'); + expect(document.password).toBe('password'); expect(UserDocumentSchema.path('password').options.select).toBe(false); }); diff --git a/packages/dal/test/mongodb/repositories/team.test.ts b/packages/dal/test/mongodb/business/user/team/repository.test.ts similarity index 72% rename from packages/dal/test/mongodb/repositories/team.test.ts rename to packages/dal/test/mongodb/business/user/team/repository.test.ts index ba0c3e2afd37..001c6f7cab75 100644 --- a/packages/dal/test/mongodb/repositories/team.test.ts +++ b/packages/dal/test/mongodb/business/user/team/repository.test.ts @@ -1,15 +1,19 @@ import { Types, type Model, type Mongoose } from 'mongoose'; import { describe, expect, it, vi } from 'vitest'; -import { DatabaseInvalidArgumentError } from '../../../db'; -import type { TeamMemberDetail } from '../../../domain/team'; -import type { MemberGroupMongooseSchemaType } from '../../../mongodb/models/memberGroup'; -import type { OrgMongooseSchemaType } from '../../../mongodb/models/org'; -import type { TeamMongooseSchemaType } from '../../../mongodb/models/team'; +import { DatabaseInvalidArgumentError } from '../../../../../db'; +import type { TeamMemberDetail } from '../../../../../business/support/user/team/entity'; +import type { MemberGroupMongooseSchemaType } from '../../../../../mongodb/business/support/user/team/group/schema'; +import type { OrgMemberMongooseSchemaType } from '../../../../../mongodb/business/support/user/team/org/member/schema'; +import { MongoOrgRepository } from '../../../../../mongodb/business/support/user/team/org/repository'; +import type { OrgMongooseSchemaType } from '../../../../../mongodb/business/support/user/team/org/schema'; +import type { TeamMongooseSchemaType } from '../../../../../mongodb/business/support/user/team/schema'; import type { TeamMemberDocument, TeamMemberMongooseSchemaType -} from '../../../mongodb/models/teamMember'; -import { MongoTeamRepository } from '../../../mongodb/repositories/team'; +} from '../../../../../mongodb/business/support/user/team/member/schema'; +import { MongoTeamRepository } from '../../../../../mongodb/business/support/user/team/repository'; +import { MongoGroupRepository } from '../../../../../mongodb/business/support/user/team/group/repository'; +import type { GroupMemberMongooseSchemaType } from '../../../../../mongodb/business/support/user/team/group/member/schema'; const tmbId = '507f1f77bcf86cd799439011'; const teamId = '507f1f77bcf86cd799439012'; @@ -34,14 +38,32 @@ const createQuery = (value: T) => ({ const createRepository = () => { const queries = { - findById: createQuery(tmbDocument), - updateAvatar: createQuery(tmbDocument), + findById: createQuery(tmbDocument), + updateAvatar: createQuery(tmbDocument), existingTmb: createQuery(null) }; const teamCreate = vi.fn(async () => [{ _id: new Types.ObjectId(teamId) }]); const tmbCreate = vi.fn(async () => [{ ...tmbDocument, toObject: vi.fn(() => tmbDocument) }]); - const groupCreate = vi.fn(async () => [{}]); - const orgCreate = vi.fn(async () => [{}]); + const groupCreate = vi.fn(async () => [ + { + toObject: () => ({ + _id: new Types.ObjectId(), + teamId: new Types.ObjectId(teamId), + name: 'DEFAULT_GROUP' + }) + } + ]); + const orgCreate = vi.fn(async () => [ + { + toObject: () => ({ + _id: new Types.ObjectId(), + teamId: new Types.ObjectId(teamId), + pathId: 'root', + path: '', + name: 'ROOT' + }) + } + ]); const teamMemberModel = { findById: vi.fn(() => queries.findById), findByIdAndUpdate: vi.fn(() => queries.updateAvatar), @@ -61,11 +83,21 @@ const createRepository = () => { } as unknown as Model }; + const groupRepository = new MongoGroupRepository( + model.memberGroupModel, + {} as Model + ); + const orgRepository = new MongoOrgRepository( + model.orgModel, + {} as Model + ); + const repository = new MongoTeamRepository( model.teamModel, model.teamMemberModel, - model.memberGroupModel, - model.orgModel + undefined, + groupRepository, + orgRepository ); return { repository, model, queries, teamCreate, tmbCreate, groupCreate, orgCreate }; @@ -162,7 +194,7 @@ describe('MongoTeamRepository.createDefaultTeam', () => { expect(groupCreate).toHaveBeenCalledWith( [ { - teamId: new Types.ObjectId(teamId), + teamId, name: 'DEFAULT_GROUP', avatar: '/icon/logo.svg' } @@ -172,7 +204,7 @@ describe('MongoTeamRepository.createDefaultTeam', () => { expect(orgCreate).toHaveBeenCalledWith( [ { - teamId: new Types.ObjectId(teamId), + teamId, name: 'ROOT', path: '' } diff --git a/packages/dal/test/mongodb/models/team.test.ts b/packages/dal/test/mongodb/business/user/team/schema.test.ts similarity index 85% rename from packages/dal/test/mongodb/models/team.test.ts rename to packages/dal/test/mongodb/business/user/team/schema.test.ts index d24abb707f91..73f884438835 100644 --- a/packages/dal/test/mongodb/models/team.test.ts +++ b/packages/dal/test/mongodb/business/user/team/schema.test.ts @@ -2,12 +2,18 @@ import { Mongoose } from 'mongoose'; import { describe, expect, it } from 'vitest'; import { MemberGroupDocumentSchema, - getMemberGroupModel -} from '../../../mongodb/models/memberGroup'; -import { OrgDocumentSchema, getOrgModel } from '../../../mongodb/models/org'; -import { TeamDocumentSchema, getTeamModel } from '../../../mongodb/models/team'; -import { TeamMemberDocumentSchema, getTeamMemberModel } from '../../../mongodb/models/teamMember'; -import { TmpDataDocumentSchema, getTmpDataModel } from '../../../mongodb/models/tmpData'; + getMemberGroupModel, + OrgDocumentSchema, + getOrgModel, + TeamDocumentSchema, + getTeamModel, + TeamMemberDocumentSchema, + getTeamMemberModel +} from '../../../../../mongodb/business/support/user/team'; +import { + TmpDataDocumentSchema, + getTmpDataModel +} from '../../../../../mongodb/business/support/user/verification'; describe('TeamDocumentSchema', () => { it('matches production defaults and declares production indexes', () => { diff --git a/packages/dal/test/mongodb/repositories/tmpData.test.ts b/packages/dal/test/mongodb/business/user/verification/repository.test.ts similarity index 91% rename from packages/dal/test/mongodb/repositories/tmpData.test.ts rename to packages/dal/test/mongodb/business/user/verification/repository.test.ts index bcb46b095ddf..df46f7882378 100644 --- a/packages/dal/test/mongodb/repositories/tmpData.test.ts +++ b/packages/dal/test/mongodb/business/user/verification/repository.test.ts @@ -1,7 +1,10 @@ import type { Model } from 'mongoose'; import { describe, expect, it, vi } from 'vitest'; -import type { TmpDataDocument, TmpDataMongooseSchemaType } from '../../../mongodb/models/tmpData'; -import { MongoTmpDataRepository } from '../../../mongodb/repositories/tmpData'; +import type { + TmpDataDocument, + TmpDataMongooseSchemaType +} from '../../../../../mongodb/business/support/user/verification/schema'; +import { MongoTmpDataRepository } from '../../../../../mongodb/business/support/user/verification/repository'; const document: TmpDataDocument = { dataId: 'verification:v1:login:password:user@example.com', diff --git a/packages/dal/test/mongodb/middleware.test.ts b/packages/dal/test/mongodb/middleware.test.ts index 0c20c3260c51..ca5fd23047f7 100644 --- a/packages/dal/test/mongodb/middleware.test.ts +++ b/packages/dal/test/mongodb/middleware.test.ts @@ -7,13 +7,23 @@ afterEach(() => { setDalLogger(undefined); }); +type SchemaHookApi = { + execPre: ( + name: string, + context: Record, + callback: (error: unknown) => void + ) => Promise; + execPost: (...args: unknown[]) => Promise; +}; + const runSaveHooks = async (schema: Schema, startTime: number) => { const context: Record = { name: 'item' }; - await schema.s.hooks.execPre('save', context, (error: unknown) => { + const hooks = (schema as unknown as { s: { hooks: SchemaHookApi } }).s.hooks; + await hooks.execPre('save', context, (error: unknown) => { if (error) throw error; }); context._startTime = startTime; - await schema.s.hooks.execPost('save', context, [context], (error: unknown) => { + await hooks.execPost('save', context, [context], (error: unknown) => { if (error) throw error; }); }; diff --git a/packages/dal/test/redis/bullmq-services.test.ts b/packages/dal/test/redis/bullmq-services.test.ts index 2708953bca04..a5ec63670ea2 100644 --- a/packages/dal/test/redis/bullmq-services.test.ts +++ b/packages/dal/test/redis/bullmq-services.test.ts @@ -1,15 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { BullMQBinding } from '@fastgpt/dal/redis/bullmq'; import { AppDeleteMQService, - appDeleteMQService -} from '@fastgpt/dal/redis/bullmq/services/appDelete'; -import { + appDeleteMQService, + CollectionUpdateMQService, DatasetSyncMQService, - datasetSyncMQService -} from '@fastgpt/dal/redis/bullmq/services/datasetSync'; -import { CollectionUpdateMQService } from '@fastgpt/dal/redis/bullmq/services/collectionUpdate'; -import { S3FileDeleteMQService } from '@fastgpt/dal/redis/bullmq/services/s3FileDelete'; + datasetSyncMQService, + S3FileDeleteMQService, + type BullMQBinding +} from '@fastgpt/dal/redis/bullmq'; describe('BullMQ business services', () => { beforeEach(() => { diff --git a/packages/dal/transaction/index.ts b/packages/dal/transaction/index.ts deleted file mode 100644 index 262bc67635b8..000000000000 --- a/packages/dal/transaction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { TransactionContext, TransactionRunner } from './types'; diff --git a/packages/dal/vitest.integration.config.ts b/packages/dal/vitest.integration.config.ts new file mode 100644 index 000000000000..ce96f9c74954 --- /dev/null +++ b/packages/dal/vitest.integration.config.ts @@ -0,0 +1,18 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@fastgpt': resolve('..') + } + }, + test: { + globalSetup: resolve(import.meta.dirname, 'test/integrations/mongodb/global-setup.ts'), + fileParallelism: false, + testTimeout: 20000, + hookTimeout: 30000, + outputFile: 'test-results.integration.mongodb.json', + include: ['test/integrations/mongodb/**/*.integration.test.ts'] + } +}); diff --git a/packages/service/common/dal/index.ts b/packages/service/common/dal/index.ts index 8f86285e72fc..28c2f0b6c107 100644 --- a/packages/service/common/dal/index.ts +++ b/packages/service/common/dal/index.ts @@ -1,18 +1,22 @@ import type { DatabaseAdapter } from '@fastgpt/dal/db'; import { serviceEnv } from '../../env'; -import { createMongoDal } from './mongo'; +import { createMongoAdapter } from './mongo'; export type { DatabaseAdapter } from '@fastgpt/dal/db'; -export type { UserRepository } from '@fastgpt/dal/ports'; -export type { TeamRepository } from '@fastgpt/dal/ports'; -export type { TmpDataRepository } from '@fastgpt/dal/ports'; -export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/transaction'; +export type { + GroupRepository, + OrgRepository, + TeamRepository, + TmpDataRepository, + UserRepository +} from '@fastgpt/dal/business'; +export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/db'; -function selectDal(): DatabaseAdapter { +function selectAdapter(): DatabaseAdapter { // env schema 默认 mongo;被 mock 的 serviceEnv 可能缺少该字段,这里保持同样的默认语义。 switch (serviceEnv.DAL_DB_TYPE ?? 'mongo') { case 'mongo': { - return createMongoDal(); + return createMongoAdapter(); } default: { @@ -22,6 +26,13 @@ function selectDal(): DatabaseAdapter { } // 只实例化一次 adapter,保证 userRepository 与 transactionRunner 来自同一个 adapter 实例。 -const dal = selectDal(); +const dal = selectAdapter(); -export const { transactionRunner, userRepository, teamRepository, tmpDataRepository } = dal; +export const { + transactionRunner, + userRepository, + teamRepository, + groupRepository, + orgRepository, + tmpDataRepository +} = dal; diff --git a/packages/service/common/dal/mongo/config.ts b/packages/service/common/dal/mongo/config.ts deleted file mode 100644 index 93d52414f1dd..000000000000 --- a/packages/service/common/dal/mongo/config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { serviceEnv } from '../../../env'; -import type { ConnectOptions, Mongoose } from 'mongoose'; -import { getLogger, LogCategories } from '../../logger'; - -const logger = getLogger(LogCategories.INFRA.MONGO); - -export const createDefaultMongooseConnectOptions = (): ConnectOptions => { - const maxConnecting = Math.max(5, serviceEnv.DB_MAX_LINK); - - return { - bufferCommands: true, - maxConnecting, - maxPoolSize: maxConnecting, - minPoolSize: 1, - connectTimeoutMS: 60_000, - waitQueueTimeoutMS: 60_000, - socketTimeoutMS: 60_000, - maxIdleTimeMS: 300_000, - retryWrites: true, - retryReads: true, - serverSelectionTimeoutMS: 10_000, - heartbeatFrequencyMS: 5_000 - }; -}; - -export const registerMongooseListeners = (db: Mongoose) => { - db.connection.removeAllListeners('error'); - db.connection.removeAllListeners('connected'); - db.connection.removeAllListeners('disconnected'); - - db.connection.on('error', (error) => { - logger.error('DAL MongoDB connection error', { error, readyState: db.connection.readyState }); - }); - db.connection.on('connected', () => logger.info('DAL MongoDB connected successfully')); - db.connection.on('disconnected', () => logger.warn('DAL MongoDB disconnected')); -}; diff --git a/packages/service/common/dal/mongo/connection.ts b/packages/service/common/dal/mongo/connection.ts deleted file mode 100644 index f11a82461812..000000000000 --- a/packages/service/common/dal/mongo/connection.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Mongoose } from 'mongoose'; -import { delay } from '@fastgpt/global/common/system/utils'; -import { getLogger, LogCategories } from '../../logger'; -import { serviceEnv } from '../../../env'; -import { createDefaultMongooseConnectOptions, registerMongooseListeners } from './config'; -import { syncDalModelIndexes } from './lifecycle'; - -const logger = getLogger(LogCategories.INFRA.MONGO); - -export class MongooseConnection { - private readonly mongoose: Mongoose; - - constructor() { - this.mongoose = new Mongoose(); - } - - get client(): Mongoose { - return this.mongoose; - } - - async connect(url: string = serviceEnv.MONGODB_URI): Promise { - const db = this.mongoose; - - if (db.connection.readyState !== 0) { - return db; - } - - db.set('strictQuery', 'throw'); - - registerMongooseListeners(db); - - try { - const options = createDefaultMongooseConnectOptions(); - await db.connect(url, { - ...options - }); - // DAL Model 索引同步与旧 Model 同样为异步补偿:失败只记录日志,不阻塞启动。 - void syncDalModelIndexes(db).catch((error) => { - logger.warn('DAL MongoDB index sync failed during connect', { error }); - }); - return db; - } catch (error) { - logger.error('DAL MongoDB connection failed, will retry', { error }); - try { - await db.disconnect(); - } catch (disconnectError) { - logger.warn('DAL MongoDB disconnect failed during retry', { error: disconnectError }); - } - await delay(1000); - return this.connect(url); - } - } - - async disconnect(): Promise { - if (this.mongoose.connection.readyState !== 0) { - await this.mongoose.disconnect(); - } - } -} - -export const connection = new MongooseConnection(); diff --git a/packages/service/common/dal/mongo/index.ts b/packages/service/common/dal/mongo/index.ts index 2e1d3d75b4f1..b6591ac834fd 100644 --- a/packages/service/common/dal/mongo/index.ts +++ b/packages/service/common/dal/mongo/index.ts @@ -2,7 +2,7 @@ import { MongoAdapter } from '@fastgpt/dal/mongodb'; import { setDalLogger } from '@fastgpt/dal/mongodb'; import type { DatabaseAdapter } from '@fastgpt/dal/db'; import type { Mongoose } from 'mongoose'; -import { connection } from './connection'; +import { connectionMongo } from '../../mongo'; import { getLogger, LogCategories } from '../../logger'; // 把业务 logger 注入 DAL 慢查询中间件,避免 DAL 包反向依赖 service 日志实现。 @@ -11,6 +11,6 @@ if (LogCategories.INFRA?.MONGO) { setDalLogger(getLogger(LogCategories.INFRA.MONGO)); } -export const createMongoDal = (client: Mongoose = connection.client): DatabaseAdapter => { +export const createMongoAdapter = (client: Mongoose = connectionMongo): DatabaseAdapter => { return new MongoAdapter({ client }); }; diff --git a/packages/service/common/dal/mongo/lifecycle.ts b/packages/service/common/dal/mongo/lifecycle.ts index c6a3dcae4b34..af6885555eb4 100644 --- a/packages/service/common/dal/mongo/lifecycle.ts +++ b/packages/service/common/dal/mongo/lifecycle.ts @@ -1,12 +1,16 @@ import type { Mongoose } from 'mongoose'; import { getLogger, LogCategories } from '../../logger'; import { MongoIndexManager } from '../../mongo'; -import { getMemberGroupModel } from '@fastgpt/dal/mongodb/models/memberGroup'; -import { getOrgModel } from '@fastgpt/dal/mongodb/models/org'; -import { getTeamModel } from '@fastgpt/dal/mongodb/models/team'; -import { getTeamMemberModel } from '@fastgpt/dal/mongodb/models/teamMember'; -import { getTmpDataModel } from '@fastgpt/dal/mongodb/models/tmpData'; -import { getUserModel } from '@fastgpt/dal/mongodb/models/user'; +import { + getMemberGroupModel, + getGroupMemberModel, + getOrgModel, + getOrgMemberModel, + getTeamMemberModel, + getTeamModel, + getTmpDataModel, + getUserModel +} from '@fastgpt/dal/mongodb/business'; import { serviceEnv } from '../../../env'; const logger = getLogger(LogCategories.INFRA.MONGO); @@ -31,7 +35,9 @@ export const syncDalModelIndexes = async (client: Mongoose) => { getTeamModel(client), getTeamMemberModel(client), getMemberGroupModel(client), + getGroupMemberModel(client), getOrgModel(client), + getOrgMemberModel(client), getTmpDataModel(client) ]; diff --git a/packages/service/support/invoke/invoke.ts b/packages/service/support/invoke/invoke.ts index b596e13f23b2..62a7c219ac56 100644 --- a/packages/service/support/invoke/invoke.ts +++ b/packages/service/support/invoke/invoke.ts @@ -17,9 +17,8 @@ import { removeS3TTL } from '../../common/s3/utils'; import { serviceEnv } from '../../env'; import { getGroupsByTmbId } from '../permission/memberGroup/controllers'; import { getOrgsByTmbId } from '../permission/org/controllers'; -import { MongoOrgModel } from '../permission/org/orgSchema'; import { getUserDetail } from '../user/controller'; -import { MongoTeam } from '../user/team/teamSchema'; +import { orgRepository, teamRepository } from '../../common/dal'; import { InvokeFileUploadSchema, InvokeSessionSchema, type InvokeFileUploadType } from './type'; import type { InvokeSessionType } from './type'; import { addHours } from 'date-fns'; @@ -124,25 +123,13 @@ export class InvokeProcessor { getUserDetail({ tmbId }), getOrgsByTmbId({ teamId, tmbId }), getGroupsByTmbId({ tmbId, teamId }), - MongoTeam.findById(teamId, { - name: 1 - }).lean() + teamRepository.findTeamById(teamId) ]); if (!team) throw new Error('Team not found'); const orgInfos = orgs.length - ? await MongoOrgModel.find( - { - _id: { - $in: orgs.map((org) => org.orgId) - } - }, - { - name: 1, - pathId: 1 - } - ).lean() + ? await orgRepository.findOrgsByIds(orgs.map((org) => String(org.orgId))) : []; return { diff --git a/packages/service/support/marketing/interface/crm.ts b/packages/service/support/marketing/interface/crm.ts index c70c1210cf86..bf483bdb6c32 100644 --- a/packages/service/support/marketing/interface/crm.ts +++ b/packages/service/support/marketing/interface/crm.ts @@ -1,8 +1,7 @@ import { successMarkerCache, type SuccessMarkerParams } from '@fastgpt/dal/redis/caches'; import { FastGPT_SEM_Schema } from '@fastgpt/global/support/marketing/type'; import { getLogger, LogCategories } from '../../../common/logger'; -import { userRepository } from '../../../common/dal'; -import { MongoTeam } from '../../user/team/teamSchema'; +import { teamRepository, userRepository } from '../../../common/dal'; import { isCRMReportingConfigured, reportCRMEnterpriseRechargeAmount, @@ -81,7 +80,7 @@ const reportCRMTeamEventOnce = async ({ } try { - const team = await MongoTeam.findById(teamId, 'ownerId name').lean(); + const team = await teamRepository.findTeamById(teamId); if (!team?.ownerId) return; const user = await userRepository.findSemById(String(team.ownerId)); diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 5872c4ed62ef..fae7748d1295 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -15,9 +15,7 @@ import type { CollaboratorItemDetailType, CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; -import { MongoTeamMember } from '../../support/user/team/teamMemberSchema'; -import { MongoOrgModel } from './org/orgSchema'; -import { MongoMemberGroupModel } from './memberGroup/memberGroupSchema'; +import { groupRepository, orgRepository, teamRepository } from '../../common/dal'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; /** get resource permission for a team member @@ -154,16 +152,15 @@ export const getClbsInfo = async ({ if (clb.groupId) groupIds.push(clb.groupId); } - const infos = ( - await Promise.all([ - MongoTeamMember.find({ _id: { $in: tmbIds }, teamId }, '_id name avatar').lean(), - MongoOrgModel.find({ _id: { $in: orgIds }, teamId }, '_id name avatar').lean(), - MongoMemberGroupModel.find({ _id: { $in: groupIds }, teamId }, '_id name avatar').lean() - ]) - ).flat(); + const [members, orgs, groups] = await Promise.all([ + teamRepository.findMembersByIds(tmbIds, { teamId }), + orgRepository.findOrgsByIds(orgIds), + groupRepository.findMemberGroupsByIds(groupIds) + ]); + const infos = [...members, ...orgs, ...groups].filter((info) => info.teamId === teamId); return clbs.map((clb) => { - const info = infos.find((info) => info._id === getCollaboratorId(clb)); + const info = infos.find((info) => info.id === getCollaboratorId(clb)); return { ...clb, diff --git a/packages/service/support/permission/dataClean/danglingPermission.ts b/packages/service/support/permission/dataClean/danglingPermission.ts index fd832d1ea36e..314f854ede5d 100644 --- a/packages/service/support/permission/dataClean/danglingPermission.ts +++ b/packages/service/support/permission/dataClean/danglingPermission.ts @@ -9,11 +9,8 @@ import { Types } from '../../../common/mongo'; import { MongoAgentSkills } from '../../../core/ai/skill/model/schema'; import { MongoApp } from '../../../core/app/schema'; import { MongoDataset } from '../../../core/dataset/schema'; -import { MongoMemberGroupModel } from '../memberGroup/memberGroupSchema'; -import { MongoOrgModel } from '../org/orgSchema'; import { MongoResourcePermission } from '../schema'; -import { MongoTeamMember } from '../../user/team/teamMemberSchema'; -import { MongoTeam } from '../../user/team/teamSchema'; +import { groupRepository, orgRepository, teamRepository } from '../../../common/dal'; type PermissionReferenceDoc = { _id: Types.ObjectId; @@ -117,12 +114,10 @@ async function findDanglingPermissionsInBatch( const orgIds = compactUniqueIds(permissions.map((permission) => permission.orgId)); const [teams, teamMembers, groups, orgs, resourceReferences] = await Promise.all([ - MongoTeam.find({ _id: { $in: teamIds } }, '_id').lean(), - MongoTeamMember.find({ _id: { $in: tmbIds } }, '_id teamId').lean(), - MongoMemberGroupModel.find({ _id: { $in: groupIds } }, '_id teamId').lean< - TeamScopedReferenceDoc[] - >(), - MongoOrgModel.find({ _id: { $in: orgIds } }, '_id teamId').lean(), + teamRepository.findTeamReferencesByIds(teamIds.map(stringifyId)), + teamRepository.findMemberReferencesByIds(tmbIds.map(stringifyId)), + groupRepository.findMemberGroupReferencesByIds(groupIds.map(stringifyId)), + orgRepository.findOrgReferencesByIds(orgIds.map(stringifyId)), Promise.all( resourceReferenceConfigs.map(async (config) => { const ids = compactUniqueIds( @@ -136,11 +131,15 @@ async function findDanglingPermissionsInBatch( ) ]); - const existingTeamIds = new Set(teams.map((team) => stringifyId(team._id))); + const existingTeamIds = new Set(teams.map((team) => team.id)); const existingSubjectReferenceSets = { - tmbId: toTeamScopedReferenceSet(teamMembers), - groupId: toTeamScopedReferenceSet(groups), - orgId: toTeamScopedReferenceSet(orgs) + tmbId: toTeamScopedReferenceSet( + teamMembers.map((member) => ({ _id: member.id, teamId: member.teamId })) + ), + groupId: toTeamScopedReferenceSet( + groups.map((group) => ({ _id: group.id, teamId: group.teamId })) + ), + orgId: toTeamScopedReferenceSet(orgs.map((org) => ({ _id: org.id, teamId: org.teamId }))) }; const subjectReasonMap = { tmbId: 'missingTeamMember', diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 3620a8b24f1d..95021222ec8e 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -1,85 +1,74 @@ -import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; -import { MongoGroupMemberModel } from './groupMemberSchema'; -import { MongoMemberGroupModel } from './memberGroupSchema'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { type ClientSession } from 'mongoose'; import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; import { type AuthModeType, type AuthResponseType } from '../type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { parseHeaderCert } from '../auth/common'; +import { groupRepository, type TransactionContext } from '../../../common/dal'; + +const toLegacyGroup = ( + group: NonNullable>> +) => ({ + _id: group.id, + teamId: group.teamId, + name: group.name, + avatar: group.avatar, + updateTime: group.updateTime +}); /** * Get the default group of a team * @param{Object} obj * @param{string} obj.teamId - * @param{ClientSession} obj.session + * @param{TransactionContext} obj.context */ export const getTeamDefaultGroup = async ({ teamId, - session + context }: { teamId: string; - session?: ClientSession; + context?: TransactionContext; }) => { - const group = await MongoMemberGroupModel.findOne({ teamId, name: DefaultGroupName }, undefined, { - session - }).lean(); - - // Create the default group if it does not exist - if (!group) { - const [group] = await MongoMemberGroupModel.create( - [ - { - teamId, - name: DefaultGroupName - } - ], - { session } - ); + const group = await groupRepository.findMemberGroupByTeamAndName( + teamId, + DefaultGroupName, + context + ); + if (group) return toLegacyGroup(group); - return group; - } - return group; + return toLegacyGroup( + await groupRepository.createMemberGroup({ teamId, name: DefaultGroupName }, context) + ); }; export const getGroupsByTmbId = async ({ tmbId, teamId, role, - session + context }: { tmbId: string; teamId: string; role?: `${GroupMemberRole}`[]; - session?: ClientSession; -}) => - ( - await Promise.all([ - ( - await MongoGroupMemberModel.find( - { - tmbId, - groupId: { - $exists: true - }, - ...(role ? { role: { $in: role } } : {}) - }, - undefined, - { session } - ) - .populate<{ group: MemberGroupSchemaType }>('group') - .lean() - ).map((item) => item.group), - role ? [] : getTeamDefaultGroup({ teamId, session }) - ]) - ).flat(); + context?: TransactionContext; +}) => [ + ...(await groupRepository.findGroupsByTmbId(teamId, tmbId, role, context)).map((group) => + toLegacyGroup(group) + ), + ...(role + ? [] + : await getTeamDefaultGroup({ teamId, context }).then((group) => (group ? [group] : []))) +]; export const getGroupMembersByGroupId = async (groupId: string) => { - return await MongoGroupMemberModel.find({ - groupId - }).lean(); + const members = await groupRepository.findGroupMembersByGroupId(groupId); + return members.map((member) => ({ + _id: member.id, + groupId: member.groupId, + tmbId: member.tmbId, + role: member.role + })); }; // auth group member role @@ -104,7 +93,7 @@ export const authGroupMemberRole = async ({ }; } const [groupMember, tmb] = await Promise.all([ - MongoGroupMemberModel.findOne({ groupId, tmbId }), + groupRepository.findGroupMember(groupId, tmbId), getTmbInfoByTmbId({ tmbId }) ]); diff --git a/packages/service/support/permission/org/controllers.ts b/packages/service/support/permission/org/controllers.ts index dd0d7dbab133..109a27b128e1 100644 --- a/packages/service/support/permission/org/controllers.ts +++ b/packages/service/support/permission/org/controllers.ts @@ -1,12 +1,23 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; -import type { ClientSession } from 'mongoose'; -import { MongoOrgModel } from './orgSchema'; -import { MongoOrgMemberModel } from './orgMemberSchema'; -import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant'; +import { orgRepository, type TransactionContext } from '../../../common/dal'; + +const toLegacyOrg = (org: NonNullable>>) => ({ + _id: org.id, + teamId: org.teamId, + pathId: org.pathId, + path: org.path, + name: org.name, + avatar: org.avatar, + description: org.description, + updateTime: org.updateTime +}); export const getOrgsByTmbId = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) => - MongoOrgMemberModel.find({ teamId, tmbId }, 'orgId').lean(); + (await orgRepository.findOrgMembersByTmbId(teamId, tmbId)).map((member) => ({ + _id: member.orgId, + orgId: member.orgId + })); export const getOrgIdSetWithParentByTmbId = async ({ teamId, @@ -15,10 +26,10 @@ export const getOrgIdSetWithParentByTmbId = async ({ teamId: string; tmbId: string; }) => { - const orgMembers = await MongoOrgMemberModel.find({ teamId, tmbId }, 'orgId').lean(); + const orgMembers = await orgRepository.findOrgMembersByTmbId(teamId, tmbId); - const orgIds = Array.from(new Set(orgMembers.map((item) => String(item.orgId)))); - const orgs = await MongoOrgModel.find({ _id: { $in: orgIds } }, 'path').lean(); + const orgIds = Array.from(new Set(orgMembers.map((item) => item.orgId))); + const orgs = await orgRepository.findOrgsByIds(orgIds); const pathIdList = new Set( orgs @@ -28,14 +39,8 @@ export const getOrgIdSetWithParentByTmbId = async ({ }) .flat() ); - const parentOrgs = await MongoOrgModel.find( - { - teamId, - pathId: { $in: Array.from(pathIdList) } - }, - '_id' - ).lean(); - const parentOrgIds = parentOrgs.map((item) => String(item._id)); + const parentOrgs = await orgRepository.findOrgsByTeamAndPathIds(teamId, Array.from(pathIdList)); + const parentOrgIds = parentOrgs.map((item) => item.id); return new Set([...orgIds, ...parentOrgIds]); }; @@ -43,53 +48,48 @@ export const getOrgIdSetWithParentByTmbId = async ({ export const getChildrenByOrg = async ({ org, teamId, - session + context }: { org: OrgSchemaType; teamId: string; - session?: ClientSession; + context?: TransactionContext; }) => { - return MongoOrgModel.find( - { teamId, path: { $regex: `^${getOrgChildrenPath(org)}` } }, - undefined, - { - session - } - ).lean(); + return (await orgRepository.findOrgChildren(org, teamId, context)).map((item) => ({ + _id: item.id, + teamId: item.teamId, + pathId: item.pathId, + path: item.path, + name: item.name, + avatar: item.avatar, + description: item.description, + updateTime: item.updateTime + })); }; export const getOrgAndChildren = async ({ orgId, teamId, - session + context }: { orgId: string; teamId: string; - session?: ClientSession; + context?: TransactionContext; }) => { - const org = await MongoOrgModel.findOne({ _id: orgId, teamId }, undefined, { session }).lean(); + const org = await orgRepository.findOrgById(orgId, teamId, context); if (!org) { return Promise.reject(TeamErrEnum.orgNotExist); } - const children = await getChildrenByOrg({ org, teamId, session }); - return { org, children }; + const children = await orgRepository.findOrgChildren(org, teamId, context); + return { org: toLegacyOrg(org), children: children.map(toLegacyOrg) }; }; export async function createRootOrg({ teamId, - session + context }: { teamId: string; - session?: ClientSession; + context?: TransactionContext; }) { - return MongoOrgModel.create( - [ - { - teamId, - name: 'ROOT', - path: '' - } - ], - { session, ordered: true } - ); + const org = await orgRepository.createOrg({ teamId, name: 'ROOT', path: '' }, context); + return [toLegacyOrg(org)]; } diff --git a/packages/service/support/permission/teamLimit.ts b/packages/service/support/permission/teamLimit.ts index 358b8ab993a1..5666b55f0781 100644 --- a/packages/service/support/permission/teamLimit.ts +++ b/packages/service/support/permission/teamLimit.ts @@ -5,8 +5,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { SystemErrEnum } from '@fastgpt/global/common/error/code/system'; import { AppTypeEnum, ToolTypeList, AppFolderTypeList } from '@fastgpt/global/core/app/constants'; -import { MongoTeamMember } from '../user/team/teamMemberSchema'; -import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; +import { teamRepository } from '../../common/dal'; import { getVectorCountByTeamId } from '../../common/vectorDB/controller'; import { serviceEnv } from '../../env'; @@ -30,10 +29,7 @@ export const checkTeamMemberLimit = async (teamId: string, newCount: number) => getTeamStandPlan({ teamId }), - MongoTeamMember.countDocuments({ - teamId, - status: { $ne: TeamMemberStatusEnum.leave } - }) + teamRepository.countMembersByTeamId(teamId, { includeLeft: false }) ]); if (standard?.maxTeamMember && newCount + memberCount > standard.maxTeamMember) { diff --git a/packages/service/support/tmpData/controller.ts b/packages/service/support/tmpData/controller.ts index 3a247471d377..324d271ed12d 100644 --- a/packages/service/support/tmpData/controller.ts +++ b/packages/service/support/tmpData/controller.ts @@ -4,7 +4,7 @@ import { type TmpDataMetadata, type TmpDataType } from '@fastgpt/global/support/tmpData/constants'; -import { MongoTmpData } from './schema'; +import { tmpDataRepository } from '../../common/dal'; import { type TmpDataSchema } from '@fastgpt/global/support/tmpData/type'; import { addMilliseconds } from 'date-fns'; @@ -19,9 +19,9 @@ export async function getTmpData({ type: T; metadata: TmpDataMetadata; }) { - return (await MongoTmpData.findOne({ - dataId: getDataId(type, metadata) - }).lean()) as TmpDataSchema> | null; + return (await tmpDataRepository.findByDataId(getDataId(type, metadata))) as TmpDataSchema< + TmpDataType + > | null; } export function setTmpData({ @@ -33,17 +33,9 @@ export function setTmpData({ metadata: TmpDataMetadata; data: TmpDataType; }) { - return MongoTmpData.updateOne( - { - dataId: getDataId(type, metadata) - }, - { - dataId: getDataId(type, metadata), - data, - expireAt: addMilliseconds(Date.now(), TmpDataExpireTime[type]) - }, - { - upsert: true - } - ); + return tmpDataRepository.upsert({ + dataId: getDataId(type, metadata), + data, + expireAt: addMilliseconds(Date.now(), TmpDataExpireTime[type]) + }); } diff --git a/packages/service/support/tmpData/verification.ts b/packages/service/support/tmpData/verification.ts index f93a210d287a..8f4bb0510b45 100644 --- a/packages/service/support/tmpData/verification.ts +++ b/packages/service/support/tmpData/verification.ts @@ -122,37 +122,40 @@ export const verification = { */ createIfInactive: async (params: VerificationCreateParams) => { const dataId = getDataId(params); - const sessionOptions = params.session ? { session: params.session } : {}; - - await MongoTmpData.deleteOne( - { - dataId, - expireAt: { $lte: new Date() } - }, - sessionOptions - ); - - try { - await MongoTmpData.create( - [ - { - dataId, - data: params.data, - expireAt: getExpireAt(params.ttlPreset) - } - ], - sessionOptions - ); - return true; - } catch (error) { - if (isMongoDuplicateKeyError(error)) return false; - throw error; + if (params.session) { + const sessionOptions = { session: params.session }; + + await MongoTmpData.deleteOne({ dataId, expireAt: { $lte: new Date() } }, sessionOptions); + + try { + await MongoTmpData.create( + [{ dataId, data: params.data, expireAt: getExpireAt(params.ttlPreset) }], + sessionOptions + ); + return true; + } catch (error) { + if (isMongoDuplicateKeyError(error)) return false; + throw error; + } } + + return tmpDataRepository.createIfInactive({ + dataId, + data: params.data, + expireAt: getExpireAt(params.ttlPreset) + }); }, /** 覆盖同一场景、类型和 key 的材料,并刷新过期时间。 */ upsert: async (params: VerificationUpsertParams) => { const dataId = getDataId(params); + if (!params.session) { + return tmpDataRepository.upsert({ + dataId, + data: params.data, + expireAt: getExpireAt(params.ttlPreset) + }); + } return MongoTmpData.updateOne( { dataId }, @@ -167,6 +170,14 @@ export const verification = { /** 只更新仍在有效期内的已有材料,避免回调重新创建或刷新过期材料。 */ updateIfActive: async (params: VerificationUpdateParams) => { + if (!params.session) { + return tmpDataRepository.updateIfActive({ + dataId: getDataId(params), + data: params.data, + expireAt: getExpireAt(params.ttlPreset) + }); + } + return MongoTmpData.updateOne( { dataId: getDataId(params), @@ -185,10 +196,14 @@ export const verification = { /** 只删除仍有效且匹配当前材料内容的记录,避免清理并发请求新写入的验证码。 */ deleteIfMatch: async (params: VerificationDeleteParams) => { const { session, ...filterParams } = params; + if (!session) { + return tmpDataRepository.deleteActiveMaterial({ + dataId: getDataId(filterParams), + match: filterParams.match as Record | undefined + }); + } - return MongoTmpData.deleteOne(getActiveFilter(filterParams), { - ...(session ? { session } : {}) - }); + return MongoTmpData.deleteOne(getActiveFilter(filterParams), { session }); }, /** 读取仍在有效期内的材料,不主动改变材料生命周期。 */ @@ -196,7 +211,12 @@ export const verification = { params: VerificationGetParams ): Promise | null> => { const { session, ...filterParams } = params; - const result = await findActiveRecord(filterParams, session); + const result = session + ? await findActiveRecord(filterParams, session) + : await tmpDataRepository.findActiveMaterial({ + dataId: getDataId(filterParams), + match: filterParams.match as Record | undefined + }); return result ? (result.data as VerificationMaterial) : null; }, @@ -204,26 +224,25 @@ export const verification = { /** 判断材料是否仍在有效期内,用于区分未完成状态和已过期状态。 */ hasActive: async (params: VerificationGetParams): Promise => { const { session, ...filterParams } = params; - const result = await findActiveRecord(filterParams, session); + const result = session + ? await findActiveRecord(filterParams, session) + : await tmpDataRepository.findActiveMaterial({ + dataId: getDataId(filterParams), + match: filterParams.match as Record | undefined + }); return Boolean(result); }, /** 通过精确 dataId 候选查找唯一有效材料;多个候选命中视为数据冲突。 */ findUniqueActiveDataId: async (dataIds: readonly string[]): Promise => { - const results = await MongoTmpData.find({ - dataId: { $in: [...new Set(dataIds)] }, - expireAt: { $gt: new Date() } - }) - .select({ dataId: 1 }) - .limit(2) - .lean(); + const results = await tmpDataRepository.findActiveDataIds(dataIds); if (results.length > 1) { throw new Error('Verification material data id conflict'); } - return results[0]?.dataId ?? null; + return results[0] ?? null; }, /** 在同一 Mongo 事务内读取材料、执行业务回调,并在回调成功后消费材料。 */ diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index 24da88e65de4..deefbd26fc2b 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -1,168 +1,92 @@ -import { type TeamSchema, type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; -import { type ClientSession, Types } from '../../../common/mongo'; +import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberRoleEnum, - TeamMemberStatusEnum, - notLeaveStatus + TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; -import { MongoTeamMember } from './teamMemberSchema'; -import { MongoTeam } from './teamSchema'; import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; -import { getTmbPermission } from '../../permission/controller'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant'; -import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema'; -import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { getTmbPermission } from '../../permission/controller'; import { getAIApi } from '../../../core/ai/config'; -import { createRootOrg } from '../../permission/org/controllers'; import { getS3AvatarSource } from '../../../common/s3/sources/avatar'; -import { getLogger, LogCategories } from '../../../common/logger'; +import { groupRepository, transactionRunner, teamRepository } from '../../../common/dal'; +import type { TransactionContext } from '../../../common/dal'; -const logger = getLogger(LogCategories.MODULE.USER.TEAM); +type TeamMemberRelations = Awaited>; -async function getTeamMember( - match: Record, - session?: ClientSession -): Promise { - const query = MongoTeamMember.findOne(match).populate<{ team: TeamSchema }>('team'); - if (session) query.session(session); - const tmb = await query.lean(); - if (!tmb) { +/** 将 DAL 的成员、团队实体转换为 service 层历史使用的权限上下文。 */ +const getTeamMember = async (relations: TeamMemberRelations): Promise => { + if (!relations?.team) return Promise.reject('member not exist'); + + const { member, team } = relations; + if ( + member.status === TeamMemberStatusEnum.leave || + member.status === TeamMemberStatusEnum.forbidden + ) { return Promise.reject('member not exist'); } const role = (await getTmbPermission({ resourceType: PerResourceTypeEnum.team, - teamId: tmb.teamId, - tmbId: tmb._id + teamId: member.teamId, + tmbId: member.id })) ?? TeamDefaultRoleVal; + const openaiAccount = + team.openaiAccount?.key && team.openaiAccount.baseUrl ? team.openaiAccount : undefined; return { - userId: String(tmb.userId), - teamId: String(tmb.teamId), - teamAvatar: tmb.team.avatar, - teamName: tmb.team.name, - memberName: tmb.name, - avatar: tmb.avatar, - balance: tmb.team.balance, - tmbId: String(tmb._id), - role: tmb.role, - status: tmb.status, + userId: member.userId, + teamId: member.teamId, + teamAvatar: team.avatar, + teamName: team.name, + memberName: member.name, + avatar: member.avatar ?? '', + balance: team.balance, + tmbId: member.id, + role: member.role as TeamTmbItemType['role'], + status: member.status as TeamTmbItemType['status'], permission: new TeamPermission({ role, - isOwner: tmb.role === TeamMemberRoleEnum.owner + isOwner: member.role === TeamMemberRoleEnum.owner }), - notificationAccount: tmb.team.notificationAccount, - - openaiAccount: tmb.team.openaiAccount, - externalWorkflowVariables: tmb.team.externalWorkflowVariables, - isWecomTeam: !!tmb.team.meta?.wecom + notificationAccount: team.notificationAccount, + openaiAccount, + externalWorkflowVariables: team.externalWorkflowVariables, + isWecomTeam: !!team.meta?.wecom }; -} - -export const getTeamOwner = async (teamId: string) => { - const tmb = await MongoTeamMember.findOne({ - teamId, - role: TeamMemberRoleEnum.owner - }).lean(); - return tmb; }; -export async function getTmbInfoByTmbId({ - tmbId, - session -}: { - tmbId: string; - session?: ClientSession; -}) { +export const getTeamOwner = (teamId: string) => teamRepository.findOwnerByTeamId(teamId); + +export async function getTmbInfoByTmbId({ tmbId }: { tmbId: string }) { if (!tmbId) { return Promise.reject('tmbId or userId is required'); } - return getTeamMember( - { - _id: new Types.ObjectId(String(tmbId)), - status: notLeaveStatus - }, - session - ); + return getTeamMember(await teamRepository.findMemberRelationsById(tmbId)); } -export async function getUserDefaultTeam({ - userId, - session -}: { - userId: string; - session?: ClientSession; -}) { +export async function getUserDefaultTeam({ userId }: { userId: string }) { if (!userId) { return Promise.reject('tmbId or userId is required'); } - return getTeamMember({ userId: new Types.ObjectId(userId) }, session); + return getTeamMember(await teamRepository.findMemberRelationsByUserId(userId)); } export async function createDefaultTeam({ userId, teamName = 'My Team', avatar = '/icon/logo.svg', - session + context }: { userId: string; teamName?: string; avatar?: string; - session: ClientSession; + context?: TransactionContext; }) { - // auth default team - const tmb = await MongoTeamMember.findOne({ - userId: new Types.ObjectId(userId) - }); - - if (!tmb) { - // create team - const [{ _id: insertedId }] = await MongoTeam.create( - [ - { - ownerId: userId, - name: teamName, - avatar, - createTime: new Date() - } - ], - { session } - ); - // create team member - const [tmb] = await MongoTeamMember.create( - [ - { - teamId: insertedId, - userId, - name: 'Owner', - role: TeamMemberRoleEnum.owner, - status: TeamMemberStatusEnum.active, - createTime: new Date() - } - ], - { session } - ); - // create default group - await MongoMemberGroupModel.create( - [ - { - teamId: tmb.teamId, - name: DefaultGroupName, - avatar - } - ], - { session } - ); - await createRootOrg({ teamId: tmb.teamId, session }); - logger.info('Default team created', { userId, teamId: tmb.teamId, tmbId: tmb._id }); - return tmb; - } else { - logger.info('Default team exists', { userId }); - } + return teamRepository.createDefaultTeam({ userId, teamName, avatar, context }); } export async function updateTeam({ @@ -172,13 +96,14 @@ export async function updateTeam({ openaiAccount, externalWorkflowVariable }: UpdateTeamProps & { teamId: string }) { - // auth openai key if (openaiAccount?.key) { - const baseUrl = openaiAccount?.baseUrl || 'https://api.openai.com/v1'; - openaiAccount.baseUrl = baseUrl; + const normalizedOpenaiAccount = { + ...openaiAccount, + baseUrl: openaiAccount.baseUrl || 'https://api.openai.com/v1' + }; const { ai } = getAIApi({ - userKey: openaiAccount + userKey: normalizedOpenaiAccount }); const response = await ai.chat.completions.create({ @@ -188,72 +113,30 @@ export async function updateTeam({ if (response?.choices?.[0]?.message?.content === undefined) { return Promise.reject('Key response is empty'); } - } - - return mongoSessionRun(async (session) => { - const unsetObj = (() => { - const obj: Record = {}; - if (openaiAccount?.key === '') { - obj.openaiAccount = 1; - } - if (externalWorkflowVariable) { - if (externalWorkflowVariable.value === '') { - obj[`externalWorkflowVariables.${externalWorkflowVariable.key}`] = 1; - } - } - if (Object.keys(obj).length === 0) { - return undefined; - } - return { - $unset: obj - }; - })(); - const setObj = (() => { - const obj: Record = {}; - if (openaiAccount?.key && openaiAccount?.baseUrl) { - obj.openaiAccount = openaiAccount; - } - if (externalWorkflowVariable) { - if (externalWorkflowVariable.value !== '') { - obj[`externalWorkflowVariables.${externalWorkflowVariable.key}`] = - externalWorkflowVariable.value; - } - } - if (Object.keys(obj).length === 0) { - return undefined; - } - return obj; - })(); + openaiAccount = normalizedOpenaiAccount; + } - // This is where we get the old team - const team = await MongoTeam.findByIdAndUpdate( + const oldTeam = await teamRepository.findTeamById(teamId); + await transactionRunner.withTransaction(async (context) => { + await teamRepository.updateTeam( teamId, { - $set: { - ...(name ? { name } : {}), - ...(avatar ? { avatar } : {}), - ...setObj - }, - ...unsetObj + name, + avatar, + openaiAccount: openaiAccount?.key ? openaiAccount : undefined, + clearOpenaiAccount: openaiAccount?.key === '', + externalWorkflowVariable }, - { session } + context ); - // Update member group avatar if (avatar) { - await MongoMemberGroupModel.updateOne( - { - teamId: teamId, - name: DefaultGroupName - }, - { - avatar - }, - { session } - ); - - await getS3AvatarSource().refreshAvatar(avatar, team?.avatar, session); + await groupRepository.updateMemberGroupAvatar(teamId, DefaultGroupName, avatar, context); } }); + + if (avatar) { + await getS3AvatarSource().refreshAvatar(avatar, oldTeam?.avatar); + } } diff --git a/packages/service/support/user/team/delete/processor.ts b/packages/service/support/user/team/delete/processor.ts index 1bf8b4ca2754..81a40bb0c4f8 100644 --- a/packages/service/support/user/team/delete/processor.ts +++ b/packages/service/support/user/team/delete/processor.ts @@ -2,14 +2,9 @@ import type { Processor } from '@fastgpt/dal/redis/bullmq'; import { type TeamDeleteJobData } from './index'; import { MongoImage } from '../../../../common/file/image/schema'; import { MongoOpenApi } from '../../../openapi/schema'; -import { MongoGroupMemberModel } from '../../../permission/memberGroup/groupMemberSchema'; -import { MongoMemberGroupModel } from '../../../permission/memberGroup/memberGroupSchema'; -import { MongoOrgMemberModel } from '../../../permission/org/orgMemberSchema'; -import { MongoOrgModel } from '../../../permission/org/orgSchema'; import { MongoResourcePermission } from '../../../permission/schema'; import { delUserAllSession } from '../../session'; -import { MongoTeamMember } from '../teamMemberSchema'; -import { MongoTeam } from '../teamSchema'; +import { groupRepository, orgRepository, teamRepository } from '../../../../common/dal'; import { MongoMcpKey } from '../../../mcp/schema'; import { MongoChatSetting } from '../../../../core/chat/setting/schema'; import { MongoChatFavouriteApp } from '../../../../core/chat/favouriteApp/schema'; @@ -32,7 +27,7 @@ export const teamDeleteProcessor: Processor = async (job) => try { // 1. 检查团队是否存在 - const team = await MongoTeam.findById(teamId); + const team = await teamRepository.findTeamById(teamId); if (!team) { logger.warn('Team not found for deletion', { teamId }); return; @@ -96,40 +91,21 @@ export const teamDeleteProcessor: Processor = async (job) => }); // 删除群组 - const groups = await MongoMemberGroupModel.find({ teamId }); - await MongoGroupMemberModel.deleteMany({ - groupId: { $in: groups.map((item) => item._id) } - }); - await MongoMemberGroupModel.deleteMany({ - teamId - }); + await groupRepository.deleteMemberGroupsByTeamId(teamId); // 删除组织 - await MongoOrgModel.deleteMany({ - teamId - }); - await MongoOrgMemberModel.deleteMany({ - teamId - }); + await orgRepository.deleteOrgsByTeamId(teamId); // 7. 删除成员 session 和成员信息 - const members = await MongoTeamMember.find({ - teamId - }); + const members = await teamRepository.findMembersByTeamId(teamId); // 删除所有成员的 session await Promise.all(members.map((member) => delUserAllSession(member.userId))); - await MongoTeamMember.deleteMany({ - teamId - }); + await teamRepository.deleteMembersByTeamId(teamId); // 8. 清理团队敏感信息 - team.notificationAccount = ''; - team.openaiAccount = undefined; - team.externalWorkflowVariables = undefined; - team.meta = undefined; - await team.save(); + await teamRepository.clearTeamSensitiveData(teamId); logger.info('Team delete completed', { teamId, diff --git a/packages/service/support/user/team/utils.ts b/packages/service/support/user/team/utils.ts index c6745a80f534..8b846aa53a98 100644 --- a/packages/service/support/user/team/utils.ts +++ b/packages/service/support/user/team/utils.ts @@ -1,60 +1,125 @@ -import { MongoTeamMember } from '../../user/team/teamMemberSchema'; -import { type UserModelSchema } from '@fastgpt/global/support/user/type'; -import { type TeamSchema } from '@fastgpt/global/support/user/team/type'; +import { type SourceMemberType } from '@fastgpt/global/support/user/type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; +import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; +import { teamRepository } from '../../../common/dal'; -// TODO: 数据库优化 +/** 返回运行时用户信息,DAL 负责成员、用户和团队的关联查询。 */ export async function getRunningUserInfoByTmbId(tmbId: string) { - if (tmbId) { - const tmb = await MongoTeamMember.findById(tmbId, 'teamId name userId') // team_members name is the user's name - .populate<{ team: TeamSchema; user: UserModelSchema }>([ - { - path: 'team', - select: 'name' - }, - { - path: 'user', - select: 'username contact' - } - ]) - .lean(); - - if (!tmb) return Promise.reject(TeamErrEnum.notUser); - - return { - username: tmb.user.username, - teamName: tmb.team.name, - memberName: tmb.name, - contact: tmb.user.contact || '', - teamId: tmb.teamId, - tmbId: tmb._id - }; - } + if (!tmbId) return Promise.reject(TeamErrEnum.notUser); + + const relations = await teamRepository.findMemberRelationsById(tmbId); + if (!relations?.team || !relations.user) return Promise.reject(TeamErrEnum.notUser); - return Promise.reject(TeamErrEnum.notUser); + return { + username: relations.user.username, + teamName: relations.team.name, + memberName: relations.member.name, + contact: relations.user.contact ?? '', + teamId: relations.member.teamId, + tmbId: relations.member.id + }; } +/** 返回工作流运行时需要的时区和第三方模型配置。 */ export async function getUserChatInfo(tmbId: string) { - const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId') - .populate<{ user: UserModelSchema; team: TeamSchema }>([ - { - path: 'user', - select: 'timezone' - }, - { - path: 'team', - select: 'openaiAccount externalWorkflowVariables' - } - ]) - .lean(); - - if (!tmb) return Promise.reject(TeamErrEnum.notUser); + const relations = await teamRepository.findMemberRelationsById(tmbId); + if (!relations?.team || !relations.user) return Promise.reject(TeamErrEnum.notUser); + const openaiAccount = + relations.team.openaiAccount?.key && relations.team.openaiAccount.baseUrl + ? relations.team.openaiAccount + : undefined; return { - timezone: tmb.user.timezone, + timezone: relations.user.timezone ?? 'Asia/Shanghai', externalProvider: { - openaiAccount: tmb.team.openaiAccount, - externalWorkflowVariables: tmb.team.externalWorkflowVariables + openaiAccount, + externalWorkflowVariables: relations.team.externalWorkflowVariables } }; } + +/* export dataset limit */ +export const updateExportDatasetLimit = async (teamId: string) => { + try { + await teamRepository.updateTeamLimit(teamId, { + lastExportDatasetTime: new Date() + }); + } catch {} +}; + +export const checkExportDatasetLimit = async ({ + teamId, + limitMinutes = 0 +}: { + teamId: string; + limitMinutes?: number; +}) => { + const limitMinutesAgo = new Date(Date.now() - limitMinutes * 60 * 1000); + const team = await teamRepository.findTeamById(teamId); + if (team?.limit?.lastExportDatasetTime && team.limit.lastExportDatasetTime > limitMinutesAgo) { + return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅可导出一次。`); + } +}; + +/* web sync limit */ +export const updateWebSyncLimit = async (teamId: string) => { + try { + await teamRepository.updateTeamLimit(teamId, { + lastWebsiteSyncTime: new Date() + }); + } catch {} +}; + +/** 清除站点同步冷却时间,空同步不应占用后续手动同步机会。 */ +export const clearWebSyncLimit = async (teamId: string) => { + try { + await teamRepository.updateTeamLimit(teamId, { + lastWebsiteSyncTime: null + }); + } catch {} +}; + +export const checkWebSyncLimit = async ({ + teamId, + limitMinutes = 0 +}: { + teamId: string; + limitMinutes?: number; +}) => { + const limitMinutesAgo = new Date(Date.now() - limitMinutes * 60 * 1000); + const team = await teamRepository.findTeamById(teamId); + if (team?.limit?.lastWebsiteSyncTime && team.limit.lastWebsiteSyncTime > limitMinutesAgo) { + return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅使用一次同步功能。`); + } +}; + +/** 为列表补充成员信息;找不到成员的条目沿用旧逻辑直接跳过。 */ +export async function addSourceMember({ + list +}: { + list: T[]; +}): Promise> { + if (!Array.isArray(list)) return []; + + const tmbIdList = list + .map((item) => (item.tmbId ? String(item.tmbId) : undefined)) + .filter((id): id is string => Boolean(id)); + const tmbList = await teamRepository.findMembersByIds(tmbIdList); + const tmbMap = new Map(tmbList.map((tmb) => [tmb.id, tmb])); + + const result: Array = []; + for (const item of list) { + const tmb = tmbMap.get(String(item.tmbId)); + if (!tmb) continue; + + result.push({ + ...item, + sourceMember: { + name: tmb.name, + avatar: tmb.avatar ?? '', + status: (tmb.status ?? TeamMemberStatusEnum.active) as TeamMemberStatusEnum + } + }); + } + return result; +} diff --git a/packages/service/support/user/utils.ts b/packages/service/support/user/utils.ts index 350d9971abf0..f9b52b652659 100644 --- a/packages/service/support/user/utils.ts +++ b/packages/service/support/user/utils.ts @@ -1,14 +1,12 @@ import { type SourceMemberType } from '@fastgpt/global/support/user/type'; -import { MongoTeam } from './team/teamSchema'; -import { MongoTeamMember } from './team/teamMemberSchema'; -import { type ClientSession } from '../../common/mongo'; import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; +import { teamRepository } from '../../common/dal'; /* export dataset limit */ export const updateExportDatasetLimit = async (teamId: string) => { try { - await MongoTeam.findByIdAndUpdate(teamId, { - 'limit.lastExportDatasetTime': new Date() + await teamRepository.updateTeamLimit(teamId, { + lastExportDatasetTime: new Date() }); } catch {} }; @@ -21,19 +19,10 @@ export const checkExportDatasetLimit = async ({ }) => { const limitMinutesAgo = new Date(Date.now() - limitMinutes * 60 * 1000); - // auth export times - const authTimes = await MongoTeam.findOne( - { - _id: teamId, - $or: [ - { 'limit.lastExportDatasetTime': { $exists: false } }, - { 'limit.lastExportDatasetTime': { $lte: limitMinutesAgo } } - ] - }, - '_id limit' - ); + const team = await teamRepository.findTeamById(teamId); - if (!authTimes) { + const lastExportDatasetTime = team?.limit?.lastExportDatasetTime; + if (!team || (lastExportDatasetTime && lastExportDatasetTime > limitMinutesAgo)) { return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅可导出一次。`); } }; @@ -41,8 +30,8 @@ export const checkExportDatasetLimit = async ({ /* web sync limit */ export const updateWebSyncLimit = async (teamId: string) => { try { - await MongoTeam.findByIdAndUpdate(teamId, { - 'limit.lastWebsiteSyncTime': new Date() + await teamRepository.updateTeamLimit(teamId, { + lastWebsiteSyncTime: new Date() }); } catch {} }; @@ -55,10 +44,8 @@ export const updateWebSyncLimit = async (teamId: string) => { */ export const clearWebSyncLimit = async (teamId: string) => { try { - await MongoTeam.findByIdAndUpdate(teamId, { - $unset: { - 'limit.lastWebsiteSyncTime': 1 - } + await teamRepository.updateTeamLimit(teamId, { + lastWebsiteSyncTime: null }); } catch {} }; @@ -72,19 +59,10 @@ export const checkWebSyncLimit = async ({ }) => { const limitMinutesAgo = new Date(Date.now() - limitMinutes * 60 * 1000); - // auth export times - const authTimes = await MongoTeam.findOne( - { - _id: teamId, - $or: [ - { 'limit.lastWebsiteSyncTime': { $exists: false } }, - { 'limit.lastWebsiteSyncTime': { $lte: limitMinutesAgo } } - ] - }, - '_id limit' - ); + const team = await teamRepository.findTeamById(teamId); - if (!authTimes) { + const lastWebsiteSyncTime = team?.limit?.lastWebsiteSyncTime; + if (!team || (lastWebsiteSyncTime && lastWebsiteSyncTime > limitMinutesAgo)) { return Promise.reject(`每个团队,每 ${limitMinutes} 分钟仅使用一次同步功能。`); } }; @@ -96,43 +74,30 @@ export const checkWebSyncLimit = async ({ * @returns The list with the sourceMember property added. */ export async function addSourceMember({ - list, - session + list }: { list: T[]; - session?: ClientSession; }): Promise> { if (!Array.isArray(list)) return []; const tmbIdList = list .map((item) => (item.tmbId ? String(item.tmbId) : undefined)) - .filter(Boolean); - const tmbList = await MongoTeamMember.find( - { - _id: { $in: tmbIdList } - }, - 'tmbId name avatar status', - { - session - } - ).lean(); - - return list - .map((item) => { - const tmb = tmbList.find((tmb) => String(tmb._id) === String(item.tmbId)); - if (!tmb) return; + .filter((id): id is string => Boolean(id)); + const tmbList = await teamRepository.findMembersByIds(tmbIdList); - // @ts-ignore - const formatItem = typeof item.toObject === 'function' ? item.toObject() : item; + const result: Array = []; + for (const item of list) { + const tmb = tmbList.find((member) => member.id === String(item.tmbId)); + if (!tmb) continue; - return { - ...formatItem, - sourceMember: { - name: tmb.name, - avatar: tmb.avatar, - status: tmb.status ?? TeamMemberStatusEnum.active - } - }; - }) - .filter(Boolean) as Array; + result.push({ + ...item, + sourceMember: { + name: tmb.name, + avatar: tmb.avatar ?? '', + status: (tmb.status ?? TeamMemberStatusEnum.active) as TeamMemberStatusEnum + } + }); + } + return result; } diff --git a/packages/service/test/support/marketing/crmInterface.test.ts b/packages/service/test/support/marketing/crmInterface.test.ts index b5cf6f6d8d9b..ad423fbcac4d 100644 --- a/packages/service/test/support/marketing/crmInterface.test.ts +++ b/packages/service/test/support/marketing/crmInterface.test.ts @@ -26,12 +26,9 @@ vi.mock('@fastgpt/service/common/logger', () => ({ vi.mock('@fastgpt/service/common/dal', () => ({ userRepository: { findSemById: (...args: unknown[]) => mocks.findUser(...args) - } -})); - -vi.mock('@fastgpt/service/support/user/team/teamSchema', () => ({ - MongoTeam: { - findById: (...args: unknown[]) => ({ lean: () => mocks.findTeam(...args) }) + }, + teamRepository: { + findTeamById: (...args: unknown[]) => mocks.findTeam(...args) } })); @@ -81,7 +78,7 @@ describe('CRM team lifecycle interface', () => { it('resolves the team owner visitor and marks the team only after CRM succeeds', async () => { await reportCRMTeamRechargeOnce({ teamId: 'team-1' }); - expect(mocks.findTeam).toHaveBeenCalledWith('team-1', 'ownerId'); + expect(mocks.findTeam).toHaveBeenCalledWith('team-1'); expect(mocks.findUser).toHaveBeenCalledWith('user-1'); expect(mocks.report).toHaveBeenCalledWith({ visitorId: 'stored-visitor', diff --git a/packages/service/test/support/permission/teamLimit.test.ts b/packages/service/test/support/permission/teamLimit.test.ts index e5e119ab1677..655af7730e12 100644 --- a/packages/service/test/support/permission/teamLimit.test.ts +++ b/packages/service/test/support/permission/teamLimit.test.ts @@ -12,12 +12,11 @@ import { import * as walletUtils from '@fastgpt/service/support/wallet/sub/utils'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import { teamRepository } from '@fastgpt/service/common/dal'; import * as vectorController from '@fastgpt/service/common/vectorDB/controller'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { SystemErrEnum } from '@fastgpt/global/common/error/code/system'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants'; @@ -140,7 +139,7 @@ describe('checkTeamMemberLimit', () => { }); it('当团队成员数量未超限时正常通过', async () => { - vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(5); + vi.spyOn(teamRepository, 'countMembersByTeamId').mockResolvedValue(5); const mockStandard = { standard: { @@ -153,7 +152,7 @@ describe('checkTeamMemberLimit', () => { }); it('当新增成员后超过限制时抛出错误', async () => { - vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(8); + vi.spyOn(teamRepository, 'countMembersByTeamId').mockResolvedValue(8); const mockStandard = { standard: { @@ -166,7 +165,7 @@ describe('checkTeamMemberLimit', () => { }); it('当新增成员后刚好达到限制时正常通过', async () => { - vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(8); + vi.spyOn(teamRepository, 'countMembersByTeamId').mockResolvedValue(8); const mockStandard = { standard: { @@ -179,7 +178,7 @@ describe('checkTeamMemberLimit', () => { }); it('当 maxTeamMember 未设置时不限制', async () => { - vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(100); + vi.spyOn(teamRepository, 'countMembersByTeamId').mockResolvedValue(100); const mockStandard = { standard: {} @@ -190,7 +189,7 @@ describe('checkTeamMemberLimit', () => { }); it('查询时排除已离开的成员', async () => { - const countSpy = vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(5); + const countSpy = vi.spyOn(teamRepository, 'countMembersByTeamId').mockResolvedValue(5); const mockStandard = { standard: { @@ -201,10 +200,7 @@ describe('checkTeamMemberLimit', () => { await checkTeamMemberLimit(mockTeamId, 2); - expect(countSpy).toHaveBeenCalledWith({ - teamId: mockTeamId, - status: { $ne: TeamMemberStatusEnum.leave } - }); + expect(countSpy).toHaveBeenCalledWith(mockTeamId, { includeLeft: false }); }); }); diff --git a/packages/service/test/support/user/account/verification/password/service.test.ts b/packages/service/test/support/user/account/verification/password/service.test.ts index 72dd5d25ab7a..f88ad0e02790 100644 --- a/packages/service/test/support/user/account/verification/password/service.test.ts +++ b/packages/service/test/support/user/account/verification/password/service.test.ts @@ -3,12 +3,12 @@ import type { PasswordVerificationDependencies, PasswordVerificationUser } from '@fastgpt/service/support/user/account/verification/password/type'; -import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTmpData } from '@fastgpt/service/support/tmpData/schema'; import { getDataId, VerificationMaterialError } from '@fastgpt/service/support/tmpData/verification'; +import { userRepository } from '@fastgpt/service/common/dal'; import type { TransactionContext } from '@fastgpt/service/common/dal'; import { UserErrEnum } from '@fastgpt/global/common/error/code/user'; import { describe, expect, it, vi } from 'vitest'; @@ -87,7 +87,7 @@ describe('PasswordVerificationService default adapters', () => { expect(authMaterial?.expireAt.getTime()).toBeGreaterThanOrEqual(beforeIssue + 30_000); expect(authMaterial?.expireAt.getTime()).toBeLessThanOrEqual(afterIssue + 30_000); - const storedUser = await MongoUser.create({ username, password }); + const storedUser = await userRepository.create({ username, password }); const result = await service.withVerifiedCredentials( { username, password, code, purpose: 'login' }, async ({ user }) => user @@ -98,7 +98,7 @@ describe('PasswordVerificationService default adapters', () => { dataId: getDataId({ scene: 'login', type: 'password', key: username }) }) ).resolves.toBeNull(); - expect(result.id).toBe(String(storedUser._id)); + expect(result.id).toBe(storedUser.id); }); }); diff --git a/packages/service/test/support/user/utils.test.ts b/packages/service/test/support/user/utils.test.ts index 594f8908699a..7dc0fe026449 100644 --- a/packages/service/test/support/user/utils.test.ts +++ b/packages/service/test/support/user/utils.test.ts @@ -1,16 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { clearWebSyncLimit } from '../../../support/user/utils'; -import { MongoTeam } from '../../../support/user/team/teamSchema'; +import { teamRepository } from '../../../common/dal'; -vi.mock('../../../support/user/team/teamSchema', () => ({ - MongoTeam: { - findByIdAndUpdate: vi.fn() - } -})); - -vi.mock('../../../support/user/team/teamMemberSchema', () => ({ - MongoTeamMember: { - find: vi.fn() +vi.mock('../../../common/dal', () => ({ + teamRepository: { + updateTeamLimit: vi.fn() } })); @@ -24,10 +18,8 @@ describe('support user utils', () => { await clearWebSyncLimit(teamId); - expect(MongoTeam.findByIdAndUpdate).toHaveBeenCalledWith(teamId, { - $unset: { - 'limit.lastWebsiteSyncTime': 1 - } + expect(teamRepository.updateTeamLimit).toHaveBeenCalledWith(teamId, { + lastWebsiteSyncTime: null }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52a6d984f8c0..1b2a6e0b0a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -442,6 +442,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.13.3 + mongodb-memory-server: + specifier: 'catalog:' + version: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) typescript: specifier: 'catalog:' version: 6.0.3 diff --git a/projects/app/AGENTS.md b/projects/app/AGENTS.md new file mode 100644 index 000000000000..643577dfaef3 --- /dev/null +++ b/projects/app/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/projects/app/CLAUDE.md b/projects/app/CLAUDE.md new file mode 100644 index 000000000000..43c994c2d361 --- /dev/null +++ b/projects/app/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/projects/app/src/instrumentation-node.ts b/projects/app/src/instrumentation-node.ts index bf3188516694..e992be882c35 100644 --- a/projects/app/src/instrumentation-node.ts +++ b/projects/app/src/instrumentation-node.ts @@ -14,8 +14,8 @@ export async function registerNodeInstrumentation() { const [ { connectMongo }, - { connection }, { connectionMongo, connectionLogMongo, MONGO_URL, MONGO_LOG_URL }, + { syncDalModelIndexes }, { systemStartCb }, { initGlobalVariables, getInitConfig, initSystemPluginTags, initAppTemplateTypes }, { initVectorStore }, @@ -40,8 +40,8 @@ export async function registerNodeInstrumentation() { { validateAgentSandboxProxyEnv } ] = await Promise.all([ import('@fastgpt/service/common/mongo/init'), - import('@fastgpt/service/common/dal/mongo/connection'), import('@fastgpt/service/common/mongo/index'), + import('@fastgpt/service/common/dal/mongo/lifecycle'), import('@fastgpt/service/common/system/tools'), import('@/service/common/system'), import('@fastgpt/service/common/vectorDB/controller'), @@ -120,12 +120,17 @@ export async function registerNodeInstrumentation() { runInitializationStep({ step: 'connect-main-mongo', stage: InitialErrorEnum.MONGO_ERROR, - action: () => - connectMongo({ + action: async () => { + await connectMongo({ db: connectionMongo, url: MONGO_URL, connectedCb: () => startMongoWatch() - }), + }); + // DAL 与旧 Model 共用主 Mongo client,但仍需同步 DAL Schema 的索引声明。 + void syncDalModelIndexes(connectionMongo).catch((error) => { + logger.warn('DAL MongoDB index sync failed during connect', { error }); + }); + }, logger, getErrText, meta: { @@ -146,16 +151,6 @@ export async function registerNodeInstrumentation() { mongoLogUrl: MONGO_LOG_URL } }), - runInitializationStep({ - step: 'connect-dal-mongo', - stage: InitialErrorEnum.MONGO_ERROR, - action: () => connection.connect(MONGO_URL), - logger, - getErrText, - meta: { - mongoUrl: MONGO_URL - } - }), runInitializationStep({ step: 'init-bullmq-workers', stage: InitialErrorEnum.REDIS_ERROR, diff --git a/projects/app/src/pages/api/admin/initv4141.ts b/projects/app/src/pages/api/admin/initv4141.ts index 7ee1fa12f3e1..e5968cc0d62f 100644 --- a/projects/app/src/pages/api/admin/initv4141.ts +++ b/projects/app/src/pages/api/admin/initv4141.ts @@ -11,7 +11,7 @@ import { getLogger } from '@fastgpt/service/common/logger'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; -import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; +import { teamRepository } from '@fastgpt/service/common/dal'; import { type NextApiRequest, type NextApiResponse } from 'next'; const logger = getLogger(['initv4141']); @@ -172,14 +172,14 @@ async function appSplitMigration(teamId: string) { async function handler(req: NextApiRequest, _res: NextApiResponse) { await authCert({ req, authRoot: true }); - const allTeamIds = await MongoTeam.find({}, '_id').lean(); + const allTeamIds = await teamRepository.findAllTeamIds(); logger.info(`Starting app split migration, teamIds: ${allTeamIds.length}`); const failed = []; const skipedMigrated = []; const skipedNoFolder = []; const success = []; - for await (const { _id: teamId } of allTeamIds) { + for await (const teamId of allTeamIds) { try { const res = await appSplitMigration(teamId); if (res === 'migrated') { diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index 36cf7f1a3c18..92eecd96351f 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -23,7 +23,7 @@ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { checkTeamAppTypeLimit } from '@fastgpt/service/support/permission/teamLimit'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import { teamRepository } from '@fastgpt/service/common/dal'; import { type ApiRequestProps } from '@fastgpt/next/type'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; @@ -75,11 +75,9 @@ async function handler(req: ApiRequestProps) { appCheckType: type === AppTypeEnum.workflowTool ? 'tool' : 'app' }); - const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId') - .populate<{ - user: { username: string }; - }>('user', 'username') - .lean(); + const relations = await teamRepository.findMemberRelationsById(tmbId); + const tmb = relations?.member; + const user = relations?.user; // 创建app const appId = await onCreateApp({ @@ -110,7 +108,7 @@ async function handler(req: ApiRequestProps) { teamId, tmbId, userAvatar: tmb?.avatar, - username: tmb?.user?.username, + username: user?.username, templateId, isRoot }); diff --git a/projects/app/src/pages/api/core/app/logs/exportLogs.ts b/projects/app/src/pages/api/core/app/logs/exportLogs.ts index 9863414e11f2..fa3739d3eb6c 100644 --- a/projects/app/src/pages/api/core/app/logs/exportLogs.ts +++ b/projects/app/src/pages/api/core/app/logs/exportLogs.ts @@ -13,7 +13,7 @@ import { ChatItemCollectionName, ChatItemResponseCollectionName } from '@fastgpt/service/core/chat/constants'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import { teamRepository } from '@fastgpt/service/common/dal'; import { ChatSourceTypeEnum, type ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants'; import { sanitizeCsvField } from '@fastgpt/service/common/file/csv'; @@ -85,28 +85,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) { ); // Get members - const teamMemberWithContact = await MongoTeamMember.aggregate([ - { $match: { teamId: new Types.ObjectId(teamId) } }, - { - $lookup: { - from: 'users', - localField: 'userId', - foreignField: '_id', - as: 'user' - } - }, - { - $project: { - memberId: '$_id', - teamId: 1, - userId: 1, - name: 1, - role: 1, - status: 1, - contact: { $ifNull: [{ $arrayElemAt: ['$user.contact', 0] }, '-'] } - } - } - ]); + const teamMemberWithContact = await teamRepository.findMemberRelationsByTeamId(teamId); const where = { appId: new Types.ObjectId(appId), @@ -447,7 +426,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) { const titleStr = doc.customTitle || doc.title || ''; const tmbName = doc.outLinkUid ? doc.outLinkUid - : teamMemberWithContact.find((member) => String(member.memberId) === String(doc.tmbId))?.name; + : teamMemberWithContact.find((member) => member.member.id === String(doc.tmbId))?.member.name; const region = getLocationFromIp(doc.originIp, locale); const valueMap: Record any> = { diff --git a/projects/app/src/pages/api/core/app/logs/getUsers.ts b/projects/app/src/pages/api/core/app/logs/getUsers.ts index be4fe33b293f..dc9841a65349 100644 --- a/projects/app/src/pages/api/core/app/logs/getUsers.ts +++ b/projects/app/src/pages/api/core/app/logs/getUsers.ts @@ -3,7 +3,7 @@ import { Types } from '@fastgpt/service/common/mongo'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { NextAPI } from '@/service/middleware/entry'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import { teamRepository } from '@fastgpt/service/common/dal'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import type { ApiRequestProps } from '@fastgpt/next/type'; @@ -76,16 +76,10 @@ async function handler(req: ApiRequestProps): Promise { .map((item) => item._id.tmbId); const teamMembers = tmbIds.length - ? await MongoTeamMember.find( - { - _id: { $in: tmbIds }, - teamId: new Types.ObjectId(teamId) - }, - '_id name avatar' - ).lean() + ? await teamRepository.findMembersByIds(tmbIds.map(String), { teamId }) : []; - const tmbMap = new Map(teamMembers.map((m) => [String(m._id), m])); + const tmbMap = new Map(teamMembers.map((m) => [m.id, m])); const searchPattern = searchKey ? new RegExp(replaceRegChars(searchKey), 'i') : null; diff --git a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts index 020c34a6491d..715ae54e84fb 100644 --- a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts +++ b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts @@ -1,7 +1,6 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common'; -import { userRepository } from '@fastgpt/service/common/dal'; +import { teamRepository, userRepository } from '@fastgpt/service/common/dal'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { i18nT } from '@fastgpt/global/common/i18n/utils'; import { NextAPI } from '@/service/middleware/entry'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -26,7 +25,7 @@ async function handler( }).body; const { tmbId, teamId, sessionId } = await authCert({ req, authToken: true }); - const tmb = await MongoTeamMember.findById(tmbId); + const tmb = await teamRepository.findMemberById(tmbId); if (!tmb) { return Promise.reject('can not find it'); } diff --git a/projects/app/src/pages/api/support/user/team/plan/getTeamPlanStatus.ts b/projects/app/src/pages/api/support/user/team/plan/getTeamPlanStatus.ts index 3ca177e64143..3d326acc5a3e 100644 --- a/projects/app/src/pages/api/support/user/team/plan/getTeamPlanStatus.ts +++ b/projects/app/src/pages/api/support/user/team/plan/getTeamPlanStatus.ts @@ -8,8 +8,7 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { getVectorCountByTeamId } from '@fastgpt/service/common/vectorDB/controller'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; -import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant'; +import { teamRepository } from '@fastgpt/service/common/dal'; import { MongoAppRegistration } from '@fastgpt/service/support/appRegistration/schema'; import { GetTeamPlanStatusQuerySchema, @@ -42,10 +41,7 @@ async function handler( getTeamPlanStatus({ teamId }), - MongoTeamMember.countDocuments({ - teamId, - status: { $ne: TeamMemberStatusEnum.leave } - }), + teamRepository.countMembersByTeamId(teamId, { includeLeft: false }), MongoApp.countDocuments({ teamId, type: { @@ -70,7 +66,7 @@ async function handler( usedDatasetIndexSize, usedRegistrationCount }; - } catch (error) {} + } catch {} })(); return GetTeamPlanStatusResponseSchema.parse(planStatusResult); diff --git a/projects/app/src/service/mongo.ts b/projects/app/src/service/mongo.ts index fbad8505ae9e..82e976e1e5a0 100644 --- a/projects/app/src/service/mongo.ts +++ b/projects/app/src/service/mongo.ts @@ -1,4 +1,5 @@ import { exit } from 'process'; +import { hashStr } from '@fastgpt/global/common/string/tools'; import { transactionRunner, userRepository, teamRepository } from '@fastgpt/service/common/dal'; import { getLogger, LogCategories } from '@fastgpt/service/common/logger'; import { appEnv } from '@/env'; @@ -7,19 +8,20 @@ const logger = getLogger(LogCategories.SYSTEM); export async function initRootUser(retry = 3): Promise { try { - const psw = appEnv.DEFAULT_ROOT_PSW; + // 环境变量保存明文,DAL 约定只接收并保存已经哈希的密码。 + const password = hashStr(appEnv.DEFAULT_ROOT_PSW); // root 用户与默认团队在同一个 DAL 事务内初始化,避免混用两种事务上下文。 await transactionRunner.withTransaction(async (context) => { const rootUser = await userRepository.findByUsername('root'); // init root user if (rootUser) { - await userRepository.updateById(rootUser.id, { password: psw }, context); + await userRepository.updateById(rootUser.id, { password }, context); } else { const created = await userRepository.create( { username: 'root', - password: psw + password }, context ); diff --git a/projects/app/src/service/support/permission/auth/chatCompletion.ts b/projects/app/src/service/support/permission/auth/chatCompletion.ts index 87f066fae8c0..0a9149d2a86d 100644 --- a/projects/app/src/service/support/permission/auth/chatCompletion.ts +++ b/projects/app/src/service/support/permission/auth/chatCompletion.ts @@ -5,13 +5,11 @@ import type { } from '@fastgpt/global/openapi/core/chat/completion/api'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; -import { notLeaveStatus } from '@fastgpt/global/support/user/team/constant'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { buildChatSourceQuery } from '@fastgpt/service/core/chat/source'; import { authApp, authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; -import { userRepository } from '@fastgpt/service/common/dal'; -import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; +import { teamRepository, userRepository } from '@fastgpt/service/common/dal'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { UserError } from '@fastgpt/global/common/error/utils'; @@ -55,26 +53,16 @@ export const resolveChatCompletionEffectiveTmbId = async ({ const [memberByTmbId, memberByUsername] = await Promise.all([ authProxy.tmbId - ? MongoTeamMember.findOne({ - _id: authProxy.tmbId, - teamId, - status: notLeaveStatus - }) - .select('_id userId teamId') - .lean() + ? teamRepository.findMemberByIdInTeam(authProxy.tmbId, teamId, { includeLeft: false }) : null, username ? (async () => { const userId = await userRepository.findIdByUsername(username); if (!userId) return null; - return MongoTeamMember.findOne({ - teamId, - userId, - status: notLeaveStatus - }) - .select('_id userId teamId') - .lean(); + return teamRepository.findMemberByTeamAndUser(teamId, userId, { + includeLeft: false + }); })() : null ]); @@ -83,11 +71,7 @@ export const resolveChatCompletionEffectiveTmbId = async ({ return Promise.reject(ChatErrEnum.unAuthChat); } - if ( - memberByTmbId && - memberByUsername && - String(memberByTmbId._id) !== String(memberByUsername._id) - ) { + if (memberByTmbId && memberByUsername && memberByTmbId.id !== memberByUsername.id) { return Promise.reject(ChatErrEnum.unAuthChat); } @@ -97,7 +81,7 @@ export const resolveChatCompletionEffectiveTmbId = async ({ } return { - tmbId: String(member._id), + tmbId: member.id, isProxy: true }; }; diff --git a/projects/app/test/api/support/user/account/loginByPassword.test.ts b/projects/app/test/api/support/user/account/loginByPassword.test.ts index a3fd5cae1da0..47450e39c618 100644 --- a/projects/app/test/api/support/user/account/loginByPassword.test.ts +++ b/projects/app/test/api/support/user/account/loginByPassword.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import * as loginApi from '@/pages/api/support/user/account/loginByPassword'; import { MongoUser } from '@fastgpt/service/support/user/schema'; +import { hashStr } from '@fastgpt/global/common/string/tools'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; @@ -26,6 +27,9 @@ const saveLoginCode = (username: string, code = '123456') => { upsert: true } ); +const testPassword = hashStr('testpassword'); +const rootPassword = hashStr('rootpassword'); + describe('loginByPassword API', () => { let testUser: any; let testTeam: any; @@ -68,7 +72,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: '123456', language: 'zh-CN' } @@ -103,7 +107,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: '', - password: 'testpassword', + password: testPassword, code: '123456', language: 'zh-CN' } @@ -131,7 +135,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: 'wrongcode', language: 'zh-CN' } @@ -147,7 +151,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'nonexistentuser', - password: 'testpassword', + password: testPassword, code: '123456', language: 'zh-CN' } @@ -165,7 +169,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: '123456', language: 'zh-CN' } @@ -193,7 +197,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: '123456', language: 'en' } @@ -210,7 +214,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: '123456', fastgpt_sem: { visitor_id: 'visitor-1' @@ -233,7 +237,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'testuser', - password: 'testpassword', + password: testPassword, code: '123456', fastgpt_sem: { visitor_id: 'incoming-visitor' @@ -279,7 +283,7 @@ describe('loginByPassword API', () => { const res = await Call, any>(loginApi.default, { body: { username: 'root', - password: 'rootpassword', + password: rootPassword, code: '123456', language: 'zh-CN' } diff --git a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts index 356661d805ee..caafbde9f745 100644 --- a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts +++ b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts @@ -4,10 +4,14 @@ import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { hashStr } from '@fastgpt/global/common/string/tools'; import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils'; import type { ResetExpiredPswBodyType } from '@fastgpt/global/openapi/support/user/account/password/api'; import { Call } from '@test/utils/request'; +const oldPassword = hashStr('oldpassword'); +const newPassword = hashStr('newpassword'); + const originalPasswordExpiredMonth = process.env.PASSWORD_EXPIRED_MONTH; const loadResetExpiredPswApi = async () => { vi.resetModules(); @@ -57,7 +61,7 @@ describe('resetExpiredPsw API', () => { const res = await Call, any>( resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, + body: { newPsw: newPassword }, auth: { userId: String(testUser._id), teamId: String(testTeam._id), @@ -79,10 +83,10 @@ describe('resetExpiredPsw API', () => { const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime(); expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime()); await expect( - MongoUser.findOne({ _id: testUser._id, password: 'newhashedpassword' }) + MongoUser.collection.findOne({ _id: testUser._id, password: newPassword }) ).resolves.not.toBeNull(); await expect( - MongoUser.findOne({ _id: testUser._id, password: 'oldpassword' }) + MongoUser.collection.findOne({ _id: testUser._id, password: oldPassword }) ).resolves.toBeNull(); }); @@ -97,7 +101,7 @@ describe('resetExpiredPsw API', () => { const res = await Call, any>( resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, + body: { newPsw: newPassword }, auth: { userId: String(testUser._id), teamId: String(testTeam._id), @@ -124,7 +128,7 @@ describe('resetExpiredPsw API', () => { const res = await Call, any>( resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, + body: { newPsw: newPassword }, auth: { userId: String(testUser._id), teamId: String(testTeam._id), @@ -166,7 +170,7 @@ describe('resetExpiredPsw API', () => { const res = await Call, any>( resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' }, + body: { newPsw: newPassword }, auth: { userId: nonExistentId, teamId: String(testTeam._id), @@ -186,7 +190,7 @@ describe('resetExpiredPsw API', () => { const res = await Call, any>( resetExpiredPswApi.default, { - body: { newPsw: 'newhashedpassword' } + body: { newPsw: newPassword } } ); diff --git a/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts b/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts index 969d2e4c541b..d93678754a97 100644 --- a/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts +++ b/projects/app/test/api/support/user/account/updatePasswordByOld.test.ts @@ -4,11 +4,16 @@ import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; +import { hashStr } from '@fastgpt/global/common/string/tools'; import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils'; import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api'; import { Call } from '@test/utils/request'; import { ApiRequestInputParseError } from '@fastgpt/service/common/zod/requestParseError'; +const oldPassword = hashStr('oldpassword'); +const newPassword = hashStr('newpassword'); +const wrongPassword = hashStr('wrongpassword'); + describe('updatePasswordByOld API', () => { let testUser: any; let testTeam: any; @@ -17,7 +22,7 @@ describe('updatePasswordByOld API', () => { beforeEach(async () => { testUser = await MongoUser.create({ username: 'testuser', - password: 'oldhashpassword', + password: 'oldpassword', status: UserStatusEnum.active }); testTeam = await MongoTeam.create({ @@ -46,7 +51,7 @@ describe('updatePasswordByOld API', () => { const res = await Call, any>( updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }, + body: { oldPsw: oldPassword, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any } ); @@ -58,10 +63,10 @@ describe('updatePasswordByOld API', () => { expect(updatedUser?.password).toBeDefined(); expect(updatedUser?.passwordUpdateTime).toBeDefined(); await expect( - MongoUser.findOne({ _id: testUser._id, password: 'newhashpassword' }) + MongoUser.collection.findOne({ _id: testUser._id, password: newPassword }) ).resolves.not.toBeNull(); await expect( - MongoUser.findOne({ _id: testUser._id, password: 'oldhashpassword' }) + MongoUser.collection.findOne({ _id: testUser._id, password: oldPassword }) ).resolves.toBeNull(); }); @@ -69,7 +74,7 @@ describe('updatePasswordByOld API', () => { const res = await Call, any>( updatePasswordApi.default, { - body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' }, + body: { oldPsw: wrongPassword, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any } ); @@ -85,7 +90,7 @@ describe('updatePasswordByOld API', () => { const res = await Call, any>( updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' }, + body: { oldPsw: oldPassword, newPsw: oldPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any } ); @@ -95,7 +100,7 @@ describe('updatePasswordByOld API', () => { it('should reject when oldPsw is missing', async () => { const res = await Call, any>(updatePasswordApi.default, { - body: { newPsw: 'newhashpassword' }, + body: { newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -105,7 +110,7 @@ describe('updatePasswordByOld API', () => { it('should reject when newPsw is missing', async () => { const res = await Call, any>(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword' }, + body: { oldPsw: oldPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -127,7 +132,7 @@ describe('updatePasswordByOld API', () => { const res = await Call, any>( updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' } + body: { oldPsw: oldPassword, newPsw: newPassword } } ); @@ -139,7 +144,7 @@ describe('updatePasswordByOld API', () => { it('should reject oldPsw as MongoDB operator object ($ne injection)', async () => { // GHSA-jxvr-h2vx-p73r Step 3: oldPsw: {"$ne": ""} bypasses old password check const res = await Call, any>(updatePasswordApi.default, { - body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' }, + body: { oldPsw: { $ne: '' }, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -154,7 +159,7 @@ describe('updatePasswordByOld API', () => { it('should reject oldPsw with $regex injection', async () => { const res = await Call, any>(updatePasswordApi.default, { - body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' }, + body: { oldPsw: { $regex: '.*' }, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -164,7 +169,7 @@ describe('updatePasswordByOld API', () => { it('should reject newPsw as non-string type', async () => { const res = await Call, any>(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } }, + body: { oldPsw: oldPassword, newPsw: { $ne: '' } }, auth: makeAuth(testUser, testTeam, testTmb) as any }); diff --git a/test/mocks/common/mongo.ts b/test/mocks/common/mongo.ts index 978e97e571b8..22947c069606 100644 --- a/test/mocks/common/mongo.ts +++ b/test/mocks/common/mongo.ts @@ -2,8 +2,6 @@ import { vi } from 'vitest'; import { randomUUID } from 'crypto'; import type { Mongoose } from '@fastgpt/service/common/mongo'; import { connectionMongo } from '@fastgpt/service/common/mongo'; -import { connection } from '@fastgpt/service/common/dal/mongo/connection'; -import { serviceEnv } from '@fastgpt/service/env'; const fileDbPrefix = `fastgpt_test_${process.env.VITEST_WORKER_ID ?? '0'}_${randomUUID().replaceAll( '-', @@ -14,12 +12,6 @@ const connectPromises = new WeakMap>(); let dbSeq = 0; const getDbName = (db: Mongoose) => { - // 主业务库:DAL 连接实例与 connectionMongo 共享同一测试库, - // 保证迁移期旧 Model 写入的数据可以被 DAL Repository 读到(生产环境两者指向同一数据库)。 - if (db === connection.client) { - return getDbName(connectionMongo); - } - const existing = dbNames.get(db); if (existing) return existing; @@ -75,17 +67,6 @@ vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any }; }); -/** - * Mock DAL 独立连接:复用同一套按测试文件/实例隔离的数据库分配, - * 并让 DAL 连接实例与 connectionMongo 共享主业务库。 - */ -vi.mock(import('@fastgpt/service/common/dal/mongo/connection'), async (importOriginal: any) => { - const mod = await importOriginal(); - mod.connection.connect = async (url: string = serviceEnv.MONGODB_URI) => - connectTestMongo({ db: mod.connection.client, url }); - return mod; -}); - /** * Mock mongoSessionRun to avoid transaction issues in tests * MongoDB transactions require replica set, which may not be available in test environment diff --git a/test/setup.ts b/test/setup.ts index 2238ece1a38d..355d4f23a357 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -7,7 +7,6 @@ import { afterAll, beforeAll, beforeEach, inject, onTestFinished, vi } from 'vit import setupModels from './setupModels'; import { clean } from './datas/users'; import { connectionLogMongo, connectionMongo } from '@fastgpt/service/common/mongo'; -import { connection } from '@fastgpt/service/common/dal/mongo/connection'; import { loadVectorDBEnv } from './utils/env'; import type { Mongoose } from '@fastgpt/service/common/mongo'; @@ -32,7 +31,6 @@ beforeAll(async () => { vi.stubEnv('MONGODB_URI', inject('MONGODB_URI')); await connectMongo({ db: connectionMongo, url: inject('MONGODB_URI') }); await connectMongo({ db: connectionLogMongo, url: inject('MONGODB_URI') }); - await connection.connect(inject('MONGODB_URI')); initGlobalVariables(); global.systemEnv = {} as any; @@ -62,7 +60,6 @@ afterAll(async () => { await connectionLogMongo?.connection.db?.dropDatabase(); await connectionMongo?.disconnect(); await connectionLogMongo?.disconnect(); - await connection.disconnect(); }); beforeEach(async () => { From a931b1913c82aed1e28b015d457f0f8a80c15196 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Thu, 20 Aug 2026 17:25:35 +0800 Subject: [PATCH 12/14] update pro --- pnpm-lock.yaml | 356 +++++++++++++++++++++++++++++++++++++------------ pro | 2 +- 2 files changed, 271 insertions(+), 87 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b2a6e0b0a43..a34441e7319d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -271,7 +271,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: catalog:lint - version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) husky: specifier: ^8.0.3 version: 8.0.3 @@ -289,7 +289,7 @@ importers: version: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) next-i18next: specifier: 'catalog:' - version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) prettier: specifier: catalog:lint version: 3.8.3 @@ -776,7 +776,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 'catalog:' - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 'catalog:' version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -857,7 +857,7 @@ importers: version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 'catalog:' - version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -936,7 +936,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 'catalog:' - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 'catalog:' version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -987,7 +987,7 @@ importers: version: 1.14.3 '@scalar/api-reference-react': specifier: ^0.9.59 - version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) + version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3) '@t3-oss/env-core': specifier: 'catalog:' version: 0.13.10(typescript@6.0.3)(zod@4.4.3) @@ -1012,9 +1012,6 @@ importers: assert: specifier: ^2.0.0 version: 2.1.0 - autoprefixer: - specifier: ^10.4.19 - version: 10.5.4(postcss@8.5.26) axios: specifier: 'catalog:' version: 1.18.1 @@ -1080,7 +1077,7 @@ importers: version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 'catalog:' - version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nodemailer: specifier: ^7.0.11 version: 7.0.13 @@ -1132,9 +1129,6 @@ importers: sass: specifier: ^1.58.3 version: 1.85.1 - tailwindcss: - specifier: ^3.0 - version: 3.4.18(tsx@4.20.6)(yaml@2.8.4) utf-8-validate: specifier: ^5.0.10 version: 5.0.10 @@ -1189,10 +1183,10 @@ importers: version: 0.25.12 eslint: specifier: catalog:lint - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: catalog:lint - version: 16.3.0(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + version: 16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) mongodb-memory-server: specifier: 'catalog:' version: 10.4.3(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.9) @@ -1204,7 +1198,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) pro/browser-sandbox: dependencies: @@ -1330,7 +1324,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 'catalog:' - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 'catalog:' version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1492,7 +1486,7 @@ importers: version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 'catalog:' - version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -1613,10 +1607,10 @@ importers: version: 15.5.13 eslint: specifier: catalog:lint - version: 9.39.4(jiti@2.7.0) + version: 9.39.4(jiti@1.21.7) eslint-config-next: specifier: catalog:lint - version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) tailwindcss: specifier: ^3 version: 3.4.18(tsx@4.20.6)(yaml@2.8.4) @@ -1628,10 +1622,10 @@ importers: version: 6.0.3 typescript-eslint: specifier: catalog:lint - version: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + version: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) projects/code-sandbox: dependencies: @@ -1713,7 +1707,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 'catalog:' - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 'catalog:' version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1758,7 +1752,7 @@ importers: version: 16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 'catalog:' - version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) react: specifier: ^18 version: 18.3.1 @@ -1789,7 +1783,7 @@ importers: version: 9.39.4(jiti@2.7.0) eslint-config-next: specifier: catalog:lint - version: 16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + version: 16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) postcss: specifier: 'catalog:' version: 8.5.26 @@ -7435,13 +7429,6 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - autoprefixer@10.5.4: - resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -9323,9 +9310,6 @@ packages: resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} engines: {node: '>=0.8'} - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@9.1.7: resolution: {integrity: sha512-nKxBkIO4IPkMEqcBbbATxsVjwPYShKl051yhBv9628iAH6JLeHD0siBHxkL62oQzMC1+GNX73XtPjgP753ufuw==} peerDependencies: @@ -16342,7 +16326,7 @@ snapshots: '@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': + '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': dependencies: '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/cache': 11.14.0 @@ -17298,6 +17282,10 @@ snapshots: dependencies: tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.4) + '@headlessui/tailwindcss@0.2.2(tailwindcss@4.2.4)': + dependencies: + tailwindcss: 4.2.4 + '@headlessui/vue@1.7.23(vue@3.5.40(typescript@6.0.3))': dependencies: '@tanstack/vue-virtual': 3.13.12(vue@3.5.40(typescript@6.0.3)) @@ -19285,6 +19273,44 @@ snapshots: - universal-cookie - zod + '@scalar/agent-chat@0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3)': + dependencies: + '@ai-sdk/vue': 3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.4.3) + '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/json-magic': 0.12.19 + '@scalar/openapi-types': 0.9.4 + '@scalar/schemas': 0.8.0 + '@scalar/themes': 0.17.2 + '@scalar/types': 0.17.0 + '@scalar/use-toasts': 0.10.4(typescript@6.0.3) + '@scalar/validation': 0.6.2 + '@scalar/workspace-store': 0.56.0(typescript@6.0.3) + '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) + ai: 6.0.33(zod@4.4.3) + js-base64: 3.7.8 + neverpanic: 0.0.8 + truncate-json: 3.0.1 + vue: 3.5.40(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - idb-keyval + - jwt-decode + - nprogress + - qrcode + - sortablejs + - supports-color + - tailwindcss + - typescript + - universal-cookie + - zod + '@scalar/api-client@3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)': dependencies: '@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4)) @@ -19333,6 +19359,54 @@ snapshots: - typescript - universal-cookie + '@scalar/api-client@3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)': + dependencies: + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) + '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) + '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/oas-utils': 0.19.9(typescript@6.0.3) + '@scalar/openapi-types': 0.9.4 + '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/snippetz': 0.9.24 + '@scalar/themes': 0.17.2 + '@scalar/typebox': 0.1.3 + '@scalar/types': 0.17.0 + '@scalar/use-codemirror': 0.14.14(typescript@6.0.3) + '@scalar/use-hooks': 0.4.9(typescript@6.0.3) + '@scalar/use-toasts': 0.10.4(typescript@6.0.3) + '@scalar/workspace-store': 0.56.0(typescript@6.0.3) + '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) + '@vueuse/integrations': 13.9.0(axios@1.18.1)(focus-trap@7.8.0)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.40(typescript@6.0.3)) + focus-trap: 7.8.0 + fuse.js: 7.1.0 + js-base64: 3.7.8 + jsonc-parser: 3.3.1 + nanoid: 5.1.16 + pretty-ms: 9.3.0 + radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) + set-cookie-parser: 3.1.0 + vue: 3.5.40(typescript@6.0.3) + yaml: 2.8.4 + zod: 4.4.3 + transitivePeerDependencies: + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - idb-keyval + - jwt-decode + - nprogress + - qrcode + - sortablejs + - supports-color + - tailwindcss + - typescript + - universal-cookie + '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3)': dependencies: '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3) @@ -19355,6 +19429,28 @@ snapshots: - universal-cookie - zod + '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3)': + dependencies: + '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3) + '@scalar/types': 0.17.0 + react: 18.3.1 + transitivePeerDependencies: + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - idb-keyval + - jwt-decode + - nprogress + - qrcode + - sortablejs + - supports-color + - tailwindcss + - typescript + - universal-cookie + - zod + '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.4.3)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) @@ -19399,6 +19495,50 @@ snapshots: - universal-cookie - zod + '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3)': + dependencies: + '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) + '@scalar/agent-chat': 0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.4.3) + '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/code-highlight': 0.4.3 + '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/oas-utils': 0.19.9(typescript@6.0.3) + '@scalar/schemas': 0.8.0 + '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/snippetz': 0.9.24 + '@scalar/themes': 0.17.2 + '@scalar/types': 0.17.0 + '@scalar/use-hooks': 0.4.9(typescript@6.0.3) + '@scalar/use-toasts': 0.10.4(typescript@6.0.3) + '@scalar/validation': 0.6.2 + '@scalar/workspace-store': 0.56.0(typescript@6.0.3) + '@unhead/vue': 2.1.17(vue@3.5.40(typescript@6.0.3)) + '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) + fuse.js: 7.1.0 + microdiff: 1.5.0 + nanoid: 5.1.16 + vue: 3.5.40(typescript@6.0.3) + yaml: 2.8.4 + transitivePeerDependencies: + - '@vue/composition-api' + - async-validator + - axios + - change-case + - drauu + - idb-keyval + - jwt-decode + - nprogress + - qrcode + - sortablejs + - supports-color + - tailwindcss + - typescript + - universal-cookie + - zod + '@scalar/asyncapi-upgrader@0.1.4': dependencies: '@scalar/helpers': 0.9.2 @@ -19421,6 +19561,24 @@ snapshots: - tailwindcss - typescript + '@scalar/blocks@0.1.9(tailwindcss@4.2.4)(typescript@6.0.3)': + dependencies: + '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/snippetz': 0.9.24 + '@scalar/themes': 0.17.2 + '@scalar/types': 0.17.0 + '@scalar/workspace-store': 0.56.0(typescript@6.0.3) + '@types/har-format': 1.2.16 + js-base64: 3.7.8 + vue: 3.5.40(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + - tailwindcss + - typescript + '@scalar/code-highlight@0.4.3': dependencies: hast-util-to-text: 4.0.2 @@ -19463,6 +19621,28 @@ snapshots: - tailwindcss - typescript + '@scalar/components@0.27.10(tailwindcss@4.2.4)(typescript@6.0.3)': + dependencies: + '@floating-ui/utils': 0.2.10 + '@floating-ui/vue': 1.1.9(vue@3.5.40(typescript@6.0.3)) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) + '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) + '@scalar/code-highlight': 0.4.3 + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/themes': 0.17.2 + '@scalar/use-hooks': 0.4.9(typescript@6.0.3) + '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) + cva: 1.0.0-beta.4(typescript@6.0.3) + radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) + vue: 3.5.40(typescript@6.0.3) + vue-component-type-helpers: 3.3.9 + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + - tailwindcss + - typescript + '@scalar/helpers@0.9.2': {} '@scalar/icons@0.7.5(typescript@6.0.3)': @@ -19518,6 +19698,21 @@ snapshots: - tailwindcss - typescript + '@scalar/sidebar@0.9.34(tailwindcss@4.2.4)(typescript@6.0.3)': + dependencies: + '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) + '@scalar/helpers': 0.9.2 + '@scalar/icons': 0.7.5(typescript@6.0.3) + '@scalar/themes': 0.17.2 + '@scalar/use-hooks': 0.4.9(typescript@6.0.3) + '@scalar/workspace-store': 0.56.0(typescript@6.0.3) + vue: 3.5.40(typescript@6.0.3) + transitivePeerDependencies: + - '@vue/composition-api' + - supports-color + - tailwindcss + - typescript + '@scalar/snippetz@0.9.24': dependencies: '@scalar/helpers': 0.9.2 @@ -21500,15 +21695,6 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.5.4(postcss@8.5.26): - dependencies: - browserslist: 4.28.7 - caniuse-lite: 1.0.30001806 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.26 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -23066,18 +23252,18 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + eslint-config-next@16.3.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@2.7.0)) - eslint: 9.39.4(jiti@2.7.0) + '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@1.21.7)) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@1.21.7)) globals: 16.4.0 - typescript-eslint: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + typescript-eslint: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -23086,18 +23272,18 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@16.3.0(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): + eslint-config-next@16.3.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@1.21.7)) - eslint: 9.39.4(jiti@1.21.7) + '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@2.7.0)) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) globals: 16.4.0 - typescript-eslint: 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + typescript-eslint: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -23114,58 +23300,58 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) get-tsconfig: 4.14.3 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.17 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) get-tsconfig: 4.14.3 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.17 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) - eslint: 9.39.4(jiti@2.7.0) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -23174,9 +23360,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) hasown: 2.0.4 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23188,13 +23374,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -23203,9 +23389,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23890,8 +24076,6 @@ snapshots: frac@1.1.2: {} - fraction.js@5.3.4: {} - framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -26674,7 +26858,7 @@ snapshots: transitivePeerDependencies: - supports-color - next-i18next@15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-i18next@15.4.2(i18next@23.16.8)(next@16.3.0(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.10 '@types/hoist-non-react-statics': 3.3.6 diff --git a/pro b/pro index ea04fa64a29c..319b8d60bbb2 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit ea04fa64a29c69737b3a02ac14ed2a4f32e33a41 +Subproject commit 319b8d60bbb2dcc2769f740e2795189da09e1bff From 8fc6675c7eeba0e64adb002e2bc8907fa2fdd4d2 Mon Sep 17 00:00:00 2001 From: Xianquan Date: Thu, 20 Aug 2026 17:52:59 +0800 Subject: [PATCH 13/14] fix: resolve mongodb refactor checks --- .../dal/mongodb/business/support/user/team/org/schema.ts | 4 +++- packages/service/support/permission/org/controllers.ts | 4 ++-- pro | 2 +- .../test/api/support/user/account/resetExpiredPsw.test.ts | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/dal/mongodb/business/support/user/team/org/schema.ts b/packages/dal/mongodb/business/support/user/team/org/schema.ts index 4ac814dfdff0..ab4157f0a797 100644 --- a/packages/dal/mongodb/business/support/user/team/org/schema.ts +++ b/packages/dal/mongodb/business/support/user/team/org/schema.ts @@ -19,7 +19,9 @@ export const OrgDocumentSchema = new Schema( }, path: { type: String, - required: true + required: function (this: { path?: unknown }) { + return typeof this.path !== 'string'; + } }, name: { type: String, diff --git a/packages/service/support/permission/org/controllers.ts b/packages/service/support/permission/org/controllers.ts index 109a27b128e1..35974a94f6ca 100644 --- a/packages/service/support/permission/org/controllers.ts +++ b/packages/service/support/permission/org/controllers.ts @@ -8,9 +8,9 @@ const toLegacyOrg = (org: NonNullable diff --git a/pro b/pro index 319b8d60bbb2..e058dfb64c5e 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 319b8d60bbb2dcc2769f740e2795189da09e1bff +Subproject commit e058dfb64c5e4158e5eca6f9b4b80e9d11ae77ec diff --git a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts index caafbde9f745..022218e44a99 100644 --- a/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts +++ b/projects/app/test/api/support/user/account/resetExpiredPsw.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import resetExpiredPswHandler from '@/pages/api/support/user/account/resetExpiredPsw'; import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; @@ -199,6 +198,7 @@ describe('resetExpiredPsw API', () => { it('should reject newPsw as non-string (injection guard)', async () => { vi.stubEnv('PASSWORD_EXPIRED_MONTH', '1'); + const resetExpiredPswApi = await loadResetExpiredPswApi(); const twoMonthsAgo = new Date(); twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2); @@ -223,8 +223,9 @@ describe('resetExpiredPsw API', () => { it('should reject string body as input parse error', async () => { vi.stubEnv('PASSWORD_EXPIRED_MONTH', '1'); + const resetExpiredPswApi = await loadResetExpiredPswApi(); - const res = await Call(resetExpiredPswHandler, { + const res = await Call(resetExpiredPswApi.default, { body: 'newhashedpassword', auth: { userId: String(testUser._id), From 78cf544856d37248643c4aedd41cfe0e30b9b83b Mon Sep 17 00:00:00 2001 From: Xianquan Date: Thu, 20 Aug 2026 18:03:22 +0800 Subject: [PATCH 14/14] test: align pro password API expectation --- pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro b/pro index e058dfb64c5e..1e0976b1a485 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit e058dfb64c5e4158e5eca6f9b4b80e9d11ae77ec +Subproject commit 1e0976b1a485bf49af212f721e9af3d20393cfa4