diff --git a/.agents/design/dal-basic-architecture.md b/.agents/design/dal-basic-architecture.md new file mode 100644 index 000000000000..da02d9c1ba1b --- /dev/null +++ b/.agents/design/dal-basic-architecture.md @@ -0,0 +1,177 @@ +# DAL 基础架构(User 垂直切片) + +## 目标 + +让业务层依赖数据库无关的实体和 Repository 能力,MongoDB、SQL-like 数据库只负责实现适配器。 + +## 分层 + +```text +business/support/ 按业务域组织数据库无关 DTO、实体和 Repository 合同 +db/ Mongo 与 SQL-like adapter 共用的物理表名、错误、并发和事务抽象 +mongodb/ Mongo 通用工具、索引、生命周期能力,以及 business 下的 Mongo 业务实现 +sql/ 后续增加 SQL-like 的 business 实现,复用 business/support 中的合同 +``` + +对外 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 当前实现 + +- `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 和事务执行器 + +## 约束 + +1. 上层不得依赖 Mongo Model、Mongoose Query 或 Mongo 查询对象。 +2. `EntityId` 暂不限制为 24 位,以保留 UUID、SQL sequence 等后续实现空间。 +3. 数据库字段命名、ID 类型、查询参数转换必须集中在 adapter/mapper 中。 +4. 新增数据库实现时复用 `business/support/` 中的实体、DTO 和 Repository 合同,不复制业务实体定义。 +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` 在内部自行判断是否自动重试,该策略不进入公共错误合同。 + +## 业务侧试接入 + +`packages/service/common/dal/` 是业务侧的新 DAL 组装目录,不修改原有 `common/mongo`。Mongo DAL 在 service 层拥有**独立连接**,不复用 `connectionMongo`,连接生命周期由 dal/mongo 自己管理。 + +目录结构: + +```text +packages/service/common/dal/ + index.ts # 数据库无关出口:按 DAL_DB_TYPE 组装各业务 Repository,导出 transactionRunner 及接口类型 + mongo/ + index.ts # Mongo composition root:createMongoAdapter(client?),唯一接触 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(接入 `DAL_DB_TYPE=sql` 分支)~~ 已延后:整个 MongoDB 重构完成后再考虑,另起项目跟进。 +- [x] 统一 DAL 的分页、唯一约束错误和并发更新抽象。 +- [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..22da4c52e4e9 --- /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/model.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 new file mode 100644 index 000000000000..901564c91481 --- /dev/null +++ b/.agents/design/dal-m9-user-call-migration.md @@ -0,0 +1,89 @@ +# 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? + +## 决策记录(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/.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/.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/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/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/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/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/business/support/user/entity.ts b/packages/dal/business/support/user/entity.ts new file mode 100644 index 000000000000..f09e09ff7466 --- /dev/null +++ b/packages/dal/business/support/user/entity.ts @@ -0,0 +1,37 @@ +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 '../../../db/types'; + +export const UserOpenaiAccountSchema = z.object({ + key: z.string().optional(), + baseUrl: z.string().optional() +}); + +export const UserMetaSchema = z.object({ + isActivatedWecomLicense: z.boolean().optional() +}); + +/** 用户实体是跨数据库对外暴露的稳定形状,不包含 Mongo 文档元数据或密码。 */ +export const UserSchema = z.object({ + id: EntityIdSchema, + status: z.enum(UserStatusEnum), + username: z.string(), + passwordUpdateTime: z.date().optional(), + createTime: z.date(), + promotionRate: z.number(), + openaiAccount: UserOpenaiAccountSchema.optional(), + timezone: z.string(), + 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; 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/business/support/user/repository.ts b/packages/dal/business/support/user/repository.ts new file mode 100644 index 000000000000..ad149f49aef3 --- /dev/null +++ b/packages/dal/business/support/user/repository.ts @@ -0,0 +1,34 @@ +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; + findByUsername(username: string, context?: TransactionContext): Promise; + findIdByUsername(username: string, context?: TransactionContext): Promise; + findPasswordUpdateTimeById( + 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 + ): Promise; + create(input: CreateUser, context?: TransactionContext): Promise; + updateById(id: EntityId, patch: UpdateUser, context?: TransactionContext): Promise; + /** 仅当记录仍符合 expected 时更新,记录存在但状态不匹配时抛出 DB_CONFLICT。 */ + updateByIdIfState( + id: EntityId, + expected: ExpectedState, + patch: UpdateUser, + context?: TransactionContext + ): Promise; +}; 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/business/support/user/verification/dto.ts b/packages/dal/business/support/user/verification/dto.ts new file mode 100644 index 000000000000..c9077995e465 --- /dev/null +++ b/packages/dal/business/support/user/verification/dto.ts @@ -0,0 +1,8 @@ +import z from 'zod'; + +/** 查询有效材料的最小过滤条件;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/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 new file mode 100644 index 000000000000..53dc4405563f --- /dev/null +++ b/packages/dal/db/adapter.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 000000000000..02b9efd1e792 --- /dev/null +++ b/packages/dal/db/concurrency.ts @@ -0,0 +1,19 @@ +import type { EntityId } from './types'; +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/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..b6b9b882b250 --- /dev/null +++ b/packages/dal/db/index.ts @@ -0,0 +1,21 @@ +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 { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseOperationError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError, + 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'; +export type { TransactionContext, TransactionRunner } from './transaction'; 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/db/tables.ts b/packages/dal/db/tables.ts new file mode 100644 index 000000000000..5aac2df47bc0 --- /dev/null +++ b/packages/dal/db/tables.ts @@ -0,0 +1,16 @@ +/** Mongo collection 与 SQL-like table 共用的物理名称。 */ +export const tables = { + user: 'users', + 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', + teamEnterpriseAuthTask: 'team_enterprise_auth_tasks' +} as const; + +export type TableName = (typeof tables)[keyof typeof tables]; diff --git a/packages/dal/db/transaction.ts b/packages/dal/db/transaction.ts new file mode 100644 index 000000000000..3d272c2000b5 --- /dev/null +++ b/packages/dal/db/transaction.ts @@ -0,0 +1,5 @@ +export type TransactionContext = symbol; + +export type TransactionRunner = { + withTransaction(handler: (context: TransactionContext) => Promise): Promise; +}; diff --git a/packages/dal/db/types.ts b/packages/dal/db/types.ts new file mode 100644 index 000000000000..56c43ec999c0 --- /dev/null +++ b/packages/dal/db/types.ts @@ -0,0 +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/index.ts b/packages/dal/index.ts new file mode 100644 index 000000000000..f7c359ee18c0 --- /dev/null +++ b/packages/dal/index.ts @@ -0,0 +1,3 @@ +export * from './business'; +export * from './db'; +export * from './mongodb'; diff --git a/packages/dal/mongodb/adapter.ts b/packages/dal/mongodb/adapter.ts new file mode 100644 index 000000000000..e97e41abcb1b --- /dev/null +++ b/packages/dal/mongodb/adapter.ts @@ -0,0 +1,42 @@ +import mongoose, { type Mongoose } from 'mongoose'; +import type { DatabaseAdapter } from '../db'; +import type { DatabaseErrorAdapter } from '../db'; +import { MongoErrorAdapter } from './errors'; +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 = { + client?: Mongoose; + errorAdapter?: DatabaseErrorAdapter; +}; + +/** MongoDB adapter,保证所有仓储和事务共享同一个 Mongoose client。 */ +export class MongoAdapter implements DatabaseAdapter { + readonly userRepository; + readonly teamRepository; + readonly groupRepository; + readonly orgRepository; + readonly tmpDataRepository; + readonly transactionRunner; + readonly errorAdapter; + + constructor({ + client = mongoose, + errorAdapter = new MongoErrorAdapter() + }: MongoAdapterDependencies = {}) { + this.errorAdapter = errorAdapter; + this.userRepository = createMongoUserRepository(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/business/support/user/entity.ts b/packages/dal/mongodb/business/support/user/entity.ts new file mode 100644 index 000000000000..2e8fbf28424e --- /dev/null +++ b/packages/dal/mongodb/business/support/user/entity.ts @@ -0,0 +1,42 @@ +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 type { User } from '../../../../business/support/user/entity'; +import { UserSchema } from '../../../../business/support/user/entity'; +import type { UserDocument } from './schema'; +import { toEntityId } from '../../../utils'; + +/** Mongo 文档映射为共享 User 实体时使用的生产默认值。 */ +export const userDefaultFieldValues: { + timezone: string; + promotionRate: number; + language: string; + tags: UserTagsType[]; +} = { + timezone: 'Asia/Shanghai', + promotionRate: 0, + language: LangEnum.zh_CN, + tags: [] +}; + +/** 将 Mongo 用户文档转换为数据库无关的 User 实体,并隐藏 Mongo 元数据和密码。 */ +export const toUser = (document: UserDocument): User => + UserSchema.parse({ + id: toEntityId(document._id), + status: document.status ?? UserStatusEnum.active, + username: document.username, + passwordUpdateTime: document.passwordUpdateTime ?? undefined, + createTime: document.createTime ?? document._id.getTimestamp(), + 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 ?? 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/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/business/support/user/repository.ts b/packages/dal/mongodb/business/support/user/repository.ts new file mode 100644 index 000000000000..fbca6173f703 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/repository.ts @@ -0,0 +1,176 @@ +import mongoose, { type Model, type Mongoose, type Query } from 'mongoose'; +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( + 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 { + return this.execute(async () => { + const document = await this.withSession( + this.model.findById(toMongoObjectId(id)), + context + ).lean(); + return document ? toUser(document) : null; + }); + } + + async findByUsername(username: string, context?: TransactionContext): Promise { + return this.execute(async () => { + const document = await this.withSession( + this.model.findOne({ username }), + context + ).lean(); + return document ? toUser(document) : null; + }); + } + + /** 仅投影 _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 + ): 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(filter).select('+password'), + context + ).lean(); + return document && document.password === credentials.password ? toUser(document) : null; + }); + } + + /** 仅投影 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); + const [document] = await this.model.create([input], session ? { session } : undefined); + return toUser(document.toObject() as UserDocument); + }); + } + + async updateById( + id: EntityId, + patch: UpdateUser, + 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 }, { new: true }), + context + ).lean(); + return document ? toUser(document) : null; + }); + } + + async updateByIdIfState( + id: EntityId, + expected: ExpectedState, + 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 = ( + client: Mongoose, + errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter() +) => new MongoUserRepository(getUserModel(client), errorAdapter); diff --git a/packages/dal/mongodb/business/support/user/schema.ts b/packages/dal/mongodb/business/support/user/schema.ts new file mode 100644 index 000000000000..a197e7c3cf51 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/schema.ts @@ -0,0 +1,79 @@ +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 '../../../model'; + +export const UserDocumentSchema = new Schema({ + status: { + type: String, + enum: Object.keys(userStatusMap), + default: UserStatusEnum.active + }, + username: { + type: String, + required: true + }, + password: { + type: String, + required: true, + // API 边界已经完成哈希,DAL 按不透明凭据原样保存,避免重复哈希。 + 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.options + }, + 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 getUserModel = (client: Mongoose = mongoose): Model => { + return getDalModel(client, 'User', UserDocumentSchema, tables.user); +}; 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/business/support/user/team/group/schema.ts b/packages/dal/mongodb/business/support/user/team/group/schema.ts new file mode 100644 index 000000000000..4f8b8d2d4609 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/group/schema.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 '../../../../../model'; + +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/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/business/support/user/team/member/schema.ts b/packages/dal/mongodb/business/support/user/team/member/schema.ts new file mode 100644 index 000000000000..c6110b1ab5cc --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/member/schema.ts @@ -0,0 +1,77 @@ +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 '../../../../../model'; + +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 +}); + +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 } +}); +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/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/business/support/user/team/org/schema.ts b/packages/dal/mongodb/business/support/user/team/org/schema.ts new file mode 100644 index 000000000000..ab4157f0a797 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/org/schema.ts @@ -0,0 +1,57 @@ +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 '../../../../../model'; + +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: function (this: { path?: unknown }) { + return typeof this.path !== 'string'; + } + }, + 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/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/business/support/user/team/schema.ts b/packages/dal/mongodb/business/support/user/team/schema.ts new file mode 100644 index 000000000000..b6dbb237bfbe --- /dev/null +++ b/packages/dal/mongodb/business/support/user/team/schema.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 '../../../../model'; + +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/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/business/support/user/verification/schema.ts b/packages/dal/mongodb/business/support/user/verification/schema.ts new file mode 100644 index 000000000000..0015b2063151 --- /dev/null +++ b/packages/dal/mongodb/business/support/user/verification/schema.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 '../../../../model'; + +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/concurrency.ts b/packages/dal/mongodb/concurrency.ts new file mode 100644 index 000000000000..2236cc49ef37 --- /dev/null +++ b/packages/dal/mongodb/concurrency.ts @@ -0,0 +1,30 @@ +import type { ClientSession, FilterQuery, Model, UpdateQuery } from 'mongoose'; +import type { EntityId } from '../db/types'; +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/errors.ts b/packages/dal/mongodb/errors.ts new file mode 100644 index 000000000000..0faa113acabb --- /dev/null +++ b/packages/dal/mongodb/errors.ts @@ -0,0 +1,84 @@ +import { + DatabaseConflictError, + DatabaseExecutionError, + DatabaseInvalidArgumentError, + DatabaseTimeoutError, + DatabaseUnavailableError, + DatabaseUniqueConstraintError, + type DatabaseOperationError, + 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(); +}; + +// 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): DatabaseOperationError { + if (isDatabaseOperationError(error)) return error; + if (error instanceof MongoInvalidArgumentError) { + return new DatabaseInvalidArgumentError(error); + } + + const record = toErrorRecord(error); + // 序列化/包装后的驱动错误可能携带字符串 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 + }); + } + if (record.name === 'ValidationError' || record.name === 'CastError') { + return new DatabaseInvalidArgumentError(error); + } + if ( + conflictCodes.has(code) || + record.codeName === 'WriteConflict' || + hasErrorLabel(record, 'TransientTransactionError') + ) { + return new DatabaseConflictError(error); + } + if (timeoutCodes.has(code) || timeoutNames.has(String(record.name))) { + return new DatabaseTimeoutError(error); + } + if (unavailableCodes.has(code) || 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 new file mode 100644 index 000000000000..9cb94e2f6dc8 --- /dev/null +++ b/packages/dal/mongodb/index.ts @@ -0,0 +1,13 @@ +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'; +export { setDalLogger, getDalLogger } from './logger'; +export type { DalLogger } from './logger'; +export { createSlowQueryMiddleware as addDalCommonMiddleware } from './middleware'; +export { getDalModel } from './model'; +export * from './business'; diff --git a/packages/dal/mongodb/indexes.ts b/packages/dal/mongodb/indexes.ts new file mode 100644 index 000000000000..a29557222899 --- /dev/null +++ b/packages/dal/mongodb/indexes.ts @@ -0,0 +1,72 @@ +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 (Object.keys(key).length === 0) { + throw new Error('defineIndex: key must not be empty'); + } + + 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); + // 返回副本,防止外部直接修改 schema 上登记的内部元数据。 + return Array.isArray(indexes) ? [...indexes] : []; +}; diff --git a/packages/dal/mongodb/logger.ts b/packages/dal/mongodb/logger.ts new file mode 100644 index 000000000000..5d1695b6be5f --- /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/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/model.ts b/packages/dal/mongodb/model.ts new file mode 100644 index 000000000000..5c8607f9ba1d --- /dev/null +++ b/packages/dal/mongodb/model.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/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/transaction.ts b/packages/dal/mongodb/transaction.ts new file mode 100644 index 000000000000..01871467e200 --- /dev/null +++ b/packages/dal/mongodb/transaction.ts @@ -0,0 +1,67 @@ +import mongoose, { type ClientSession, type Mongoose } from 'mongoose'; +import type { TransactionContext, TransactionRunner } from '../db/transaction'; +import { isDatabaseOperationError, type DatabaseErrorAdapter } from '../db'; +import { MongoErrorAdapter, MongoInvalidArgumentError } from './errors'; + +const sessions = new WeakMap(); + +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 MongoTransactionRunner implements TransactionRunner { + constructor( + private readonly client: Mongoose = mongoose, + private readonly errorAdapter: DatabaseErrorAdapter = new MongoErrorAdapter(), + private readonly commitTimeoutMS = 60000 + ) {} + + async withTransaction(handler: (context: TransactionContext) => Promise): Promise { + const session = await this.errorAdapter.execute(() => this.client.startSession()); + const context = Symbol('mongo-transaction'); + sessions.set(context, session); + let operationError: unknown; + + try { + 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: this.commitTimeoutMS } + ); + } catch (error) { + operationError = + error instanceof TransactionHandlerError + ? error.originalError + : this.errorAdapter.adapt(error); + throw operationError; + } + } finally { + sessions.delete(context); + try { + await session.endSession(); + } catch (error) { + // 事务已提交或已失败,session 清理失败不应覆盖原结果,记录日志避免静默丢失。 + console.warn('MongoTransactionRunner: failed to end MongoDB session', error); + } + } + } +} 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..ed35f1fc24d3 --- /dev/null +++ b/packages/dal/mongodb/utils.ts @@ -0,0 +1,16 @@ +import { Types } from 'mongoose'; +import type { EntityId } from '../db/types'; +import { MongoInvalidArgumentError } from './errors'; + +export function toEntityId(value: unknown) { + if (value instanceof Types.ObjectId) return value.toHexString(); + if (typeof value === 'string') return value; + + throw new MongoInvalidArgumentError("mapping id value error: invalid 'id' value type"); +} + +export const toMongoObjectId = (id: EntityId) => { + // 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/package.json b/packages/dal/package.json index 9fff50550cac..0e9997cd5e84 100644 --- a/packages/dal/package.json +++ b/packages/dal/package.json @@ -3,10 +3,14 @@ "version": "1.0.0", "type": "module", "exports": { + ".": "./index.ts", + "./business": "./business/index.ts", + "./db": "./db/index.ts", + "./mongodb": "./mongodb/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" @@ -14,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" }, @@ -24,11 +29,13 @@ "dependencies": { "@fastgpt/global": "workspace:*", "bullmq": "5.52.2", + "mongoose": "catalog:", "ioredis": "^5.11.1", "zod": "catalog:" }, "devDependencies": { "@types/node": "catalog:", + "mongodb-memory-server": "catalog:", "typescript": "catalog:", "vitest": "catalog:" } 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/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/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 new file mode 100644 index 000000000000..0444431b68a3 --- /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/business/support/user/repository'; +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/business/user/entity.test.ts b/packages/dal/test/mongodb/business/user/entity.test.ts new file mode 100644 index 000000000000..b5c27469fde5 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/entity.test.ts @@ -0,0 +1,101 @@ +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/business/support/user/entity'; +import type { UserDocument } from '../../../../mongodb/business/support/user/schema'; + +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('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, + 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/business/user/repository.test.ts b/packages/dal/test/mongodb/business/user/repository.test.ts new file mode 100644 index 000000000000..5f432707eb95 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/repository.test.ts @@ -0,0 +1,387 @@ +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/business/support/user/schema'; +import { MongoUserRepository } from '../../../../mongodb/business/support/user/repository'; +import { MongoTransactionRunner } from '../../../../mongodb/transaction'; +import { + DatabaseConflictError, + 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(), + select: vi.fn().mockReturnThis(), + 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), + 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), + findOneAndUpdate: vi.fn(() => queries.casUpdate), + exists + } as unknown as Model; + + return { repository: new MongoUserRepository(model), model, queries, createdDocument, exists }; +}; + +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.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.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('loads the stored password hash and compares it outside the query filter', async () => { + const { repository, model, queries } = createRepository(); + + const user = await repository.findByCredentials({ + username: 'user@example.com', + password: 'hashed-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: 'hashed-password' }); + + expect(model.findOne).toHaveBeenCalledWith({ _id: new Types.ObjectId(userId) }); + }); + + it('returns null when the stored hash does not match', async () => { + const { repository, queries } = createRepository(); + queries.findOne.lean.mockResolvedValueOnce({ + ...document, + password: 'another-hash' + }); + + await expect( + repository.findByCredentials({ + username: 'user@example.com', + password: 'wrong-hash' + }) + ).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(); + }); + + 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', () => { + 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'); + }); +}); + +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(); + }); +}); diff --git a/packages/dal/test/mongodb/business/user/schema.test.ts b/packages/dal/test/mongodb/business/user/schema.test.ts new file mode 100644 index 000000000000..822b87a85289 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/schema.test.ts @@ -0,0 +1,34 @@ +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/business/support/user/schema'; + +describe('UserDocumentSchema', () => { + 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' }); + + 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('password'); + expect(document.password).toBe('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/business/user/team/repository.test.ts b/packages/dal/test/mongodb/business/user/team/repository.test.ts new file mode 100644 index 000000000000..001c6f7cab75 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/team/repository.test.ts @@ -0,0 +1,226 @@ +import { Types, type Model, type Mongoose } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +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/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'; +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 () => [ + { + 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), + 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 groupRepository = new MongoGroupRepository( + model.memberGroupModel, + {} as Model + ); + const orgRepository = new MongoOrgRepository( + model.orgModel, + {} as Model + ); + + const repository = new MongoTeamRepository( + model.teamModel, + model.teamMemberModel, + undefined, + groupRepository, + orgRepository + ); + + 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, + name: 'DEFAULT_GROUP', + avatar: '/icon/logo.svg' + } + ], + undefined + ); + expect(orgCreate).toHaveBeenCalledWith( + [ + { + 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/business/user/team/schema.test.ts b/packages/dal/test/mongodb/business/user/team/schema.test.ts new file mode 100644 index 000000000000..73f884438835 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/team/schema.test.ts @@ -0,0 +1,89 @@ +import { Mongoose } from 'mongoose'; +import { describe, expect, it } from 'vitest'; +import { + MemberGroupDocumentSchema, + 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', () => { + 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/business/user/verification/repository.test.ts b/packages/dal/test/mongodb/business/user/verification/repository.test.ts new file mode 100644 index 000000000000..df46f7882378 --- /dev/null +++ b/packages/dal/test/mongodb/business/user/verification/repository.test.ts @@ -0,0 +1,87 @@ +import type { Model } from 'mongoose'; +import { describe, expect, it, vi } from 'vitest'; +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', + 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/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/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/middleware.test.ts b/packages/dal/test/mongodb/middleware.test.ts new file mode 100644 index 000000000000..ca5fd23047f7 --- /dev/null +++ b/packages/dal/test/mongodb/middleware.test.ts @@ -0,0 +1,60 @@ +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); +}); + +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' }; + 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 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/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/transaction.test.ts b/packages/dal/test/mongodb/transaction.test.ts new file mode 100644 index 000000000000..8d1508791421 --- /dev/null +++ b/packages/dal/test/mongodb/transaction.test.ts @@ -0,0 +1,124 @@ +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 and commit driver errors and keeps session cleanup best-effort', 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); + + // 事务已提交后 endSession 失败不应让调用方误以为事务失败(记录日志即可)。 + const { client: endClient, session: endSession } = createRuntime(); + vi.mocked(endSession.endSession).mockRejectedValueOnce({ name: 'MongoNetworkError' }); + await expect( + new MongoTransactionRunner(endClient).withTransaction(async () => undefined) + ).resolves.toBeUndefined(); + }); + + 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..a9fd1d1a1ebe --- /dev/null +++ b/packages/dal/test/mongodb/utils.test.ts @@ -0,0 +1,34 @@ +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'); + }); + + it('rejects short hex-like strings that ObjectId.isValid accepts', () => { + expect(() => toMongoObjectId('abcdefghijkl')).toThrow(MongoInvalidArgumentError); + }); +}); 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/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 }, 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/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..28c2f0b6c107 --- /dev/null +++ b/packages/service/common/dal/index.ts @@ -0,0 +1,38 @@ +import type { DatabaseAdapter } from '@fastgpt/dal/db'; +import { serviceEnv } from '../../env'; +import { createMongoAdapter } from './mongo'; + +export type { DatabaseAdapter } from '@fastgpt/dal/db'; +export type { + GroupRepository, + OrgRepository, + TeamRepository, + TmpDataRepository, + UserRepository +} from '@fastgpt/dal/business'; +export type { TransactionContext, TransactionRunner } from '@fastgpt/dal/db'; + +function selectAdapter(): DatabaseAdapter { + // env schema 默认 mongo;被 mock 的 serviceEnv 可能缺少该字段,这里保持同样的默认语义。 + switch (serviceEnv.DAL_DB_TYPE ?? 'mongo') { + case 'mongo': { + return createMongoAdapter(); + } + + default: { + throw new Error(`DAL_DB_TYPE=${serviceEnv.DAL_DB_TYPE} is not implemented yet`); + } + } +} + +// 只实例化一次 adapter,保证 userRepository 与 transactionRunner 来自同一个 adapter 实例。 +const dal = selectAdapter(); + +export const { + transactionRunner, + userRepository, + teamRepository, + groupRepository, + orgRepository, + tmpDataRepository +} = dal; diff --git a/packages/service/common/dal/mongo/index.ts b/packages/service/common/dal/mongo/index.ts new file mode 100644 index 000000000000..b6591ac834fd --- /dev/null +++ b/packages/service/common/dal/mongo/index.ts @@ -0,0 +1,16 @@ +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 { connectionMongo } from '../../mongo'; +import { getLogger, LogCategories } from '../../logger'; + +// 把业务 logger 注入 DAL 慢查询中间件,避免 DAL 包反向依赖 service 日志实现。 +// 测试环境可能只 mock 部分 LogCategories,这里做存在性防护,未注入时 DAL 退回 console。 +if (LogCategories.INFRA?.MONGO) { + setDalLogger(getLogger(LogCategories.INFRA.MONGO)); +} + +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 new file mode 100644 index 000000000000..af6885555eb4 --- /dev/null +++ b/packages/service/common/dal/mongo/lifecycle.ts @@ -0,0 +1,55 @@ +import type { Mongoose } from 'mongoose'; +import { getLogger, LogCategories } from '../../logger'; +import { MongoIndexManager } from '../../mongo'; +import { + getMemberGroupModel, + getGroupMemberModel, + getOrgModel, + getOrgMemberModel, + getTeamMemberModel, + getTeamModel, + getTmpDataModel, + getUserModel +} from '@fastgpt/dal/mongodb/business'; +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), + getGroupMemberModel(client), + getOrgModel(client), + getOrgMemberModel(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/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/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 679c4e1a59b9..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 { MongoUser } from '../../user/schema'; -import { MongoTeam } from '../../user/team/teamSchema'; +import { teamRepository, userRepository } from '../../../common/dal'; import { isCRMReportingConfigured, reportCRMEnterpriseRechargeAmount, @@ -81,10 +80,10 @@ 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 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/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..35974a94f6ca 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 ?? new Date() +}); 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/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/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 d4f380069cbb..8f4bb0510b45 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'); @@ -115,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 }, @@ -160,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), @@ -178,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 }); }, /** 读取仍在有效期内的材料,不主动改变材料生命周期。 */ @@ -189,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; }, @@ -197,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 事务内读取材料、执行业务回调,并在回调成功后消费材料。 */ @@ -244,3 +270,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 b17bd6f6fe7a..738d3a6c1a56 100644 --- a/packages/service/support/user/controller.ts +++ b/packages/service/support/user/controller.ts @@ -1,16 +1,15 @@ 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'; 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; } @@ -18,29 +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 query = MongoUser.findById(tmb.userId); - if (session) query.session(session); - const user = await query; + const user = await userRepository.findById(String(tmb.userId)); if (!user) { return Promise.reject(ERROR_ENUM.unAuthorization); @@ -53,7 +48,7 @@ export async function getUserDetail({ }; return { - _id: user._id, + _id: user.id, username: user.username, avatar: tmb.avatar, timezone: user.timezone, 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 906148ee38ff..ad423fbcac4d 100644 --- a/packages/service/test/support/marketing/crmInterface.test.ts +++ b/packages/service/test/support/marketing/crmInterface.test.ts @@ -23,15 +23,12 @@ 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/support/user/team/teamSchema', () => ({ - MongoTeam: { - findById: (...args: unknown[]) => ({ lean: () => mocks.findTeam(...args) }) +vi.mock('@fastgpt/service/common/dal', () => ({ + userRepository: { + findSemById: (...args: unknown[]) => mocks.findUser(...args) + }, + teamRepository: { + findTeamById: (...args: unknown[]) => mocks.findTeam(...args) } })); @@ -81,8 +78,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'); + expect(mocks.findUser).toHaveBeenCalledWith('user-1'); expect(mocks.report).toHaveBeenCalledWith({ visitorId: 'stored-visitor', event: 'recharge', 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 8c15e21d18ef..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,13 +3,13 @@ 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 type { ClientSession } from '@fastgpt/service/common/mongo'; +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'; @@ -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 @@ -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(String(result._id)).toBe(String(storedUser._id)); + expect(result.id).toBe(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/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 22d538a5d701..a34441e7319d 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,13 +265,13 @@ 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) 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 @@ -280,10 +286,10 @@ 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) + 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 @@ -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 @@ -426,19 +432,25 @@ 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.1.12 + version: 4.4.3 devDependencies: '@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 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 +462,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 +483,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 +498,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 +510,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 +559,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 +577,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 +628,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 +664,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 +718,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 @@ -764,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) @@ -845,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 @@ -924,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) @@ -975,10 +987,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@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.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) @@ -1056,16 +1068,16 @@ 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) 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 @@ -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 @@ -1177,7 +1189,7 @@ importers: version: 16.3.0(eslint@9.39.4(jiti@2.7.0))(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@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: @@ -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: @@ -1312,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) @@ -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,16 +1477,16 @@ 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) 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 @@ -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 @@ -1598,7 +1610,7 @@ importers: 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@1.21.7))(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) @@ -1613,7 +1625,7 @@ importers: version: 8.59.2(eslint@9.39.4(jiti@1.21.7))(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@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: @@ -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: @@ -1695,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) @@ -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,13 +1746,13 @@ 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) 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 @@ -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:' @@ -1771,10 +1783,10 @@ 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.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'} @@ -7818,15 +7623,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 +8687,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 +8777,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 +9009,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 +9019,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 +9035,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 +9100,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 +9227,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==} @@ -9714,12 +9495,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 +9734,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 +9972,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,40 +11304,13 @@ 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==} - engines: {node: '>=16.20.1'} - - mongodb-memory-server@10.1.4: - resolution: {integrity: sha512-+oKQ/kc3CX+816oPFRtaF0CN4vNcGKNjpOQe4bHo/21A3pMD+lC7Xz1EX5HP7siCX4iCpVchDMmCOFXVQSGkUg==} + mongodb-memory-server-core@10.4.3: + resolution: {integrity: sha512-IPjlw73IoSYopnqBibQKxmAXMbOEPf5uGAOsBcaUiNH/TOI7V19WO+K7n5KYtnQ9FqzLGLpvwCGuPOTBSg4s5Q==} engines: {node: '>=16.20.1'} - mongodb@6.14.2: - resolution: {integrity: sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==} + mongodb-memory-server@10.4.3: + resolution: {integrity: sha512-CDZvFisXvGIigsIw5gqH6r9NI/zxGa/uRdutgUL/isuJh+inj0YXb7Ykw6oFMFzqgTJWb7x0I5DpzrqCstBWpg==} 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==} @@ -11599,8 +11339,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 +11394,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 +11404,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 +11418,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 +11542,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 +11634,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 +11902,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 +12016,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 +12138,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 +12887,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 +12999,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 +13282,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 +13441,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 +13679,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 +13687,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 +13746,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 +13959,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 +13968,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,46 +14251,6 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - vite@6.2.2: - resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.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 - vite@6.4.3: resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14700,47 +14332,6 @@ packages: 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 - 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 - void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -14815,10 +14406,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 +14649,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 +14741,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 +14771,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 +14877,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 +15535,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 +15549,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 +15566,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 +15610,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 +15625,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 +15657,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 +15679,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 +15694,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 +15927,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 +16040,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 +16208,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 +16241,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 +16264,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: @@ -16754,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 @@ -16789,7 +16361,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 +16370,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 +16595,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 +16887,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 +16897,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 +16964,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 +17204,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 +17253,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 @@ -17735,9 +17291,9 @@ snapshots: '@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 +17602,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 +17835,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 +17847,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 +17912,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 +17953,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 +18028,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 +19151,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 +19235,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,7 +19251,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) + ai: 6.0.33(zod@4.4.3) js-base64: 3.7.8 neverpanic: 0.0.8 truncate-json: 3.0.1 @@ -19796,9 +19273,9 @@ 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.1.12)': + '@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.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@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 @@ -19812,7 +19289,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) + ai: 6.0.33(zod@4.4.3) js-base64: 3.7.8 neverpanic: 0.0.8 truncate-json: 3.0.1 @@ -19930,9 +19407,9 @@ snapshots: - 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)': + '@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.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,9 +19429,9 @@ 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)': + '@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.1.12) + '@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: @@ -19974,10 +19451,10 @@ snapshots: - 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,10 +19495,10 @@ 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)': + '@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.1.12) + '@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 @@ -20766,7 +20243,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 +20284,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 +20357,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 +20545,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 +20580,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 +20707,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 +20719,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 +20773,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 +20833,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 +20842,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 +20860,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 +20924,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 +20932,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 +20947,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 +20978,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 +21097,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 +21157,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 +21168,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 +21176,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 +21196,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 +21208,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 +21240,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 +21341,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 +21365,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 +21377,7 @@ snapshots: acorn@8.16.0: {} - acorn@8.18.0: - optional: true + acorn@8.18.0: {} address@1.2.2: {} @@ -21989,7 +21405,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 +21416,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: @@ -22294,7 +21710,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 +21858,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 +21906,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 +22335,7 @@ snapshots: core-js-compat@3.41.0: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.7 core-js@3.13.1: {} @@ -23475,7 +22889,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 +23004,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 +23079,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 +23135,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 +23150,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,13 +23252,13 @@ 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): + 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@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-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)) @@ -23860,13 +23272,13 @@ snapshots: - 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): + 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@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@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-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)) @@ -23880,27 +23292,7 @@ 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): - dependencies: - '@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@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@2.7.0))(typescript@6.0.3) - optionalDependencies: - typescript: 6.0.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 is-core-module: 2.16.1 @@ -23908,70 +23300,44 @@ snapshots: 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)) - 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)): 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 + 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)) 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) - 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(@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@1.21.7)): + 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) + '@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(@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 - 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) - 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 @@ -23985,7 +23351,7 @@ snapshots: 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)): + 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 @@ -23996,37 +23362,8 @@ snapshots: 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) - 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)): - 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@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 + 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 minimatch: 3.1.5 @@ -24037,7 +23374,7 @@ 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 @@ -24055,7 +23392,7 @@ snapshots: 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-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 + hasown: 2.0.4 is-core-module: 2.16.1 is-glob: 4.0.3 minimatch: 3.1.5 @@ -24081,7 +23418,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 +23437,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 +23448,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 +23477,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 +23499,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 +23606,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 +23679,6 @@ snapshots: eventemitter3@4.0.7: {} - eventemitter3@5.0.1: {} - eventemitter3@5.0.4: {} events-universal@1.0.1: @@ -24355,13 +23690,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 +23702,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 +23767,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 +23826,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 +23846,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 +23866,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 +23983,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: {} @@ -24827,7 +24148,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) @@ -24876,7 +24197,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 +24298,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 +24670,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 +24691,6 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.7: {} - hono@4.12.27: {} hookable@6.1.1: {} @@ -25498,7 +24809,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 +24931,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 +24965,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 +25380,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 +25560,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 +25724,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 +26650,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 +26703,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 +26713,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 +26729,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 +26743,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 +26752,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 +26836,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 +26848,7 @@ snapshots: negotiator@1.0.0: {} - netmask@2.0.2: {} + netmask@2.1.1: {} neverpanic@0.0.8: {} @@ -27562,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 @@ -27680,7 +26976,7 @@ snapshots: node-hex@1.0.1: {} - node-releases@2.0.37: {} + node-releases@2.0.53: {} nodemailer@7.0.13: {} @@ -27768,8 +27064,6 @@ snapshots: obuf@1.1.2: {} - obug@2.1.1: {} - obug@2.1.4: {} on-exit-leak-free@2.1.2: {} @@ -27817,15 +27111,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 +27294,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 +27377,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 +27477,6 @@ snapshots: picomatch@4.0.3: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} pify@2.3.0: {} @@ -28252,30 +27542,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 +27582,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 +27813,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 +27989,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 +28585,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 +28728,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} - - semver@7.8.0: {} - semver@7.8.5: {} send@0.19.0: @@ -29875,8 +29124,6 @@ snapshots: statuses@2.0.2: {} - std-env@4.1.0: {} - std-env@4.2.0: {} stdin-discarder@0.1.0: @@ -29959,7 +29206,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 +29239,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 +29319,6 @@ snapshots: strnum@1.1.2: {} - strnum@2.2.3: {} - strnum@2.4.2: dependencies: anynum: 1.0.1 @@ -30183,11 +29428,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 +29644,6 @@ snapshots: tinyexec@1.0.4: {} - tinyexec@1.1.2: {} - tinyexec@1.3.0: {} tinyglobby@0.2.15: @@ -30408,18 +29651,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 +29705,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 +29788,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 +29898,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 +29945,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 +30072,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 +30123,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 +30291,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 +30394,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 +30521,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 +30777,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 +30799,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..1e0976b1a485 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 65c90f06c137bbfa424532bbd90cb6e192d0b4eb +Subproject commit 1e0976b1a485bf49af212f721e9af3d20393cfa4 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/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 7006c0d43073..e992be882c35 100644 --- a/projects/app/src/instrumentation-node.ts +++ b/projects/app/src/instrumentation-node.ts @@ -15,6 +15,7 @@ export async function registerNodeInstrumentation() { const [ { connectMongo }, { connectionMongo, connectionLogMongo, MONGO_URL, MONGO_LOG_URL }, + { syncDalModelIndexes }, { systemStartCb }, { initGlobalVariables, getInitConfig, initSystemPluginTags, initAppTemplateTypes }, { initVectorStore }, @@ -40,6 +41,7 @@ export async function registerNodeInstrumentation() { ] = await Promise.all([ import('@fastgpt/service/common/mongo/init'), 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'), @@ -118,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: { 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/checkPswExpired.ts b/projects/app/src/pages/api/support/user/account/checkPswExpired.ts index ce423581a083..535f8ed0fce3 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,13 +11,13 @@ async function handler( ): Promise { const { userId } = await authCert({ req, authToken: true }); - const user = await MongoUser.findById(userId, 'passwordUpdateTime'); + 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/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/resetExpiredPsw.ts b/projects/app/src/pages/api/support/user/account/resetExpiredPsw.ts index d13dff5b8107..cb52359389bd 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,29 +18,24 @@ async function resetExpiredPswHandler(req: ApiRequestProps): Promise; 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/pages/api/support/user/account/updatePasswordByOld.ts b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts index 9090c92583eb..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 { MongoUser } from '@fastgpt/service/support/user/schema'; +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'; @@ -9,6 +8,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; import { UpdatePasswordByOldBodySchema, + UpdatePasswordByOldResponseSchema, type UpdatePasswordByOldBodyType, type UpdatePasswordByOldResponseType } from '@fastgpt/global/openapi/support/user/account/password/api'; @@ -25,14 +25,14 @@ 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'); } const userId = tmb.userId; // auth old password - const user = await MongoUser.findOne({ - _id: userId, + const user = await userRepository.findByCredentials({ + id: String(userId), password: oldPsw }); @@ -45,7 +45,7 @@ async function handler( } // 更新对应的记录 - await MongoUser.findByIdAndUpdate(userId, { + await userRepository.updateById(String(userId), { password: newPsw, passwordUpdateTime: new Date() }); @@ -60,7 +60,7 @@ async function handler( params: {} }); })(); - return user; + return UpdatePasswordByOldResponseSchema.parse(undefined); } export default NextAPI(handler); 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 64d50d4935d6..82e976e1e5a0 100644 --- a/projects/app/src/service/mongo.ts +++ b/projects/app/src/service/mongo.ts @@ -1,8 +1,6 @@ -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 { 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'; @@ -10,33 +8,28 @@ 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 || ''; + // 环境变量保存明文,DAL 约定只接收并保存已经哈希的密码。 + const password = hashStr(appEnv.DEFAULT_ROOT_PSW); - 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 }, context); } else { - const [{ _id }] = await MongoUser.create( - [ - { - username: 'root', - password: hashStr(psw) - } - ], - { session, ordered: true } + const created = await userRepository.create( + { + username: 'root', + password + }, + 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/src/service/support/permission/auth/chatCompletion.ts b/projects/app/src/service/support/permission/auth/chatCompletion.ts index 44f994e7468d..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 { MongoUser } from '@fastgpt/service/support/user/schema'; -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 user = await MongoUser.findOne({ username }).select('_id').lean(); - if (!user) return null; - - return MongoTeamMember.findOne({ - teamId, - userId: user._id, - status: notLeaveStatus - }) - .select('_id userId teamId') - .lean(); + const userId = await userRepository.findIdByUsername(username); + if (!userId) return null; + + 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 dfe76f8883bd..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' } @@ -92,7 +96,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) }); @@ -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 b400ed15410f..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,13 +1,16 @@ 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'; 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(); @@ -54,16 +57,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: newPassword }, + 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 +81,12 @@ describe('resetExpiredPsw API', () => { expect(updatedUser?.passwordUpdateTime).toBeDefined(); const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime(); expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime()); + await expect( + MongoUser.collection.findOne({ _id: testUser._id, password: newPassword }) + ).resolves.not.toBeNull(); + await expect( + MongoUser.collection.findOne({ _id: testUser._id, password: oldPassword }) + ).resolves.toBeNull(); }); it('should reject when password is not expired (PASSWORD_EXPIRED_MONTH not set)', async () => { @@ -85,16 +97,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: newPassword }, + 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 +124,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: newPassword }, + 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 +166,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: newPassword }, + 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,15 +186,19 @@ 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: newPassword } + } + ); expect(res.code).toBe(500); }); 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); @@ -179,7 +206,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), @@ -196,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), 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..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({ @@ -43,23 +48,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: oldPassword, newPsw: newPassword }, + 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.collection.findOne({ _id: testUser._id, password: newPassword }) + ).resolves.not.toBeNull(); + await expect( + MongoUser.collection.findOne({ _id: testUser._id, password: oldPassword }) + ).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: newPassword }, + auth: makeAuth(testUser, testTeam, testTmb) as any + } + ); expect(res.code).toBe(500); @@ -69,17 +87,20 @@ 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: oldPassword, newPsw: oldPassword }, + 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, { - body: { newPsw: 'newhashpassword' }, + const res = await Call, any>(updatePasswordApi.default, { + body: { newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -88,8 +109,8 @@ describe('updatePasswordByOld API', () => { }); it('should reject when newPsw is missing', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword' }, + const res = await Call, any>(updatePasswordApi.default, { + body: { oldPsw: oldPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -108,9 +129,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: oldPassword, newPsw: newPassword } + } + ); expect(res.code).toBe(500); }); @@ -119,8 +143,8 @@ 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, { - body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' }, + const res = await Call, any>(updatePasswordApi.default, { + body: { oldPsw: { $ne: '' }, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -134,8 +158,8 @@ describe('updatePasswordByOld API', () => { }); it('should reject oldPsw with $regex injection', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' }, + const res = await Call, any>(updatePasswordApi.default, { + body: { oldPsw: { $regex: '.*' }, newPsw: newPassword }, auth: makeAuth(testUser, testTeam, testTmb) as any }); @@ -144,8 +168,8 @@ describe('updatePasswordByOld API', () => { }); it('should reject newPsw as non-string type', async () => { - const res = await Call(updatePasswordApi.default, { - body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } }, + const res = await Call, any>(updatePasswordApi.default, { + body: { oldPsw: oldPassword, newPsw: { $ne: '' } }, auth: makeAuth(testUser, testTeam, testTmb) as any }); 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 a69b2bfd232a..22947c069606 100644 --- a/test/mocks/common/mongo.ts +++ b/test/mocks/common/mongo.ts @@ -1,6 +1,7 @@ import { vi } from 'vitest'; import { randomUUID } from 'crypto'; import type { Mongoose } from '@fastgpt/service/common/mongo'; +import { connectionMongo } from '@fastgpt/service/common/mongo'; const fileDbPrefix = `fastgpt_test_${process.env.VITEST_WORKER_ID ?? '0'}_${randomUUID().replaceAll( '-',