Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

ObjectDataPage「Save as view」把 URL filter 三元组原样写进 ViewItem,产出过不了 ViewItemSchema #3419

Closed

Description

@yinlianghui

发现于 #3375(给 viewEnvelope 补 ViewItem spec 一致性 pin 时),不在该 PR 的 fence 内(属运行时行为改动),故单独记录。

现象

packages/app-shell/src/views/ObjectDataPage.tsx 的 handleSaveAsView(约 250-254 行)把当前 URL 上的下钻条件原样塞进要持久化的 view spec:

const spec: Record< string, any > = {
  ...config,
  columns: ...,
  ...(urlFilters.length ? { filter: urlFilters } : {}),
};
const env = viewEnvelope(objectName ?? '', spec, { ... });
await createRuntimeMetadata('view', env.name, env, { metadataClient });

而 urlFilters 是 FilterTriple[](drillUrlFilters.ts:23:[string, string, unknown] —— 三元组数组),@objectstack/spec 的 ViewItem list config 里 filter 要求的是 ViewFilterRuleSchema 对象数组({ field, operator, value },且 operator 取规范枚举 equals 等,不是 =)。

复现(spec@17.0.0-rc.2,即当前 lockfile 解析版本)

把 viewEnvelope 在该调用点的实际产出喂给记录门:

config.filter = [['stage','=','open']]
ViewItemSchema.safeParse(env) -> false
[{"expected":"object","code":"invalid_type","path":["config","filter",0],
  "message":"Invalid input: expected object, received array"}]

对照:builder 那条路径是对的 —— viewFilterFold.foldFilterGroupToSpecRules(viewFilterFold.ts:122)产出 { field, operator, value },并且用 normalizeFilterOperator 把 operator 规范化过,产出可以过门。也就是说同一个 filter 键在本仓存在两种写法,只有 fold 那条符合 spec。

影响

带下钻条件时点「Save as view」,写出去的是一条 off-spec 的 ViewItem。ADR-0017 记录门若在发布/校验侧收紧,这条 view 会被拒;在此之前它以第二种事实方言的形式沉淀下来。

建议方向(交由 triage 定夺)

契约优先:在 producer 侧把三元组折成 spec 的 rule 对象再进 viewEnvelope(复用 normalizeFilterOperator,与 viewFilterFold 同一出口),而不是在消费侧容忍三元组。

备注

#3375 的 pin 只覆盖 viewEnvelope 自身的产出契约,对这条走的是「按 spec 形状的 filter rules」用例;三元组这条留给本 issue,以免把运行时改动混进纯 pin PR。

Activity

  1. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    分诊:bug + pm:queue(objectui 分片 PM,会话 session_01GTRjn8xBqp75dk7kFupVRt)。

    判级理由:非观察类 finding——有具名落点(ObjectDataPage.handleSaveAsView)、有 rc.2 下的 safeParse 复现、用户今天就能踩到(带下钻条件点 Save as view 即写出 off-spec ViewItem)。属 declared≠enforced 家族:同一个 config.filter 键在仓内存在两种事实方言,仅 viewFilterFold 一条合 spec。

    修复方向认同 issue 建议:producer 侧折三元组(复用 normalizeFilterOperator,与 viewFilterFold 同一出口),不做消费端容忍(AGENTS.md #0.1)。#3375 的 pin(PR #3420)已把 spec 形状的 filter 用例钉住,本单落地后应补一条「三元组入口 → 折叠后过门」的正向用例。

    文件面预告:packages/app-shell/src/views/ObjectDataPage.tsx(+ 可能 drillUrlFilters.ts 读侧复用)——与在飞三单(components/collaboration、fields、runtime-metadata-persistence)互不相交,待下一派发窗口。


    Generated by Claude Code

  2. self-assigned this
    on Aug 6, 2026
  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    认领:objectui 分片 PM 并行批第 26 单
    会话:session_01GTRjn8xBqp75dk7kFupVRt
    分支:claude/issue-3419-saveasview-filter-fold
    Worktree:objectui-issue-3419
    文件面:packages/app-shell/src/views/ObjectDataPage.tsx + 该包测试 + changeset(viewFilterFold/normalizeFilterOperator 只 import 复用,不改;越界即停,报告说明)
    与同批第 27 单(#3422,packages/fields)文件面互不相交。


    Generated by Claude Code

  4. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    验收:ACCEPT → PR #3427(并行批第 26 单,会话 session_01GTRjn8xBqp75dk7kFupVRt),CI 全绿,已转 ready + auto-merge。

    复核要点:

    1. 前提复核超出 issue 本身:issue 假设「复用 normalizeFilterOperator」即可,dev 实测证明它只折词形别名、不认 =/>= 等符号——于是加了符号→spec 别名桥接 TRIPLE_OP_TO_SPEC_ALIAS,且 range 半边由 URL_FILTER_OPS 反转派生(URL 契约新增 range 操作符自动被桥接),规范化仍只发生在 normalizeFilterOperator 一处。不是第二张规范映射表,裁定得到遵守。
    2. declared=enforced 贯彻:每条折叠结果过 ViewFilterRuleSchema.safeParse;无规范拼写的 operator 从持久化 view 里丢弃(debug 提示)而非写 off-spec——丢一条好过整条 view 在 publish 被整体拒。
    3. 可验证性取舍正确:buildSaveAsViewSpec 提出为导出 seam,否则 call site 退回原始三元组时 helper 测试依然全绿——反向验证(6 红 4 绿,方向先声明)恰好证明该 seam 承重。运行时 schema memo 的三元组刻意未动(那是 ListView 要的 AST 形状)。
    4. 顺带发现 [finding] 已保存视图的 ViewFilterRule[] 被原样当 FilterNode 送进 $filter,未在客户端折成 AST #3431(saved view 的 ViewFilterRule[] 原样进 $filter AST 位)已核实存在、带 finding 标签,进发现分诊轮池。

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions