|
| 1 | +--- |
| 2 | +"@objectstack/metadata-protocol": patch |
| 3 | +"@objectstack/objectql": patch |
| 4 | +--- |
| 5 | + |
| 6 | +fix(data): `sort` / `select` / `expand` naming a field that does not exist are rejected, not silently dropped (#4226) |
| 7 | + |
| 8 | +The list path has four axes on which a caller names a field. `filter` was |
| 9 | +closed over #4134 / #4164 / #4181 / #4121 — a filter the server cannot apply is |
| 10 | +now a 400, never a 200 over the wrong rows. The other three still leaked, all |
| 11 | +answering `200`: |
| 12 | + |
| 13 | +``` |
| 14 | +sort=no_such_field -> 200 CAEBD byte-identical to "no sort at all" |
| 15 | +select=no_such_field -> 200 <every field> asked for one column, got all of them |
| 16 | +expand=no_such_rel -> 200 <no such key> no relation, no complaint |
| 17 | +``` |
| 18 | + |
| 19 | +Each is now refused at the shared normalizer, so `GET /data/:object`, |
| 20 | +`GET /data/:object/:id`, `POST /data/:object/query`, the export route and the |
| 21 | +runtime dispatcher give one answer instead of five. |
| 22 | + |
| 23 | +- **`sort` → `400 INVALID_SORT`.** The row set is unchanged, so this is not |
| 24 | + #4181's "returned everything" — it is worse in one specific way: `sort` + |
| 25 | + `top` is how a caller asks for "the latest N", and a dropped sort makes that |
| 26 | + an arbitrary N that nothing in the response reveals. This is the list half of |
| 27 | + the bug #4181 fixed on the export route's `orderby`. `INVALID_SORT` had sat |
| 28 | + in the standard catalog since it was written with no emitter. |
| 29 | +- **`select` → `400 INVALID_FIELD`.** `engine.find()` drops unknown columns |
| 30 | + (deliberate `SELECT *` tolerance) and then falls back to `*` when that empties |
| 31 | + the projection, and the two compose into `?select=<typo>` asking for ONE |
| 32 | + column and receiving EVERY column — a parameter whose purpose is to return |
| 33 | + less, failing by returning more, against both FLS and data minimisation. The |
| 34 | + partially-unknown case (`?select=title,no_such`) is refused on the same terms: |
| 35 | + half a projection is not the one that was asked for, and the tolerant reading |
| 36 | + would have to explain why `?status=<typo>` is a 400 and `?select=<typo>` is |
| 37 | + not, on one endpoint, about one field map. |
| 38 | +- **`expand` → `400 INVALID_FIELD`.** The lightest of the three — same rows, |
| 39 | + same columns, the relation simply is not there — but the response cannot be |
| 40 | + told apart from "every foreign key is null", and the client renders raw ids |
| 41 | + where names belong. A name that is no field at all and a name that is a field |
| 42 | + holding no reference (`?expand=title`) get different messages, since the fixes |
| 43 | + differ. |
| 44 | + |
| 45 | +**Sorts that were silently never applied now are.** Two wire spellings reached |
| 46 | +the normalizer and fell through it untouched, and every driver then declined |
| 47 | +them (`SqlDriver` guards its ORDER BY with `Array.isArray(orderBy)`): the |
| 48 | +client SDK's own declared `orderBy: string[]`, and the `{field: direction}` map |
| 49 | +that `GET /data/:object/export`, `GET /data/import/jobs` and objectui's calendar |
| 50 | +all emit. Both are now folded to `SortNode[]` — so the import-job history, which |
| 51 | +has asked for `created_at desc` since it was written and served insertion order, |
| 52 | +sorts. A sort shape that still cannot be read (a number, an entry naming no |
| 53 | +field, a direction that is neither `asc` nor `desc`) is `400 INVALID_SORT` |
| 54 | +rather than a silent no-op. |
| 55 | + |
| 56 | +**`$expand` of a `tree` field works.** `REFERENCE_VALUE_TYPES` lists `tree` |
| 57 | +among the types whose value "points at another record … the related record |
| 58 | +object in expanded form", and objectui requests it, but |
| 59 | +`engine.expandRelatedRecords` tested membership with a hand-copied `!==` chain |
| 60 | +that omitted it — so a hierarchy field came back as a raw parent id. The loop |
| 61 | +now reads the shared spec set, which is also what the new expand gate validates |
| 62 | +against, so the gate cannot admit a field the engine then skips. |
| 63 | + |
| 64 | +**What changes for callers:** requests naming a non-existent field in `sort`, |
| 65 | +`select` or `expand` now fail loudly instead of receiving an unsorted, widened |
| 66 | +or unexpanded response. Every axis naming real fields is unaffected. The |
| 67 | +engine's own tolerance is untouched — it guards internal callers (hooks, flows, |
| 68 | +expand sub-reads, registry-less hosts) that never pass through this ingress, |
| 69 | +the same tiering the object-existence and unknown-field gates already use. |
0 commit comments