teable 表规格系统架构解析:domain/table/specs 的 Specification 模式与访问者模式实践
【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable
导读
本文以 teable(AI Spreadsheet for Business)开源仓库中packages/v2/core/src/domain/table/specs/目录的架构文档为核心,深入剖析该目录所实现的"表规格(Table Spec)"系统:它如何基于经典 Specification(规格)模式封装对 Table 聚合根的筛选与变更意图,如何借助访问者(Visitor)模式将领域意图翻译为内存过滤与持久化(where/update)负载,以及TableSpecBuilder如何以流式 API 完成 and/or/not 组合。读完本文,你将掌握这套规格系统的设计动机、文件职责划分、核心接口契约,以及从源码到测试的完整验证路径,可直接用于理解或扩展 teable 的领域层代码。
1. 规格目录的定位与职责
packages/v2/core/src/domain/table/specs/ARCHITECTURE.md是该目录的架构说明文档,开篇就明确了整个目录的职责边界:
- 提供表相关的规格(Spec)与规格构建器(Spec Builder):所有关于"某张表是否满足某个条件"或"对表执行某个变更"的意图,都被封装为规格对象;
- 用于内存过滤与持久化翻译(in-memory filtering and persistence translation):同一份规格既可以调用
isSatisfiedBy在内存中对Table对象求值,也可以通过访问者接口被翻译成查询或更新负载; - 部分规格同时充当变更规格(mutate specs):例如
TableByNameSpec在内存求值时是"按名称匹配",而其mutate方法则直接执行t.rename(...),实现了"筛选即变更"的双重身份; ITableSpecVisitor定义了表级访问钩子:为 where/update 翻译提供了类型安全的访问点。
从更上层的角度看,该目录是 teable v2 核心领域层中"以规格对象统一表达查询与变更"这一设计思路的表级落点。其下的规格分为两大阵营:查询型规格(TableByBaseIdSpec、TableByIdSpec、TableByIdsSpec、TableByNameSpec、TableByNameLikeSpec等)与变更型规格(TableAddFieldSpec、TableRemoveFieldSpec、TableUpdateViewColumnMetaSpec以及庞大的field-updates/子目录)。
2. 文件清单与职责速览
原架构文档以"文件清单"的形式逐一说明了目录内每个文件的角色与目的,这是理解该系统最直接的索引。以下按原文档顺序整理,并补充目录中实际存在、值得关注的同类文件:
| 文件 | 角色 | 目的 |
|---|---|---|
ARCHITECTURE.md | 架构说明 | 描述表规格系统整体设计 |
ITableSpecVisitor.ts | 访问者接口 | 为查询/更新负载翻译提供表级访问方法 |
TableAddFieldSpec.ts | 变更规格 | 向表追加一个字段 |
TableRemoveFieldSpec.ts | 变更规格 | 从表移除一个字段 |
TableUpdateViewColumnMetaSpec.ts | 变更规格 | 在表变更过程中携带视图列元数据更新 |
TableByBaseIdSpec.ts | 查询规格 | 按 BaseId 过滤 |
TableByIdSpec.ts | 查询规格 | 按 TableId 过滤 |
TableByIdsSpec.ts | 查询规格 | 按多个 TableId 过滤 |
TableByNameLikeSpec.ts | 查询规格 | 按名称模糊匹配 |
TableByNameSpec.ts | 查询规格/变更规格 | 按名称精确匹配(mutate时执行重命名) |
TableSpecBuilder.spec.ts | 构建器测试 | 验证 and/or/not 组合行为 |
TableSpecBuilder.ts | 规格构建器 | 流式构建表规格 |
TableSpecs.spec.ts | 规格测试 | 逐一验证各规格的isSatisfiedBy |
此外,目录中还有一批未被原文档逐一列举、但同属该系统的规格:TableAddFieldsSpec(批量加字段)、TableAddSelectOptionsSpec(追加选项)、TableDuplicateFieldSpec(复制字段)、TableRenameSpec(重命名)、TableByIncomingReferenceToTableSpec(跨 Base 反向引用查询)、TableUpdateField*Spec系列(字段名、类型、约束、AI 配置、描述、错误状态等通用字段更新),以及field-updates/子目录下按字段类型组织的三十余个更新规格(单行文本、长文本、数字、日期、勾选、评分、用户、按钮、单选/多选、公式、链接、查找、汇总等,详见packages/v2/core/src/domain/table/specs/field-updates/)。visitors/子目录则提供了TableSpecEventVisitor、TableEventGeneratingSpecVisitor、FieldUpdateSemanticsVisitor等访问者实现。
3. 核心契约:ISpecification 与 ISpecVisitor
规格系统的基石位于共享层packages/v2/core/src/domain/shared/specification/,其架构说明(packages/v2/core/src/domain/shared/specification/ARCHITECTURE.md)指出该层提供"规格模式的核心抽象与组合能力",并明确组合规格会暴露子访问器(如leftSpec、rightSpec、innerSpec),供适配器进行树遍历。
3.1 ISpecification 接口
ISpecification.ts定义了每个规格必须实现的三方法契约:
export interface ISpecification<T = any, V extends ISpecVisitor = ISpecVisitor> { isSatisfiedBy(t: T): boolean; // 内存求值:t 是否满足该规格 mutate(t: T): Result<T, DomainError>; // 变更:对 t 施加规格所表达的修改 accept(v: V): Result<void, DomainError>; // 访问者:将规格分发给对应的 visit 方法 }三个方法分别对应规格模式的三种典型用途:
isSatisfiedBy:纯内存判定,返回布尔值,用于内存仓储过滤(如packages/v2/core/src/ports/memory/MemoryTableRepository.ts);mutate:返回Result<T, DomainError>,采用 neverthrow 的 Result 类型显式建模失败路径,将"规格即变更"落到实处;accept:接受一个规格访问者,交由访问者针对该规格类型执行翻译逻辑(生成 SQL where/update 负载等)。
3.2 ITableSpecVisitor:表级访问钩子
ITableSpecVisitor.ts继承自共享的ISpecVisitor,为所有表规格定义了统一的访问方法,全部返回Result<TResult, DomainError>。接口按注释分块组织,可归纳为四组:
- 既有规格组:
visitTableAddField、visitTableAddFields、visitTableAddSelectOptions、visitTableDuplicateField、visitTableRemoveField、visitTableUpdateViewColumnMeta、visitTableUpdateViewQueryDefaults、visitTableRename,以及查询类的visitTableByBaseId、visitTableById、visitTableByIds、visitTableByName、visitTableByNameLike、visitTableByIncomingReferenceToTable; - 通用字段更新组:
visitTableUpdateFieldName、visitTableUpdateFieldDbFieldName、visitTableUpdateFieldType、visitTableUpdateFieldConstraints、visitTableUpdateFieldAiConfig、visitTableUpdateFieldDescription、visitTableUpdateFieldHasError; - 按字段类型的更新组:覆盖 SingleLineText、LongText、Number、Date、Checkbox、Rating、User、Button、SingleSelect、MultipleSelect、Formula、Link、Lookup、Rollup 等字段的格式化、默认值、展示方式、选项、表达式等更新方法;
- 链接与汇总专项:
visitUpdateLinkConfig、visitUpdateLinkRelationship、visitRemoveSymmetricLinkField、visitUpdateLookupOptions、visitUpdateRollupConfig/Expression/Formatting/ShowAs/TimeZone。
从源码结构看,这套接口正是"持久化翻译"的枢纽:任何想把规格翻译成 SQL 的适配器(如 PostgreSQL 适配器中的 where/update 生成器)都通过实现该接口来完成类型安全的翻译。
3.3 组合规格与 MutateOnlySpec
共享层提供了三类组合规格:AndSpec(AND 组合)、OrSpec(OR 组合)、NotSpec(否定),并暴露leftSpec/rightSpec/innerSpec子访问器供遍历使用。变更类规格通常继承MutateOnlySpec基类,它提供中性的isSatisfiedBy(不参与内存筛选),让变更意图与筛选意图在类型层面清晰分离。
4. 查询规格实现剖析
以三个典型查询规格为例,观察"筛选"的实现方式。它们的结构高度一致:私有构造函数 + 静态create工厂 + 值访问器 +isSatisfiedBy/mutate/accept三方法。
4.1 TableByIdSpec:按 ID 精确匹配
TableByIdSpec.ts的求值逻辑极为简洁:
isSatisfiedBy(t: Table): boolean { return t.id().equals(this.tableIdValue); }mutate返回ok(t)(无变更语义,纯筛选),accept将自身分发给v.visitTableById(this)。TableByBaseIdSpec、TableByIdsSpec的实现同构,只是分别比较baseId()与对 ID 列表的逐个比对。
4.2 TableByNameSpec:兼具筛选与变更双重身份
TableByNameSpec.ts是原文档点名的"规格同时充当变更规格"的例子:
isSatisfiedBy(t: Table): boolean { return t.name().equals(this.tableNameValue); } mutate(t: Table): Result<Table, DomainError> { return t.rename(this.tableNameValue); }同一份规格,在内存筛选中表达"名称等于 X 的表",在变更场景中则直接调用Table.rename完成重命名,避免为同一意图维护两套对象。
4.3 TableByNameLikeSpec:模糊匹配
TableByNameLikeSpec.ts以includes实现名称子串匹配,mutate保持中性(ok(t)):
isSatisfiedBy(t: Table): boolean { return t.name().toString().includes(this.tableNameValue.toString()); }值得注意,该文件"字符串化后再includes"的语义与 SQL 层LIKE '%name%'的翻译由访问者适配器完成,领域层只关心"模糊匹配"这一不变意图。
5. 变更规格实现剖析:以 TableAddFieldSpec 为例
TableAddFieldSpec.ts继承自MutateOnlySpec,其mutate委托给聚合根:
mutate(t: Table): Result<Table, DomainError> { return t.addField(this.fieldValue, { domainContext: this.options?.domainContext, }); }它额外支持可选的domainContext(IDomainContext),用于在变更时携带领域上下文信息。TableRemoveFieldSpec与TableUpdateViewColumnMetaSpec遵循同样的"规格承载变更数据 →mutate委托聚合根方法 →accept分发访问者"模式,区别仅在于承载的数据类型不同。
从源码结构可以推断,这种设计的收益在于:变更意图与其副作用(事件生成、持久化翻译)解耦。visitors/目录下的TableEventGeneratingSpecVisitor与TableSpecEventVisitor(见packages/v2/core/src/domain/table/specs/visitors/)正是以访问者方式在规格被接受时生成对应领域事件的实现佐证。
6. TableSpecBuilder:流式规格组合
TableSpecBuilder.ts继承自共享层SpecBuilder,为Table提供流式构建 API,核心设计点有三个。
6.1 BaseId 的隐式注入
构造函数以baseId为可选参数;若传入,则includeBaseId默认为true,并在build()时自动将TableByBaseIdSpec前置到规格列表:
build(): Result<ISpecification<Table, ITableSpecVisitor>, DomainError> { const specs = this.includeBaseId && this.baseIdValue ? [TableByBaseIdSpec.create(this.baseIdValue), ...this.specs] : [...this.specs]; return this.buildFrom(specs); }这意味着table.specs().byName('Projects').build()天然限定在构建器所属的 Base 内。若未提供 baseId 或显式调用withoutBaseId(),则includeBaseId置为false,不再注入。byBaseId()方法则允许显式覆盖。
6.2 查询方法
byId(tableId):追加TableByIdSpec;byIds(tableIds):追加TableByIdsSpec;byIncomingReferenceToTable(tableId):追加TableByIncomingReferenceToTableSpec(用于查询引用了指定表的表,可跨 Base);byName(tableName)/byNameLike(tableName):精确/模糊名称匹配。
6.3 组合方法
andGroup(build)、orGroup(build)以回调式嵌套构建 AND/OR 子组;not(build)先构建子规格再取反。createChild创建子构建器时继承baseIdValue但关闭includeBaseId,避免子组重复注入 BaseId 条件。
7. 测试验证:组合行为与跨 Base 查询
TableSpecBuilder.spec.ts(packages/v2/core/src/domain/table/specs/TableSpecBuilder.spec.ts)是原架构文档推荐的示例文件,用 vitest 逐条验证了构建器的组合语义,是理解该系统行为的最佳起点:
- 默认注入 BaseId:
table.specs().byName('Projects').build()对同 Base 同名表返回true,对异 Base 同名表返回false; - 显式排除 BaseId:
withoutBaseId().byName(...)对两个 Base 的同名表均返回true; - 嵌套 OR 组:
orGroup(b => b.byName('Projects').byName('Tasks'))对两者均满足; - NOT 规格:
not(b => b.byName('Projects'))对 Projects 表返回false、对 Tasks 表返回true; - 名称模糊匹配:
byNameLike('Pro')命中Projects、排除Tasks; - ID 列表:
withoutBaseId().byIds([table.id()])仅命中目标表; - 跨 Base 反向引用:
Table.specs().byIncomingReferenceToTable(foreignTable.id())对持有指向 foreignTable 链接字段的 hostTable 返回true,对 foreignTable 自身与无关表返回false(测试中通过LinkFieldConfig.create({ relationship: 'manyMany', isOneWay: true, ... })构造引用关系)。
同时,TableSpecs.spec.ts与__tests__/目录下的TableUpdateFieldConstraintsSpec.spec.ts、TableUpdateFieldNameAndTypeSpec.spec.ts、TableUpdateViewColumnMetaSpec.spec.ts、TableUpdateViewQueryDefaultsSpec.spec.ts,以及field-updates/__tests__/中按字段类型的更新规格测试(如UpdateLinkConfigSpec.spec.ts、UpdateRollupSpecs.spec.ts等),共同构成对isSatisfiedBy与mutate语义的完整回归保障。
8. 工作流串联:从规格到持久化
综合共享层与表级源码,一条完整的数据流可以这样串联:
- 构建规格:通过
TableSpecBuilder(或直接调用各 Spec 的create)生成规格对象,此时只是内存中的意图描述; - 内存过滤:内存仓储(如
MemoryTableRepository)调用isSatisfiedBy(table)对聚合根逐一判定,见packages/v2/core/src/ports/memory/MemoryTableRepository.ts; - 持久化翻译:持久化适配器实现
ITableSpecVisitor,将规格的accept分发翻译为 SQL where/update 负载(ITableSpecVisitor注释明确其职责为"query/update translation payloads"); - 变更执行:变更类规格通过
mutate驱动聚合根方法(如Table.addField、Table.rename),再由事件生成类访问者产出领域事件。
这一流程使"领域意图"与"基础设施翻译"彻底解耦:新增一种表操作只需新增规格类并在访问者接口补一个visit方法,无需改动查询与存储管线。
9. 小结
domain/table/specs目录是 teable v2 领域层"规格模式 + 访问者模式"的表级落地:查询规格负责内存筛选,变更规格负责聚合根变更,ITableSpecVisitor统一翻译出口,TableSpecBuilder提供安全的 and/or/not 组合与 BaseId 隐式注入。想要进一步深入,可以按以下路径继续阅读源码:
- 共享规格抽象:
packages/v2/core/src/domain/shared/specification/,含ISpecification.ts、SpecBuilder.ts、AndSpec.ts、OrSpec.ts、NotSpec.ts、MutateOnlySpec.ts及对应架构说明ARCHITECTURE.md; - 表规格全集:
packages/v2/core/src/domain/table/specs/,含查询/变更规格、field-updates/与visitors/子目录; - 访问者实现样例:
packages/v2/core/src/domain/table/specs/visitors/,观察事件生成与字段更新语义的翻译逻辑; - 测试基线:
TableSpecBuilder.spec.ts与TableSpecs.spec.ts。
【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考