|
| 1 | +--- |
| 2 | +"@objectstack/metadata-protocol": patch |
| 3 | +"@objectstack/objectql": patch |
| 4 | +"@objectstack/spec": patch |
| 5 | +"@objectstack/rest": patch |
| 6 | +--- |
| 7 | + |
| 8 | +fix(data): `searchFields` / `groupBy` / `aggregations` naming a field that does not exist are rejected, not silently degraded (#4254) |
| 9 | + |
| 10 | +#4226 closed `sort` / `select` / `expand`; with the filter axis (#4134 / #4164 / |
| 11 | +#4181 / #4121) that made four field-naming read axes that either apply or fail. |
| 12 | +The same machine kept leaking on the remaining three, and each failure corrupted |
| 13 | +something the closed axes never touched: |
| 14 | + |
| 15 | +``` |
| 16 | +search=alpha&searchFields=no_such -> 200 MORE rows than the narrowing allowed |
| 17 | +groupBy=[no_such] -> 200 [{no_such: null, n: <true count>}] N groups collapsed into 1 |
| 18 | +sum(no_such) -> 200 0 — indistinguishable from a real zero |
| 19 | +``` |
| 20 | + |
| 21 | +Each is now refused at the shared normalizer, so `GET /data/:object`, |
| 22 | +`POST /data/:object/query`, the export route and the runtime dispatcher give |
| 23 | +one answer instead of four. |
| 24 | + |
| 25 | +- **`searchFields` → `400 INVALID_FIELD`.** The `select` failure with the sign |
| 26 | + flipped outward: the engine dropped unknown names and, when that emptied the |
| 27 | + override, fell back to the FULL searchable set — so a parameter that exists |
| 28 | + only to narrow a search widened it, and it changed which ROWS came back, not |
| 29 | + just which columns. Its only in-framework caller is `GET /data/:object/export` |
| 30 | + — the route whose `search` support just shipped so exports would stop |
| 31 | + downloading "the unsearched superset … in a file that looks authoritative"; |
| 32 | + a typo'd `searchFields` did exactly that, one parameter over. Three causes, |
| 33 | + three messages, because the fixes differ (the split #4226 drew on expand): a |
| 34 | + name that is no field is a request typo; a REAL field outside the searchable |
| 35 | + set needs the object changed (its message names the declared |
| 36 | + `searchableFields` or the auto-default's type rule, whichever applies); and |
| 37 | + a `searchableFields` entry that names no field is a STALE DECLARATION — a |
| 38 | + bug on the object, called out as such because clients (objectui's list |
| 39 | + search) echo the declaration verbatim. The allowed set is resolved by the |
| 40 | + same `@objectstack/spec/data` function the engine's search expansion |
| 41 | + consumes (`resolveSearchFieldResolution`, moved from objectql), so the gate |
| 42 | + cannot drift from what search actually scans. |
| 43 | +- **`groupBy` → `400 INVALID_FIELD`.** The in-memory aggregation path projects |
| 44 | + an unknown column as `null` for every row, so all rows landed in ONE bucket |
| 45 | + whose count is the true row count — structurally perfect, identical to "this |
| 46 | + column really holds a single value". A chart draws one bar; nothing says the |
| 47 | + grouping never ran. Native SQL aggregation errors on the same input, so which |
| 48 | + backend a deployment sits on decided the answer — the "two routes, opposite |
| 49 | + answers" split, one axis over. |
| 50 | +- **`aggregations` → `400 INVALID_FIELD`.** `sum(<typo>)` folded a column of |
| 51 | + `undefined` to `0` — the exact number an empty quarter produces, in reports |
| 52 | + whose whole job is to be believed (`avg`/`min`/`max` answered `null` the same |
| 53 | + way). `count` with no `field` (or the `'*'` sentinel) is the one legitimate |
| 54 | + field-less form and passes. |
| 55 | +- **Unreadable SHAPES on the aggregation axes → `400 INVALID_QUERY`** — the |
| 56 | + standard-catalog code that had no emitter since it was written, like |
| 57 | + `INVALID_SORT` before #4226. A string `groupBy`, an entry naming no field, a |
| 58 | + function or `dateGranularity` outside the spec enums, a missing `alias`: each |
| 59 | + slipped past the `Array.isArray` routing guard (rows returned UNGROUPED) or |
| 60 | + computed a silent placeholder (`null` results, a column keyed `"undefined"`, |
| 61 | + one bucket per raw value under an unknown granularity). |
| 62 | + |
| 63 | +Tiering is unchanged from #4226: registry + field map present → authoritative; |
| 64 | +no registry / no field map / legacy array field map → the NAME gates skip (shape |
| 65 | +gates still apply — they need no schema). The engine's own tolerance is |
| 66 | +untouched: internal callers reaching `engine.find()` / `engine.aggregate()` |
| 67 | +directly are unaffected. `@objectstack/rest` also stops logging |
| 68 | +`INVALID_FILTER` / `INVALID_SORT` / `INVALID_QUERY` rejections as |
| 69 | +"[REST] Unhandled error" — they are client mistakes the response already |
| 70 | +explains, as `INVALID_FIELD` always was. |
| 71 | + |
| 72 | +Requests that name real fields are unaffected. |
0 commit comments