Repository navigation
Commit 4702c46
fix(metadata-protocol)!: the Data API refuses an empty projection and a count flag that is neither true nor false (#26811)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XFuFUYz5AqM65JYxwmKQC8f)_
Fixes #26644
Clause-②: no (narrowing)
Metadata-protocol: none
**BREAKING** (accept-set narrowing, `patch`): the Data API refuses an
empty projection and a count flag that is neither true nor false with
`400 VALIDATION_ERROR`, where it answered 200. ADR-0087 disposition in
the changeset: `not-required (no-migration-prescription)`.
## 维护者速读
- **改了什么**:数据列表接口遇到空的 `$select`(如 `?$select=`、`?$select=,`)或不是
true/false 的 `$count`(如 `?$count=maybe`),现在直接回 400 并点名参数和取值,不再当作「没传」处理。
- **为什么改**:以前空 `$select` 会返回全部字段,乱写的 `$count` 会照样统计总数,调用方拿到看似正常的
200,发现不了自己写错了。
- **风险与回滚**:只有本来就传了错值的调用会从 200 变 400;不传参数、传合法值的请求行为不变。回滚只需还原
`packages/metadata-protocol/src/protocol.ts` 的这次改动。
-
**先看**:`packages/metadata-protocol/src/protocol.ts`(`projectionFieldsValue`、`countFlagValue`
两个函数及 `findData`/`getData`
里的调用点)、`packages/rest/src/data-empty-projection-count-door.test.ts`、`packages/runtime/src/domains/data-empty-projection-count.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/metadata-protocol exec vitest
run src/protocol.empty-projection-unknown-count.test.ts`,预期 37 个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
- **`findData` (the list normalizer every list door shares).** A
projection (`fields`, and its folded `select` / `$select` spellings)
that names no field is refused: a blank or comma-only string, or an
empty array. It used to become `[]`, which `assertProjectionFieldsExist`
has nothing to check and every driver serves as no projection, so every
column came back under a 200. A `count` / `$count` that is not the
boolean or the string `true` / `false` is refused; it used to stay
unlowered, so the `options.count === false` opt-out never matched and
the count ran. Both refusals are `400 VALIDATION_ERROR` with `param` set
to the spelling the caller wrote and a message quoting the value, thrown
before the engine is asked.
- **The accepted count values are read from the spec, not restated.**
`countFlagValue` parses with `QueryTransportParamsSchema.shape.count`
(`@objectstack/spec/data`): the boolean, the strings `true` / `false`,
and `null` (absent). No `packages/spec` file is edited.
- **`getData` (`GET /data/:object/:id`) takes the same projection
rule.** Its comment promised "gated exactly as on the list path";
refusing the empty projection on the list alone would have made that
false. A blank `select` was falsy and skipped, a comma-only one became
`[]`; both answered every column. It now asks the same helper on any
value present.
- **Unchanged.** Absent parameters keep their defaults (every column,
the count runs). A named projection, with trailing commas and spaces
trimmed as before, `$count=true`, `$count=false` and a JSON `null`
answer as before.
Which doors reach the refusal (measured by reading every `findData(` /
`getData(` caller on `origin/main` `0f3d068cf1`, and pinned per door
below):
| door | normalizer | empty projection | `$count=maybe` |
|:--|:--|:--|:--|
| `RestServer` `GET /data/:object` | `findData` | 400 `VALIDATION_ERROR`
| 400 `VALIDATION_ERROR` |
| `RestServer` `POST /data/:object/query` | `findData` (after
`FindDataRequestSchema`) | 400 `VALIDATION_ERROR` | already 400
`VALIDATION_FAILED` at the body schema, unchanged |
| runtime dispatcher `GET /data/:object` | `findData` (via
`callData('query')`) | 400 `VALIDATION_ERROR` | 400 `VALIDATION_ERROR` |
| runtime dispatcher `POST /data/:object/query` | `findData` (after
`FindDataRequestSchema`) | 400 `VALIDATION_ERROR` | the same
`FindDataRequestSchema` gate as REST (not separately measured) |
| `RestServer` `GET /data/:object/:id` | `getData` | `VALIDATION_ERROR`
at **404** (see Acceptance notes) | n/a |
| runtime dispatcher `GET /data/:object/:id` | `getData` (via
`callData('get')`) | 400 `VALIDATION_ERROR` | n/a |
The in-process callers (`data-action-routes.ts` job and export reads,
`import-runner.ts`, `connector-pull.ts`, plugin-security, plugin-auth's
user import) pass no projection or a named one; none sends an empty one
or a count flag.
## Tests
-
`packages/metadata-protocol/src/protocol.empty-projection-unknown-count.test.ts`
(new, 37 cases): every refused spelling with `code`, `status`, `param`
and the engine never reached; the preservation set (projection lists,
`count` absent / true / false / null) green in both directions.
- `packages/rest/src/data-empty-projection-count-door.test.ts` (new):
both REST list routes and the by-id route over a real `SqlDriver`
(SQLite): status, `code`, the named spelling, zero reads of the object,
and the controls.
- `packages/runtime/src/domains/data-empty-projection-count.test.ts`
(new): the dispatcher's three doors through `HttpDispatcher.dispatch()`
over a real store; a refusal leaves `dispatch()` and both host exits
(`declaredEnvelopeForThrow`, `resolveThrownHttpError`) are asked and
must agree.
- Red first, on the base `0f3d068cf1` with every closure built from it:
protocol 21 failed / 16 passed (all 21 refusals, every preservation
green); REST 3 failed / 2 passed; dispatcher 8 failed / 1 passed (every
refusal answered 200 with every column, or a `total`).
- `scripts/engine-double-contract.pinned.json`: the new by-id double's
`findOne` opens with `assertEngineFindOnePredicate`; `--write` recorded
it.
## Acceptance notes
- **REST by-id answers 404, not 400.** `crud-routes.ts`'s `GET
/data/:object/:id` catch rewrites every 400 to 404 ("a bad id is a
miss"), so every projection refusal on that route, an unknown `select`
field included (`?select=nope` answers 404 `INVALID_FIELD`, measured in
the door test), already answers 404. The empty projection joins that
answer with code `VALIDATION_ERROR`; the dispatcher's by-id door answers
400. Changing the rewrite is outside this card (reported).
- **The spec's HTTP declaration is wider than the door.**
`HttpFindQueryParamsSchema.count` is `z.coerce.boolean()` (it reads
`'false'` as `true` and admits `'maybe'`), and its `select` is
`z.string()` (admits a blank). It has no runtime reader in this tree; it
ships as `json-schema/api/HttpFindQueryParams.json` and the reference
docs. The door reads `QueryTransportParamsSchema.count`, which is exact.
`packages/spec` is outside this lane (reported).
- **A client SDK caller passing `select: []`**
(`packages/client/src/index.ts` serialises it as `select=`) now gets the
400 instead of every column. That is the refusal working as intended; no
in-repo caller passes an empty list.
- **Code choice.** The blank `$top` refusal this mirrors answers
`INVALID_REQUEST` (a ledger code); these answer the standard-catalog
`VALIDATION_ERROR`, as the dispatch ruled.
## Verification
All readings at the final head `28a10e6f8b` unless stated; exit codes
captured before any pipe; heavy runs under
`scripts/pm/os-verify-lock.sh` (its `VERDICT` line quoted).
<details><summary>Package suites and typecheck</summary>
- `pnpm build` (full, at `27848a9b`, the fix's source identical to the
head): `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/metadata-protocol test`: exit 0 · Test
Files 270 passed, 3 skipped · Tests 21653 passed, 19 skipped.
`typecheck`: exit 0. The new file re-run at the head after its double
change: 37/37.
- `pnpm --filter @objectstack/rest test`: exit 0 · Test Files 327 passed
· Tests 6503 passed, 326 skipped. `typecheck`: exit 0.
- `pnpm --filter @objectstack/runtime test`: exit 0 · Test Files 460
passed · Tests 7457 passed, 19 skipped. `typecheck` (with
`check:test-typecheck`) at the head: exit 0; the new door file 9/9.
- `eslint --no-inline-config packages/metadata-protocol packages/rest
packages/runtime`: exit 0.
</details>
<details><summary>Red first, on the base `0f3d068cf1`</summary>
- `protocol.empty-projection-unknown-count.test.ts`: Tests 21 failed, 16
passed — every refusal red (accepted, answered), every preservation
green.
- `data-empty-projection-count-door.test.ts`: 3 failed, 2 passed — e.g.
`$select= blank (the card)`: 200 with all columns `label`, `note` and
the system columns; `select=""` by id: 200, whole record.
- `data-empty-projection-count.test.ts` (dispatcher): 8 failed, 1 passed
— each refused request answered
`{"status":200,"body":{"success":true,…}}`, every column or a `total`.
</details>
<details><summary>Dogfood (the narrowing arm)</summary>
- `pnpm --filter @objectstack/dogfood test` at `25a8556236`: `VERDICT
command-exit 0` · Test Files 302 passed, 3 skipped · Tests 2507 passed,
20 skipped · no failures. The head `28a10e6f8b` differs from it only in
`packages/runtime/src/domains/data-empty-projection-count.test.ts` (a
type annotation in a test outside the dogfood suite).
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, then every family run at `28a10e6f8b`: 70
derived, 70 run, all exit 0. `--ran` reconciliation: `70 derived
famil(ies) accounted for — 70 run, 0 NOT-MEASURED`.
- One red on the first pass, fixed: `check:engine-double-contract`
pinned the new by-id double's `findOne` (now opens with
`assertEngineFindOnePredicate`; ledger recorded by `--write`).
- `test:repo` (vitest `repo` projects) not run locally: no repo-project
test or census file is in the diff; CI `Test Core` runs it.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d8b943b commit 4702c46
6 files changed
Lines changed: 683 additions & 11 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- rest/src
- runtime/src/domains
- scripts
| 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 | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| 159 | + | |
159 | 160 | | |
160 | 161 | | |
161 | 162 | | |
| |||
4621 | 4622 | | |
4622 | 4623 | | |
4623 | 4624 | | |
| 4625 | + | |
| 4626 | + | |
| 4627 | + | |
| 4628 | + | |
| 4629 | + | |
| 4630 | + | |
| 4631 | + | |
| 4632 | + | |
| 4633 | + | |
| 4634 | + | |
| 4635 | + | |
| 4636 | + | |
| 4637 | + | |
| 4638 | + | |
| 4639 | + | |
| 4640 | + | |
| 4641 | + | |
| 4642 | + | |
| 4643 | + | |
| 4644 | + | |
| 4645 | + | |
| 4646 | + | |
| 4647 | + | |
| 4648 | + | |
| 4649 | + | |
| 4650 | + | |
| 4651 | + | |
| 4652 | + | |
| 4653 | + | |
| 4654 | + | |
| 4655 | + | |
| 4656 | + | |
| 4657 | + | |
| 4658 | + | |
| 4659 | + | |
| 4660 | + | |
| 4661 | + | |
| 4662 | + | |
| 4663 | + | |
| 4664 | + | |
| 4665 | + | |
| 4666 | + | |
| 4667 | + | |
| 4668 | + | |
| 4669 | + | |
| 4670 | + | |
| 4671 | + | |
| 4672 | + | |
| 4673 | + | |
| 4674 | + | |
| 4675 | + | |
| 4676 | + | |
| 4677 | + | |
| 4678 | + | |
| 4679 | + | |
| 4680 | + | |
| 4681 | + | |
| 4682 | + | |
| 4683 | + | |
| 4684 | + | |
| 4685 | + | |
| 4686 | + | |
| 4687 | + | |
| 4688 | + | |
| 4689 | + | |
| 4690 | + | |
| 4691 | + | |
4624 | 4692 | | |
4625 | 4693 | | |
4626 | 4694 | | |
| |||
12936 | 13004 | | |
12937 | 13005 | | |
12938 | 13006 | | |
| 13007 | + | |
| 13008 | + | |
| 13009 | + | |
| 13010 | + | |
12939 | 13011 | | |
12940 | | - | |
12941 | | - | |
12942 | | - | |
| 13012 | + | |
| 13013 | + | |
| 13014 | + | |
12943 | 13015 | | |
12944 | 13016 | | |
12945 | 13017 | | |
| |||
13113 | 13185 | | |
13114 | 13186 | | |
13115 | 13187 | | |
13116 | | - | |
13117 | | - | |
13118 | | - | |
13119 | | - | |
13120 | | - | |
| 13188 | + | |
| 13189 | + | |
| 13190 | + | |
| 13191 | + | |
| 13192 | + | |
| 13193 | + | |
13121 | 13194 | | |
13122 | 13195 | | |
13123 | 13196 | | |
| |||
13511 | 13584 | | |
13512 | 13585 | | |
13513 | 13586 | | |
13514 | | - | |
13515 | | - | |
13516 | | - | |
| 13587 | + | |
| 13588 | + | |
| 13589 | + | |
| 13590 | + | |
| 13591 | + | |
| 13592 | + | |
| 13593 | + | |
13517 | 13594 | | |
13518 | 13595 | | |
13519 | 13596 | | |
| |||
0 commit comments