Skip to content

Commit 54818fe

Browse files
docs(api): sort spellings are slot-specific, not equivalent across routes (#19055)
Fixes #19027 Clause-②: no ## What the page said, and what the runtime does `content/docs/api/data-api.mdx` taught three `POST /data/:object/query` sort spellings as "all equivalent". Reproduced at the exact input `rest-server.ts` builds — `FindDataRequestSchema.safeParse({ object, query: { ...body, object } })` — and again over real HTTP against a booted `examples/app-crm`: | documented bag | schema-level | over HTTP | |:---|:---|:---| | `{"orderBy": [{"field": "created_at", "order": "desc"}]}` ⭐ lit control | `success: true` | **200**, rows descending | | `{"orderBy": ["-created_at"]}` | `query.orderBy.0` · `invalid_type` · "expected object, received string" | **400** `VALIDATION_FAILED` | | `{"orderBy": {"created_at": "desc"}}` | `query.orderBy` · `invalid_type` · "expected array, received object" | **400** `VALIDATION_FAILED` | The lit control returns 200, so the instrument is sound and the defect is the page's, as the card diagnosed. ## Where the card's diagnosis needed correcting The card says the record map and the shorthand array "are transport-slot values — they have to arrive on `$orderby` / `sort`". Measured, that holds for the map and **not** for the shorthand array. Every POST-body slot, one tree: | body value | `orderBy` | `$orderby` | `sort` | |:---|:---|:---|:---| | `[{"field": "created_at", "order": "desc"}]` | 200 | 200 | 200 | | `{"created_at": "desc"}` (and `1` / `-1`) | 400 | 200 | 200 | | `["-created_at"]` | 400 | 400 | 400 | | `"-created_at"` (bare string) | 400 | 400 | 400 | `QueryTransportParamsSchema.$orderby` is `DataEngineSortSchema`, which declares the record map and `SortNode[]` and no string form at all. So the shorthand string is not a transport-slot value on this route either — it is a **querystring** spelling, read by `normalizeSortNodes` at the ingress. Measured on the querystring: `?sort=-created_at`, `?$orderby=-created_at` and `?orderBy=-created_at` all sort; the two JSON shapes sent there are `400 INVALID_SORT`, because that route parses no JSON in this slot and reads the whole value as one field name. Had the page simply been trimmed to the card's diagnosis, a reader sent to `{"$orderby": ["-created_at"]}` would have got the same 400 from a different line. ## The fix The sentence is replaced by a per-route, per-slot table plus two short paragraphs. Every cell in it is a measurement, not a reading of the schema. The page's only mention of `POST /data/:object/query` was inside the sentence being replaced, so the table is also now that route's only description here. The status-code split is stated per route as well: a wrong *shape* in a body is `400 VALIDATION_FAILED` with the path in `fields` (`query.orderBy.0`, `query.orderBy.0.order`) and never reaches the field check, while an unknown field name is `400 INVALID_SORT` on either route. ## Verification * **Reproduction, schema level** — the three bags at the rest-server input, plus the full slot matrix above and the malformed-shape cases. * **Reproduction, HTTP level** — `pnpm dev:crm -- --fresh -p PORT` (a random high port), signed in as the seeded dev admin, `GET /data/crm_account` and `POST /data/crm_account/query`. Ascending/descending row order is the lit control on every 200 cell: `['Acme Corp','Globex Ltd','Initech']` vs `['Initech','Globex Ltd','Acme Corp']`, so an accepted-but-unapplied sort could not read as a pass. * **Post-condition probe, written before the edit** — it parses the new table and asserts each cell's verdict token equals the measured one, that the four spellings are still *shown* (so deleting them would fail it), and that no cross-route equivalence wording survives. It reports 8 failures against the pre-edit page and passes against the edited one; both legs were run with the same scoped instrument. * **Derived gates** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 39 commands, all run; exits in the report. ## No changeset — measured, not asserted No published package contains `content/docs/api/data-api.mdx`: over the 70 non-private manifests in the tree, zero sit at or above that path. Positive control, same method: `packages/spec/dist/index.js` resolves into `@objectstack/spec`, whose `files[]` ships `dist`. `apps/docs`, which renders this tree, is `private: true`. So nothing published moves and the change takes `skip-changeset`. ## Acceptance notes * `#18977` is not addressed here, and `PR #19018` is untouched by this diff — this PR edits one file, that one edits four, and they do not overlap. No `packages/spec/**` path is in this diff: the measurement showed the defect is the documentation's, which is the branch the dispatch reserved for staying on this page. * Noted, not filed: `packages/spec/src/api/odata.zod.ts` already carries the correct account of this split in a schema comment (the `#18977` cross-reference), including the sentence that the string forms are served by `normalizeSortNodes` and not by the body door. The customer-facing page had no pointer to it in either direction. Successor: the next PR to touch that schema comment or this page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0a56d3b commit 54818fe

1 file changed

Lines changed: 25 additions & 6 deletions

File tree

‎content/docs/api/data-api.mdx‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,31 @@ Why each one matters, since none of them changes which rows match:
114114
- **`expand`** — an unexpanded relation is indistinguishable from one whose
115115
foreign keys are all null, so clients render raw ids where names belong.
116116

117-
Sorts accept any of these spellings, all equivalent:
118-
`?sort=-created_at`, `?$orderby=-created_at`, and — on
119-
`POST /data/:object/query` — `{"orderBy": [{"field": "created_at", "order":
120-
"desc"}]}`, `{"orderBy": ["-created_at"]}` or `{"orderBy": {"created_at":
121-
"desc"}}`. A shape that is none of these (a number, an entry naming no field,
122-
a direction that is neither `asc` nor `desc`) is `400 INVALID_SORT`.
117+
Sort spellings are slot-specific, and the two routes take different sets — a
118+
slot that does not take a shape answers `400` instead of sorting. Measured on
119+
both routes:
120+
121+
| Sort value | On the querystring — `GET /data/:object` | In the body of `POST /data/:object/query` |
122+
|:---|:---|:---|
123+
| `-created_at`, or `created_at desc` — the shorthand string | `?sort=-created_at`, `?$orderby=-created_at` and `?orderBy=-created_at` — each of them sorts; they normalize to one parameter | `400 VALIDATION_FAILED` on every slot — `orderBy`, `$orderby` and `sort` alike |
124+
| `["-created_at"]` — an array of those shorthand strings | `400 INVALID_SORT` — this route parses no JSON here, so the whole value is read as one field name | `400 VALIDATION_FAILED` on every slot |
125+
| `{"created_at": "desc"}` — a field-to-direction map (`1` / `-1` are read as `asc` / `desc`) | `400 INVALID_SORT` — same reason | sorts on `$orderby` and on `sort`; on `orderBy` it is `400 VALIDATION_FAILED` |
126+
| `[{"field": "created_at", "order": "desc"}]` — `SortNode[]`, the canonical shape | `400 INVALID_SORT` — same reason | `{"orderBy": …}` sorts, and so do `$orderby` and `sort` |
127+
128+
So read the last column before copying a sort into a request body. The
129+
shorthand string is a querystring spelling and reaches no body slot at all; the
130+
map reaches `$orderby` and `sort` but never `orderBy`; `SortNode[]` is the one
131+
shape the canonical slot and its two transport aliases all accept.
132+
133+
Naming something that is not a field on the object is `400 INVALID_SORT` on
134+
either route — `?sort=no_such_field` and
135+
`{"orderBy": [{"field": "no_such_field", "order": "asc"}]}` alike. A body whose
136+
sort has the wrong *shape* never reaches that check: it is
137+
`400 VALIDATION_FAILED`, and the offending path is named in `fields`
138+
(`query.orderBy.0` for an entry that names no field, `query.orderBy.0.order`
139+
for a direction that is neither `asc` nor `desc`). The querystring has no body
140+
to validate first, so it reports that direction as `400 INVALID_SORT` —
141+
`?sort=name sideways`.
123142

124143
`GET /data/:object/:id` applies the same `select` and `expand` rules, so the
125144
list and single-record routes cannot disagree about one field map.

0 commit comments

Comments
 (0)