fix(types): Page/App/Dashboard validate the spec's own fields instead of passing them through (#4115 C 组) - #3063
Merged
Conversation
… of passing them through (objectstack#4115 group C)
These three renderer nodes extend BaseSchema, which is `.passthrough()`,
while their spec counterparts are strict. Every spec-only key therefore
rode through objectui completely unvalidated — 23 on App (`branding`,
`sharing`, `embed`, `objects`, `apis`, `requiredPermissions`,
`homePageId`, `protection`, the whole `_lock*`/`_package*`/`_provenance`
package envelope), 10 on Dashboard (`header`, `refreshInterval`,
`performance`, `aria`, `protection`, …) and 6 on Page (`interfaceConfig`,
`kind`, `slots`, `source`, `requires`, `aria`). A typo in any of them
(`brading: {…}`) was indistinguishable from the real key. This is the
objectstack#4120 silent-drop failure mode, objectui side.
Page carried a live consequence: `source` was never declared, so a
`kind: 'html' | 'react'` page — whose body lives in `source` — could not
be expressed at all.
The fix is NOT `.strict()`: these are component nodes and the open
envelope is deliberate. Spec fields now flow in **by reference**, the
pattern `zod/objectql.zod.ts`'s ListViewSchema already uses, via a new
`specFieldsExcept()` helper in base.zod.ts. The helper reads `.shape`
rather than calling `SpecSchema.omit({…}).partial()` because zod 4
refuses `.omit()` on an object carrying refinements — spec's PageSchema
has one, so the idiomatic form throws at import time.
Each omission is documented at its site: component-envelope keys go to
BaseSchema; Page's `type` genuinely collides (spec's is the page KIND,
objectui's is the component discriminator and the kind lives on
`pageType`); and the element schemas that are their own ledger entries
(navigation/areas/contextSelectors, widgets/globalFilters/dateRange,
regions) stay local with the migration deferred.
`.partial()` on the imported shape guarantees no future spec field can
become required and invalidate payloads already stored.
Mutation-tested: dropping a spec field from a derivation fails 3 tests by
name, and adding an untriaged objectui-only key fails the rogue-key
assertion. 78/78 type-check, 271 types + 3626 downstream assertions green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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.
承 objectstack#4115 分诊评论里划出的 C 组 —— 该组的选择理由是:它是 #4120 失效形态在 objectui 的等价物,且不需要任何重命名决策(A 组要改公开导出名,B 组要先定
type判别式的命名约定)。问题:
.passthrough()遇上.strict()AppSchema/DashboardSchema/PageSchema都extend自BaseSchema,而后者是.passthrough();spec 侧对应的三个是 strict。结果是 spec 独有的键完全不经校验穿过去:brandingsharingembedobjectsapisrequiredPermissionshomePageIdprotection+_lock*/_package*/_provenance整个包锁封套headerrefreshIntervalperformanceariaprotection+ 包锁封套interfaceConfigkindslotssourcerequiresariabrading: {…}和branding: {…}在校验层面无从区分 —— 这正是 #4120 抓到的那类静默失效。Page 还带一个活的后果:
source从未被声明,而kind: 'html' | 'react'的页面正是把正文放在source里的 —— 也就是说这类页面在这里根本无法表达。修法:不是
.strict(),是把 spec 的字段按引用引进来加
.strict()是错的 —— 这三个是组件节点,开放封套是有意为之(渲染器属性本就该穿透)。正解是让 spec 自己的字段按引用流入,也就是zod/objectql.zod.ts的ListViewSchema已经在用的范式。新增
specFieldsExcept()(base.zod.ts)承载这个模式。它读.shape而不是调SpecSchema.omit({…}).partial(),因为 zod 4 拒绝对带 refinement 的对象.omit()—— spec 的PageSchema恰好有一个,惯用写法会在 import 期直接抛异常。这个坑记在 helper 的文档注释里(踩过一次就够了)。每处 omit 都在原地写明理由:
name/label/description→ 组件外壳,归BaseSchema;type是真冲突:spec 的type是页面种类(record|app|utility|list|home),objectui 的是组件判别式('page'),种类被放在pageType。二者的调和是 B 组的重命名决策,本 PR 不动;navigation/areas/contextSelectors、widgets/globalFilters/dateRange、regions)本身就是台账里独立的条目,保持本地、迁移另议。.partial()保证未来任何 spec 字段变成必填都不会让已存的 objectui 载荷失效。闸门与变异测试
新增
page-app-dashboard-spec-parity.test.ts(19 断言),三个方向:spec 长出未分诊字段 / spec 重命名被本地覆盖的锚点 / 有人加了未分诊的本地键。另有一组断言直接钉住「这些键现在真的被校验了」——例如branding: 'blue'从通过变成拒绝,kind: 'nonsense'从通过变成拒绝,以及kind:'html' + source现在能正常表达。变异测试(两个方向都会红):
branding→ 3 个测试按名报红(expected [ 'branding' ] to deeply equal []、'branding' is still undeclared、以及 branding 校验断言);expected [ 'myUntriagedField' ] to deeply equal [])。验证
全仓
type-check78/78 绿;@object-ui/types271 断言绿;下游消费方(app-shell / plugin-dashboard / plugin-list / core)3626 断言绿 —— 确认收紧校验没有挡住任何现有渲染路径。另有一组断言专门守住「组件外壳仍然放行未知渲染器属性」,即这次收紧的只是 spec 拥有的那部分,节点本身没有被关上。🤖 Generated with Claude Code