fix(core): 已保存视图的 ViewFilterRule[] 在进 $filter 前折成 AST (#3431) - #3471
Merged
Conversation
…aches $filter (#3431) `toFilterNode` returned every array source verbatim, so a saved view's stored `ViewFilterRule[]` travelled the whole read path — ObjectView -> ListViewSchema.filter -> buildEffectiveFilter -> mergeFilterNodes -> $filter — as bare rule objects. The server refuses that: `isFilterAST` is false for an array of objects and the wire face answers 400 INVALID_FILTER, so every saved view carrying a filter rendered no rows at all. Verified against a real backend (@objectstack/*@17.0.0-rc.2, app-showcase) on the SHIPPED `showcase_task.in_progress` view: $filter=[{"field":"status","operator":"equals","value":"in_progress"}] -> HTTP 400 {"code":"INVALID_FILTER"} $filter=[["status","equals","in_progress"]] -> HTTP 200, total=2 The lowering lives at `toFilterNode` rather than at a producer because `ListViewSchema.filter` / `ViewTab.filter` are spec-declared `z.array(ViewFilterRuleSchema)`: folding one hop earlier would write AST triples into a spec-declared rule-array slot. This is the last hop before the wire and the single sink both producers share (plugin-list's buildEffectiveFilter, which feeds the grid and its export; plugin-view's ObjectView, which feeds calendar/kanban/gallery/timeline). Operators canonicalise through the spec's own `normalizeFilterOperator` — the exact exit the write side (viewFilterFold) uses — so no second operator table is introduced; all 19 VIEW_FILTER_OPERATORS are already members of VALID_AST_OPERATORS, making the fold purely structural. Unknown spellings pass through verbatim so the server still refuses them loudly. Mixed arrays (a view's rules concatenated with ?filter[...] URL triples) fold element-wise with the triples untouched; a blank-field rule is deliberately NOT lowered, because ["", op, value] passes isFilterAST and returns an empty list while the unlowered rule keeps the loud 400. Two fixtures pinned the old passthrough and are replaced: core's filter-source-merge ("passes a non-empty array source through unchanged", whose note explained it away with "the adapter translates it on the way out" — none does) and plugin-view's ObjectView.filterSources ("keeps a ViewFilterRule[]"), which sits in the rule's consumption radius outside the edited package. Fixes #3431 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
August 6, 2026 08:43
This was referenced Aug 6, 2026
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.
Fixes #3431
实证结论:服务端不接受 rule 数组(issue 的可能性 2 成立)
先给结论和证据,再谈修法。用
e2e/live/ci/start-backend.sh起了一套独立测试栈(@objectstack/*@17.0.0-rc.2—— 与pnpm-lock.yaml里@objectstack/spec同一枚 pin,即 live-e2e lane 用的那对),跑 app-showcase,端口 4031,--fresh临时库。showcase 自带一条正好命中本 issue 的已保存视图(
src/ui/views/task.view.ts):/api/v1/meta/view?object=showcase_task读回来的就是这个 rule 数组。直接打数据接口:不是猜的、也不是只看静态代码:又用仓库自己的源码把整条读路径复现了一遍(取
/meta/view的真实 metadata →ObjectView.tsx的base/baseArr/combined表达式 →resolveFilterPlaceholders→mergeFilterNodes→ 打真实后端):viewDef.filter[{"field":"status","operator":"equals","value":"in_progress"}]ListViewSchema.filter$filter[{"field":"status",…}][["status","equals","in_progress"]]也就是说:今天每一条带 filter 的已保存视图都是坏的 —— 不是"筛错",是 400、一行都不出(
record-count-bar在data.length > 0时才挂载,所以连计数条都不会出现)。静态侧与之一致(用仓库 pin 的
@objectstack/spec@17.0.0-rc.2实跑):isFilterAST([{field,…}])为false,parseFilterAST返回undefined;协议层metadata-protocol/protocol.ts对isFilterAST判否且非空的数组直接malformedFilterArrayError→ 400(objectstack#4121)。缝口裁决:折叠放在
core/filter-converter.ts的toFilterNode,不是 ObjectView派单倾向读侧 ObjectView,允许"若正确缝口在 core 则给出论证"。论证如下,四条,最后一条是决定性的:
toFilterNode本来就声明了这个形状。 它自己的注释写着"三种都合法"的第一种就是[{ field, operator, value }, ...]一个 specViewFilterRule[]—— 文档写了、实现没做。紧邻的mergeFilterNodes注释更是精准预言了本 bug("bare rule OBJECTS where the AST expects nodes …isFilterASTsays no"),而它依赖的 sink 从没实现它描述的下降。plugin-list的buildEffectiveFilter(喂 grid 和导出)和plugin-view/src/ObjectView.tsx:432(currentNamedViewConfig?.filter || activeView?.filter,喂 calendar / kanban / gallery / timeline)。只在 app-shell 的 ObjectView 折,后者仍然坏 —— 而它不在本次 fence 内。convertFiltersToAST下降的。ListViewSchema.filter/ViewTab.filter在 spec 里声明为z.array(ViewFilterRuleSchema),而 ObjectView 交给 ListView 的就是一个ListViewSchema。在 ObjectView 折,等于把 ObjectQL AST 三元组写进一个 spec 声明为 rule 数组的槽位 —— 往 spec 字段里塞 off-spec 值,正是 AGENTS.md #0.1 的镜像违规。下降必须发生在离开 spec 词汇、进入线上 AST 的最后一跳,那一跳就是toFilterNode。副作用:
ObjectView.tsx一个字节没动,URL 三元组(?filter[FIELD]=VALUE)的处理原样保留,substituteFilterTokens也仍然作用在它的 resolver 本来就为之编写的 rule 形状上。算子:没有第二张 canonical 表
算子过 spec 自己导出的
normalizeFilterOperator(@objectstack/spec/ui)—— 写入侧viewFilterFold.ts用的同一个出口,两个方向不可能漂成两种方言。而且这里根本不需要映射表:spec 的 19 个
VIEW_FILTER_OPERATORS已经全部是线上VALID_AST_OPERATORS的成员,所以下降是纯结构性的({field,operator,value}→[field,operator,value])。spec 不认识的拼法原样透传,让isFilterAST照样判否、服务端照样 400 —— 拼错必须大声失败,绝不能被"纠正"成一个合法算子。两处刻意的取舍,都实测过:
value的 rule 只发两元节点。is_empty/is_null的方向来自算子名字;补第三位会在序列化时凭空造出一个null(数组空洞 →null),['x','equals',null]是一条作者从没写过的真实{x: null}谓词。与写入侧if (c.value !== undefined)同规则。field为空串的 rule 不折。 实测[["","equals","x"]]能过isFilterAST,服务端回200+total: 0—— 一个静默的空列表;不折则它仍是 AST 位置上的对象,服务端400 INVALID_FILTER并点名该元素。响亮胜过静默空集。与写入侧丢弃空白行的判别一致。测试
反向验证方向(先预测、后执行):预测为常规 RED —— 新用例断言的是"产出了某个值"且
isFilterAST(...) === true,不是"某物没被产出",所以拿掉折叠必然红,不存在"空集导致的假绿"。执行结果与预测一致:把toFilterNode的数组分支改回return source as FilterNode,9 failed / 13 passed;恢复后 22 全绿。fixture 三分处置(逐条重判,不是批量改拼写)
packages/core/.../filter-source-merge.test.ts的passes a non-empty array source through unchanged—— 整条替换。它把缺陷本身钉成了正确行为,并且用一句"the adapter translates it on the way out"给自己作了担保。没有任何 adapter 做这件事,上面的 400 就是证据。现在钉住下降结果,并用@objectstack/spec/data的isFilterAST/parseFilterAST(后端跑的同一份代码)做判官,而不是复述一个字面量。mergeFilterNodes的wraps each source as its own child—— 改期望值。"每个 source 各自成子、绝不 spread"的意图存活,期望值随下降更新。packages/plugin-view/.../ObjectView.filterSources.test.tsx的keeps a ViewFilterRule[]—— 整条替换,且这条是跨包扫出来的:改动在packages/core,坏掉的 fixture 在packages/plugin-view(本 PR 认定的第二个生产者)。按"编辑的包"扫会漏,按规则的消费半径扫才扫得到。live e2e
新增
e2e/live/saved-view-filter.spec.ts:直接进showcase_task.in_progress这条已保存视图,断言(a)record-count-bar显示 2、(b)/api/v1/data/showcase_task没有任何 4xx 响应(失败信息会打印被拒请求的状态码、错误 code 和完整 URL,而不是只说"没有行")。暂未加进
test:e2e:live:ci白名单:live-e2e.yml自己写明"新 spec 需先在该 lane 证明无 flake 才可加入白名单",这条还没有运行记录;且package.json不在本次 fence 内。建议 nightly 跑过之后由后续 PR 提升。越界说明
packages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx不在派单 fence("两个包的测试" = app-shell + core)内。它是本次改动的必要 fixture 重判(否则 CI 直接红),属于规则消费半径内的 fixture 扫描结果,非功能改动。没有动ListView.tsx、packages/spec、objectstack 仓、content/docs/releases/。顺手记录的越界发现
not in(带空格),服务端 400;且它是第二张手工算子表 #3470 ——packages/plugin-list/src/UserFilters.tsx的私有specOperatorToAst把not_in/nin译成not in(带空格),该拼法不在VALID_AST_OPERATORS里,实测 400;且它本身就是与normalizeFilterOperator重叠的第二张手工算子表。已单独立 issue,未在本 PR 修。Generated by Claude Code