发现于 #782 / PR #785 的实施过程中(文件面只有 src/views/task.view.ts 的注释),按「越界即停」单独记录。基线 origin/main = fb48ab5,平台锁 17.0.0-rc.2。
现象
UnknownFilterTokenError 存在的意义写在它自己的错误文案里:把无法解析的占位符变成 400 + 可修的提示,而不是「原样下发给数据引擎、按字面串比较、匹配不到任何东西(或者更糟,按字典序匹配到全部)」。
但它的覆盖面止于占位符文法本身。@objectstack/spec/data:
CONTEXT_TOKEN_WRAPPED_RE = DATE_MACRO_WRAPPED_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/
classifyFilterToken(value):
const m = value.match(CONTEXT_TOKEN_WRAPPED_RE);
if (!m) return null; // ← 这里
...
return { kind: 'unknown', token, suggestion };
内层字符类只有 [a-zA-Z0-9_]。任何含非 word 字符的类占位符串——{TODAY()}、{current-user-id}、{30 days ago}、{user.id}——都在第一行 return null,于是 hasFilterToken() 判为「这棵 filter 里没有令牌」,resolveFilterTokens() 原样返回,字符串照原样到达 driver。
诊断只对拼错但仍是 word 字符的拼法生效({TODAY}、{this_quarter_start}、{14_day_agos})。拼得「更不像」的反而更安静。
实测(pinned 17.0.0-rc.2,InMemoryDriver + ObjectQL.find,一次性探针,未入库)
四行 crm_task,due_date 分别在 30 天前 / 1 天前 / 今天 / 7 天后:
classifyFilterToken:
{today} => {"kind":"date-macro","token":"today"}
{TODAY} => {"kind":"unknown","token":"TODAY"}
{TODAY()} => null
ObjectQL 读路径:
due_date < '{today}' => 2 行(恰好两行逾期,令牌解析生效)
due_date < '{TODAY}' => THROWS UnknownFilterTokenError / FILTER_TOKEN_UNKNOWN
due_date < '{TODAY()}' => 4 行 —— 全部,含下周才到期的那行
due_date = '{TODAY()}' => 0 行
第三行就是 #744 记录的那个字典序倒置:'2026-…' 排在 '{'(0x7B)之前,所以 < 比较对每一行都为真。这正是 UnknownFilterTokenError 本来要消灭的失败模式,而它在这条拼法上没有触发。 而且两种错法的观感相反:= 得到空列表(像「今天没有数据」),< 得到全表(像「过滤没写对但至少有数据」),都不像报错。
为什么打 finding 而不是 bug
用户今天碰不到,本仓库也碰不到。 hotcrm 自己的作者期守卫用的正则更宽:test/metadata-references.test.ts 的 badTokensIn() 扫 /\{([^{}]+)\}/g,{TODAY()} 会被它逮住并在测试里报 unresolvable token。所以任何视图 / 页面组件的 filter 都进不了主干。#782 里的 {TODAY()} 从头到尾只活在注释里(PR #785 已订正)。
记录它的理由是:仓库守卫比平台诊断严,这个次序反了。守卫哪天被收窄、或者某条 filter 走了守卫没遍历到的路径(page 组件的 filter 直到 #784 才纳入遍历,就是一次实例),漏下来的东西平台不会拦。
可能的修法(供上游判断,本仓库不做 workaround)
平台侧二选一,都不在 hotcrm 内解决:
- 放宽
classifyFilterToken 的识别文法(识别 /^\$?\{([^{}]*)\}$/,再按 word 字符判 known / unknown),让「长得像占位符」的一律走诊断,{TODAY()} 得到 Did you mean "{today}"?;
- 或者维持文法不变,但在
resolveFilterTokens() 的字符串分支上补一条「看起来像占位符却没被识别」的检查。
倾向 1:判定「是不是占位符」与判定「是不是合法占位符」应当是两步,现在被同一个正则合成了一步,导致「不合法」被误判成「不是」。
Refs #782、PR #785、#744 / PR #773
发现于 #782 / PR #785 的实施过程中(文件面只有
src/views/task.view.ts的注释),按「越界即停」单独记录。基线origin/main= fb48ab5,平台锁 17.0.0-rc.2。现象
UnknownFilterTokenError存在的意义写在它自己的错误文案里:把无法解析的占位符变成 400 + 可修的提示,而不是「原样下发给数据引擎、按字面串比较、匹配不到任何东西(或者更糟,按字典序匹配到全部)」。但它的覆盖面止于占位符文法本身。
@objectstack/spec/data:内层字符类只有
[a-zA-Z0-9_]。任何含非 word 字符的类占位符串——{TODAY()}、{current-user-id}、{30 days ago}、{user.id}——都在第一行return null,于是hasFilterToken()判为「这棵 filter 里没有令牌」,resolveFilterTokens()原样返回,字符串照原样到达 driver。诊断只对拼错但仍是 word 字符的拼法生效(
{TODAY}、{this_quarter_start}、{14_day_agos})。拼得「更不像」的反而更安静。实测(pinned 17.0.0-rc.2,
InMemoryDriver+ObjectQL.find,一次性探针,未入库)四行
crm_task,due_date分别在 30 天前 / 1 天前 / 今天 / 7 天后:第三行就是 #744 记录的那个字典序倒置:
'2026-…'排在'{'(0x7B)之前,所以<比较对每一行都为真。这正是UnknownFilterTokenError本来要消灭的失败模式,而它在这条拼法上没有触发。 而且两种错法的观感相反:=得到空列表(像「今天没有数据」),<得到全表(像「过滤没写对但至少有数据」),都不像报错。为什么打
finding而不是 bug用户今天碰不到,本仓库也碰不到。 hotcrm 自己的作者期守卫用的正则更宽:
test/metadata-references.test.ts的badTokensIn()扫/\{([^{}]+)\}/g,{TODAY()}会被它逮住并在测试里报unresolvable token。所以任何视图 / 页面组件的 filter 都进不了主干。#782 里的{TODAY()}从头到尾只活在注释里(PR #785 已订正)。记录它的理由是:仓库守卫比平台诊断严,这个次序反了。守卫哪天被收窄、或者某条 filter 走了守卫没遍历到的路径(page 组件的 filter 直到 #784 才纳入遍历,就是一次实例),漏下来的东西平台不会拦。
可能的修法(供上游判断,本仓库不做 workaround)
平台侧二选一,都不在 hotcrm 内解决:
classifyFilterToken的识别文法(识别/^\$?\{([^{}]*)\}$/,再按 word 字符判 known / unknown),让「长得像占位符」的一律走诊断,{TODAY()}得到Did you mean "{today}"?;resolveFilterTokens()的字符串分支上补一条「看起来像占位符却没被识别」的检查。倾向 1:判定「是不是占位符」与判定「是不是合法占位符」应当是两步,现在被同一个正则合成了一步,导致「不合法」被误判成「不是」。
Refs #782、PR #785、#744 / PR #773