Repository navigation
Commit 44ce049
fix(analytics): refuse a zero-operator field constraint on the draft preview (#19896)
Closes #19835
Clause-②: no
## What
`packages/services/service-analytics/src/preview-evaluator.ts`: the
draft-data preview now **refuses** a field constraint with zero
operators (`{ name: {} }`). Before this change it matched every row.
**The defect.** `matchesWhere`'s per-field arm iterated
`Object.entries(cond)`. An empty object has no entries, so the loop body
never ran and the row fell through to the closing `return true`. The
card's probe `matchesWhere({ name: 'Globex' }, { name: {} })` answered
`true`. The operator-vocabulary refusal that PR #19833 added cannot
reach this case: with no key there is nothing to look up. All three
shipped drivers refuse the shape (`driver-memory` `filter-refusal.ts`,
`driver-mongodb` `mongodb-filter.ts`, and `driver-sql` both at the top
level and inside combinators). This package's own `where` door also
refuses it (`filter-normalizer`'s wrapper arm). So the drafted chart
showed every row for a filter that publish refuses outright.
**The repair**, using the refusal plumbing PR #19833 added:
- `isEmptyFieldConstraint`, **mirrored locally**. It matches a plain
object with zero own keys. The prototype check keeps a `Date` / `RegExp`
/ class instance out of it, because those are comparands. The predicate
has the same shape as the drivers' copies. The exported copy lives in
`driver-memory`, which `service-analytics` does not depend on. The card
says not to add a cross-package dependency for this, so none was added.
- `previewEmptyFieldConstraintError`: the ADR-0112 `INVALID_FILTER` /
400 envelope through the existing `invalidFilterError`. No new error
code and no new export. The wording follows `emptyFieldConstraintError`:
it names the constraint and its position, and gives the two legal
repairs (name an operator, or write a direct comparand). It carries the
ruled reasoning that the shape means neither "every row" nor "no rows".
Per `check:doc-authoring`, the runtime string has no tracker number.
- **Where it fires.** (1) `assertPreviewCanEvaluate` is the
row-independent gate that runs before any row is read, and it now walks
the whole tree. It tracks the path (`where.$or[1].amount`), so `$and` /
`$or` / `$not` nesting cannot route around it. This includes an `$or`
arm that a matching row would short-circuit past, and a seed draft with
zero rows. (2) `matchesWhere`'s field arm also refuses, so a direct
caller gets the same answer. This mirrors the drivers, which judge the
shape at every depth.
- ⛔ The constraint is **not** read as "matches zero rows". That is the
other silent reading, and the ruling behind the drivers declined it.
## Evidence
New file `src/__tests__/preview-empty-field-constraint.test.ts`, 11
cases:
- The card probe, top level, empty seed, `$and`, `$or` (the
short-circuited arm), `$not`, deep nesting, and direct `matchesWhere`
under `$and`. Each asserts `code: 'INVALID_FILTER'` + `status: 400`,
never a bare `toThrow()`.
- Unchanged cases: operator constraints, implicit equality, and an empty
**node** (`where: {}` / `$and: [{}]`, which is the identity and not a
field constraint).
**Reverse verification** (fix committed first, at `3adb37d427`):
- Mutation: the pre-fix `preview-evaluator.ts` from base `dabf8d795e`
was written to disk, and the landing was proven by `grep -c
isEmptyFieldConstraint` = 0.
- Result: **8 failed | 3 passed (11)**, vitest exit 1. All 8 refusal
cases went red. The 3 unchanged cases stayed green, as expected, since
they pin behaviour that has not moved.
- Restore was `git checkout HEAD -- path` inside a trap. It was proven
by the blob hash `b4aeff76fa` equalling HEAD and by an empty `git diff
HEAD`.
- The test imports `src` by relative path, so no `dist` was involved.
**Package runs at `3adb37d427`:**
- `pnpm --filter @objectstack/service-analytics run test`: **115 files /
2453 tests passed**.
- `typecheck` (`tsc --noEmit`): exit 0. `--listFiles` includes the new
test file.
- eslint `--no-inline-config --format json` on both touched `.ts` files:
2 files, 0 errors, 0 warnings.
**Derived gate families**: `node scripts/pm/dispatch-gates.mjs` gave 60
derived. Reconciled with `--ran`: 57 exit 0, 3 NOT MEASURED, 0 unrun.
The NOT MEASURED ones are `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt`. Each exited 3
with `PREREQUISITE NOT MET` because it needs the whole-repo build. That
narrowing is declared and left to CI. `check:where-matcher`,
`check:doc-authoring`, `check:nul-bytes`, `check:test-source-alias` and
`check:published-files` are all green.
**Lint narrowing, declared.** The population is read from
`eslint.config.mjs`: both files fall under the `packages/**/*.{ts,...}`
objects, and neither is ignored (0 "file ignored" warnings). The count
is 2 files from the JSON output. Invariance: the config never enables
type-aware linting (no `parserOptions.project`), so this diff cannot
move any verdict on an untouched file. The repo-wide `pnpm lint` belongs
to CI.
## What it costs
A drafted chart whose `where` carries `{ field: {} }` now returns `400
INVALID_FILTER` in preview. Before, it rendered a number computed over
every row, and that number changed at publish. To fix a filter, name the
intended operator: `{ status: { $eq: 'open' } }` or `{ status: 'open'
}`. Changeset: `patch` for `@objectstack/service-analytics`.
## Acceptance notes
1. **Not measured**: an end-to-end reproduction through a rendered Live
Canvas draft chart. The evidence is the evaluator-level tests above, the
same scope as the card.
2. **Noted, not filed.** A `Date` in implicit-equality position has the
same enumerate-to-nothing fall-through, and this PR leaves it alone.
Probe: `matchesWhere({ d: 'zzz' }, { d: new Date('2026-05-01') })`
answers `true`. The `Date` falls into the operator-map arm, and its zero
entries become a match. It is deliberately outside this refusal, because
the drivers treat a `Date` as a comparand, not a constraint. What it
*should* answer (an instant equality through `compare`) is not pinned,
and reachability through `queryDataset` (a JSON wire body) was not
established. Owner: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3230308 commit 44ce049
3 files changed
Lines changed: 239 additions & 5 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
Lines changed: 142 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
Lines changed: 84 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
| 25 | + | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
193 | 194 | | |
194 | 195 | | |
195 | 196 | | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
196 | 257 | | |
197 | 258 | | |
198 | 259 | | |
| |||
206 | 267 | | |
207 | 268 | | |
208 | 269 | | |
209 | | - | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
210 | 281 | | |
211 | | - | |
| 282 | + | |
212 | 283 | | |
213 | 284 | | |
| 285 | + | |
214 | 286 | | |
215 | | - | |
| 287 | + | |
216 | 288 | | |
217 | 289 | | |
218 | | - | |
| 290 | + | |
219 | 291 | | |
| 292 | + | |
| 293 | + | |
220 | 294 | | |
221 | 295 | | |
222 | 296 | | |
| |||
240 | 314 | | |
241 | 315 | | |
242 | 316 | | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
243 | 322 | | |
244 | 323 | | |
245 | 324 | | |
| |||
0 commit comments