Skip to content

Commit 739c972

Browse files
fix(rest): the approvals list's non-string status refusal says what the parameter takes, and the approvals docs list all six statuses (#30569)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgMwxPY5sbyTBJiFwuYyp3nB)_ domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Fixes #30525 Clause-②: no — the same requests are refused with the same status and envelope; only the message for a non-string `status` and a docs list change Metadata-protocol: none ## 维护者速读 - 改了什么:审批列表接口收到格式不对的 `status`(比如 `?status[$ne]=pending` 这种被解析成对象的写法)时,报错文字改成说明正确写法,并列出六个允许的状态。 - 为什么改:之前的报错会写成 `"[object Object]" 不是审批状态`,对接方看不懂错在哪。 - 文档:审批文档里的状态列表原来只写了五个,现在补上 `cancelled`(平台作废的请求,比如记录被删了)。 - 风险:很低。被拒的请求和以前完全一样,状态码和返回结构都没变,只是文字更清楚。 - 先看:`packages/rest/src/approvals-routes.ts` 里 `readApprovalStatusFilter` 新加的非字符串分支。 - 需要你做的:无,席位落地。 ## What changes - **`readApprovalStatusFilter`** (`packages/rest/src/approvals-routes.ts`) answers a non-string `status` from its own arm: `The "status" query parameter must be one status, or several separated by commas, given as plain text. Allowed values: pending, approved, rejected, recalled, returned, cancelled.` It used to quote `String(raw)`, so `?status[$ne]=pending` read `"[object Object]"` as if that were the value sent. Still `400 VALIDATION_ERROR` in the nested envelope, and the service is still not asked. String values take the same path as before. - **`content/docs/automation/approvals.mdx`**: "Statuses in full" lists all six `APPROVAL_STATUSES`, with one clause on what `cancelled` means and where its reason lives (`cancel_reason`). ## Why Contract review record 6659996154 on PR #30491 (should-fixes 1 and 2), filed as #30525. ## Verification All at head `b6b000c5` (`origin/main` merged once, before the gates). - **Test.** The nested-object case in `rest-server-approvals-status-value.test.ts` now asserts the exact message, that it never contains `[object Object]`, no `data` beside the error, and that neither `listRequests` nor `countRequests` ran. File: 16/16. - **Ablation.** `scripts/ablation-replace.mjs` made the new arm unreachable (`typeof raw !== 'string'` → `typeof raw === 'symbol'`, blob `8fecd091` → `9dde4c15`): the nested-object case fails (the string path then throws on the object and the route answers 500); 15 others pass. Restored, `git diff HEAD` empty. - **Suites.** `@objectstack/rest` vitest: 393 files, 7,641 passed, 352 skipped. `pnpm --filter @objectstack/rest run typecheck`: exit 0. - **Lint.** `pnpm lint`: exit 0. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands` derived 88 commands; all 88 exit 0 after a full `turbo run build --filter='!@objectstack/docs'`. `--ran`: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (the designated thread branch, restarted from `main` after #30491 merged, as the Claim records) - [x] 5. A change that publishes carries a changeset (`patch`, `@objectstack/rest`), and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the maintainer's quick-read section is in Chinese by the seat's order). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CbAccy76jpUtQ9ZZX9 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94cef75 commit 739c972

4 files changed

Lines changed: 35 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest): the approvals request list's refusal of a non-string `status` says what the parameter takes, instead of quoting `[object Object]`
6+
7+
Clause-②: no — the same requests are refused with the same status and envelope; only the message for a non-string `status` changes
8+
Metadata-protocol: none
9+
10+
`GET /api/v1/approvals/requests?status[$ne]=pending` (a `status` the query parser turns into an object) was refused with `"[object Object]"` quoted as if it were the value sent. It is now refused with `The "status" query parameter must be one status, or several separated by commas, given as plain text. Allowed values: pending, approved, rejected, recalled, returned, cancelled.` Still `400 VALIDATION_ERROR` in the nested envelope, and the approval service is still not asked. The approvals docs page now lists all six request statuses (`cancelled` was missing).

‎content/docs/automation/approvals.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,9 @@ On finalization the request row gets `status: 'approved'` (or `'rejected'`),
737737
mirrored to the same value, and the record unlocks. The parked run resumes down
738738
the `approve` or `reject` branch.
739739

740-
Statuses in full: `pending`, `approved`, `rejected`, `recalled`, `returned` —
741-
the `APPROVAL_STATUSES` constant in `@objectstack/spec/contracts`, which is also
740+
Statuses in full: `pending`, `approved`, `rejected`, `recalled`, `returned`,
741+
`cancelled` (the platform voided the request, for example because its record was
742+
deleted; `cancel_reason` on the row says why) — the `APPROVAL_STATUSES` constant in `@objectstack/spec/contracts`, which is also
742743
what `sys_approval_request.status` offers and what `ApprovalStatus` is derived
743744
from. If you add a status there, this line is a fourth copy that nothing updates
744745
for you.

‎packages/rest/src/approvals-routes.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,18 @@ function readApprovalStatusFilter(
108108
): { status: ApprovalStatus | ApprovalStatus[] | undefined } | { refusal: string } {
109109
if (raw === undefined) return { status: undefined };
110110
const allowed: readonly string[] = APPROVAL_STATUSES;
111-
const entries = typeof raw === 'string' ? raw.split(',').map((s) => s.trim()) : [];
112-
const unknown = typeof raw === 'string'
113-
? entries.filter((s) => !allowed.includes(s))
114-
: [String(raw)];
111+
// [#30525] A value the query parser built into something other than a
112+
// string (`?status[$ne]=pending` arrives as an object) has no entry to
113+
// name: quoting `String(raw)` printed `"[object Object]"`. Say what shape
114+
// the parameter takes instead. Same 400, same envelope, service not asked.
115+
if (typeof raw !== 'string') {
116+
return {
117+
refusal: 'The "status" query parameter must be one status, or several separated by '
118+
+ `commas, given as plain text. Allowed values: ${allowed.join(', ')}.`,
119+
};
120+
}
121+
const entries = raw.split(',').map((s) => s.trim());
122+
const unknown = entries.filter((s) => !allowed.includes(s));
115123
if (unknown.length > 0) {
116124
const named = [...new Set(unknown)].map((s) => `"${s}"`).join(', ');
117125
return {

‎packages/rest/src/rest-server-approvals-status-value.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,17 @@ describe('#29880 §1 — a status value outside the declared set is refused', ()
164164
const answer = await drive({ status: { $ne: 'pending' } });
165165
expect(answer.status).toBe(400);
166166
expect(answer.body?.error?.code).toBe('VALIDATION_ERROR');
167+
expect(answer.body?.data).toBeUndefined();
167168
expect(approvals.listRequests).not.toHaveBeenCalled();
169+
expect(approvals.countRequests).not.toHaveBeenCalled();
170+
// [#30525] The message says what shape the parameter takes and lists
171+
// the allowed values; it never quotes the parser's object as a value.
172+
const message = String(answer.body?.error?.message);
173+
expect(message).toBe(
174+
'The "status" query parameter must be one status, or several separated by commas, '
175+
+ `given as plain text. Allowed values: ${APPROVAL_STATUSES.join(', ')}.`,
176+
);
177+
expect(message).not.toContain('[object Object]');
168178
});
169179

170180
it('the name and arity gates still answer first', async () => {

0 commit comments

Comments
 (0)