Repository navigation
/analytics/query 的请求体形状取决于装没装 service-analytics(降级 shim 收信封、真引擎收裸 AnalyticsQuery),且发错形状静默生成空 SELECT #3878
Description
Activity
这个 shim 的出生证明 —— 它不是产品决策,是为了圆 discovery 的一句谎
翻了一下来历。
101d5c345(2026-05-16)把 fallback 加进ObjectQLPlugin时,注释自己写明了动机:Without this,
HttpDispatcher'shandleAnalyticscannot resolve a service and/api/v1/analytics/*returns ROUTE_NOT_FOUND, even though discovery advertises the route (objectql'sgetDiscoveryhardcodesanalytics: enabled:true).当时真正的缺陷是 discovery 硬编码
enabled: true。正确的修法是让 discovery 说实话;实际做的是反过来 —— 造一个实现,让那句谎话变成真的。11 天后dacfe0fd2又补上'query' in body && 'cube' in body那个两可三元,因为初版query: (body) => protocolShim.analyticsQuery(body)把裸 body 直接喂给解构{query, cube}的函数会当场炸。所以本 issue 说的"两种方言",是补丁的补丁。
留着它的两条理由今天都已经失效
- "discovery 广告了这条路由" —— ADR-0076 D12 已经修好了(
__serviceInfo→status: 'degraded')。谎话没了,圆谎的东西也就没有存在理由了。 - "minimal 部署不该 404" —— 代码里本来就是 404:槽位为空时
domains/analytics.ts:33直接return { handled: false }。这条路径一直存在且工作正常(plugin.step2.test.ts:50还钉着"没有协议装配时getService('analytics')抛错")。shim 只是抢在它前面把槽位填了。
ADR-0076 D10 说它 "duplicates logic only (a minor maintenance cost)" —— 这句已被实证推翻
同一个安全门,因为有两个实现,修了两遍:
- 曝露 gate 对未知对象放行所依赖的「data path 会 404」并不成立 —— findData 无存在性校验(#3545 同类前提) #3770 / fix(metadata-protocol,rest): gate the data path on object existence — the 404 the exposure gate assumed (#3770) #3866 给降级 shim 的
analyticsQuery加assertObjectRegistered(protocol.ts:3635) - analytics /query 未做 cube 存在性校验,未注册名直达驱动当表名;且错误路径原样回显驱动 SQL(#3770 同类,另一子系统) #3867 / fix(analytics,runtime,types): gate cube auto-inference on object existence; stop the dispatcher boundary returning raw SQL (#3867) #3875 给真引擎的
ensureCube加isRegisteredObject门 —— PR fix(analytics,runtime,types): gate cube auto-inference on object existence; stop the dispatcher boundary returning raw SQL (#3867) #3875 自己写得很清楚:"Note this was not covered by 曝露 gate 对未知对象放行所依赖的「data path 会 404」并不成立 —— findData 无存在性校验(#3545 同类前提) #3770: that gated the protocol'sanalyticsQuery, which is the degraded fallback."
一个洞、两次实现、两个 PR,中间还夹着一次严重性误判(#3867 原文因为发错形状而低估,也就是本 issue 的成因)。这不是 "minor maintenance cost"。
补充一条本 issue 没提、但比空 SELECT 更咬人的分歧
AnalyticsQuery的规范过滤字段是where(spec/data/analytics.zod.ts:154,"canonical Query DSL FilterCondition")。降级 shim 只读query.filters(protocol.ts:3661)—— 而filters根本不是AnalyticsQuery的字段。也就是说:一个完全符合契约的带过滤请求,走到降级实现上,过滤条件被静默丢弃,返回的是全表聚合。
objectui 的 dashboard 正是这么发的(
data-objectstack:payload.where = params.filter)。空 SELECT 至少会炸;这个不炸,只是数字悄悄变大。(还有一条同源的、更严重的,涉及作用域,我单独开了 issue。)
关于建议 1(入口 Zod 校验):现有 schema 不能直接拿来用
AnalyticsQueryRequestSchema确实已经在spec/api/analytics.zod.ts:32了,但它描述的是信封({query, cube, format});连AnalyticsQuerySchema.cube都是optional的,describe 里写着 "optional when provided externally, e.g. in API request wrapper"。规范里的"唯一真源"目前记录的是 shim 的方言,不是真引擎的形状。 所以落校验之前得先定哪个是规范形状,否则把现有 schema 接到入口上会直接把真引擎的正确请求判 400。建议
退役 shim,让槽位空着,路由回到既有的 404。默认部署零影响:
serve.ts:202的ALWAYS_ON_CAPABILITIES已含analytics(注释:"foundational post-ADR-0021"),真引擎照常装;托管环境也在 host 侧强制analytics。shim 还在服务的只剩--preset minimal、程序化createStandaloneStack/createObjectQLKernel嵌入、以及不强制该 capability 的宿主 —— 这些恰恰是应该 404 的场景:给出的信号从"数字悄悄错了"变成"你没装分析包"。本 issue 的建议 1 照做不误,两件事独立:真引擎同样需要在入口拒绝畸形请求,而不是一路走到驱动炸 SQL 语法。
一并要处理的:
objectql/src/plugin.step2.test.ts:39/41/65钉住了 shim 的存在与 degraded 自述;http-dispatcher.test.ts有一条"fallback 仍然广告 analytics 路由"的断言;cloud 的service-ai-studio只判getService('analytics')真假,会被 shim 骗过去;objectui 侧需要一条"未安装分析能力"的可读提示,而不是裸 404。以上代码位置核对于
origin/main@ 93f267f。- "discovery 广告了这条路由" —— ADR-0076 D12 已经修好了(
- added a commit that references this issue
on Sep 29, 2026
按 Prime Directive #10 记录。修 #3867(PR #3875)时踩到的,是我自己被骗过去的那个坑 —— 它让我在 #3867 原文里得出了「没读到数据」的错误结论,所以值得单独收口。
同一个端点,两种请求体
POST /api/v1/analytics/query的处理方由analytics服务槽决定,而这个槽有两个实现:① 降级 shim(
packages/metadata-protocol/src/plugin.ts)—— 收{cube, query}信封,并且明确做了两可兼容:② 真引擎(
AnalyticsServicePlugin,ctx.replaceService替换掉 ①)——AnalyticsService.query(query: AnalyticsQuery, context?),收裸AnalyticsQuery,即{cube, measures, dimensions, filters, …}全在顶层。没有信封兼容。所以:
{"cube":"x","measures":["count"]}{"cube":"x","query":{"measures":["count"]}}同一个 URL、同一份 OpenAPI/discovery 描述,请求体契约却取决于部署里装了哪个插件。按 Prime Directive #12,这是「一个契约,N 种方言」;而且 ① 那个
'query' in body && 'cube' in body的两可兼容本身就是 #12 说的「不要在消费端加宽容 fallback」的样本。发错形状不报错,静默生成空 SELECT
这才是真正咬人的地方。真引擎收到带信封的 body 时,顶层没有
measures/dimensions,于是ensureCube推断出一个没有任何列的 cube,SQL 编译成:注意
SELECT和FROM之间是空的。没有任何一层说「你的请求体形状不对」,它一路走到驱动才因为 SQL 语法炸掉。我在 #3867 的第一次实证就是这么发的 payload,拿到语法错误,于是写下「只报了语法错误,没有读到数据,不宜当成数据泄露断言」。用正确形状重测才发现能读到真实行 —— 结论差了一个量级。一个静默接受畸形输入的端点会把调查者引向错误结论,这本身就是修它的理由。
(PR #3875 之后这条响应变成
Internal server error,SQL 不再回显;但「不报形状错误」这一点没变,只是更难诊断了 —— 这也是为什么应该在入口把它挡掉,而不是靠错误信息去猜。)建议(未决策,留给维护者)
AnalyticsQuery为唯一契约,不符合就 400 并指出缺什么。AnalyticsQuery在@objectstack/spec/contracts里已有类型;按 Prime Directive Add metamodel interfaces for ObjectQL/ObjectUI contract #1 应该有对应的 Zod schema 作为唯一真源。这一条独立于下面两条,单独做就能消除「静默空 SELECT」。AnalyticsQuery为规范形状,信封走readAliasedConfig一类的声明式 shim(warn 一次、可 lint、可按计划移除),而不是留一个裸'query' in body三元。getMeta/generateSql两侧签名是否也有同类分歧 —— shim 的getMeta忽略cubeName参数、generateSql直接返回{sql:null},与真引擎的行为差别是否已在 discovery 的degraded自述里如实反映。倾向先做 1(小、独立、立刻消除静默失败),2 排在其后。
关联:#3867、PR #3875、#3770、ADR-0021、ADR-0076 D10/D12。