首页
/ LibreChat @librechat/data-schemas:用 Mongoose Schema、Model 工厂与数据库方法层组织多租户 AI 应用的数据

LibreChat @librechat/data-schemas:用 Mongoose Schema、Model 工厂与数据库方法层组织多租户 AI 应用的数据

2026-09-05 10:12:22作者:戚魁泉Nursing

本篇以 packages/data-schemas/README.md 为主体,系统讲解 LibreChat 数据层包 @librechat/data-schemas 的分层架构:Schema 定义、TypeScript 类型、Model 工厂函数与数据库方法层四类约定,并结合仓库中 user schema 实现createModels 汇总服务端接入代码 等真实源码,给出完整的新实体接入七步流程与索引、虚拟属性等最佳实践,读完后你能够独立在该包中新增实体、编写数据库方法并通过类型检查。

一、包定位:LibreChat 的数据库访问底座

@librechat/data-schemas 是 LibreChat 的数据库 Schema、模型、类型与操作方法的集合包,基于 Mongoose ODM 构建。从 package.json 可以看到它的关键工程属性:

  • 包名为 @librechat/data-schemas,当前版本 0.0.68type: module,MIT 协议;
  • 构建产物为双格式:dist/index.cjs(CommonJS)与 dist/index.mjs(ESM),通过 exports 字段分别暴露 importrequire 条件,因此既服务 ESM 的 API 端,也可被传统 CJS 代码引入;
  • 通过 peerDependencies 声明运行时依赖边界:mongoose ^8.24.1librechat-data-provider(枚举常量来源)、winston(日志)、meilisearch(搜索同步)、jsonwebtokenklona 等均由宿主应用提供;
  • 开发侧使用 jest ^30 + mongodb-memory-server 做内存 MongoDB 集成测试,构建工具为 tsdown

README 给出的包结构(README 原文)如下:

packages/data-schemas/
├── src/
│   ├── schema/         # Mongoose schema definitions
│   ├── models/         # Model factory functions
│   ├── types/          # TypeScript type definitions
│   ├── methods/        # Database operation methods
│   ├── common/        # Shared constants and enums
│   ├── config/        # Configuration files (winston, etc.)
│   └── index.ts        # Main package exports

对照当前仓库的实际目录,src/ 下还包含若干文档未列出的扩展子目录:app/(面向应用层接口的辅助逻辑)、admin/(管理端能力,如 capabilities)、crypto/migrations/(索引迁移、回填任务)与 utils/(租户上下文、事务、对象操作等工具)。主入口 src/index.ts 汇总导出 createModels()createMethods()、全部类型、租户上下文(tenantStoragegetTenantId 等)以及索引迁移函数(createMCPAuthorityLookupIndexesdropSupersededTenantIndexes 等),是整个包的对外契约面。

二、架构约定:Schema、Types、Model 工厂与 Methods 四层

README 的核心是四个架构模式约定。下面逐层说明,并用仓库源码印证这些约定是如何落地的。

2.1 Schema 层(src/schema/

约定要点:

  • 命名:文件名使用小写(如 user.tsaccessRole.ts);
  • 导入:从 ~/types 导入类型以获得 TypeScript 支持;
  • 导出:仅以 default 导出 schema。

README 给出的示例:

import { Schema } from 'mongoose';
import type { IUser } from '~/types';

const userSchema = new Schema<IUser>(
  {
    name: { type: String },
    email: { type: String, required: true },
    // ... other fields
  },
  { timestamps: true }
);

export default userSchema;

仓库中真实的 schema/user.ts 完全遵循该约定,并展示了生产级 schema 的更多细节:

const userSchema: Schema<IUser> = new Schema<IUser>(
  {
    username: { type: String, lowercase: true, default: '' },
    email: {
      type: String,
      required: [true, "can't be blank"],
      lowercase: true,
      match: [/\S+@\S+\.\S+/, 'is invalid'],
      index: true,
    },
    password: {
      type: String,
      trim: true,
      minlength: 8,
      maxlength: 128,
      select: false, // 敏感字段默认不进入查询结果
    },
    totpSecret: { type: String, select: false }, // 2FA 密钥同样 select: false
    expiresAt: { type: Date, expires: 604800 }, // MongoDB TTL:7 天
    tenantId: { type: String, index: true },
  },
  { timestamps: true },
);

可以看到几个 README「最佳实践」中提到的手法在此均有落地:select: false 用于把 passwordtotpSecretbackupCodes 等敏感字段排除出默认查询投影;expires 定义 TTL 索引让 MongoDB 自动清理过期会话字段;timestamps: true 统一维护 createdAt/updatedAt

2.2 类型层(src/types/

约定要点:

  • 基础类型:定义不含 Mongoose Document 属性的 plain type;
  • Document 接口:在基础类型上叠加 Document_id
  • 枚举/常量:相关枚举放在类型文件中,若是跨实体共享则放入 common/

示例(README 原文):

import type { Document, Types } from 'mongoose';

export type User = {
  name?: string;
  email: string;
  // ... other fields
};

export type IUser = User &
  Document & {
    _id: Types.ObjectId;
  };

IUser 这类「接口类型」正是 schema 层 new Schema<IUser> 泛型与 methods 层 Model<IUser> 断言的共同类型来源,形成「types → schema → models → methods」的类型传递链。跨实体共享的常量(如 common/permissions.ts 中的权限位)则集中放在 src/common/ 下。

2.3 Model 工厂函数(src/models/

约定要点:

  • 函数命名create[EntityName]Model
  • 单例模式:创建前检查 mongoose.models 是否已注册同名模型;
  • 类型安全:使用 types 中对应的接口。

README 示例:

import userSchema from '~/schema/user';
import type * as t from '~/types';

export function createUserModel(mongoose: typeof import('mongoose')) {
  return mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
}

仓库中真实的 models/user.ts 在此基础上多了一步——注册前调用租户隔离插件:

export function createUserModel(mongoose: typeof import('mongoose')): Model<t.IUser> {
  applyTenantIsolation(userSchema);
  return mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
}

applyTenantIsolation 定义在 models/plugins/tenantIsolation.ts,配套的 models/plugins/tenantIsolation.spec.ts 等测试文件印证了这是该包强制的多租户数据边界机制。工厂函数把 mongoose 作为参数显式注入而非模块内直接 import mongoose,正是为了在 Jest 环境中可以注入不同的 mongoose 实例做隔离测试。

所有工厂函数在 models/index.ts 中汇总为 createModels(mongoose),返回值逐一标注了类型(User: ReturnType<typeof createUserModel> 等)。当前版本共注册约 48 个模型,覆盖用户与会话(UserTokenSessionBalance)、对话核心(ConversationMessageChatProjectToolCall)、Agent/MCP/Skill(AgentAgentApiKeyMCPServerSkillSkillFileAgentTriggerDeliveryAgentQueuedTurn)、权限审计(AccessRoleAclEntrySystemGrantAuditLog)等域。

该文件还有一段值得注意的实现:

/**
 * Background index builds fail silently unless an 'index' listener is
 * attached (e.g. Amazon DocumentDB <5.0 rejecting partialFilterExpression),
 * leaving unique constraints unenforced with no trace in the logs.
 */
for (const model of Object.values(models)) {
  if (model.listenerCount('index') === 0) {
    model.on('index', (error?: Error) => {
      if (error) {
        logger.error(`Index build failed for "${model.modelName}": ${error.message}`);
      }
    });
  }
}

从这段注释可以推断:在 Amazon DocumentDB 5.0 以下版本等环境中,后台索引构建失败(例如拒绝 partialFilterExpression)会静默吞掉错误,导致唯一约束形同虚设;因此 createModels 统一为未挂载监听器的模型补挂 index 事件监听,把建索引失败暴露到日志。

2.4 数据库方法层(src/methods/

约定要点:

  • 函数命名create[EntityName]Methods
  • 返回类型:导出方法对象的类型(ReturnType<typeof ...>);
  • 操作范围:包含 CRUD 与实体特有查询。

README 示例:

import type { Model } from 'mongoose';
import type { IUser } from '~/types';

export function createUserMethods(mongoose: typeof import('mongoose')) {
  async function findUserById(userId: string): Promise<IUser | null> {
    const User = mongoose.models.User as Model<IUser>;
    return await User.findById(userId).lean();
  }

  async function createUser(userData: Partial<IUser>): Promise<IUser> {
    const User = mongoose.models.User as Model<IUser>;
    return await User.create(userData);
  }

  return {
    findUserById,
    createUser,
    // ... other methods
  };
}

export type UserMethods = ReturnType<typeof createUserMethods>;

注意方法内部统一用 .lean() 做读取(返回纯 JSON 而非 Mongoose Document),这是 README 最佳实践第 6 条「读操作不需要 Mongoose 文档方法时使用 .lean()」的直接体现。

methods/index.ts 把各实体的方法按复杂度分层组织(源码注释中明确标注了 Tier):

  • Tier 1 — 简单 CRUD:actionassistantbannertoolCallpreset 等;
  • Tier 2 — 中等复杂度,注入服务依赖:conversationTagmessageconversationchatProject 等;
  • Tier 3 — 复杂逻辑,较重的依赖注入:tx(令牌事务)、transactionspendTokenspromptskillschedulequeuedTurntriggerDelivery 等;
  • Tier 5 — Agent:agent
  • 另有 Config、MCP authority proofs、Insights 等独立分组。

所有方法通过展开运算符合并进 createMethods(mongoose) 返回的对象,并导出一个 AllMethods 交叉类型(UserMethods & SessionMethods & ...),使调用方获得完整的类型提示。主入口 src/index.ts 除方法对象外,还重导出了一批常量与领域错误(如 RoleConflictErrorAgentQueuedTurnCapacityErrorMCPAuthorityProofError 以及 MAX_AUDIT_LOG_LIMITDEFAULT_REFRESH_TOKEN_EXPIRY 等),使该包不仅是数据访问层,也是数据域常量的单一出处。

三、实战:真实索引设计 —— 以 User Schema 为例

README「Common Patterns」给出了复合索引与部分唯一索引的写法,而 schema/user.ts 提供了一个完整的真实案例:

userSchema.index({ email: 1, tenantId: 1 }, { unique: true });
userSchema.index({ role: 1, tenantId: 1 });
userSchema.index({ idOnTheSource: 1, openidIssuer: 1, tenantId: 1 });

const oAuthIdFields = [
  'googleId', 'facebookId', 'openidId', 'samlId',
  'ldapId', 'githubId', 'discordId', 'appleId',
] as const;

for (const field of oAuthIdFields) {
  if (field === 'openidId') {
    userSchema.index(
      { openidId: 1, openidIssuer: 1, tenantId: 1 },
      { unique: true, partialFilterExpression: { openidId: { $exists: true } } },
    );
    continue;
  }
  userSchema.index(
    { [field]: 1, tenantId: 1 },
    { unique: true, partialFilterExpression: { [field]: { $exists: true } } },
  );
}

这段代码体现了三层设计意图:

  1. 租户维度复合唯一email + tenantId 唯一,保证不同租户下邮箱可以重复、同租户内邮箱唯一,这是多租户 SaaS 的标准索引形态;
  2. 部分索引(partial index):OAuth 外部 ID 字段对绝大多数用户是缺失的,若做全量唯一索引,所有缺失值会互相冲突;用 partialFilterExpression: { field: { $exists: true } } 只对存在该字段的文档建立唯一约束,正是 README 中该模式的真实应用;
  3. openidId 特殊处理:OIDC 的 openidId 需要与 openidIssuer 联合唯一(同一 subject 在不同 issuer 下可能重复),因此其部分索引是三字段复合键。

配合 createModels 中统一的索引失败监听(第二节 2.3),这些约束在 DocumentDB 等受限环境中的失效也能被日志捕获。

四、新增实体七步流程(完整继承 README)

README 给出了在 data-schemas 包中新增实体的标准七步流程。以下是完整步骤,每步均保留 README 的原始代码模板。

Step 1:创建类型定义

新建 src/types/[entityName].ts

import type { Document, Types } from 'mongoose';

export type EntityName = {
  /** Field description */
  fieldName: string;
  // ... other fields
};

export type IEntityName = EntityName &
  Document & {
    _id: Types.ObjectId;
  };

Step 2:更新 Types 索引

src/types/index.ts 中加入:

export * from './entityName';

Step 3:创建 Schema

新建 src/schema/[entityName].ts

import { Schema } from 'mongoose';
import type { IEntityName } from '~/types';

const entityNameSchema = new Schema<IEntityName>(
  {
    fieldName: { type: String, required: true },
    // ... other fields
  },
  { timestamps: true }
);

export default entityNameSchema;

Step 4:创建 Model 工厂

新建 src/models/[entityName].ts

import entityNameSchema from '~/schema/entityName';
import type * as t from '~/types';

export function createEntityNameModel(mongoose: typeof import('mongoose')) {
  return (
    mongoose.models.EntityName ||
    mongoose.model<t.IEntityName>('EntityName', entityNameSchema)
  );
}

Step 5:更新 Models 索引

src/models/index.ts 中:

  1. 导入工厂函数:
import { createEntityNameModel } from './entityName';
  1. createModels() 的返回对象中加入(同时按现有风格为返回值标注 EntityName: ReturnType<typeof createEntityNameModel> 类型):
EntityName: createEntityNameModel(mongoose),

注意:createModels 返回类型是显式声明的对象类型(见 models/index.ts),漏掉类型标注会导致类型收窄不完整。

Step 6:创建数据库方法

新建 src/methods/[entityName].ts

import type { Model, Types } from 'mongoose';
import type { IEntityName } from '~/types';

export function createEntityNameMethods(mongoose: typeof import('mongoose')) {
  async function findEntityById(id: string | Types.ObjectId): Promise<IEntityName | null> {
    const EntityName = mongoose.models.EntityName as Model<IEntityName>;
    return await EntityName.findById(id).lean();
  }

  // ... other methods

  return {
    findEntityById,
    // ... other methods
  };
}

export type EntityNameMethods = ReturnType<typeof createEntityNameMethods>;

Step 7:更新 Methods 索引

src/methods/index.ts 中:

  1. 导入方法与类型:
import { createEntityNameMethods, type EntityNameMethods } from './entityName';
  1. createMethods() 的返回对象中展开:
...createEntityNameMethods(mongoose),
  1. EntityNameMethods 加入 AllMethods 交叉类型:
export type AllMethods = UserMethods &
  // ... other methods
  EntityNameMethods;

(现有 createMethods 的合并写法与类型导出参见 methods/index.ts。)

五、最佳实践与常用模式

5.1 README 六条最佳实践

  1. 一致命名:文件名小写,类型/接口用 PascalCase;
  2. 类型安全:始终使用 TypeScript 类型,避免 any
  3. JSDoc 注释:为复杂字段与方法编写文档注释(如 schema/user.tsidOnTheSource 的注释说明其与 TPrincipal schema 的一致性);
  4. 索引:在 schema 文件中为查询模式定义数据库索引以保证性能;
  5. 校验:使用 Mongoose schema 校验保证数据完整性(如 email 的 match 正则、password 的长度约束);
  6. Lean 查询:读操作不需要 Mongoose 文档方法时使用 .lean()

5.2 枚举与常量

共享枚举放在 src/common/(README 示例):

// src/common/permissions.ts
export enum PermissionBits {
  VIEW = 1,
  EDIT = 2,
  DELETE = 4,
  SHARE = 8,
}

仓库中 src/common/ 实际包含 enum.tspermissions.tspagination.ts 等共享定义;此外部分枚举常量也直接从 librechat-data-provider 包引入(如 schema/user.ts 中用于 role 默认值与 personalization.statefulCodeEnvironment 枚举的 SystemRolesSTATEFUL_CODE_ENVIRONMENTS),以与前端共享同一份常量。

5.3 复合索引与部分索引

schema.index({ field1: 1, field2: 1 });
schema.index(
  { uniqueField: 1 },
  {
    unique: true,
    partialFilterExpression: { uniqueField: { $exists: true } }
  }
);

真实案例见上文第三节的 User schema OAuth ID 部分唯一索引。

5.4 虚拟属性(Virtuals)

schema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

虚拟属性是文档级计算属性,不会写入 MongoDB;若需要在查询中过滤或索引该值,则应使用真实字段并配合默认值或更新逻辑。

六、服务端如何消费该包

理解包如何被 API 端调用,有助于理解其设计取舍。api/db/index.js 是整个数据层的服务端入口:

const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { connectDb } = require('./connect');

// createModels MUST run before requiring indexSync.
// indexSync.js captures mongoose.models.Message and mongoose.models.Conversation
// at module load time. If those models are not registered first, all MeiliSearch
// sync operations will silently fail on every startup.
createModels(mongoose);

const indexSync = require('./indexSync');

module.exports = { connectDb, indexSync };

这里有两条可复用的经验:

  1. 加载顺序即契约:MeiliSearch 同步模块 indexSync 在模块加载时就捕获 mongoose.models.Messagemongoose.models.Conversation 的引用,因此 createModels 必须先于 require('./indexSync') 执行,否则搜索同步会在每次启动时静默失败;
  2. CJS 宿主消费 ESM 包api/ 侧是 CommonJS,通过 require('@librechat/data-schemas') 命中 exportsrequire 条件加载 dist/index.cjs,这解释了 package.json 中双格式构建与 sideEffects: false 的必要性。

而 Model 工厂把 mongoose 实例作为参数注入(而非包内单例),既避免了包与宿主 mongoose 实例不一致的经典陷阱,也让 jest.globalSetup.mjs 配合 mongodb-memory-server 的内存数据库集成测试成为可能——methods 目录下大量的 .spec.ts(如 user.methods.spec.tsconversation.spec.tsaclEntry.tenant.spec.ts)正是这一测试策略的产物。

七、构建与验证

README「Testing」一节要求新增实体时确保:类型可编译、模型可成功创建、方法处理边界情况(null 检查、校验)、索引与查询模式匹配。对应到工程操作上:

# 构建(clean + tsdown,产物 dist/ 同时含 .cjs 与 .mjs)
npm run build          # 或 bun run b:build
npm run build:watch    # 开发监听模式

# 测试(基于 jest + mongodb-memory-server)
npm run test           # 交互模式 --watch
npm run test:ci        # CI 模式 --coverage

验证顺序建议:先运行 npm run build 确认类型与产物无误,再运行 npm run test:ci 确认方法层与租户隔离等行为的测试通过;最后检查 createModels 的返回类型标注与 createMethodsAllMethods 交叉类型均已包含新实体。

八、小结

@librechat/data-schemas 用「types → schema → model 工厂 → methods」四层约定,把 LibreChat 从用户会话到 Agent/MCP/Skill、从令牌事务到审计日志的全部数据域收敛到一个强类型、可测试、可多租户隔离的包中。掌握本文的四层约定与新增实体七步流程后,你可以直接对照 models/index.tsmethods/index.ts 的现有实现,为该项目安全地扩展新的持久化实体。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384