You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 524befb
Browse filesBrowse the repository at this point in the historyBrowse files
fix(rest)!: export refuses an unserved format and clone refuses a non-object overrides (#27659)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_Fixes#27500Fixes#27489
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill teaches either door's malformed values, and
`content/docs/api/data-api.mdx` describes only the accepted shapes,
which are unchanged
## 维护者速读
- **改了什么**:导出接口 `GET /data/OBJ/export` 收到不支持的 `format`(如 pdf、xml、空值)时返回
400,不再悄悄给一份 CSV;克隆接口 `POST /data/OBJ/ID/clone` 收到不是对象的
`overrides`(字符串、数字、数组、null)时返回 400,不再悄悄当成普通复制。
- **为什么改**:调用方要的东西没给,却拿到 200/201 成功,脚本和集成无法察觉;数组 overrides 还会被按下标合并进新记录。
- **风险与回滚**:风险是曾经发送这类错误值并"侥幸成功"的调用方现在会收到 400;不传 `format`、传
csv/json/xlsx、传对象 overrides 或不传 body 的行为完全不变。回滚即还原
`data-action-routes.ts` 的两处读取。
- **先看**:`packages/rest/src/data-action-routes.ts`(导出的
`readExportFormat` 与克隆的 `isFieldMap`
守卫)、`packages/rest/src/export-format-refusal.test.ts`、`packages/rest/src/clone-overrides-refusal.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run
src/export-format-refusal.test.ts
src/clone-overrides-refusal.test.ts`,预期 2 个文件 28 条全部通过。
- **需要你做的**:无,席位落地。
## What changes
One door, one reading per parameter, both in
`packages/rest/src/data-action-routes.ts`; one commit per card.
- **#27500, export `format`.** `readExportFormat` reads the parameter:
absent is `csv`; a present value, trimmed and case-insensitive (the
reading the route's own `template=true` mode already gives `format`),
must be one of the route's served set `csv`, `json`, `xlsx`. Anything
else answers `400` through the shared nested writer (`sendError` from
`@objectstack/types`): `error.code: 'VALIDATION_ERROR'`, a message
naming the value and the served set. The refusal comes before
`findData`, so no row is read and no export header is set. The set is
the route's own, not the spec's wider `ExportFormat` enum (`jsonl`,
`parquet` and the rest were never served and are refused);
`packages/spec` is not edited.
- **`format=` (empty) is decided: refused.** An empty value is a value
the caller wrote, the same reading this route already gives `limit=`
(`emptyIsAbsent: false`) and that `template=true` gives `format=`.
- **#27489, clone `overrides`.** The handler still accepts both `{
"overrides": {...} }` and a bare field map. A nested `overrides`, or a
bare body, that is not a JSON object (a string, number, boolean, `null`
or an array) answers `400` `VALIDATION_ERROR` naming `overrides`, before
`cloneData` is called. That is the triage's locus: at the REST clone
door, before `cloneData`. `packages/metadata-protocol` is not edited.
- **`"overrides": null` is decided: refused**, not read as absent. A
bare `null` body stays what the transport already makes it (`req.body ??
{}`, a plain copy), and no body at all is a plain copy as before.
- **Envelope and code.** Both refusals use the nested ADR-0112 envelope
that this route's query gates already answer, through the shared writer,
so `check:route-envelope` counts no new hand-built body.
`VALIDATION_ERROR` is a `StandardErrorCode` already on the ledger; no
code is minted.
## Tests
Both suites drive the real `HonoHttpServer` (the adapter `os serve`
mounts), so the query string and the JSON body are parsed by the
production code.
- `packages/rest/src/export-format-refusal.test.ts`: refusal for `pdf`,
`xml`, empty, blank, and `jsonl` (in the spec enum, never served). Each
asserts `status 400`, `error.code`, that the message names `"format"`
and the set, that there is no `X-Export-Format`, and that `findData` was
never called. Preservation: absent → csv with the row streamed;
`csv`/`CSV`/`json`/`JSON`/` json `/`xlsx`/`Xlsx` each answer their own
`X-Export-Format`.
- `packages/rest/src/clone-overrides-refusal.test.ts`: refusal for
nested string, number, `true`, `null`, array, empty array, and bare
array, string and number bodies. Each asserts `status 400`,
`error.code`, the message naming `"overrides"`, and that `cloneData` was
never called. Preservation: nested map, bare map, `{"overrides":{}}`,
`{}` and bare `null` reach `cloneData` with the expected `overrides`,
and no body at all is a `201` plain copy.
## Acceptance notes
- **The changeset declares `Clause-②: no (narrowing)` where both claims
say `Clause-②: no`.** Both doors now refuse values they used to accept
and answer as success, which is an accept-set narrowing (AGENTS.md
Post-Task Checklist step 3). So the changeset carries the BREAKING
banner, its migration, and the ADR-0087 disposition (`not-required
(no-migration-prescription)`), at `patch` level because the kind is
`none`. Seat: confirm or correct the arm.
- **Not changed, noted.** A body carrying `overrides` and other
top-level keys still reads only `overrides` and ignores the rest, as
before. This is outside both cards.
- **Commit shape.** There are two card commits, then `test(rest): type
the clone refusal test's parsed body` (a cast that `tsconfig.test.json`
needed, folded separately because rewriting the pushed branch was
refused in this session), then the changeset commit, then one merge of
`origin/main`.
## Verification
All readings are at head `035ecdcf` (after one merge of `origin/main`)
unless a line says otherwise. Box seconds are shared-box seconds.
<details><summary>Package tests and typecheck: @objectstack/rest
green</summary>
- `pnpm --filter @objectstack/rest build && pnpm --filter
@objectstack/rest test && pnpm --filter @objectstack/rest typecheck`
under the verify lock: `VERDICT command-exit 0`. Test Files 337 passed
(337), Tests 6615 passed and 326 skipped. `tsc --noEmit` was clean, and
`check:test-typecheck` was OK with 0 files and 0 errors in the debt
ledger.
- New suites alone: Test Files 2 passed (2), Tests 28 passed (28).
- `eslint --no-inline-config packages/rest`: exit 0.
</details>
<details><summary>Ablations: both negative pins go red with the old
coercion put back, and are restored to blob == HEAD</summary>
Each leg used `node scripts/ablation-replace.mjs` in wrap mode, with a
shell trap restoring `HEAD`. The tests import `./rest-server` from
`src/`, so no `dist/` leg applies.
- **#27500.** `readExportFormat`'s return was replaced with the old
`value === 'json' ? 'json' : value === 'xlsx' ? 'xlsx' : 'csv'`. The
anchor went from 1 to 0 and the blob from `4a4b352aa086` to
`39d51ee7fd0d`. Result: `export-format-refusal.test.ts` had 5 failed and
8 passed (all five refusal cases: `expected 200 to be 400`). Restored:
blob after restore `4a4b352aa086` == HEAD, `git diff HEAD` empty.
- **#27489.** The guard was removed and the old `...(overrides && typeof
overrides === 'object' ? { overrides } : {})` pass-through put back. The
anchor went from 1 to 0 and the blob from `4a4b352aa086` to
`8491a123aeeb`. Result: `clone-overrides-refusal.test.ts` had 9 failed
and 6 passed (all nine refusal cases: `expected 201 to be 400`).
Restored: blob == HEAD, `git diff HEAD` empty.
</details>
<details><summary>Derived gate families: 59 derived, 59 run, all exit
0</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 59 commands at `035ecdcf`. Each was
run with its exit code captured before any pipe, and all 59 exited 0.
- `--ran` reconciliation: `59 derived famil(ies) accounted for — 59 run,
0 NOT-MEASURED (a DERIVED zero — all 59 recorded an exit code and none
of them is 3)`.
- On the earlier pre-merge run, two families answered PREREQUISITE NOT
MET (exit 3): `check:dual-build-cjs-loads` had no `dist/` yet, and
`check-plugin-teardown-shape --self-test` was on a shallow checkout.
Both exited 0 on the final-head run above.
- Open PRs touching the same files: 0 of 43, so no `merge-tree` probe
was needed.
</details>
<details><summary>Dogfood battery (required by the narrowing arm): 2
timeouts, both unrelated and green on re-run</summary>
- Under the lock, `pnpm --filter '@objectstack/dogfood^...' build &&
pnpm --filter @objectstack/dogfood test` at `035ecdcf`: `VERDICT
command-exit 1`, held 3349 s. Test Files: 2 failed, 349 passed, 3
skipped (354). Tests: 2 failed, 2820 passed, 20 skipped (2842).
- The two failures were both `Test timed out in 5000ms`:
`object-designer-field-reorder.dogfood.test.ts` (creates and publishes
the object in its declared order) and
`wall-package-refusal.dogfood.test.ts` (a package install, upgrade and
uninstall round trip). Neither file calls the data export or data clone
door.
- Re-run of those two files alone, while another worktree's turbo build
held the box: the same two tests timed out at about 5.4 s. With
`--testTimeout=30000`: Test Files 2 passed (2), Tests 24 passed (24).
Read: box load, not this diff.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(rest)!: the export door refuses an unserved `format`, and the clone door refuses a non-object `overrides`, instead of answering as if they were not sent (#27500, #27489)
6
+
7
+
Clause-②: no (narrowing) — `GET /data/:object/export` refuses a `format` outside csv, json and xlsx, and `POST /data/:object/:id/clone` refuses an `overrides` (or bare body) that is not a JSON object; both used to answer success with the value ignored. Nothing is widened.
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: two REST doors now refuse request values they used to ignore. There is nothing for a migration to rewrite and no tombstone. -->
11
+
12
+
**BREAKING** accept-set narrowing at two REST data doors (`@objectstack/rest`).
13
+
14
+
-**Export, before.** Any `format` other than `json` or `xlsx` was answered as csv: `?format=pdf`, `?format=xml` and `?format=` (empty) answered `200` with a csv attachment and `X-Export-Format: csv`.
15
+
-**Export, now.** A present `format` must be `csv`, `json` or `xlsx` (case-insensitive, surrounding blanks trimmed). Anything else, the empty value included, answers `400` with `error.code: 'VALIDATION_ERROR'`, naming the value and the served set, before any row is read. The spec's `ExportFormat` enum lists more values (for example `jsonl`, `parquet`); this route never served them and refuses them too.
16
+
-**Clone, before.**`{"overrides": "x"}` (or a number, `true`, `null`) was dropped and the door answered `201` with a plain copy; an array reached the clone and was merged index-wise. A bare array or string body behaved the same way.
17
+
-**Clone, now.** A nested `overrides`, or a bare body, that is not a JSON object answers `400` with `error.code: 'VALIDATION_ERROR'`, naming `overrides`, before the clone runs. `"overrides": null` is refused, not read as absent.
18
+
-**Unchanged.** An absent `format` is csv. An object `overrides`, a bare field map, `{}`, and no body at all clone exactly as before. The SDK's `data.clone` and `data.export` send only accepted shapes.
19
+
-**Who sees it.** An API caller or script that sends an unserved export format, or a clone body whose `overrides` is not an object. Such a request used to succeed while meaning something other than what was sent.
20
+
21
+
**The remedy.** Export: send `csv`, `json` or `xlsx`, or omit `format` for csv. Clone: send `overrides` as an object of field values, or omit it (or send no body) for a plain copy.
0 commit comments