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.

fix(core/plugin-grid): lookup 等关系字段在动作/条件格式表达式里没有通用写法——展开与投影让同一条谓词在三个面得到三种结论 #3501

Closed

Description

@baozhoutao

lookup / master_detail / user 字段在动作 visible / disabled / 条件格式的表达式里没有一种写法是通用的:同一条谓词在列表、详情、批量三个面上会得到三种不同结论,而且两种失败都是静默的。

复现(对着真实 runtime 实测,showcase showcase_task.project,master_detail → showcase_project)

服务端 $expand(wire 上是 populate=)把外键 id 原地替换成整条关联记录:

GET /api/v1/data/showcase_task?top=1
  → "project": "MURYjcjLhoir9Vzk"

GET /api/v1/data/showcase_task?top=1&populate=project
  → "project": { "id": "MURYjcjLhoir9Vzk", "name": "Website Relaunch", ... }

而客户端 $expand 的范围是 buildExpandFields(fields, columns) —— 视图把哪个关系字段当作「列」显示,就展开哪个。这是一个纯展示决策,写谓词的人根本不参与。再叠加 $select 只按列构造(ListView.tsx 的 selectFields / ObjectGrid.tsx 的 getSelectFields),同一个字段在客户端一共有四种形态。把这四种形态喂给 CEL 引擎(@objectstack/formula)实测:

记录里的值 record.project == "P1" record.project.id == "P1"
'P1'(未展开) true fault
{ id: 'P1', … }(已展开) false true
null(关系为空) false fault
缺席(不在 $select 里) fault fault

没有一列是全对的,作者写哪种都会在某个面上翻车。而且 fault 的后果按面而不同,两个方向都错:

  • 行内 kebab / 批量栏(evalRowPredicate,fallback: false)→ fail-closed,按钮对所有人静默消失;
  • 走 evaluateCondition 不带 throwOnError 的宽松路径 → catch { return true } → fail-open,按钮对所有人显示。

服务端那一侧只有第一行:CEL 看到的是存储值,也就是 id。所以「客户端与服务端对同一条谓词得到相同结论」(ADR-0036 / ADR-0058 反复申明的目标)在关系字段上根本不成立。

方案

  1. 谓词作用域按服务端语义绑定记录 —— 新增 toPredicateRecord(record, fields)(packages/core/src/utils/predicate-record.ts):按对象自己的字段类型(EXPANDABLE_FIELD_TYPES,与决定展开的是同一个集合),把已展开的关系值折回它代表的 id;multiple: true 的数组逐元素折。由 schema 驱动而不是嗅探形状 —— 「带 id 键的对象」会误伤 json 字段。接进四个求值口:evalRowPredicate / resolveConditionalFormatting(core)、useRowPredicate(react)、partitionBulkRows(plugin-grid)、page:header 的两个 ExpressionEvaluator(components/containers.tsx);调用方(ObjectGrid / ListView / 详情页头)把 objectSchema.fields 传下去。展示侧不动 —— 详情页标题插值照旧读原始 ctx.data,关系字段仍然显示名称。

  2. $select 补上谓词读到的字段 —— 新增 collectPredicateFieldRefs / listViewPredicates(packages/core/src/utils/predicate-fields.ts),从条件格式、rowActionDefs、bulkActionDefs、对象动作、userActions 覆盖里收集 record.x / data.x 引用,与对象已声明字段取交集后加进 $select(ObjectGrid 与 ListView 各自的投影处)。交集是必须的:未知列不是每个后端都会忽略,cloud runtime 会直接返回空结果集,那样一个拼错的谓词就能把整个列表清零。不加 $expand —— 谓词要的就是裸外键,只展开有列要显示的那些。

  3. 测试:toPredicateRecord 的四种形态 + 数组 + 非关系字段不受影响 + 引用返回;collectPredicateFieldRefs 的三种谓词形态与 record./data. 前缀;四个面上同一条 record.<lookup> == os.user.id 得到同一结论。

验收

同一条 visible: 'record.owner == os.user.id',在列表工具栏、行内 kebab、详情页头、批量栏,以及服务端 CEL,对同一用户给出相同结论;关系字段为空(null)时干净地求值为假而不是 fault;谓词引用的字段即使不是列也在 payload 里。

Activity

  1. self-assigned this
    on Aug 6, 2026
  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊:pm:queue(objectui 为整仓座位,⛔ 按协议不打 domain:*)

    落点核实(说得出落在哪个文件才算分诊完):方案 1/2 的新增面 packages/core/src/utils/predicate-record.ts、predicate-fields.ts 在 origin/main 上确不存在(同目录的 expand-fields.ts 在,即正文所指 buildExpandFields 的出处)⇒ 前提成立、非重复实现;接入面 evalRowPredicate / resolveConditionalFormatting(core)、useRowPredicate(react)、partitionBulkRows(plugin-grid)、components/containers.tsx 的两个 ExpressionEvaluator,以及 ObjectGrid.tsx / ListView.tsx 的投影处 —— 全部在本仓。

    过时前提检查:packages/core/src/utils 最近三条提交(2a9513d #3471 / 509104a #3415 / 85c4c9c #3399)与 plugin-grid 的 6195841 均未触及谓词求值面 ⇒ 复现表未被顺手改掉。

    为什么不走跨座位转移:落点不含 packages/spec,也不需要 objectstack 侧改动 —— 服务端 CEL 那一列是对照基准(谓词看到的是存储值 = id),本单要做的是把客户端折回该基准,不是改契约。因此既非 feature 级跨仓单,也不触 spec 单一所有者面 ⇒ 留在 objectui 队列。

    分类理由(入队):四种形态 × 两种写法的实测表 + 两个方向的失败(行内 kebab fallback:false fail-closed、宽松路径 catch { return true } fail-open)+ 一句话可陈述的验收(同一条 record.owner == os.user.id 在四个面与服务端结论一致)⇒ 具体缺陷,不是观察项。

    交叉链接(不扩范围):#3367(finding — toPredicateInput 在 core canonical 与 react 渲染器侧有两份实现)与本单方案 1 接的是同一族求值入口 ⇒ 实施时顺手核一遍两份实现是否会各自漏接 toPredicateRecord(漏一处就是第五种形态),但收敛 #3367 本身不属本单范围。#3374 是 spec pin 联动清单,与本单无关。

    ⚠️ 本单在分诊前已有 assignee(@baozhoutao)—— 分诊座位只打标签,未动认领、未改 assignee。

    查重:objectui open issue/PR 按谓词 / 展开 / lookup 关键词搜过,无第二个入口。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    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

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions