Teable v2 Specification 模式核心架构解析:统一领域筛选、变更与 SQL 查询翻译
Teable v2 Specification 模式核心架构解析统一领域筛选、变更与 SQL 查询翻译【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teableSpecification规范模式是 Teable v2 领域模型中最核心的行为抽象之一。本文围绕 packages/v2/core/src/domain/shared/specification/ARCHITECTURE.md 的架构说明展开深入解析其核心接口契约、And/Or/Not组合子、SpecBuilder构建器、双通道求值机制以及它们如何被内存仓库用于过滤、被 Postgres 适配器翻译成 Kysely SQL 查询。读完本文你将掌握这套一套规范、内存求值 持久化翻译架构的完整设计思路并能在 Teable v2 的领域代码中自如使用与扩展。一、职责定位一套规范两种用途按照架构文档的职责声明domain/shared/specification目录承担三项核心职责提供 Specification 模式的核心抽象与组合能力核心抽象与组合组合型规范对外暴露子访问器如leftSpec、rightSpec、innerSpec供适配器层做树形遍历将内存中的规范树翻译成底层查询同时服务于内存求值与持久化翻译两条执行路径。也就是说领域层开发者只需要用规范描述业务约束例如表名为 X 且属于 Base Y不必关心它最终是被用于内存过滤还是生成 SQL翻译工作交由适配器完成。这正是 Specification Visitor 组合模式的典型收益领域逻辑与存储实现解耦。二、核心契约ISpecification 三方法接口一切规范的起点是 ISpecification.tsexport interface ISpecificationT any, V extends ISpecVisitor ISpecVisitor { isSatisfiedBy(t: T): boolean; mutate(t: T): ResultT, DomainError; accept(v: V): Resultvoid, DomainError; }三个方法各司其职方法签名职责典型使用场景isSatisfiedBy(t: T) boolean纯内存布尔判断对象t是否满足该规范内存仓库过滤、测试断言mutate(t: T) ResultT, DomainError对对象执行变更并返回新对象领域状态更新、写操作转换accept(v: V) Resultvoid, DomainError接受访问者把规范树交给外部解释器适配器翻译成 SQL/查询条件值得注意的是mutate与accept的返回类型都是neverthrow的Result本仓库统一使用neverthrow做显式错误处理即错误通过Result..., DomainError传递而不是抛异常。DomainError来自同目录上级的../DomainError。ISpecification还带有两个泛型参数T是被规范约束的领域对象类型V是访问者类型默认为ISpecVisitor。对应的访问者契约在 ISpecVisitor.tsexport interface ISpecVisitor { visit(spec: ISpecificationany, any): Resultvoid, DomainError; }访问者只需要实现一个visit方法。配合accept就构成了标准的Visitor 双分派规范对象知道自己是什么类型从而能访问leftSpec()/innerSpec()而访问者决定每个节点怎么被解释。三、组合子AndSpec / OrSpec / NotSpec架构文档明确列出三种组合规范它们通过子访问器暴露内部结构供适配器遍历。3.1 AndSpec与组合实现位于 AndSpec.ts构造时接收left与right两个子规范并暴露leftSpec()/rightSpec()访问器isSatisfiedBy(t: T): boolean { return this.left.isSatisfiedBy(t) this.right.isSatisfiedBy(t); } mutate(t: T): ResultT, DomainError { return this.left.mutate(t).andThen((next) this.right.mutate(next)); } accept(v: V): Resultvoid, DomainError { return v .visit(this) .andThen(() this.left.accept(v)) .andThen(() this.right.accept(v)) .map(() undefined); }三个方法的语义高度一致先左后右。求值左右必须同时满足变更先执行left.mutate(t)若成功再把结果next传给right.mutate形成串行管道这也保证了变更的顺序性访问先visit自身让访问者知道当前是AndSpec节点再依次accept左右子树。文件末尾还导出了工厂函数andSpec(left, right)直接返回ResultAndSpecT, V, DomainError。3.2 OrSpec或组合实现位于 OrSpec.ts。isSatisfiedBy用||而mutate采用了短路策略mutate(t: T): ResultT, DomainError { if (this.left.isSatisfiedBy(t)) return this.left.mutate(t); if (this.right.isSatisfiedBy(t)) return this.right.mutate(t); return ok(t); }即哪个分支当前满足条件就只执行哪个分支的变更两个都不满足则原样返回ok(t)——这避免了或语义下对同一对象做两次互斥变更的歧义。OrSpec.accept是最值得研究的部分它区分了两类访问者accept(v: V): Resultvoid, DomainError { const visited v.visit(this); if (isSpecFilterVisitor(v)) { // 过滤型访问者克隆左右访问者各自求值最后用 or 合并 const leftVisitor v.clone(); const rightVisitor v.clone(); return visited .andThen(() this.left.accept(leftVisitor as unknown as V)) .andThen(() this.right.accept(rightVisitor as unknown as V)) .andThen(() leftVisitor.where()) .andThen((leftCond) rightVisitor.where().map((rightCond) v.or(leftCond, rightCond))) .andThen((cond) v.addCond(cond)) .map(() undefined); } // 普通访问者顺序遍历即可 return visited .andThen(() this.left.accept(v)) .andThen(() this.right.accept(v)) .map(() undefined); }关键点在于对OR语义不能简单把左右子条件直接叠加叠加等于 AND而必须为左右分支各克隆一份访问者、分别累积条件最后调用v.or(leftCond, rightCond)合并成一个条件再通过addCond放回访问者。isSpecFilterVisitor来自 visitors/ISpecFilterVisitor.ts用于在运行时识别过滤型访问者。3.3 NotSpec非组合实现位于 NotSpec.ts只包裹一个inner子规范暴露innerSpec()访问器isSatisfiedBy(t: T): boolean { return !this.inner.isSatisfiedBy(t); } mutate(t: T): ResultT, DomainError { return ok(t); }由于非是纯逻辑否定NotSpec.mutate直接返回ok(t)不产生变更。在过滤访问者路径下accept会克隆一个内部访问者求值innerCond再调用v.not(innerCond)取反后addCond。三种组合子各自都导出了对应的工厂函数andSpec/orSpec/notSpec便于链式书写。四、MutateOnlySpec纯变更规范的中性基类实现位于 MutateOnlySpec.tsexport abstract class MutateOnlySpecT, V extends ISpecVisitor ISpecVisitor implements ISpecificationT, V { isSatisfiedBy(_: T): boolean { return true; } abstract mutate(t: T): ResultT, DomainError; abstract accept(v: V): Resultvoid, DomainError; }这类规范只关心对对象做什么变更不关心对象是否满足条件因此isSatisfiedBy恒为true中性判断子类只需实现mutate与accept。在 Teable v2 中大量更新字段名更新字段类型添加字段等写操作规范都可以基于此基类实现——参考 TableSchemaUpdateVisitor.ts 中导入的TableUpdateFieldNameSpec、TableUpdateFieldTypeSpec、TableAddFieldSpec等一长串更新型规范。五、SpecBuilder规范树的声明式构建SpecBuilder.ts 提供了组合构建的基类避免手动new AndSpec(new AndSpec(...))的嵌套地狱export type SpecBuilderMode and | or; export abstract class SpecBuilderT, V extends ISpecVisitor, B extends SpecBuilderT, V, B { protected readonly specs: ArrayISpecificationT, V []; protected readonly errors: DomainError[] []; protected readonly mode: SpecBuilderMode; protected constructor(mode: SpecBuilderMode and) { ... } protected addSpec(spec): void { ... } protected addNotSpec(spec): void { this.specs.push(new NotSpec(spec)); } protected addGroup(mode, build: (builder: B) B): void { ... } // 子构建器 分组 protected recordError(error: DomainError | string): void { ... } protected buildFrom(specs): ResultISpecificationT, V, DomainError { ... } build(): ResultISpecificationT, V, DomainError { return this.buildFrom(this.specs); } protected abstract createChild(mode: SpecBuilderMode): B; }核心机制值得逐条展开模式mode构造时可传入and默认或or决定最终build时多个规范按哪种方式折叠组合addNotSpec直接以NotSpec包装后入栈让非条件的书写与普通规范一致addGroup用createChild(mode)生成一个子构建器在子构建器中用回调函数继续堆积条件子构建器build()成功后整体作为一个规范加入父构建器——这天然支持AND (A OR B)这类嵌套分组错误累积构建过程中通过recordError收集错误DomainError | string字符串会被自动包装为domainError.validation而不是立即失败最终在buildFrom统一处理有错误则返回首个错误或聚合错误code: spec.builderdetails.errors为所有错误消息数组边界情形specs为空时返回err(domainError.validation({ message: Empty specification }))只有 1 个时直接ok(specs[0])多个时用reduce按模式折叠成AndSpec/OrSpec链。build()返回Result调用方必须处理空规范构建错误等失败分支从类型层面杜绝了没加任何条件就查询全表这类隐患。配套的还有 composeAndSpecs.ts提供三个实用函数composeAndSpecs(specs)把规范数组折叠为单个AndSpec链空数组返回错误composeAndSpecsOrUndefined(specs)失败时返回undefined的宽容版本flattenAndSpecs(spec)递归地把AndSpec树拍平成扁平数组——当适配器需要对条件重新分组合并时非常有用。六、访问者体系从布尔判断到 where 条件架构文档将访问者单独放在visitors/子目录见 visitors/ARCHITECTURE.md包含三个文件AbstractSpecFilterVisitor.ts抽象基类维护一个内部condValueaddCond在已有条件时自动用and合并where()在无任何条件时返回err(domainError.validation({ message: Empty where condition }))子类必须实现clone/and/or/not四个抽象方法ISpecFilterVisitor.ts过滤型访问者的运行时识别接口配合isSpecFilterVisitor类型守卫NoopSpecVisitor.ts空操作访问者作为默认占位或测试兜底。从源码结构可以推断AbstractSpecFilterVisitor的意义在于访问者只负责累积条件片段具体条件的形态字符串、对象、SQL 表达式完全由子类决定。例如单元测试中的TestFilterVisitor把条件累积成(A AND B)这样的括号字符串而生产环境的 Postgres 访问者则把条件累积成 Kysely 的表达式构建器回调。七、内存求值MemoryTableRepository 的真实用法架构文档给出的第一个示例是 MemoryTableRepository.ts。这是核心领域包内置的内存表仓库用于测试与轻量运行其中规范被直接用于内存过滤const found this.savedTables.find((t) spec.isSatisfiedBy(t)); const filtered this.savedTables.filter((t) spec.isSatisfiedBy(t)); return ok(this.savedTables.filter((t) spec.isSatisfiedBy(t)).length);从这些调用可以看出find/filter/count三类查询都直接复用同一个规范对象的isSatisfiedBy规范就是内存仓库的查询语言完全不需要为不同查询类型编写不同过滤逻辑。八、持久化翻译Postgres 适配器中的过滤访问者架构文档指出组合规范暴露子访问器供适配器遍历这一设计的真实落地在 v2 的 Postgres 适配层。以 TableWhereVisitor.ts 为例export class TableWhereVisitor extends AbstractSpecFilterVisitorITableMetaWhere implements ITableSpecVisitorITableMetaWhere { constructor(private readonly state: TableQueryState active) { super(); if (state active) { this.addCond((eb) eb.eb(deleted_time, is, null)); this.addCond(() sqlbooleantable_meta.provision_state ready); } } ... }ITableMetaWhere是(eb: ExpressionBuilderV1TeableDatabase, table_meta) ExpressionSqlBool即 Kysely 的 where 回调。该访问者把软删除过滤、provision 状态过滤等公共约束在构造时预先addCond随后实现ITableSpecVisitor中每个具体规范的visit分支TableByIdSpec、TableByNameSpec、TableByNameLikeSpec、TableByIdsSpec、TableByIncomingReferenceToTableSpec等把规范树最终翻译成 SQL 条件。同类实现还包括 TableMetaUpdateVisitor.ts元数据更新翻译与 TableSchemaUpdateVisitor.tsDDL 级 schema 变更翻译实现了超 40 种字段/视图更新规范的 visit 分支。这正是文档中Composition specs expose child accessors for traversal helpers in adapters的含义适配器访问者沿着AndSpec.leftSpec()/rightSpec()、OrSpec、NotSpec.innerSpec()递归下降把内存规范树无损翻译为 SQL 表达式树。九、测试验证行为即规格架构文档列出的两个测试文件把上述行为钉死为规格SpecBasics.spec.ts核心包内的行为测试覆盖AndSpec/OrSpec/NotSpec的isSatisfiedBy与mutate组合语义如left-right经AndSpec.mutate后变为left-right-L-R验证变更的串行管道普通访问者SpyVisitor与过滤访问者TestFilterVisitor两条accept路径断言OrSpec过滤结果为(LEFT OR RIGHT)、NotSpec为(NOT LEFT)SpecBuilder的空构建失败、单规范直通、and/or 模式折叠、子构建器分组与组内错误上报NoopSpecVisitor的兜底访问TableSpecBuilder.spec.ts真实的领域构建器用法验证table.specs().byName(...)默认包含 Base ID 约束跨 Base 同名表不命中、withoutBaseId()可显式排除、orGroup支持嵌套 OR 分组、以及 not 规范的组合。想深入理解构建器 组合子如何组织成真实领域查询的读者可以从 TableSpecBuilder.spec.ts 入手再对照Table领域对象的specs()入口阅读实现。十、目录文件速查packages/v2/core/src/domain/shared/specification/目录下的完整文件角色如下继承自架构文档文件角色说明ARCHITECTURE.md目录架构说明描述 spec 核心AndSpec.ts规范组合AND 两个规范ISpecVisitor.ts访问者接口定义规范访问 APIISpecification.ts规范接口isSatisfiedBy/mutate/accept契约MutateOnlySpec.ts规范基类仅变更型规范isSatisfiedBy中性NotSpec.ts规范组合否定一个规范OrSpec.ts规范组合OR 两个规范SpecBasics.spec.ts测试验证基础规范行为SpecBuilder.ts构建器基类通过 and/or 分组组合规范总结Teable v2 的 Specification 架构是一套一次建模、两处求值的优雅设计领域层用ISpecification三方法契约与And/Or/Not组合子描述业务约束内存场景直接调用isSatisfiedBy过滤对象见 MemoryTableRepository.ts持久化场景则通过accept 过滤访问者见 TableWhereVisitor.ts把规范树翻译为 Kysely SQL。SpecBuilder与composeAndSpecs让规范树的组装既声明式又具备完善的错误处理。理解这套架构是深入阅读 Teable v2 领域模型尤其是packages/v2/core/src/domain/table/specs/下各领域规范与各 Postgres 适配器实现的前提。【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考