Repository navigation
Commit f3f7b62
fix(rest)!: an empty
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XG3uopXrSJccJc9XqnMV8ty)_
Fixes #28548
Clause-②: no (narrowing) — `GET /meta/app` / `GET /meta/apps` with an
empty `?id=`, and `GET /meta/view` / `GET /meta/views` with an empty
`?object=` (on `RestServer` and on the runtime dispatcher, which hand
their store answer to the same list chain), are refused with `400
VALIDATION_ERROR` naming the parameter; they answered 200 with the
caller's whole visible list before. Nothing is widened: no export,
answer member, error code or status is added.
Metadata-protocol: none
## 维护者速读
- **改了什么**:`GET /api/v1/meta/app?id=`(id 为空)和 `GET
/api/v1/meta/view?object=`(object 为空)现在回 400
`VALIDATION_ERROR`,消息点名参数,不再返回调用者能看到的全部应用或全部视图。
- **为什么改**:以前空 id 被当成“没写”,调用方本想取一个应用,名字却是空的,结果拿到整张应用列表,看起来像正常结果;视图列表的空
`object` 是同一个毛病,席位要求在本 PR 一并修。和 #27484(空 `$searchFields`
被当成没写)同一类,按同样方式拒收。
- **风险与回滚**:只影响发了空 `id` 或空 `object` 的 API 调用方;`@objectstack/client` 和
Console 读这两个列表都不带这两个参数。非空值、不带参数、重复参数的回答都不变。回滚 = revert 本 PR。
-
**先看**:`packages/rest/src/meta-item-read-gate.ts`(`createMetaListAnswer`
第 3、4 步和
`refuseEmptyListFilter`)、`packages/rest/src/meta-app-publish-gate.test.ts`(有意翻转的钉子)。
- **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run
src/meta-app-publish-gate.test.ts`,预期 27 个用例全过(空 id 三种调用者都回 400,空格 id
仍回空列表,空 object 回 400)。
- **需要你做的**:无,席位落地(进队前欠一次契约复核)。
## What changes
- **`createMetaListAnswer` step 3**
(`packages/rest/src/meta-item-read-gate.ts`): the app filter used to
skip an empty `id` (`typeof appIdFilter === 'string' && appIdFilter !==
''`), so the request was served as if no filter had been sent. An empty
value now goes to the new `refuseEmptyListFilter()`, which throws `400
VALIDATION_ERROR` with the message `Query parameter 'id' names no app;
got "". …`. The throw reaches each transport's own error door
(`RestServer`'s `handleRouteError`, the dispatcher's `errorFromThrown`),
the way `refuseUnknownMetaListType` already answers on this route, so
neither transport hand-builds a body.
- **`createMetaListAnswer` step 4** (seat-directed fold, same class):
`?object=` on `view` was read with a truthiness test (`query?.object`),
so `GET /meta/view?object=` listed every view, unsorted and including
views bound to no object, as if no filter had been sent. An empty value
now goes to the same helper: `400 VALIDATION_ERROR`, message `Query
parameter 'object' names no object; got "". …`. A named object still
switches; `?object=` on other types is still ignored.
- **Order kept.** Both refusals sit in steps 3 and 4, after the
per-caller visibility gate (step 2), which keeps its place. The chain
stays the one place a list step lives, so both transports answer the
same.
- **Unchanged.** A non-empty `id` narrows as before (one app, or an
empty list when it names none, `' '` included). No `id` lists every app
the caller may see. A repeated `id` is still the arity 400 at the door
entry. `?id=` on any other type is still ignored.
- **Docblock** of `createMetaListAnswer` steps 3 and 4 updated to say an
empty value is refused.
- **Pin flipped on purpose**: `meta-app-publish-gate.test.ts` "an ABSENT
id still returns the whole published set" asserted that `?id=` (empty)
returns the full list, citing an unset `select` as its producer. No
producer sends it (see Acceptance notes), and the card's expected result
rules the other way, so that half moves to the new refusal case.
- **Changeset** `.changeset/28548-empty-app-id-refused.md`: `patch` on
`@objectstack/rest`, BREAKING banner, ADR-0087 `not-required
(no-migration-prescription)`. `@objectstack/runtime` changes only a
test, which its `files[]` does not ship.
### Which precedent, and why
The card offers two outcomes: refuse with a 400 naming the parameter, or
match nothing as `' '` does. This follows the precedent PR #27670 set
for #27484: that change refused a blank `$searchFields` with `400
VALIDATION_ERROR` and a message naming the parameter, rather than
reading it as a value that matches nothing. Along the four axes: there
is no real caller that sends an empty `id` (measured below), so the loud
answer costs nobody; a refusal is the contract-first answer and the one
that stops an AI-written client from mistaking a blank name for a real
filter; and it adds no gate or surface, only a refusal on the existing
door.
## Tests
<details><summary>Red first, then green (head 9c45ac5)</summary>
- `id` (first commit): red on the base code with the new tests —
`meta-app-publish-gate.test.ts` "an EMPTY id is refused…" (`expected 200
to be 400`) and `meta-list-projection-parity.test.ts` row A "the
reference moves" on the new `?id=` assertion. The row A matrix cells for
`?id= (empty)` passed on the base, because both transports agreed on the
wrong 200.
- `object` (second commit): red on 9246901 with the new tests — "an
EMPTY view `object` is refused…" (`view: expected 200 to be 400`, 1
failed / 26 passed) and row A on the new `?object=` assertion (1 failed
/ 934 passed).
- Green on 9c45ac5 after a `rest` rebuild:
`meta-app-publish-gate.test.ts` +
`meta-view-dashboard-audience-gate.test.ts` 46/46;
`meta-list-projection-parity.test.ts` + `dispatcher-query-arity.test.ts`
968/968.
- New cases: an empty `id` as a builder (`studio.access`), as
`manage_users` and as a caller holding nothing, on `/meta/app` and
`/meta/apps`; an empty `object` on `/meta/view` and `/meta/views`. Each
asserts `status` 400, `code` `VALIDATION_ERROR`, the message's first
clause, and no items. The dispatcher's twin is pinned by the new `?id=
(empty)` and `?object= (empty)` probes in `@objectstack/runtime`'s list
parity census: every type × caller cell must answer the same `status` +
`code` + items on both transports, and row A asserts `400
VALIDATION_ERROR` with no items for each.
- Controls: `?id=crm` returns that one app, no `id` returns the visible
list, `?id=` with a space still returns `200 []`, repeated `id` stays
400, `?id=` on `view` is still ignored; `?object=lead` still switches to
that object's views and no `object` still lists them all (asserted
inside the new object case).
</details>
<details><summary>Ablation (each leg red on both suites, then restored
to the HEAD blob)</summary>
- `id` leg (on 9246901): `--anchor " const appIdFilter = metaType ===
'app' ? query?.id : undefined;" --replacement " const appIdFilter =
metaType === 'app' && query?.id !== '' ? query?.id : ((globalThis as
any).ABLATE_28548 = undefined);"`, which puts back the old skip. Anchor
1 → 0, blob 24ba18a → acc770c9; rebuilt; preflight hit `dist/index.js`
+ `dist/index.cjs`. Red: the rest case (`caller holding [studio.access]:
expected 200 to be 400`) and runtime row A. Restored: blob == HEAD
24ba18a, `git diff HEAD` empty; rebuilt; `--absent` clean.
- `object` leg (on 9c45ac5): `--anchor " if (metaType === 'view' &&
query?.object === '') refuseEmptyListFilter('object', 'object',
'view');" --replacement " if ((globalThis as any).ABLATE_28548B === 'on'
&& metaType === 'view' && query?.object === '')
refuseEmptyListFilter('object', 'object', 'view');"`, which lets the
empty value fall back to the old truthiness skip. Anchor 1 → 0, blob
3530175 → 15af187f; rebuilt; preflight hit `dist/index.js` +
`dist/index.cjs`. Red: the rest object case (`view: expected 200 to be
400`, 1 failed / 26 passed) and runtime row A (1 failed / 934 passed).
Restored: blob == HEAD 3530175, `git diff HEAD` empty; rebuilt;
`--absent` clean over 6 built files, tree clean.
</details>
<details><summary>Package suites, typecheck, lint (head
9c45ac5)</summary>
- `pnpm --filter @objectstack/rest test` :: exit 0 · Test Files 362
passed (362) · Tests 6907 passed, 332 skipped
- `pnpm --filter @objectstack/runtime exec vitest run
src/domains/meta-list-projection-parity.test.ts
src/domains/dispatcher-query-arity.test.ts` :: exit 0 · 968 passed (the
parity file is the only runtime file changed; it is in the `repo`
project, which CI's Test Core runs)
- `pnpm --filter @objectstack/rest typecheck` :: exit 0
- `pnpm --filter @objectstack/runtime typecheck` :: exit 0
(`@objectstack/driver-turso` built first, a test-only import outside the
closure)
- `pnpm exec eslint --no-inline-config packages/rest packages/runtime`
:: exit 0
</details>
<details><summary>Dogfood battery (9c45ac5)</summary>
`pnpm exec turbo run build --concurrency=2
--filter='@objectstack/dogfood^...'` then `pnpm --filter
@objectstack/dogfood test` under `os-verify-lock.sh`, no shard, no path:
`VERDICT command-exit 0` · Test Files 364 passed, 3 skipped (367) ·
Tests 2934 passed, 28 skipped (2962) · no failures. (Also green on
9246901 with the same counts.)
</details>
## Acceptance notes
- Producers checked for an empty `id` or `object`:
`@objectstack/client`'s `meta.getItems(type, { packageId })` sends no
`id`; no file under `ui/packages`, `ui/apps`, `packages/client/src` or
`packages/cli/src` builds a `meta/app?id=` request, and the console
reads `meta.getItems('view')` with no query. The QA ledger rows under
`docs/qa/platform-checklist` that read `/meta/app?id=` all name an app;
none asserts the empty answer.
- The refusal body on `RestServer` is the flat `{ error, code }` its
route error door writes for a thrown refusal (the same door
`refuseUnknownMetaListType` uses on this route); the repeated-`id` 400
at the door entry writes the nested `{ error: { code, message } }`. Both
carry `VALIDATION_ERROR`. The dispatcher writes its own envelope with
the same `error.code`. Not changed here.
- `?id=` with a space keeps its answer (`200 []`): the ruling keeps
every non-empty `id` as it was. #27484 refused a whitespace-only
`$searchFields`, but there the value is split into names and trimmed; an
app name is compared whole.
- The empty `?object=` refusal was folded in on the seat's direction
(four adjacency conditions: same class, same file, mechanical, same gate
family); the claim's file surface already covered identical
empty-as-absent filters in this gate.
- Branch `claude/project-thread-cq2t0t` is the card thread's
harness-pinned branch, named in claim 6554674218; it carries no issue
number. Signature: `domain:server#3 · os-project-manager ·
session_01FEkQ5pQoUJCqKk6SJQb6Qx`.
- No open PR touched `packages/rest/src/meta-item-read-gate.ts` at the
claim's read.
## Verification
Local subset, head `9c45ac50`: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` derived 60 families; every
one run with its exit code captured before any pipe, all 60 `exit 0`
(`check:type-check-debt` under `os-verify-lock.sh`). `--ran` reconcile:
"60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN". Three first-pass readings
were not verdicts and were re-run once the parallel dogfood closure
build had settled: `check:dts-closure` (exit 1 while that build was
rewriting dists; 239/239 declaration files present on re-run),
`check:dual-build-cjs-loads` (exit 3, missing dists; passed after the
remaining seven were built) and `check:type-check-debt` (exit 99, lock
held by the dogfood battery). `check-plugin-teardown-shape.mjs
--self-test` ran after fetching its pinned fixture commit. The same 60
were all `exit 0` on 9246901. CI-run families: see this PR's
check-runs.
## 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 on the touched packages.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [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 summary
block is the dispatch contract's Chinese section).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5pQoUJCqKk6SJQb6Qx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent (#28602)1 parent 819b101 commit f3f7b62
4 files changed
Lines changed: 128 additions & 12 deletions
File tree
- .changeset
- packages
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
283 | 283 | | |
284 | 284 | | |
285 | 285 | | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
291 | 347 | | |
292 | 348 | | |
293 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3402 | 3402 | | |
3403 | 3403 | | |
3404 | 3404 | | |
| 3405 | + | |
| 3406 | + | |
| 3407 | + | |
| 3408 | + | |
| 3409 | + | |
| 3410 | + | |
| 3411 | + | |
| 3412 | + | |
| 3413 | + | |
| 3414 | + | |
| 3415 | + | |
| 3416 | + | |
| 3417 | + | |
| 3418 | + | |
| 3419 | + | |
| 3420 | + | |
| 3421 | + | |
| 3422 | + | |
| 3423 | + | |
| 3424 | + | |
| 3425 | + | |
| 3426 | + | |
| 3427 | + | |
| 3428 | + | |
| 3429 | + | |
| 3430 | + | |
3405 | 3431 | | |
3406 | 3432 | | |
3407 | 3433 | | |
| |||
3488 | 3514 | | |
3489 | 3515 | | |
3490 | 3516 | | |
3491 | | - | |
3492 | | - | |
3493 | | - | |
| 3517 | + | |
| 3518 | + | |
| 3519 | + | |
| 3520 | + | |
3494 | 3521 | | |
3495 | | - | |
| 3522 | + | |
| 3523 | + | |
3496 | 3524 | | |
3497 | 3525 | | |
3498 | 3526 | | |
| |||
3546 | 3574 | | |
3547 | 3575 | | |
3548 | 3576 | | |
3549 | | - | |
| 3577 | + | |
| 3578 | + | |
3550 | 3579 | | |
3551 | | - | |
| 3580 | + | |
| 3581 | + | |
3552 | 3582 | | |
3553 | 3583 | | |
3554 | 3584 | | |
| |||
3557 | 3587 | | |
3558 | 3588 | | |
3559 | 3589 | | |
3560 | | - | |
| 3590 | + | |
| 3591 | + | |
| 3592 | + | |
3561 | 3593 | | |
3562 | 3594 | | |
3563 | 3595 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
522 | 522 | | |
523 | 523 | | |
524 | 524 | | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
525 | 528 | | |
526 | 529 | | |
| 530 | + | |
| 531 | + | |
527 | 532 | | |
528 | 533 | | |
529 | 534 | | |
| |||
680 | 685 | | |
681 | 686 | | |
682 | 687 | | |
| 688 | + | |
| 689 | + | |
683 | 690 | | |
684 | 691 | | |
| 692 | + | |
| 693 | + | |
685 | 694 | | |
686 | 695 | | |
687 | 696 | | |
| |||
0 commit comments