Skip to content

fix(data): implicit field filters AND-merge with an explicit filter instead of being silently dropped (#4164) - #4190

Merged
os-zhuang merged 1 commit into
mainfrom
claude/rest-list-unknown-query-params-6syn6f
Jul 30, 2026
Merged

fix(data): implicit field filters AND-merge with an explicit filter instead of being silently dropped (#4164)#4190
os-zhuang merged 1 commit into
mainfrom
claude/rest-list-unknown-query-params-6syn6f

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4164#4134(已合并,#4169)的镜像案:那边是未知参数清零,这边是已知字段参数被显式 filter 挤掉——写下去的收窄谓词既不生效也不报错,作为野键漂在 AST 顶层,响应多返回

语义(维护者已拍板:AND 合并)

?filter={"priority":"medium"}&status=todo 现在按字面意思执行:两个谓词都生效,{ $and: [显式, 隐式] }

选这个组合子不是发明——是对齐三处既有先例:engine 层折叠 $search 谓词、expand 的 FK 匹配 + 声明式 filter、嵌套 expand 的 AST where,全部用同一个 { $and: [...] } 模式;且 $and#3774 conformance 套件在四个 FilterCondition 后端上钉死的组合子,不踩驱动方言。

实现决定

验证

协议层protocol-data.test.ts):翻转了 #4134 PR 里特意留下的 pin 测试(它当时就注明"等一次 deliberate change"——就是这次),新增:多隐式键合成单臂、矛盾对、空 filter/空串、count 看到合并 where、$filter 字符串别名同形、非 object where 原样保留(pin 到 #4181)。

真实引擎protocol-unknown-query-param.test.ts):先给迷你驱动的 matchesWhere 教会 $and忽略 $and 的 matcher 会给一个啥都没干的合并放行——这是本测试文件自身的坑),然后钉:显式 8 行 ∩ title → 1 行;矛盾 → 0;top=3total=8(合并谓词的匹配数,不是 10 也不是 3)。

跑起来的服务器(showcase + 真实 SQL 驱动,已自行关闭):

?filter={"priority":"medium"}               -> 200 total=4   (仅显式)
?status=todo                                -> 200 total=2   (仅隐式)
?filter={"priority":"medium"}&status=todo   -> 200 total=1   (AND 交集;修复前 = 4,隐式被丢)
?filter={"priority":"medium"}&status=done   -> 200 total=2   (medium∩done)
?filter=…&status=todo&top=1                 -> 200 total=1   (分页 total = 交集)
?filter=…&pageSize=5                        -> 400 INVALID_FIELD(#4134 不回归)

turbo run test 132/132 全绿(首轮有 4 个包在高并发下抖动,--concurrency=4 重跑全过,单独重跑也全过——资源竞争,非本改动);eslint 干净。

影响面

in-repo 调用方没有"filter + 裸字段"混用(#4134 时已清点:objectui 适配器/console/marketplace/import/export/公开表单全部只发保留参数);flows 与 MCP agent 是唯二可能混用的入口,恰是意图从此被忠实执行的受益者。会变化的只有依赖丢弃行为的外部调用方——它们拿到的从"更宽的错误集合"变为"按其所写的交集"。

复现边角时坐实并归档的新 issue

#4181无法解析的 filter JSON 字符串被静默忽略,返回未过滤整页(真实 InMemoryDriver 实测 filter='{status:done'200 total=3,与 baseline 相同)——#3948 家族目前最恶性的成员,方向是全量多返回。同 issue 里附带坐实了 filter 静默压过 where 的别名优先级问题。本 PR 刻意不动它们(源头修法不同),只用测试把边界 pin 住。


Generated by Claude Code

…nstead of being silently dropped (#4164)

`?filter={"status":"open"}&owner_id=usr_1` applied only the explicit filter:
the implicit-filter pass ran under `if (!options.where)`, so once an explicit
filter claimed `where`, leftover REAL field params were neither merged nor
reported — they rode to the engine as stray top-level AST keys no driver
reads, and the response over-returned. The mirror of #4134's silent zero.

They now compose the way the request reads: `{ $and: [explicit, implicit] }`
— the same combinator the engine already uses to fold the `$search` predicate
and an expand's declared filter into an existing `where` (engine.ts), and one
the #3774 filter-logic conformance suite pins across every FilterCondition
backend. Decisions:

- #4134's field gate runs BEFORE the merge, so every merged key is a verified
  field name — the merge can never introduce a zero-matching predicate. That
  ordering is why this fix is safe now and was not safe before #4134.
- Contradictory sides (`?filter={"status":"open"}&status=closed`) apply both
  predicates and intersect to an HONEST zero — no same-field special case.
- A vacuous explicit filter (`?filter={}`, `?filter=`) leaves the implicit
  predicates standing alone instead of wrapping them in a one-armed `$and`.
- A non-object truthy `where` (the unparseable-filter-JSON tolerance keeps the
  raw string, #4181) is deliberately NOT merged into — that path keeps its
  pre-#4164 shape until the tolerance is fixed at the source. Pinned by test.
- `engine.count` reads the same `options.where`, so pagination totals are
  computed over the merged predicate and cannot disagree with `records`.

Verified against a real engine (mini-driver taught to honor `$and` — a matcher
that skips it would green-light a merge that does nothing) and live on the
showcase app: explicit 4 rows, implicit 2 rows, merged intersection 1 row,
paged `total` computed over the intersection; `?filter=…&pageSize=5` still
400s (#4134 regression guard).

Filed while reproducing the excluded edge: #4181 — an unparseable `filter`
string is silently ignored and returns the UNFILTERED page (worst member of
the #3948 family), plus the `filter`-beats-`where` alias precedence.

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

vercel Bot commented Jul 30, 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)
objectstack Ignored Ignored Jul 30, 2026 2:38pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 14:47
@os-zhuang
os-zhuang merged commit f5fe061 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/rest-list-unknown-query-params-6syn6f branch July 30, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

REST 列表:显式 filter 在场时,同时传的字段级参数被静默丢弃(#4134 的邻居)

2 participants