fix(analytics,rest): 五族 dataset 拒收自带 DATASET_INVALID/400 信封,路由的 message 正则名单收到只剩一条 - #5723
Merged
Merged
Conversation
…D/400 and shrink the route's message list to one entry (#5367) `POST /analytics/dataset/query` classified six error families by matching hardcoded substrings of their message text, because all six producers were bare `throw new Error(...)`. That made their HTTP status a property of their wording: a rephrasing with no logic change moved a refusal from 400 to 500 with nothing going red. #5352/#5366 delivered declared/loud/tested for that accommodation; Prime Directive #12 also requires removable on a schedule. Five producers now declare the verdict themselves through a new `dataset-refusal.ts` (`datasetInvalidError`, `DATASET_INVALID`/400, the same shape as `invalidFilterError`): - dataset-compiler: unsupported aggregate; undeclared relationship path - dataset-executor: unselected order key; totals grouping outside the selection - native-sql-strategy: join outside the declared allowlist Their five entries are deleted from the route's regex. `read-scope-sql` keeps its entry on purpose: its ten fail-closed refusals lower an admin-authored RLS policy, not caller input, so the right code/status is a separate judgement. `DATASET_INVALID` is registered under `@objectstack/service-analytics` in ERROR_CODE_LEDGER for provenance (the union is unchanged), and the constructor types it as `RegisteredErrorCode` so an unregistered code fails `tsc`.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 02:20
This was referenced Aug 6, 2026
Merged
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.
Refs #5367
前提重验(基于 origin/main,行号已漂)
立单时的六条生产方全部仍是裸
throw new Error(...),前提成立。行号如 issue 预告的那样漂了:dataset-compiler.ts:137dataset-compiler.ts:137(未漂)dataset-compiler.ts:305dataset-compiler.ts:305(未漂)dataset-executor.ts:436/:596:438/:598(+2,#5691)native-sql-strategy.ts:222:223(+1)read-scope-sql.ts十处rest-server.ts的正则名单原样在:6811,#5667 的looksLikeInternalErrorLeak收窄在其后的 ③ 分支。做了什么
新增
packages/services/service-analytics/src/dataset-refusal.ts,导出datasetInvalidError(message)—— 与同包filter-normalizer.ts的invalidFilterError(INVALID_FILTER/400)、analytics-service.ts的assertMeasureFields/assertDimensionFields(INVALID_FIELD/400)同形,DATASET_INVALID/ 400。五处改为经它抛出:dataset-compiler.ts—— v1 无法下降的 aggregate;dimension/measure 走了include未声明的 relationship 路径dataset-executor.ts——order键不在选中的 dimension/measure 里;totals分组不是选中 dimension 的子集native-sql-strategy.ts—— join 不在 dataset 声明的 allowlist 里rest-server.ts的正则名单删掉对应五条,只剩/read-scope-sql/,并在原地写清「这是退休时间表的最后一项」以及为什么它单独留着。落点选择的代码证据:
invalidFilterError是filter-normalizer.ts里的非导出function,五处又跨三个文件(含strategies/子目录),所以复用不了、必须新建一个共享模块;放在同包src/下,只被这三个文件 import,不进index.ts(不扩公共面)。DATASET_INVALID不是新造的 code(#5352 起路由就在答它),但按 ADR-0112 D3 的 provenance 规则,在ERROR_CODE_LEDGER里给@objectstack/service-analytics也补了一行(union 不变,REGISTERED_ERROR_CODES/ErrorCode/ 生成产物全部不变 ——check:generated10 项全绿可证)。构造器里把 code 标注成RegisteredErrorCode,所以「没登记的 code」是编译错误,而不是某条路由运行时才拒的 body。native-sql-strategy.ts:223动手前的核实(PM 硬性要求)结论:属调用方错误,可以信封化。
joins的每一个 alias 都由qualifyAndRegisterJoin注册,而在 dataset 路由上能注册出 allowlist 之外的 alias 的输入只有请求自己的:lookupMember对「点分但没有任何 cube dimension 声明」的成员返回合成{ sql: member },于是selection.dimensions、selection.timeDimensions以及runtimeFilter里写成account.name的成员都会落到这里。dataset 自己的 dimension/measure 到不了:compileDataset的assertDeclared在编译期就拒了未声明的 relationship 路径(本 PR 里同样是 400DATASET_INVALID,两者判一致而非分叉),而resolveMeasureSql根本没有合成兜底。唯一的非调用方触发形态是给手写 cube 用的 legacy
config.getAllowedRelationships钩子(那时不匹配属宿主配置错误)—— 但它在/analytics/dataset/query上不可达:queryDataset先registerDataset,getAllowedRelationships因此总是从datasetRegistry作答,不会 fall through 到钩子。这段推理写进了代码注释。刻意保持裸
Error的三类(写在dataset-refusal.ts头注释,并有 pin 测试)read-scope-sql.ts十处 —— 输入是安全服务从管理员策略编译出的 RLSFilterCondition和编译器生成的 join alias,都不是调用方输入;dataset-compiler.ts:135「non-derived measure 没有 aggregate」,spec refinement 已保证,到达即我们的 bug;objectql-strategy.ts的回显 SQL 渲染器已经明确表过这个态(/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333:"Deliberately NOTinvalidFilterError's 400 envelope")。所以这个模块刻意不是「本包唯一的拒收出口」(那是
invalidFilterError对filter-normalizer.ts才能作出的声明),它是「本包拒收调用方的出口」。#5366 既有回归用例的逐条改判
三处引用了被删条目,按「这条 fixture 钉的正是我删掉的那根肢体」处理 —— 不是批量重拼措辞:
analytics-filter-refusal-envelope.test.ts的FALLBACK六例 —— 原断言是「裸Error+ 这段措辞 → 400」,改判后再断言它就是在断言缺陷本身。所以整块换掉:read-scope-sql一例保留(它仍靠名单);另外五族每族钉两个方向 —— 带信封 → 400DATASET_INVALID(经分支 ①),同一条措辞裸抛 → 500(这一半才是「正则条目真的删掉了」的证据)。analytics-dataset-dimension-gate.test.ts(fix(service-analytics,rest): analytics dimension 的源字段闸门 —— 不存在的 dimension 答 400 INVALID_FIELD,dataset 500 不再回显 SQL (#5520) #5667)的 ② 半(原本发裸Error靠名单)→ 改成发带信封的错误走分支 ①,并新增 ③ 半覆盖仅存的read-scope-sql条目。fix(service-analytics,rest): analytics dimension 的源字段闸门 —— 不存在的 dimension 答 400 INVALID_FIELD,dataset 500 不再回显 SQL (#5520) #5667 的 8 例其余部分零改动、全绿。analytics-routes.test.ts的「maps a dataset D-C compile error to 400」 → 同一条 message 加上信封;外部断言(400 /DATASET_INVALID)一字未改。三处的外部可见行为都没变,变的是内部机制;两处需要改造 fixture(裸 → 带信封),一处只需加信封。已按规则消费半径扫过全仓(
grep五条措辞 +DATASET_INVALID),packages/rest之外没有别的 fixture 依赖这串正则。新增覆盖
packages/services/service-analytics/src/__tests__/dataset-refusal-envelope.test.ts(15 例)—— 照 analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352 那份的两段式:先钉拒收集合没动(每族真实生产方 + 一条「相邻的合法输入仍然编译」的反面控制,改动前后都绿),再钉信封;第三段钉「刻意留裸的」——read-scope-sql两例(断言code/status仍为undefined,并在注释里写明它一旦翻转就必须同 PR 删掉路由最后一条)+ 内部不变量一例。packages/rest/src/analytics-dataset-refusal-envelope.test.ts(8 例)—— 五族端到端穿过真实AnalyticsService(aggregate 路径四族 + raw-SQL 路径一族),精确断言 400 /DATASET_INVALID/ message 指名内容 / body 是 4xx 形状(message而非error);两条正面控制(两条路径各一条,其中 raw-SQL 那条用include: ['account']让同一个点分 selection 合法,证明 ⑤ 是关于 allowlist 的判决而不是「点分成员一律被拒」)。反向验证(方向先预测,后实测)
方向 A —— 把五处生产方改回裸
throw new Error(...),重建 service-analytics:code/status为undefined),第一段 + meta + 第三段保持绿;端到端 5 例转红成 500,两条正面控制与 meta 保持绿;另外三个 rest 文件不受影响(它们自造错误)。Tests 5 failed | 10 passed (15);rest 四文件Tests 5 failed | 49 passed (54),断言原文AssertionError: expected 500 to be 400。与预测一致,纯红方向(不是 fix(lint): 收敛 validate-expressions / validate-security-posture 的 spec 不声明键??别名读法 (#5017) #5046 的「诊断变多」也不是 validateOrgAxisRedLines 仍留着三条 spec 合法 stack 到不了的别名分支(objects[].rowLevelSecurity / .rls / permissionSets)—— #4984 修了 sharing 那一半 #5009 的倒置:规范信封分支排在 catch 的第一位,而原来会救这五族的兜底已经删掉,所以「生产方停止声明」只有一个外部后果)。方向 B —— 把五条正则加回去(生产方仍带信封):
analytics-filter-refusal-envelope.test.ts的「裸 → 500」5 例转红;「带信封 → 400」5 例保持绿(① 在 ② 之前);端到端文件完全保持绿。Tests 5 failed | 49 passed (54),断言原文AssertionError: expected 400 to be 500,端到端文件全绿。与预测一致。这个不对称值得写下来:端到端文件在设计上测不出被加回的正则条目(信封先答),名单删除只由「裸 → 500」那一半钉住 —— 所以那一半不是冗余断言,它是这个 PR 唯一的名单守卫。两个方向跑完后已
git checkout复原并重新验证全绿。验证
pnpm --filter @objectstack/service-analytics test→Test Files 54 passed (54)/Tests 954 passed (954)pnpm --filter @objectstack/rest test→Test Files 52 passed (52)/Tests 764 passed (764)packages/spec的error-code-ledger.test.ts+errors.test.ts+error-catalog-docs.test.ts→ 38 passedpnpm --filter @objectstack/spec check:generated→ 10/10 up to datepnpm check:error-code-casing→ self-test 17 例 + 2982 文件全绿;pnpm check:route-envelope→ 8 模块 7 conformant / 0 ratcheted / 1 exemptnode scripts/check-nul-bytes.mjs→ OK(5618 文件);另按纪律自扫grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'覆盖本 PR 全部改动文件 → cleanpnpm check:type-check-coverage→ OK(63/78 + 15 DEBT);两个 DEBT 包直接量:@objectstack/resttsc --noEmit2 errors(与 ledger 记的 2 一致)、@objectstack/service-analytics7 errors,没有一条落在本 PR 新增/修改的文件上origin/main(eb26126d5)后重跑上述全部范围外发现(已按 #4949 先搜重)
dataset-compiler跨库 JOIN /includerelationship 不存在 / 超跳数上限;dataset-executor的dateRange解析失败与三处compareTo;native-sql-strategy的「cube 没有这个 measure」与「measure type 不认识」)。与本单同族,但把它们一起做会把 500 → 400 的行为变更混进一个声称「外部行为不变」的 PR,故单独立项。queryDataset里还有第二个 message 嗅探器isMissingSourceError,命中即静默返回空结果 ——dataset-compiler的一条拒收措辞已经命中它,只因抛点在 try 之外才没出事 #5717[finding]——analytics-service.ts里还有第二个 message 嗅探器isMissingSourceError,命中即静默返回空结果;dataset-compiler.ts:260的措辞(含relationship与does not exist)已经命中它,今天无害的唯一原因是抛点在 try 之外。dormant,给finding标签、不排队。gen:schemarmSync 整个json-schema/会顺手抹掉gen:openapi的产物,rest 的 openapi 路由测试随后 503 假红——check:generated原地跑 build-schemas 也触发 #5371 ——pnpm --filter @objectstack/spec check:generated跑绿的同时会删掉packages/spec/json-schema/openapi.json(gen:schema整目录rmSync,而gen:openapi无 gate 不会补回),随后@objectstack/rest有 8 例 openapi 测试假红成 503。本次复现了两遍;重跑gen:openapi即恢复,与本 PR 无关。Generated by Claude Code