Skip to content

bug(objectql): hook 条件对着「只含本次更新字段」的残缺 record 求值,取不到的字段被吞成 false —— 审计 hook 静默不触发 #4770

Description

@os-zhuang

TL;DR

声明式 hook 的 condition 是拿 update payload(只有本次改动的字段)求值的,不是完整记录。条件里引用了一个本次没改的字段 → No such key → 被 catch 成 falsehook 该触发时不触发,只留一条默认日志级别下会被当噪音划过去的 warn

50185a8"fail closed on unevaluable validation predicates, and make the merged record total", #4649)刚给 validation 谓词修了同一类问题。hook 条件这条路径没跟上,而且它是静默失败。

现场

pnpm dev(showcase)每次启动刷 10 条:

WARN [hook] condition evaluation failed; treating as false
  {"hook":"showcase_audit_task_completion","condition":"record.done == true",
   "error":"No such key: done\n\n>    1 | record.done == true\n                ^"}

showcase_task 确实有 done 字段 —— 不是数据模型问题:

// examples/app-showcase/src/data/objects/task.object.ts
done  boolean

根因

packages/objectql/src/hook-wrappers.ts:327

function pickRecordPayload(ctx: HookContext): any {
  const input: any = ctx.input ?? {};
  if (input && typeof input === 'object' && input.data && typeof input.data === 'object') {
    return input.data;          // ← 只有本次 update 的字段
  }
  if (ctx.previous && typeof ctx.previous === 'object') {
    return ctx.previous;        // ← 够不到:上面已经 return 了
  }
  return input;
}

input.data 的优先级高于 ctx.previous,而且两者从不合并。于是一个 afterUpdate hook 的条件只能引用"本次恰好被改到"的字段;引用任何其它字段都会炸。

炸了之后(hook-wrappers.ts:86):

const r = ExpressionEngine.evaluate<boolean>(expr, { record: record ?? {} });
if (!r.ok) {
  logger.warn('[hook] condition evaluation failed; treating as false', {...});
  return false;      // ← 吞掉
}

为什么这比噪音危险

showcase_audit_task_completion 是个审计 hook。它的语义是"任务被标记完成时留痕"。现在的行为是:只要那次 update 的 payload 里没带 done,审计就静默不发生 —— 而这恰恰是最常见的情况(改个 status、改个 assignee)。审计记录缺失,且没有任何东西会告诉你缺了。

"treating as false" 对不同 hook 意味着相反的风险方向:对一个 guard 型 hook 是放行,对一个审计型 hook 是漏记。无论哪种,"表达式求不出值"和"表达式求出 false"被压成了同一个结果

修法

  1. 让 record 完整:与 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 对齐 —— 把 ctx.previousinput.data 合并后再求值(stored ⊕ payload),这样条件引用任何已存在字段都成立。
  2. 区分"求不出值"和"值为 false":至少不要静默。Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 对 validation 选的是 fail closed;hook 的正确方向取决于 hook 类别,需要定夺 —— 但当前"一律吞成 false + warn"肯定不是答案。

建议 1 先做,它把绝大多数 case 直接消灭掉;2 作为兜底语义单独讨论。

复现

rm -rf examples/app-showcase/.objectstack && pnpm dev

启动日志里搜 condition evaluation failed

Metadata

Metadata

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions