fix(data): searchFields / groupBy / aggregations 指向不存在的字段时被拒绝,而不是静默降级 (#4254) - #4315
Merged
Merged
Conversation
…ld are rejected, not silently degraded (#4254) #4226 closed sort / select / expand; the same machine kept leaking on the remaining three field-naming read axes, and each failure corrupted something the closed axes never touched: search=alpha&searchFields=no_such -> 200 MORE rows than the narrowing allowed groupBy=[no_such] -> 200 [{no_such: null, n: <true count>}] sum(no_such) -> 200 0 - indistinguishable from a real zero Each is now refused at the shared normalizer (findData), so the list route, POST /data/:object/query, the export route and the runtime dispatcher give one answer instead of four. - searchFields -> 400 INVALID_FIELD. The select failure with the sign flipped outward: dropped unknown names emptied the override, which fell back to the FULL searchable set - a narrowing parameter that widened, changing which ROWS came back. Three messages (typo / real-but-unsearchable / stale searchableFields declaration), because the fixes differ. The allowed set is resolved by the same spec/data function the engine's search expansion consumes (resolveSearchFieldResolution, moved from objectql), so gate and engine cannot drift. - groupBy -> 400 INVALID_FIELD. The in-memory fallback projected an unknown column as null for every row: N groups collapsed into one null-keyed bucket carrying the true row count. - aggregations -> 400 INVALID_FIELD. sum(<typo>) folded undefined to 0; avg/min/max answered null. count with no field (or '*') stays legal. - Unreadable SHAPES on the aggregation axes -> 400 INVALID_QUERY - the catalog code that had no emitter, like INVALID_SORT before #4226. Tiering mirrors #4226 (no registry / no field map / legacy array map -> name gates skip; shape gates still apply). Engine tolerance for internal callers is untouched. @objectstack/rest stops logging INVALID_FILTER / INVALID_SORT / INVALID_QUERY rejections as unhandled errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-field-degradation-4240d5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…-field-degradation-4240d5
…-field-degradation-4240d5 # Conflicts: # packages/metadata-protocol/src/protocol.ts
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.
Closes #4254
TL;DR
#4226(PR #4240)把
sort/select/expand收口后,同一台机器在剩下三条点名字段的轴上继续漏气。三条现在都在共享的 normalizer(findData)里被拒绝,于是GET /data/:object、POST /data/:object/query、export 路由和 runtime dispatcher 给出同一个答案:三条轴分别怎么改的
searchFields→400 INVALID_FIELD,三段式消息issue 点名要与 #4226 给
expand的三段式同构,落地为三条消息,因为修法不同:Did you mean建议;点号路径(parent_id.title)单独提示「search 只扫本对象自己的列」,因为引擎按精确名求交集,头段校验会把回退兜回来。searchableFields的对象指向声明本身;auto-default 的对象说明该字段被排除的原因(类型 / 系统列 / hidden)并给出「声明searchableFields」的出路。searchableFields里声明了、字段却不存在 —— 陈旧声明,bug 在对象上。单独一条消息是因为 objectui 的列表搜索把schema.searchableFields原样回声成$searchFields(ListView.tsx),把它报成「调用方拼错」会让人去修根本没写错的请求。仍然是 400:全部请求名都陈旧时,引擎的回退会扫默认全集 —— 正是本轴要堵的「要收窄、结果放宽」。两支(全未知 / 部分未知)都拒,与 issue 的裁决一致。allowed 集由
resolveSearchFieldResolution解析 —— 该函数从 objectql 移入@objectstack/spec/data(search-fields.ts),引擎的 search 展开和这个 gate 消费同一份实现,gate 不可能放行一个引擎会丢弃的名字,也不可能拒掉一个引擎会扫的(#4240 用REFERENCE_VALUE_TYPES堵 expand 漂移的同一手法)。引擎侧resolveSearchFields的容忍未动(内部调用方不经过 ingress)。覆盖引擎实际读取的每种拼写:
searchFields/$searchFields(逗号串与数组)以及search: { query, fields }对象形态(数组与逗号串——第二种是复查引擎时发现的:engine.find经requestedFields也消费字符串形态,只按Array.isArray镜像会留下缺口)。报错引用调用方真正写下的参数名。groupBy→400 INVALID_FIELDin-memory 回退路径把未知列对每行投影成
undefined ?? null,所有行进同一个桶:[{no_such: null, n: 3}]——n是真实行数,结构完全合法,图表照画一根柱子。SQL 原生路径则把GROUP BY no_such交给数据库(SqlDriver是否吞错未定)——issue §4 点名的「两条路径可能给出相反答案」,在共享 ingress 收口后两条路径先给出同一个 400。字符串与结构化{field, dateGranularity}两种形态都校验;按精确名判定(分组语义就是本对象的列)。aggregations→400 INVALID_FIELDsum(<拼错>)把一列undefined折成0,和真实的「本季度 0」同形;avg/min/max得null同理。aggregations[].field逐项校验;count无field(或'*'哨兵)是唯一合法的无字段形态,放行。顺带:无法读取的形态 →
400 INVALID_QUERY(catalog 里首个 emitter)groupBy: "status"(裸字符串)、[42]、{dateGranularity:'month'}(没有 field)、枚举外的 function / dateGranularity、缺alias—— 每一种此前要么被Array.isArray路由守卫忽略(行未分组原样返回),要么算出静默占位值(null结果、键名"undefined"的列、未知粒度下一行一桶)。INVALID_QUERY("Malformed query syntax")自写进标准 catalog 起没有任何 emitter —— 与 #4240 启用INVALID_SORT同一姿势。形态检查不依赖 registry(分层里 legacy/registry-less 宿主也拒形态,只跳过字段名检查),与 #4196 投影形态检查同序。分层与边界
sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 完全一致:registry + 字段表在 → 权威;无 registry / 无字段表 / legacy 数组字段表 → 字段名 gate 跳过(形态 gate 仍生效)。四条旧轴 + 三条新轴共用同一次resolveQueryFields解析。404 OBJECT_NOT_FOUND,任何新 gate 不得把它变成 400(有 pin)。engine.aggregate直调,包括 analytics service 与 action-execution)不经过本 ingress,不受影响。@objectstack/rest:isExpectedQueryRejection补上INVALID_FILTER/INVALID_SORT/INVALID_QUERY—— 前两个自 REST 列表:无法解析的filterJSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181 / fix(data):sort/select/expand指向不存在的字段时被拒绝,而不是静默丢弃 (#4226) #4240 起每次拒绝都被同时记为 "[REST] Unhandled error",本来就该在这个名单里。测试
packages/objectql/src/query-expression-conformance.test.ts新增#4254describe 块(37 个用例,全文件 77 个全绿),照搬 #4240 的纪律:$or/$contains求值(否则任何 search 都全命中,「searchFields 真的收窄了行集」的断言 vacuously 绿),聚合用例删掉 driver 的aggregatestub 走引擎真正的 in-memory 回退(issue 实测的那条路径)。search=a命中 title 与 notes 各一行 →searchFields=title收到一行 →searchFields=no_such400(此前是两行);groupBy=[status]真分两组 →[no_such]400(此前一桶);sum(estimate)真合计 →sum(no_such)400(此前 0)。count(*)两种拼写放行、七轴合成请求、404 优先、legacy 数组字段表只降级字段名检查。pnpm test:132/132 任务绿(含 dogfood HTTP 级 430 例)。check:generated(api-surface 已重生成)、check:liveness、check:exported-any均绿。文档
data-api.mdx在 #4240 的「Nor is a sort…」旁新增三轴一节(请求 → 结果对照表 + 各轴为何要紧);参数表补search/searchFields行;error-catalog.mdx的INVALID_QUERY从占位描述改为写明其 emitter,INVALID_FIELD列全七条轴;queries.mdx/query-syntax.mdx在聚合与搜索小节各加 ingress 行为 callout。search 会格 ledger(search-conformance.ledger.ts)的 enforcement 指针随实现迁移更新。对调用方的影响
searchFields/groupBy/aggregations[].field中点名不存在字段的请求现在显式失败,而不是收到一个被放宽 / 未分组 / 合计为 0 的 200。searchableFields声明里若有陈旧条目(字段后来改名),objectui 回声该声明的列表搜索会开始收到 400(消息直指对象与修法)。配套的 authoring-time lint(searchableFields ⊆ fields)已作为后续任务另开。关联
sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 / fix(data):sort/select/expand指向不存在的字段时被拒绝,而不是静默丢弃 (#4226) #4240(sort / select / expand,本 issue 明确划出的剩余部分)?pageSize=5返回 200 + 空列表 #4134 / REST 列表:显式filter在场时,同时传的字段级参数被静默丢弃(#4134 的邻居) #4164 / REST 列表:无法解析的filterJSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181 / A filter array that isn't a valid AST reaches the driver as an opaquewhere— reject it at the protocol instead #4121(filter 轴四案)· A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948(原则出处)searchFieldsoverride 语义)· refactor(spec,plugin-security)!: QueryAST 不再声明没有执行器运行的成员 — joins/windowFunctions 墓碑化,liveness ledger 接管请求面 (#4286) #4294(QueryAST 墓碑与 liveness ledger,已合并进本分支)Generated with Claude Code