teable 表规格系统架构解析:domain/table/specs 的 Specification 模式与访问者模式实践
2026/9/13 19:46:04 网站建设 项目流程

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 核心领域层中"以规格对象统一表达查询与变更"这一设计思路的表级落点。其下的规格分为两大阵营:查询型规格TableByBaseIdSpecTableByIdSpecTableByIdsSpecTableByNameSpecTableByNameLikeSpec等)与变更型规格TableAddFieldSpecTableRemoveFieldSpecTableUpdateViewColumnMetaSpec以及庞大的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/子目录则提供了TableSpecEventVisitorTableEventGeneratingSpecVisitorFieldUpdateSemanticsVisitor等访问者实现。

3. 核心契约:ISpecification 与 ISpecVisitor

规格系统的基石位于共享层packages/v2/core/src/domain/shared/specification/,其架构说明(packages/v2/core/src/domain/shared/specification/ARCHITECTURE.md)指出该层提供"规格模式的核心抽象与组合能力",并明确组合规格会暴露子访问器(如leftSpecrightSpecinnerSpec),供适配器进行树遍历。

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>。接口按注释分块组织,可归纳为四组:

  • 既有规格组visitTableAddFieldvisitTableAddFieldsvisitTableAddSelectOptionsvisitTableDuplicateFieldvisitTableRemoveFieldvisitTableUpdateViewColumnMetavisitTableUpdateViewQueryDefaultsvisitTableRename,以及查询类的visitTableByBaseIdvisitTableByIdvisitTableByIdsvisitTableByNamevisitTableByNameLikevisitTableByIncomingReferenceToTable
  • 通用字段更新组visitTableUpdateFieldNamevisitTableUpdateFieldDbFieldNamevisitTableUpdateFieldTypevisitTableUpdateFieldConstraintsvisitTableUpdateFieldAiConfigvisitTableUpdateFieldDescriptionvisitTableUpdateFieldHasError
  • 按字段类型的更新组:覆盖 SingleLineText、LongText、Number、Date、Checkbox、Rating、User、Button、SingleSelect、MultipleSelect、Formula、Link、Lookup、Rollup 等字段的格式化、默认值、展示方式、选项、表达式等更新方法;
  • 链接与汇总专项visitUpdateLinkConfigvisitUpdateLinkRelationshipvisitRemoveSymmetricLinkFieldvisitUpdateLookupOptionsvisitUpdateRollupConfig/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)TableByBaseIdSpecTableByIdsSpec的实现同构,只是分别比较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.tsincludes实现名称子串匹配,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, }); }

它额外支持可选的domainContextIDomainContext),用于在变更时携带领域上下文信息。TableRemoveFieldSpecTableUpdateViewColumnMetaSpec遵循同样的"规格承载变更数据 →mutate委托聚合根方法 →accept分发访问者"模式,区别仅在于承载的数据类型不同。

从源码结构可以推断,这种设计的收益在于:变更意图与其副作用(事件生成、持久化翻译)解耦visitors/目录下的TableEventGeneratingSpecVisitorTableSpecEventVisitor(见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.tspackages/v2/core/src/domain/table/specs/TableSpecBuilder.spec.ts)是原架构文档推荐的示例文件,用 vitest 逐条验证了构建器的组合语义,是理解该系统行为的最佳起点:

  • 默认注入 BaseIdtable.specs().byName('Projects').build()对同 Base 同名表返回true,对异 Base 同名表返回false
  • 显式排除 BaseIdwithoutBaseId().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.tsTableUpdateFieldNameAndTypeSpec.spec.tsTableUpdateViewColumnMetaSpec.spec.tsTableUpdateViewQueryDefaultsSpec.spec.ts,以及field-updates/__tests__/中按字段类型的更新规格测试(如UpdateLinkConfigSpec.spec.tsUpdateRollupSpecs.spec.ts等),共同构成对isSatisfiedBymutate语义的完整回归保障。

8. 工作流串联:从规格到持久化

综合共享层与表级源码,一条完整的数据流可以这样串联:

  1. 构建规格:通过TableSpecBuilder(或直接调用各 Spec 的create)生成规格对象,此时只是内存中的意图描述;
  2. 内存过滤:内存仓储(如MemoryTableRepository)调用isSatisfiedBy(table)对聚合根逐一判定,见packages/v2/core/src/ports/memory/MemoryTableRepository.ts
  3. 持久化翻译:持久化适配器实现ITableSpecVisitor,将规格的accept分发翻译为 SQL where/update 负载(ITableSpecVisitor注释明确其职责为"query/update translation payloads");
  4. 变更执行:变更类规格通过mutate驱动聚合根方法(如Table.addFieldTable.rename),再由事件生成类访问者产出领域事件。

这一流程使"领域意图"与"基础设施翻译"彻底解耦:新增一种表操作只需新增规格类并在访问者接口补一个visit方法,无需改动查询与存储管线。

9. 小结

domain/table/specs目录是 teable v2 领域层"规格模式 + 访问者模式"的表级落地:查询规格负责内存筛选,变更规格负责聚合根变更,ITableSpecVisitor统一翻译出口,TableSpecBuilder提供安全的 and/or/not 组合与 BaseId 隐式注入。想要进一步深入,可以按以下路径继续阅读源码:

  • 共享规格抽象:packages/v2/core/src/domain/shared/specification/,含ISpecification.tsSpecBuilder.tsAndSpec.tsOrSpec.tsNotSpec.tsMutateOnlySpec.ts及对应架构说明ARCHITECTURE.md
  • 表规格全集:packages/v2/core/src/domain/table/specs/,含查询/变更规格、field-updates/visitors/子目录;
  • 访问者实现样例:packages/v2/core/src/domain/table/specs/visitors/,观察事件生成与字段更新语义的翻译逻辑;
  • 测试基线:TableSpecBuilder.spec.tsTableSpecs.spec.ts

【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询