Skip to content

fix(filters): evaluate {token} placeholders on the server read path (#3582) - #3809

Merged
os-zhuang merged 2 commits into
mainfrom
claude/template-tokens-filters-notifications-nicmhd
Jul 28, 2026
Merged

fix(filters): evaluate {token} placeholders on the server read path (#3582)#3809
os-zhuang merged 2 commits into
mainfrom
claude/template-tokens-filters-notifications-nicmhd

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3582.

问题

筛选值以 JSON 形式传输,所以按时间/按人筛选的切片只能写占位符,不能写代码:

filter: { close_date: { $gte: '{current_year_start}' }, owner: '{current_user_id}' }

词表早已在 @objectstack/spec 里(date-macros.zod.tscontext-tokens.zod.ts),objectstack build 也会拒绝词表之外的 token(#3574)。缺的是真正代入值的那一半:服务端从来没有做过替换。占位符原样到达 driver,被当成字符串比较,匹配不到任何行。scripts/analytics-reconcile/macros.ts 里写得很直白 —— "this repo has no runtime resolver" —— 所以那个对账脚本只能自己先解析一遍。

这个失败是看不见的:一个空的 metric widget,和一个本来就等于 0 的指标长得一模一样。应用只能绕开它,在模块加载时算好日期 —— 于是"今年"被冻结进了构建产物。

Issue 里报的三条,分别对应到当前代码是这样:

# 现状
日期 token 不生效 ✅ 本 PR 修复(服务端没有 resolver)
{current_user} 静默不匹配 构建期已被 #3574 的 lint 拦下;本 PR 补上运行期同样的大声报错并给出改法
通知模板不能穿透 lookup 穿透本身已有 config.expand opt-in(#3475)+ 构建期告警(#3426);本 PR 修掉残留的字面量 "undefined"

关于 issue 标题里的 "CEL tokens":筛选值不支持 CEL(cel`…` 是 seed loader 的机制,spec 的 date-macros 文档里明确把 CEL 列为 out of scope)。issue 给的例子全部是 date macro,已覆盖。

改动

新增 resolveFilterTokens()(@objectstack/core),接到筛选条件必经的两个服务端接缝:

  • ObjectQL 读路径find / findOne / count / aggregate。REST 查询、相关列表、保存视图的筛选、sharing graph 读取因此全部生效。它跑在中间件链之前,所以只检查作者写的筛选;下游注入的 RLS/sharing 筛选是用具体值拼的,不含占位符。
  • Analytics dataset executor — dataset 自身的 filter、widget 的 runtimeFilter、measure 级筛选、time dimension 的 dateRange。这条路必须单独调用:NativeSQLStrategy 直接编译裸 SQL 并绑定比较值,dashboard widget 根本不经过 engine.find() —— 这正是这类 widget 一直显示 0 的原因。

语义上的几点决定:

  • 日期 token 解析为 ISO 字符串;转成列的落盘形式仍然是 driver 的职责(SqlDriver.temporalFilterValue),存储约定保持单一事实来源。

  • 日历边界跟随 ExecutionContext.timezone;每次 resolve 只取一个瞬间,所以 >= {current_month_start} / < {next_month_start} 这一对不可能跨月错开。

  • 月/季/年运算先归一到周期起点再步进,参数化文法按月长 clamp:从 3 月 31 日算 {1_month_ago} 得到 2 月 28 日,而不是溢出到 3 月 3 日。

  • {current_user_id} / {current_org_id}userId / tenantId。两者都没有时抛错,不解析成 null —— null 比较值在多数 driver 上退化成 IS NULL,会把筛选本想排除的行交回来。

  • 无法识别的占位符抛错,并带上近似拼写的改法({current_user}{current_user_id}),与构建期 lint 保持一致。

    ⚠️ 随之明确的一条约束:整体形如 {...} 的筛选值一律按占位符解释,因此无法用它表示字面量。这是 Dashboard widget filters do not interpolate {current_user_id} — user-scoped widgets silently render 0 #3574 的 lint 文案里已经写下的约定,本 PR 只是让运行期与之对齐。

  • 两个错误都带 status: 400 + code,直接命中 REST 层已有的 4xx 透传分支 —— 报成可修复的客户端错误,而不是 500。

另外:notify 不再把字面量字符串 "undefined" 当收件人发出去。to: ['{record.owner.manager}'] 在标量外键 id 上走 .manager,解析为空,而 String(undefined) 把它变成了一个幽灵收件人 —— emit "成功"、谁也没收到、任何地方都没有报错。现在解析为空的收件人会被丢弃;若一个都不剩,节点失败并指名是哪个模板没解析出来,同时指向 start 节点的 config.expand(#3475,那才是真正能 hydrate 关系的开关)。

文档

修正了几处仍然声称"只在客户端解析"的说法:date-macros.zod.ts / context-tokens.zod.ts 的模块注释、skills/objectstack-query/rules/filters.mdskills/objectstack-ui/SKILL.mdcontent/docs/data-modeling/analytics.mdx,以及 analytics-reconcile/macros.ts 里那句已经过时的 "no runtime resolver"。同时把 *_end日历日(datetime 列应改用半开区间 < {next_*_start})这一点写进了文档 —— 它一直是词表的既定语义,只是从没写明。

验证

  • 新增 43 个测试:packages/core/src/utils/filter-tokens.test.ts(28)、packages/objectql/src/engine-filter-tokens.test.ts(9)、packages/services/service-analytics/src/__tests__/dataset-filter-tokens.test.ts(6),另有 3 个补进 notify-node.test.ts。engine / analytics 的断言都落在驱动真正收到的东西上(driver AST、绑定的 SQL 参数),而不是中间结果。
  • pnpm build 通过;pnpm test 132/132 task 全绿。

一个范围外的发现(会另开 issue)

flow 节点的 config.filter 会先经过 flow 模板引擎,后者在该位置拥有 {…} 的解释权,并把任何非 flow 变量的 token 抹成空。所以 flow 的筛选用不了 date macro,而且是静默的。这是既有行为,不在本 PR 范围内,已在 skill 文档里加了一句说明,并会单独开 issue 跟进。

🤖 Generated with Claude Code

https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1


Generated by Claude Code

…3582)

Filter values travel as JSON, so a time- or user-scoped slice writes a
placeholder instead of code:

    filter: { close_date: { $gte: '{current_year_start}' }, owner: '{current_user_id}' }

The vocabulary has been in `@objectstack/spec` for a while
(`date-macros.zod.ts`, `context-tokens.zod.ts`) and `objectstack build`
rejects tokens outside it (#3574). What was missing is the half that
SUBSTITUTES A VALUE: nothing on the server ever did. The placeholder reached
the driver as the literal string, compared as text, and matched nothing.
`scripts/analytics-reconcile/macros.ts` said it outright — "this repo has no
runtime resolver" — and pre-resolved tokens itself so the harness would not
diverge.

That failure is invisible: an empty widget looks exactly like a metric that is
legitimately zero. Apps worked around it by computing dates at module load,
which freezes "this year" into the built artifact.

`resolveFilterTokens()` in `@objectstack/core` is the evaluator, wired into the
two server-side seams a filter passes through:

  - ObjectQL read path — find / findOne / count / aggregate, so REST queries,
    related lists, saved-view filters and sharing-graph reads all resolve. It
    runs BEFORE the middleware chain, so only author-supplied filters are
    inspected; RLS/sharing filters are injected downstream from concrete values.

  - Analytics dataset executor — the dataset's intrinsic `filter`, a widget's
    `runtimeFilter`, measure-scoped filters, time-dimension `dateRange`s. This
    path needs its own call: NativeSQLStrategy compiles raw SQL and binds
    comparands directly, so a dashboard widget never passes engine.find().

Semantics:

  - Date tokens resolve to ISO strings; turning those into a column's on-disk
    form stays the driver's job (SqlDriver.temporalFilterValue), so there is
    still exactly one source of truth for the storage convention.
  - Calendar boundaries follow ExecutionContext.timezone, and one instant is
    pinned per resolve call so a `>= {current_month_start}` /
    `< {next_month_start}` pair can never straddle a boundary.
  - Month/quarter/year arithmetic normalizes to the period start before
    stepping, and the parameterised grammar clamps day-of-month, so
    `{1_month_ago}` from Mar 31 is Feb 28 rather than rolling to Mar 3.
  - `{current_user_id}` / `{current_org_id}` read userId / tenantId. A request
    carrying neither throws instead of resolving to null — a null comparand
    degrades to IS NULL on most drivers and would return the rows the filter
    was written to exclude.
  - An unrecognised placeholder throws, carrying the near-miss fix
    (`{current_user}` -> `{current_user_id}`), matching what the build lint
    already enforces. Both errors carry status 400 + a code, so the REST layer's
    generic 4xx passthrough reports them as fixable client errors, not 500s.

Also: `notify` no longer sends the literal string "undefined" as an audience
member. `to: ['{record.owner.manager}']` walks `.manager` on a scalar foreign-
key id, resolves to nothing, and `String(undefined)` turned that into a phantom
recipient — the emit "succeeded", addressed nobody, and said nothing anywhere.
Unresolved recipients are dropped, and a node left with none fails naming the
offending template and pointing at the start node's `config.expand` (#3475),
which does hydrate the relation.

Docs corrected where they claimed resolution was client-side only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdCvGtER9SzJopS4Yh3Pa1
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 7:01am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec.

116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/core, packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/core, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 07:14
@os-zhuang
os-zhuang merged commit 763931e into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/template-tokens-filters-notifications-nicmhd branch July 28, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Template tokens in filter values and notification templates are not evaluated at runtime

2 participants