NocoBase 数据源管理之 IField 接口详解:字段抽象、FieldOptions 与类型注册机制
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
IField是 NocoBase 数据源管理(Data Source Manager)层中所有字段必须实现的统一接口,它通过FieldOptions承载字段的元数据,让上层业务(接口层、界面层、校验逻辑)与底层数据库实现解耦。本文基于 i-field.md 文档,结合packages/core/data-source-manager与packages/core/database的真实源码,完整讲解FieldOptions各属性的含义、IField的接口约定,以及字段类型注册、数据库类型映射与字段生命周期管理的底层实现,帮助你掌握 NocoBase 中"定义一个字段"背后发生的完整链路。
背景:IField 在数据源抽象层中的位置
NocoBase 的数据源管理模块(packages/core/data-source-manager)在数据库之上建立了一层统一的抽象接口。这一层定义了五类核心契约:
IField:字段抽象(本文主题);- ICollection:数据模型(表)抽象;
- ICollectionManager:模型管理器的抽象;
- IRepository:数据操作(增删改查)抽象;
- IModel:数据记录(行)抽象。
IField是其中最基础的单元:一个Collection(数据表)由若干IField(字段)组成,字段承载了列名、数据库原始类型、逻辑类型、界面 Schema(uiSchema)、主键/唯一约束等全部元信息。无论底层是关系型数据库(如 PostgreSQL、MySQL、SQLite)还是其他类型的数据源,业务代码看到的都是统一的IField。
FieldOptions:字段元数据的唯一载体
IField的核心是options属性,其类型为FieldOptions。原文档给出了如下定义:
export type FieldOptions = { name: string; field: string; rawType: string; type: string; description?: string; interface?: string; uiSchema?: any; possibleTypes?: string[]; defaultValue?: any; primaryKey: boolean; unique: boolean; allowNull?: boolean; autoIncrement?: boolean; [key: string]: any; };在仓库当前的源码中,该类型定义于 packages/core/data-source-manager/src/types.ts,其中primaryKey与unique已变为可选属性(primaryKey?: boolean),整体语义保持一致。各属性含义如下:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 字段在业务层的逻辑名称,即代码中访问字段时使用的名字 |
field | string | 是 | 字段对应的数据库列名(物理列名),可与name不同 |
rawType | string | 是 | 底层数据库返回的原始类型(如integer、text、jsonb) |
type | string | 是 | NocoBase 内部的逻辑字段类型(如string、integer、json) |
description? | string | 否 | 字段描述 |
interface? | string | 否 | 字段在界面层使用的接口类型(如input、integer、datetime),决定 UI 渲染组件与校验规则 |
uiSchema? | any | 否 | 字段的 UI Schema(JSON Schema 扩展),描述前端渲染方式 |
possibleTypes? | string[] | 否 | 该字段可能的类型候选列表,通常用于类型推断 |
defaultValue? | any | 否 | 字段默认值 |
primaryKey | boolean | 是 | 是否为联合主键的一部分 |
unique | boolean | 是 | 是否唯一 |
allowNull? | boolean | 否 | 是否允许为空(NULL) |
autoIncrement? | boolean | 否 | 是否自增 |
[key: string]: any | — | — | 索引签名,允许携带任意扩展属性 |
两个名称字段需要特别注意区分:name是业务逻辑名,field是数据库物理列名。例如一个字段逻辑名为userName,其数据库列名可以是user_name。在Collection的实现中,getField(name)按逻辑名查找,而getFieldByField(field)则按物理列名查找(见 collection.ts)。
rawType与type的区分同样关键:rawType来自数据库的原始类型报告,而type是 NocoBase 内部统一后的逻辑类型。二者的桥接工作由数据库 introspection 机制完成(下文详述)。
IField 接口:字段契约与关联字段扩展
原文档中IField接口的定义如下:
export interface IField { options: FieldOptions; }仓库当前源码 types.ts 在文档基础上补充了isRelationField()方法,并扩展出关联字段与字段接口两类契约:
export interface IField { options: FieldOptions; isRelationField(): boolean; } export interface IRelationField extends IField { targetCollection(): ICollection; } export interface IFieldInterface { options: FieldOptions; toString(value: any, ctx?: any): string; toValue(str: string, ctx?: any): any; }三个接口的分工是:
IField:所有字段的最基本契约,只需要持有options并提供isRelationField()判断。普通字段返回false,关联字段(belongsTo、hasMany 等)返回true;IRelationField:在IField基础上增加targetCollection(),返回关联目标模型,供关联数据操作使用;IFieldInterface:字段在界面层的"接口适配器",负责toString()/toValue()两个方向的类型转换(例如把界面字符串解析为数据库值、把数据库值序列化为界面字符串)。
在数据库层 packages/core/database/src/fields/field.ts 中,基类Field同样提供了isRelationField()(默认返回false),并实现了options、name、type等访问器,子类如RelationField(见 relation-field.ts)会覆写该方法。
属性的最小实现:CollectionField
IField接口在数据源管理层的默认实现是CollectionField类,定义于 packages/core/data-source-manager/src/collection-field.ts:
export class CollectionField implements IField { options: FieldOptions; constructor(options: FieldOptions) { this.updateOptions(options); } updateOptions(options: any) { this.options = { ...this.options, ...options, }; } isRelationField(): boolean { return false; } }可以看到,CollectionField是一个极简的元数据容器:构造时通过updateOptions将传入的FieldOptions合并进this.options(支持后续增量更新),isRelationField()恒返回false——因为在这一抽象层,关联判断留给具体数据源的实现(如数据库层的RelationField)去覆写。它不关心字段如何落库、如何校验,只负责"持有并更新字段元数据"。
字段的生命周期:从定义到查询
字段不是孤立存在的,它由Collection统一管理。在 packages/core/data-source-manager/src/collection.ts 中:
setFields(fields: any[]) { const fieldNames = this.fields.keys(); for (const fieldName of fieldNames) { this.removeField(fieldName); } for (const field of fields) { this.setField(field.name, field); } } setField(name: string, options: any) { const field = new CollectionField(options); this.fields.set(name, field); return field; } removeField(name: string) { this.fields.delete(name); } getField(name: string) { return this.fields.get(name); } getFieldByField(field: string): IField { for (const item of this.fields.values()) { if (item.options.field === field) { return item; } } return null; } getFields() { return [...this.fields.values()]; }关键实现细节:
- 字段存储在一个
Map<string, IField>中,键为字段逻辑名name; setField(name, options)每次都会新建CollectionField并覆盖写入,返回字段实例;setFields采用"先清空再写入"的策略,用于整体替换字段集合(updateOptions内部会调用);getFieldByField遍历全部字段,按物理列名options.field匹配——当业务名与列名不一致时,这个 API 用于按列名反查字段。
在CollectionOptions中,字段以fields: FieldOptions[]数组形式声明(见 types.ts)。因此一个典型的数据表定义如下:
const collectionOptions = { name: 'users', fields: [ { name: 'id', field: 'id', type: 'integer', primaryKey: true, autoIncrement: true }, { name: 'username', field: 'username', type: 'string', unique: true }, { name: 'nickname', field: 'nickname', type: 'string', allowNull: true }, { name: 'age', field: 'age', type: 'integer', defaultValue: 0 }, ], };字段类型注册与数值字段识别
ICollectionManager提供了字段类型的注册与查询能力(见 types.ts 与 collection-manager.ts):
registerFieldTypes(types)/registerFieldInterfaces(interfaces):批量注册字段类型/接口;registerFieldInterface(name, fieldInterface):注册单个字段接口;getFieldInterface(name):按名称获取字段接口构造器;isNumericField(field?):判断字段是否为数值类型。
isNumericField的判定逻辑实现在 packages/core/data-source-manager/src/utils.ts:
const NUMERIC_FIELD_TYPES = new Set(['integer', 'bigInt', 'float', 'double', 'decimal']); export function isNumericField(field?: { options?: { type?: string } }) { const fieldType = field?.options?.type; return typeof fieldType === 'string' && NUMERIC_FIELD_TYPES.has(fieldType); }即:当字段的options.type属于integer、bigInt、float、double、decimal之一时,即被视为数值字段。该能力被上层用于决定排序、聚合、输入组件等行为,例如界面层对数值字段渲染数字输入框并启用stringMode。
数据库原始类型到字段接口的映射
rawType(数据库原始类型)与interface(界面接口)之间如何建立关系?答案是 introspection 阶段完成的。在 packages/core/data-source-manager/src/database-introspector/type-interface-map.ts 中维护了一张完整的类型映射表,例如:
array、json、jsonb→interface: 'json',UI 组件为Input.JSON(autoSize: { minRows: 5 });date、datetime、datetimeTz→interface: 'datetime',UI 组件为DatePicker(dateFormat: 'YYYY-MM-DD');integer→interface: 'integer',UI 组件为InputNumber(stringMode: true, step: '1'),校验规则x-validator: 'integer';float、double、real、decimal→interface: 'number',UI 组件为InputNumber;string、uid→interface: 'input',UI 组件为Input;text→interface: 'textarea',UI 组件为Input.TextArea;boolean→interface: 'checkbox',UI 组件为Checkbox;password→interface: 'password',UI 组件为Password,且标记hidden: true。
这张映射表的产物正是FieldOptions中的interface与uiSchema字段——它把"数据库里存什么类型"与"界面上用什么组件编辑"两个问题一次性解决。关系类与地理类类型(belongsTo、hasMany、point、polygon等)在表中留空,交由关联字段或后续实现处理。
数据库层的字段基类:从元数据到真实列
数据源管理层的IField是"契约",真正把字段落库的是数据库层(packages/core/database)的Field基类(field.ts)。其职责包括:
bind()/unbind()(第 180-207 行):将字段绑定到 Sequelize 模型——写入model.rawAttributes、刷新属性、处理索引与自增属性,或从模型中移除;toSequelize()(第 209-239 行):将options转换为 Sequelize 列定义,包括通过normalizeDataType规范化数据类型,以及对richText接口的富文本 HTML 消毒(sanitizeRichTextHtml);columnName()(第 134-144 行):返回物理列名——优先取options.field,否则在underscored模式下将name转成 snake_case;remove()(第 129-132 行):删除字段并移除对应索引;sync()(第 101-109 行):以alter模式同步表结构;existsInDb()(第 146-174 行):按方言(sqlite / mysql / mariadb / 其他)生成查询 SQL,判断列是否已存在于数据库中。
数据库层实现了丰富的字段子类(见 packages/core/database/src/fields 目录):string-field、number-field、boolean-field、date-field、json-field、array-field、password-field、uuid-field、nanoid-field、snowflake-id-field、virtual-field,以及关系类belongs-to-field、belongs-to-many-field、has-many-field、has-one-field、relation-field等。这些子类覆写dataType、toSequelize、additionalSequelizeOptions等方法,把各自的逻辑类型翻译为具体数据库列定义。通过这种分层,IField保证上层永远面对统一元数据,而类型差异被隔离在底层。
实践视角:IField 的典型使用场景
结合以上机制,IField在 NocoBase 中的典型使用场景可以归纳为三类:
- 定义数据表字段:通过
CollectionManager.defineCollection({ name, fields: FieldOptions[] })声明字段(见 collection-manager.ts),Collection构造时自动调用setFields将每个FieldOptions包装为CollectionField; - 读取与检索字段元数据:通过
collection.getField(name)、collection.getFieldByField(field)、collection.getFields()获取字段实例并读取options(如primaryKey、unique、uiSchema); - 扩展字段能力:通过
collectionManager.registerFieldInterface(name, Class)注册自定义字段接口,通过registerFieldTypes/registerModels/registerRepositories扩展整个数据源能力集(见 collection-manager.ts)。
小结
IField接口虽然代码量很小,却是 NocoBase 多数据源抽象的关键支点:FieldOptions用统一的元数据形状同时服务数据库层与界面层,rawType/type/interface/uiSchema四元组打通了"数据库原始类型 → 逻辑类型 → 界面接口"的转换链。结合 types.ts 的接口定义、collection-field.ts 的默认实现、collection.ts 的字段生命周期管理,以及 type-interface-map.ts 的类型映射,你可以完整理解一个字段从声明到落库、再到界面渲染的整个过程。
如需继续深入数据源抽象层的其他部分,可参阅同目录下的 ICollection、ICollectionManager、IRepository、IModel,以及总览性的 contenteditable="false">【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考