Skip to content

fix(app-shell): judge a stored view by the wire gate, not the authoring gate - #3607

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5316-stored-view-wire-gate
Aug 7, 2026
Merged

fix(app-shell): judge a stored view by the wire gate, not the authoring gate#3607
yinlianghui merged 1 commit into
mainfrom
claude/issue-5316-stored-view-wire-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5316

问题

packages/app-shell/src/views/metadata-admin/clientValidation.tsview loader 把创建编辑两条路都送进了授权门(ViewItemSchema,经 viewSchemaForDraft)。

创建路这样判是对的。编辑路不是 —— 编辑器打开的是一份从 sys_metadata 取回的 body,而 stored view body 里带着平台自己写进去的键:

谁写的
isPinned 视图切换器的 pin 动作(ObjectView.tsx:882)
sortOrder 拖动重排的写入(ObjectView.tsx:931)
config.filter[].id console 过滤器/排序构建器给每行盖的 React key

updateView 先 GET 存储项再 PUT { ...current, ...partial },saveMetaItem 校验后原样落库(ADR-0005 §Validation)—— 这些键在库里是设计如此,不是脏数据。

objectstack#5074 把授权门收紧成真正的门之后(此前是 strip,这些键被静默丢弃),给某视图打过 pin 再进 metadata-admin 编辑器就会报未识别键 —— 而服务端接受同一份 body,因为它判的是 ViewMetadataSchema。客户端严于服务端,方向反了。

修法

validateMetadataDraft 增加可选第四参 { mode: 'create' | 'edit' }:

  • 创建继续走授权门,一字未改;
  • 编辑ViewMetadataSchema —— view 元数据类型注册的那个 schema,也就是服务端 saveMetaItem 跑的同一个。客户端与服务端因此按构造同集,而不是靠人工维持相似。

mode 缺省为 'create'(严的那个门)。漏传只会多报,绝不会静默把门放宽 —— 这是刻意选的缺省方向。

唯一的生产调用点 ResourceEditPage.tsx{ mode: createMode ? 'create' : 'edit' };createMode 本就在该 effect 作用域内,没有新增状态,其余元数据类型的 loader 行为不变(签名多了个被忽略的参数)。

SCHEMA_CACHE 改为按 mode:type 作键 —— 否则 view 会把先到的那个 mode 的 schema 喂给另一个 mode。

为什么是 ViewMetadataSchema 而不是更窄的 ViewItemWireSchema

两者都在顶层声明了 isPinned / sortOrder,所以只看 issue 正文里那两个键分不出高下。实测把它们分开的是另外两条:

  1. ViewItemWireSchema 只覆盖 ViewItem 记录,拒绝容器(编辑器确实会打开容器 —— 既有 pin 就钉着这条)。
  2. 它仍会以 config.filter.0: unrecognized_keys 误拒构建器写入的行 id。那条 decoration 由 ViewMetadataSchemaz.preprocess(stripViewConsoleDecorations, …) 剥除,而 preprocess 跑在所有 union 成员之前,够得到成员级 .strip() 够不到的嵌套块 —— spec 在 union 定义处自己写明了这一点。

也就是说:用 ViewItemWireSchema 只能修掉一半的误拒,嵌套那一半照旧。

不是 try-both 兜底

每个 mode 恰好一个门,拒绝即终局 —— 没有「换一个再试,过一个就算过」。clientValidation.viewShapes.test.ts 原有的后两条 pin 正是看守这个的,它们原样保留且全绿

验证

先复现,再修。把编辑路的期望写成 pin,在未改动的 main 上跑,预测红:

Tests  3 failed | 9 passed (12)

红的正是三条编辑路接受性 pin,报错内容与 issue 描述逐字吻合:

config.filter.0 : Unrecognized key(s) on this filter rule: `id`.
                  … the write path removes it before validating.
(root)          : Unrecognized key(s) on this view item: `isPinned`, `sortOrder`.
                  … Pinning is per-user Studio state … the console writes it
                  through the `view` metadata API.

修后 12 passed (12)

反向验证(先定方向再跑):把 ViewMetadataSchema 换成 ViewItemWireSchema,预测恰好两条变红 —— 嵌套 decoration 那条和容器那条,而 isPinned/sortOrder 两条仍绿(wire 成员在顶层声明了它们)。实跑 2 failed | 10 passed,与预测一致。这说明钉住 schema 选型的是嵌套与容器那两条 pin,顶层那两个键单独无法区分两个候选。

全量:packages/app-shell/src/views/metadata-admin127 passed / 1169 tests;type-check 干净;lint 0 error;check-control-bytes OK。

一条已测量的副作用(已单独立单,未在本 PR 处理)

ViewMetadataSchema 是个 union,Zod 对 union 失败只给一条根级 invalid_union,映射后成为 path: '' / message: 'Invalid input'。判定仍然正确(坏 body 照样被拒,pin 有钉),但编辑路失去了逐字段内联定位和 spec 专门撰写的引导消息。

这是修法的代价,不是 bug —— 更窄的候选已被上面两条实测排除,而消除它需要展开 union 的嵌套 errors 并解决「选哪个成员呈现」的取舍(启发式实测会误选),超出本单范围。已按发现纪律另开 #3606 交分诊定级。

影响面

仅 Studio 客户端预校验。服务端不受影响。变更集:patch @object-ui/app-shell


Generated by Claude Code

…ring gate

metadata-admin's live client validation routed BOTH the create and the edit
draft through the AUTHORING schema (`ViewItemSchema` via `viewSchemaForDraft`).
Right for create, wrong for edit: the editor opens a body that came back out of
`sys_metadata`, and the platform itself writes keys into stored view bodies —
`isPinned` (view switcher pin), `sortOrder` (reorder), and a per-row `id` the
console filter/sort builders stamp on `config.filter[]`. `saveMetaItem` persists
the accepted body verbatim, so those keys are in storage by design.

Once the authoring gate was tightened, opening a pinned view reported
unrecognized keys — while the SERVER accepted the same body, validating it
against `ViewMetadataSchema`. The client was strictly stricter than the server.

`validateMetadataDraft` now takes `{ mode: 'create' | 'edit' }`. Create keeps the
authoring gate byte-identical; edit uses `ViewMetadataSchema`, the schema the
`view` metadata type registers and the server runs. `mode` defaults to 'create'
(the strict gate), so omitting it can only over-report, never widen the door.

`ViewMetadataSchema` rather than the narrower `ViewItemWireSchema`: the latter
covers only the ViewItem record and, measurably, still rejects
`config.filter[].id`, because that decoration is stripped by a `z.preprocess`
running ahead of every union member — reach a member-level `.strip()` lacks.

Not a try-both fallback: each mode has exactly one gate; a rejection is final.

Fixes objectstack-ai/objectstack#5316

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 7, 2026 3:47pm

Request Review

@github-actions github-actions Bot added the tests label Aug 7, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 15:49
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 4cf76ce Aug 7, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5316-stored-view-wire-gate branch August 7, 2026 15:49
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-DXXEayzN.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 137.51KB 35.11KB
fields (index.js) 230.87KB 56.83KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.03KB 28.88KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 186.61KB 49.34KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui 的 metadata-admin 用「授权 schema」校验一份 STORED view —— #5074 收紧后,给已保存视图打过 pin 再进编辑器会误报

2 participants