fix(spec)!: tenancy.tenantField 不再默认 'tenant_id' —— 未声明如实为 undefined,driver 回落为唯一真相 (#5315) - #5766
Merged
Conversation
… undefined (#5315) `TenancyConfigSchema.tenantField` defaulted to 'tenant_id', so parsing `tenancy: { enabled: true }` materialized a tenant column the platform does not use. The real column is `organization_id` — kernel-injected, assumed by `tenantPolicy()` in security/rls.zod.ts and by the RLS predicates. The default had no consumer it could reach. driver-sql's `computeTenantField` honours a declared tenantField only when the object actually has that field, so the materialized 'tenant_id' merely sent it looking for a column that did not exist before falling back to `organization_id` anyway — the right answer by accident. A declaration nobody reads is what ADR-0078 prohibits, spelling a word ADR-0120 refuses for the authorable vocabulary. `.optional()`, not a bare removal: `z.string()` alone is REQUIRED, which would reject `tenancy: { enabled: true }` outright. `.optional()` is what expresses "undeclared stays undefined". The effective tenant column does not move, pinned end to end (parse through ObjectSchema, then resolve through the driver) by the new `#5315 undeclared tenantField resolves to organization_id` case. Consumers verified against undefined across objectstack/objectui/cloud: `computeTenantField` truthiness-guards it; lint's `authoredTenantColumn` falls back to 'organization_id' and reads unparsed author text anyway; the `TenancyConfig` z.infer export has zero consumers, so widening it to `string | undefined` breaks nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 04:59
This was referenced Aug 6, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5315
按 #5315 评论区 2026-08-05 22:31Z 排程注记预记的方向 2(否决窗口未被行使):去掉
TenancyConfigSchema.tenantField的默认值,让「未声明」如实表达为undefined,driver-sqlcomputeTenantField的既有回落作为唯一真相。依据 ADR-0078(不留没人读的声明)+ ADR-0120 §Terminology(可授权词汇定死organization,tenant/org拒收)。一处实现细节:不是单删
.default()单删
.default('tenant_id')是错的 —— Zod 里z.string()是必填,单删会让tenancy: { enabled: true }直接 parse 失败,那是个真正的破坏性变更。裁定要的语义(未声明 =undefined)对应的是.optional()。三种写法实测:parse({ enabled: true })z.string().default('tenant_id')(改前){"enabled":true,"tenantField":"tenant_id"}z.string()(单删 default)invalid_type:tenantFieldz.string().optional()(本 PR){"enabled":true}前提复核:消费方清单 + 各自 undefined 处理实测
全仓扫描
tenantField(objectstack / objectui / cloud,排除node_modules、dist、CHANGELOG)。绝大多数命中是driver-sql内部string | null的局部参数/变量(schema-drift.ts等),与 spec 声明面无关。真正读取 spec 声明值的只有两处:computeTenantFielddriver-sql/src/sql-driver.ts:3531tenancyDecl?.tenantFieldorganization_idauthoredTenantColumnlint/src/data-model-rules.ts:88obj?.tenancy?.tenantFieldtypeof === 'string' && trim(),否则'organization_id'tenantPolicy()spec/src/security/rls.zod.ts:601= 'organization_id'isTenancyDisabled×2object.zod.ts:419、lint/validate-org-axis-red-lines.ts:187.enabledTenancyConfig类型object.zod.ts:2095z.infer导出第 2 条值得单独点名:lint 的注释原文就是 "The organization column, as an AUTHOR would have spelled it",其测试喂的是
tenancy: { tenantField: 'tenant_ref' }(没有enabled,这个输入根本过不了TenancyConfigSchema.parse)—— 证明该路径读的是未经 parse 的作者原文,物化默认值从来不在它的视野里。补充核实:
SqliteWasmDriver/TursoDriver均extends SqlDriver,共用 Add metamodel interfaces for ObjectQL/ObjectUI contract #1 的computeTenantField,无独立实现。driver-mongodb的 tenancy guard(mongodb-tenancy-guard.ts:96)只读.enabled。tenancy(tenancy.posture/tenancy.defaultOrgId(),ADR-0105 的租户服务),与对象 schema 的tenancy块无关,不受影响。tenantField与TenancyConfig均零代码引用(仅 objectui 一处 CHANGELOG 历史文本)。结论:不存在依赖「parse 后
tenantField恒有值」的消费方(运行时或类型),硬前置 1、2 通过,无 #4666 家族停手条件。类型面:
TenancyConfig['tenantField']由string变为string | undefined。因零消费方,无一处需要改逻辑,未使用任何!或 cast。顺带一提,全仓唯一声明了
tenancy块的对象是sys_sso_provider({ enabled: false }),走isTenancyDisabled直接返回null,压根不读tenantField。物化的'tenant_id'在本仓真的是零效果。行为等价性:先证红
先按错误预期写断言,在未改动的
origin/main上跑红:Received: "organization_id"一行同时给出两个事实:(a) 改动前的基线实测值就是organization_id(硬前置 3 要的那条);(b) 物化的'tenant_id'从未抵达 driver —— 它确实是死声明。翻正断言后前后同绿。关于验证方向,按实情报告:这条 pin 按设计两侧皆绿,不是「改前绿改后红」那种反向验证。它证明的是等价性,这正是本单要的东西;它的长期职责是防止有人再塞回一个指向不存在列的默认值。真正的红证据是上面那次故意写错预期的运行。
测试与门禁
合并
origin/main(前移 9 个 commit,无一触及本 PR 文件面)后整体复验:typecheck:spec / driver-sql / lint 三包全绿。(lint 首轮报Cannot find module '@objectstack/formula'等,是未构建依赖的老陷阱,补pnpm --filter '@objectstack/lint^...' build后清零,与本改动无关。)生成物走 os-regen 四步:
check:generated只报content/docs/references/**一项 stale →gen:docs重生成,tenantField由✅ required变为optional并带上新 describe(顺带修正了该页原有的不一致:上方内联类型早已写tenantField?: string,下方属性表却标 required)。合并后复跑✓ All 10 generated artifacts are up to date.authorable-surface.base.json在每次 spec build 时被重锚,带进api/Discovery:scoping、ui/ActionSession:*等他人在飞的无关键,已按 #5358 纪律git checkout还原,不进本 PR(还原后check:authorable-surface仍 ✓)。check-nul-bytesOK,并对本 PR 全部改动文件做了超出该门禁扫描范围的控制字符自查,干净。范围外发现
content/docs/protocol/objectql/schema.mdx的多租户小节仍教tenantField: tenant_id、指向一个不存在的reference: tenant对象,并把 legacy 的OS_MULTI_ORG_ENABLED当现行开关介绍(packages/types/src/env.ts:89原文标注 LEGACY)。改对它要先定该示例的教学意图(保留「自定义租户列」演示 vs 改成平台默认姿势),超出本单裁定面,已另立单,未在本 PR 修改。本 PR 内
content/docs/data-modeling/objects.mdx是直接记录这个键的授权指引,其示例正是 issue 点名的「AI 会照抄」的面,故随 schema 一并更正为「省略即回落organization_id」,并把自定义列的例子换成workspace_id。Generated by Claude Code