|
| 1 | +--- |
| 2 | +"@objectstack/core": minor |
| 3 | +"@objectstack/objectql": minor |
| 4 | +"@objectstack/service-analytics": minor |
| 5 | +"@objectstack/service-automation": patch |
| 6 | +"@objectstack/spec": patch |
| 7 | +--- |
| 8 | + |
| 9 | +feat(filters): evaluate `{filter-token}` placeholders server-side (#3582) |
| 10 | + |
| 11 | +Filter values travel as JSON, so a time- or user-scoped slice writes a |
| 12 | +placeholder instead of code: |
| 13 | + |
| 14 | +```ts |
| 15 | +filter: { close_date: { $gte: '{current_year_start}' }, owner: '{current_user_id}' } |
| 16 | +``` |
| 17 | + |
| 18 | +The vocabulary has been in `@objectstack/spec` for a while (`date-macros.zod.ts`, |
| 19 | +`context-tokens.zod.ts`) and `objectstack build` rejects tokens outside it |
| 20 | +(#3574). What was missing is the half that *substitutes a value*: **nothing on |
| 21 | +the server ever did**. A placeholder reached the driver as the literal string |
| 22 | +`'{current_year_start}'`, compared as text, and matched nothing. |
| 23 | + |
| 24 | +That failure is invisible — an empty widget looks exactly like a metric that is |
| 25 | +legitimately zero — so apps worked around it by computing dates at module load, |
| 26 | +which freezes "this year" into the built artifact and quietly goes stale. |
| 27 | + |
| 28 | +**New: `resolveFilterTokens()` in `@objectstack/core`**, wired into the two |
| 29 | +server-side seams every filter passes through: |
| 30 | + |
| 31 | +- **ObjectQL read path** — `find` / `findOne` / `count` / `aggregate`, so REST |
| 32 | + queries, related lists, saved-view filters and flow `find_records` all resolve. |
| 33 | + It runs before the middleware chain, so only author-supplied filters are |
| 34 | + inspected; RLS/sharing filters are injected downstream from concrete values. |
| 35 | +- **Analytics dataset executor** — a dataset's intrinsic `filter`, a widget's |
| 36 | + `runtimeFilter`, measure-scoped filters, and time-dimension `dateRange`s. |
| 37 | + This path needs its own call: `NativeSQLStrategy` compiles raw SQL and binds |
| 38 | + comparands directly, so a dashboard widget never passes through `engine.find()`. |
| 39 | + |
| 40 | +Behavioural notes: |
| 41 | + |
| 42 | +- Date tokens resolve to ISO strings (`YYYY-MM-DD`, or a full timestamp for |
| 43 | + `{now}` / `{N_hours_ago}` / `{N_minutes_ago}`). Turning that into a column's |
| 44 | + on-disk form stays the driver's job (`SqlDriver.temporalFilterValue`), so |
| 45 | + there is still exactly one source of truth for the storage convention. |
| 46 | +- Calendar boundaries follow `ExecutionContext.timezone`; one instant is pinned |
| 47 | + per filter tree, so a `>= {current_month_start}` / `< {next_month_start}` pair |
| 48 | + can never straddle a boundary. |
| 49 | +- `{current_org_id}` reads `ExecutionContext.tenantId`; `{current_user_id}` reads |
| 50 | + `userId`. A request carrying neither now **throws** instead of resolving to |
| 51 | + `null` — a null comparand degrades to `IS NULL` on most drivers and would hand |
| 52 | + back the rows the filter was written to exclude. |
| 53 | +- An unrecognised placeholder **throws**, carrying the near-miss fix |
| 54 | + (`{current_user}` → `{current_user_id}`, `{this_quarter_start}` → |
| 55 | + `{current_quarter_start}`). This matches what `objectstack build` already |
| 56 | + enforces. Consequence, previously implicit and now load-bearing: a filter value |
| 57 | + that is *entirely* `{...}` is always read as a placeholder, so a literal value |
| 58 | + of that shape is not expressible — rename the value. |
| 59 | + |
| 60 | +Also in this change: `notify` no longer sends the six-character string |
| 61 | +`"undefined"` as an audience member. `to: ['{record.owner.manager}']` walks |
| 62 | +`.manager` on a scalar foreign-key id, resolves to nothing, and `String(undefined)` |
| 63 | +turned that into a phantom recipient — the emit "succeeded", addressed nobody, |
| 64 | +and said nothing. Unresolved recipients are now dropped, and a node with no |
| 65 | +recipient left fails naming the offending template and pointing at the start |
| 66 | +node's `config.expand` (#3475), which does hydrate the relation. |
0 commit comments