Skip to content

一个被静默丢弃的排序键对外部调用方仍然是静默的:direction 该 400 还是继续被丢掉(#4674 第 4 项) #4721

Description

@os-zhuang

#4674 的第 4 项拆出来——那一项写的是「值得单独决定」,这就是那个单独决定。#4720 修了两个内部站点,没有动这一类。

类修复保护的不是同一批人

#4674 提了两条路:

  1. 让 QueryAST 的排序 schema 在这条轴上拒绝未知键(SortNodeSchema.strict()),direction 变成 400;
  2. 或者让 normalizer 专门识别 direction,以一条点名 order 的消息拒绝它。

两条都住在入站 findData normalizer 上。而 #4674 自己的「为什么没被抓住」第 2 条已经确认:那条路径对这次的两个站点根本不运行——它们是协议直接调 this.engine.find,在自己的守卫内侧而非后面。

所以这个类修复不会抓住催生它的那个 bug。这不构成放弃它的理由,但它把要解决的问题换了一个:

调用方 被执行的渠道 现状
内部(协议、插件、包内代码) tscEngineQueryOptions.orderBySortNodeSchema[] #4720 已恢复(两处 as any 删除)
外部(REST / RPC 上的 orderBy) normalizer 仍然静默丢弃 direction

对内部调用方,真正的教训是「别擦除类型」,而不是「加个运行时闸」。本单只关心第二行。

决定点

SortNodeSchema 不是 .strict(),所以任何未知键都被丢掉,而不是被标记。给外部调用方发一个 { field: 'updated_at', direction: 'desc' },他们得到的是一个看起来成功的升序响应——带 limit 时还意味着返回了另一批行。没有任何信号。

两个选项的代价不同:

(a) SortNodeSchema.strict() —— 覆盖面完整,但这是对已发布线协议的破坏性变更:今天任何在排序节点里多带一个键的客户端会开始收到 400。而且它把所有未知键一视同仁,而 direction 是唯一一个有已知正确译法的。

(b) 专门识别 direction,以点名 order 的消息拒绝 —— 窄,不破坏无关的多余键,并且送达一条处方而不只是一个拒绝。这正是 retiredKey() 的做法(通过一次 parse 把修复建议交到调用方手里),区别在于 direction 从来不是我们这条轴上的键,所以「墓碑」不是准确的词——它是一条外来词汇提示

倾向 (b),但 (a) 的「未知键一律拒绝」在别处是这个仓的既有姿态(#4371 让引擎拒绝未声明的选项键——只是在顶层,没有递归进排序节点),所以这里存在一致性论据,值得一并权衡。

为什么值得做

direction 不是一个凭空的拼写错误,它是同一个概念的两套活词汇:

  • SortNodeSchema{ field, order } — QueryAST / EngineQueryOptions(query.zod.ts:12)
  • IReportService.orderBy{ field, direction? } — 一份真正不同的契约(report-service.ts:29)

plugin-auth/objectql-adapter.ts:536 已经显式地在两者间翻译。也就是说这个翻译是已知必需的,只是没有在任何地方被强制;调用方忘了翻译时,结果是一个静默的错误答案而不是一个错误。这就是 ADR-0049 的形状。

顺带一提:一个更便宜的内部护栏

如果要收紧内部这一侧,能想到的最小手段是一条 lint 规则,禁止对引擎查询选项做 as any / : any —— 是类型擦除而不是缺少运行时闸让 #4674 溜过去的。这是否值得单开一单,取决于树里这种擦除还有多少。

关联:#4674#4720#4363#4371、ADR-0049

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions