Skip to content

Commit fbcbcf1

Browse files
fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) (#22265)
Fixes #22188 Clause-②: yes ## What this changes `?package=all` is the metadata list's "show everything" scope. #22128 (PR #22185) gave the item read, the save and the publish one reading of `?package=`: `metaItemPackageBinding`, under which `all`, the empty value and a non-string name no package. The other `/meta` doors forwarded the raw value, so `all` reached the store as a package id that no item is bound to. Every door that reads `?package=`, on both hosts, now reads it through that one function. - **`RestServer`** (`packages/rest/src/rest-server.ts`): the layered read (`serveMetaItemLayered`), the list (`GET /meta/:type`), the book tree (`GET /meta/book/:name/tree`) and the diagnostics sweep (`GET /meta/diagnostics`, see H3). - **The runtime dispatcher** (`packages/runtime/src/domains/meta.ts`, the `/meta` domain behind the `@objectstack/hono` `${prefix}/*` catch-all): its book tree, layered read, item branch (the read, the `?state=draft` read and the `PUT`) and list. - **Widened public surface (`Clause-②: yes`).** `@objectstack/rest`'s package entry now exports `metaItemPackageBinding`, and the runtime imports it beside `metaSaveRequestOptions`. That is the only new export. Changesets: `@objectstack/rest` `minor` with `Clause-②: yes (widening)`, and `@objectstack/runtime` `patch` with `Clause-②: no`. - Unchanged: a real package id still scopes each read and still binds a save. The item read's cache bypass (`packageScoped`, H3) still reads the raw parameter. ## Readings (H1 to H5, on this branch's base `ffb31fca`) **H1, reproduced on the real stack, each door.** The pin file below was run against the base sources before the fix. Each door has its own better-sqlite3 `:memory:` store with the real `sys_metadata*` objects and a real `ObjectStackProtocolImplementation`. The store is seeded through the protocol: `case_grid` in `com.probe.pkg`, `other_grid` in `com.other.pkg`, the env-local `local_grid`, and the book `probe_book` with the page `probe_page` in `com.probe.pkg`. | door | request | base | plain-read control (no `?package=`) | this branch | |:--|:--|:--|:--|:--| | `RestServer` | `GET /meta/view/case_grid/layers?package=all` | **404 `RESOURCE_NOT_FOUND`** | 200, overlay `live` | as the control | | `RestServer` | `GET /meta/view?package=all` | **`[]`** | `case_grid`, `local_grid`, `other_grid` | as the control | | `RestServer` | `GET /meta/book/probe_book/tree?package=all` | **200, the implicit book: label `probe_book`, no entries** | the declared book `Probe Book` with `probe_page` | as the control | | `RestServer` | `GET /meta/diagnostics?type=view&package=all` | **`scannedItems: 0`** | `scannedItems: 3`, both packages in `stats` | as the control | | `RestServer` | item read, `?state=draft` read, `PUT ?package=all` | as the control (folded by #22185) | | unchanged | | catch-all | `GET /meta/view/case_grid/layers?package=all` | **404 `RESOURCE_NOT_FOUND`** | 200, overlay `live` | as the control | | catch-all | `GET /meta/view?package=all` | **`[]`** | the three views | as the control | | catch-all | `GET /meta/book/probe_book/tree?package=all` | **the implicit book, no entries** | the declared book | as the control | | catch-all | `GET /meta/view/case_grid?package=all` | **200, but the item is decorated `_provenance: 'org'` with no `_packageId`** | `_packageId: 'com.probe.pkg'` | as the control | | catch-all | `GET /meta/view/case_grid?state=draft&package=all` | **404 `NO_DRAFT`** | 200, the draft | as the control | | catch-all | `PUT /meta/view/fresh_grid?package=all` | **row `package_id: 'all'`** (the seat's pointer re-measured) | `RestServer`: `package_id` null | `package_id` null | | catch-all | `POST /meta/view/case_grid/publish?package=all` | 404 `ROUTE_NOT_FOUND`, zero protocol calls | | unchanged: the dispatcher serves no publish, so it has no `?package=` to read | **H2, what "no package" means at the list.** Measured on both hosts: `GET /meta/view` without `?package=` lists `case_grid` (in `com.probe.pkg`), `other_grid` (in `com.other.pkg`) and `local_grid` (env-local). No package already means every package, so `metaItemPackageBinding` is the right fold for the list. The list needs no reading of `all` of its own. **H3, `:5807` and `:6582`** (line numbers on `31d24a52`). - `:5807` is `GET /meta/diagnostics`, the cross-type spec-validation sweep. It hands `?package=` to `getMetaDiagnostics`, which runs `getMetaItems({ type, packageId })` for every type it sweeps. That is a list read keyed by package, so it is in the family. `all` reached it as a literal (`scannedItems: 0`, above), so it is folded and pinned. objectui at the `.objectui-sha` pin (`a58626c8`) sends that door's `?package=` only after validating it against the project's packages (`DirectoryPage.tsx`, `StudioHomePage.tsx`), so no first-party caller sends `all` there. - `:6582` is the item read's cache bypass, `packageScoped`. It is not a read keyed by package: it asks whether the raw parameter is present, to pick the uncached arm, and since #22185 that arm reads through the fold. So `all` reaches it as a literal by design, and `?package=all` keeps the uncached arm and its served `version`. Folding it would move `?package=all` into the cached arm, which serves no `version`. It is not changed here, and the fold's docblock now says why. **H4, the export.** Kept in `rest-server.ts` and exported from the package entry: `export { metaItemPackageBinding } from './rest-server.js'`. Not moved beside `metaSaveRequestOptions`, for two reasons. - The fold serves read doors (the layered read, the list, the book tree, the sweep and the item read) as well as the save and the publish. `meta-save-request.ts`'s header scopes that module to the save's precondition and lifecycle, so the fold does not belong there. - The entry already loads `rest-server.js` for `RestServer`, so the export adds no module to the entry's closure. **H5, Studio reach (triage's p1 condition): the save half does not hold.** Read at objectui `a58626c8`, `packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx`: - The save and the publish fold `all` themselves (`readActivePackageBinding`, lines 248 to 255, used by `doSave` at 1493 to 1498 and `doPublish` at 1685 to 1688), so Studio's save omits `?package=` under the "show everything" scope. - The layered and draft reads send the raw router `?package=` (`ownerPackageId`, lines 433, 1009 to 1015 and 1502 to 1505), so they send `?package=all`. Through the catch-all they reach the dispatcher's layered and item branches, which this PR folds. - Whether the hosted runtime serves `/meta` through that catch-all: **NOT MEASURED** from this session (objectstack-ai/cloud is not readable here). ## Pins (`packages/runtime/src/domains/meta-package-all-fold.test.ts`) One file drives both hosts over real stores, as `meta-save-preconditions-parity.test.ts` does. The `RestServer` leg calls its registered handlers. The catch-all leg repeats the catch-all's four statements over a real Fetch `Request` into the real `HttpDispatcher.dispatch()`. Each row compares a door's answer to `?package=all` with the same door's answer without `?package=`, and keeps a scoping control beside it. - Per host: the layered read (control: `?package=com.probe.pkg` serves the same layers, `?package=com.other.pkg` answers 404), the list (control: each real package id lists only its own view), the book tree (control: the owning package resolves the declared book, another package falls to its implicit book), the item read, the `?state=draft` read of a draft that inherited the item's package, and the save (control: `PUT ?package=com.probe.pkg` binds the row). - `RestServer` alone: the diagnostics sweep (control: a real package id sweeps fewer items). ## Reverse verification and ablations The fix was committed first. Every leg was restored from `HEAD` and proven by blob hash and an empty `git diff HEAD`. The subject resolves by relative source import (`meta.ts`) and `@objectstack/rest` through the runtime vitest alias to source, so no `dist/` leg applies. | leg | result | rows red | |:--|:--|:--| | this branch | 13 passed | none | | base `rest-server.ts`, `index.ts` and `meta.ts` in the tree | 10 failed, 3 passed | every row except `RestServer`'s item read, draft read and save, which #22185 already folded | | ablation A: `RestServer`'s layered read back to `req.query?.package or undefined` | 1 failed, 12 passed | `RestServer`'s layered read | | ablation B: the dispatcher's item branch back to the raw read | 3 failed, 10 passed | the catch-all's item read, draft read and save | The ablations went through `node scripts/ablation-replace.mjs`. A: anchor x1 to x0, blob `4f3ef5968792` to `905c48c396c2`, restored to `4f3ef5968792` == HEAD. B: anchor x1 to x0, blob `b21893a63ef8` to `88f1ec5f41a7`, restored to `b21893a63ef8` == HEAD. ## Verification All at this branch's head `db727d0d` (`git rev-parse --short HEAD`), base `ffb31fca`, except where a line names `a13568b4` (the fix commit; `db727d0d` adds only the draft-read row to the pin file). Heavy runs went through `scripts/pm/os-verify-lock.sh`. - **Build.** `pnpm --filter '@objectstack/runtime^...' build` (the dependency closure), then `@objectstack/rest` and `@objectstack/runtime` rebuilt at head: exit 0. The built entries load: `require('packages/rest/dist/index.cjs')` gives 35 exports, `metaItemPackageBinding` among them (it answers `undefined` for `all`, the empty value and an array, and `com.probe.pkg` for `com.probe.pkg`); the ESM entry exports it too; `require('packages/runtime/dist/index.cjs')` gives 312 exports. - **Typecheck.** `pnpm --filter @objectstack/rest typecheck`: exit 0. `pnpm --filter @objectstack/runtime typecheck`: exit 0 after the rest rebuild (before it, the stale `dist/index.d.ts` answered TS2305 for the new export, as expected). The test layer printed `check:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held`, ledger unchanged. The new pin file is in that program (`tsc -p tsconfig.test.json --listFiles` lists it) with no error of its own. - **Unit tiers, at `a13568b4`.** `pnpm --filter @objectstack/rest exec vitest run --project local`: 262 files passed, 4938 passed, 326 skipped. `pnpm --filter @objectstack/runtime exec vitest run --project local`: 336 files passed, 4747 passed, 19 skipped. The `repo` projects are CI's, except `meta-list-projection-parity.test.ts`, which reads `rest-server.ts`'s handler source and was run by name. - **Named pins, at `db727d0d`.** `meta-package-all-fold.test.ts` 13 passed; with `meta-save-preconditions-parity.test.ts`, 26 passed. `meta-draft-head-package-inheritance.test.ts` and `meta-draft-read-door-census.test.ts`: 13 passed. `meta-list-projection-parity.test.ts` (`--project repo`): 682 passed. - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `db727d0d` derives 64 commands (6 paths, 420 changed lines, under the 5000 threshold), the same 64 the order named. Each was run with its exit code recorded. `--ran` reconciliation: `64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED`, 0 unrun. 63 exit 0. The full `pnpm lint` (`eslint . --no-inline-config`, whole repository) exits 0 with no findings. - **NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: exit 3 `PREREQUISITE NOT MET`** (its self-test passed, 93 cases). It reads every package's `dist/`, and 36 packages are unbuilt in this worktree. The declared narrowing in its place is the CJS and ESM entry loads of the two packages whose shipped code changed (above). CI runs the full gate. - **Upstream.** `origin/main` is 5 commits past the base (`9f0de32a`). None touches `packages/rest`, `packages/runtime` or `packages/metadata-protocol`, so no merge of `main` was taken. ## Acceptance notes - **Repeated `?package` on the dispatcher.** The dispatcher's `/meta` domain does not judge query multiplicity, as #22141 recorded. The catch-all flattens its query to one string per name, so this changes nothing there. A Node host that hands an array used to forward the array to the store as the package id; it now reads it as naming no package, as `RestServer`'s fold reads a non-string. - **`GET /meta/_drafts?packageId=`** (`RestServer` and the dispatcher) reads a differently named parameter, `packageId`, and forwards it raw. It is not a `?package=` reader, and no caller sending `all` there was measured, so it is not in this family and is not changed here. - The dispatcher serves no `POST /meta/:type/:name/publish` (404 `ROUTE_NOT_FOUND`, zero protocol calls), so it has no publish door to fold. Written by session `session_01RWZbGvPFcRKvUqASZtunCU`. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b997ea commit fbcbcf1

6 files changed

Lines changed: 409 additions & 11 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/rest": minor
3+
---
4+
5+
`metaItemPackageBinding` is exported, and every `/meta` door that reads `?package=` reads it through that function, so `?package=all` names no package on the layered read, the list, the book tree and the diagnostics sweep
6+
7+
Clause-②: yes (widening)
8+
9+
- `metaItemPackageBinding(raw)` answers the package a request's `?package=` names, or `undefined` for `all` (the metadata list's "show everything" scope), the empty value or a non-string. The item read, the save and the publish already read `?package=` through it. The runtime dispatcher's `/meta` domain now reads it through the same function.
10+
- `GET /meta/:type/:name/layers?package=all` answered `404` for an item stored in a package, and now serves the layers that the read without `?package=` serves. The Studio editor sends this read when it is opened from the list's "show everything" scope.
11+
- `GET /meta/:type?package=all` answered `[]`, and now lists what `GET /meta/:type` lists: the items of every package and the env-local ones.
12+
- `GET /meta/book/:name/tree?package=all` served the empty implicit book of a package called `all`, and now resolves the declared book as the read without `?package=` does.
13+
- `GET /meta/diagnostics?package=all` swept nothing, and now sweeps what the plain sweep sweeps.
14+
- Unchanged: a real package id still scopes each of these reads. The item read's cache bypass still reads the raw parameter, so `?package=all` keeps the uncached read and its served `version`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
The runtime dispatcher's `/meta` domain reads `?package=all` as naming no package, as `RestServer` does, so a `PUT ?package=all` through the `@objectstack/hono` catch-all no longer binds the row to a package called `all`
6+
7+
Clause-②: no
8+
9+
- The dispatcher's `PUT /meta/:type/:name?package=all` stored the row with `package_id: 'all'`. It now stores it env-local (`package_id` null), as `RestServer`'s `PUT` does.
10+
- Its layered read (`/meta/:type/:name/layers`), list (`/meta/:type`), book tree (`/meta/book/:name/tree`) and item read forwarded `all` to the store as a package id. The layered read answered `404` and the list `[]` for an item stored in a package. Each now answers `?package=all` as it answers the same request without `?package=`.
11+
- Every branch reads `?package=` through `metaItemPackageBinding` from `@objectstack/rest`, the function `RestServer`'s doors read it through.
12+
- Unchanged: a real package id still scopes each read and still binds a save.

‎packages/rest/src/index.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,13 @@ export type {
183183
MetaSaveRequestMembers,
184184
MetaSaveRequestOptions,
185185
} from './meta-save-request.js';
186+
187+
// [#22188] …and the one reading of `?package=` on every `/meta` door: the
188+
// package a request names, or none for `all` (the list's "show everything"
189+
// scope), the empty value or a non-string. `RestServer`'s doors read it, and so
190+
// does the runtime dispatcher's `/meta` domain on its own copies of them — the
191+
// layered read, the list, the book tree and the item read and save — which
192+
// forwarded `all` to the store as a package id until this landed. It stays in
193+
// `rest-server.ts`, beside the doors that read it (its docblock is the
194+
// authority); this entry already loads that module.
195+
export { metaItemPackageBinding } from './rest-server.js';

‎packages/rest/src/rest-server.ts‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,8 +1736,20 @@ function mayReadPendingDrafts(caller: unknown): boolean {
17361736
* save then judged, and a client that pinned `If-None-Match: *` on that `null`
17371737
* was refused. ⛔ Never a third inline copy at a door: the save and publish
17381738
* doors each carried one, and the read door had none.
1739+
*
1740+
* [#22188] …and on every other `/meta` door that reads `?package=`: the
1741+
* layered read, the list, the book tree and the diagnostics sweep, which each
1742+
* forwarded `all` to the store as a package id no item is bound to (the
1743+
* layered read answered `404`, the list `[]`). Without `?package=`, the list
1744+
* already spans every package and the env-local overlay, so "no package" is
1745+
* the list's "show everything". Exported from the package entry, and the
1746+
* runtime dispatcher's `/meta` domain reads `?package=` through it on its own
1747+
* copies of these doors, so the two transports cannot read `all` differently.
1748+
* The item read's cache bypass (`packageScoped`) is not a second reading: it
1749+
* asks whether the raw parameter is present to pick the uncached arm, and that
1750+
* arm asks this function.
17391751
*/
1740-
function metaItemPackageBinding(raw: unknown): string | undefined {
1752+
export function metaItemPackageBinding(raw: unknown): string | undefined {
17411753
return typeof raw === 'string' && raw !== '' && raw !== 'all' ? raw : undefined;
17421754
}
17431755

@@ -3920,7 +3932,8 @@ export class RestServer {
39203932
// `getMetaItemLayered({ packageId: ['a','b'] })`. Gated in the helper,
39213933
// not in its two callers, so both entry points answer identically.
39223934
if (refuseRepeatedQueryParams(req, res, ['package'])) return;
3923-
const layeredPackageId = req.query?.package || undefined;
3935+
// [#22188] Read through {@link metaItemPackageBinding}: `all` names no package.
3936+
const layeredPackageId = metaItemPackageBinding(req.query?.package);
39243937
// [#9454] State the ORG scope, exactly as the `/published` overlay read
39253938
// already does. Without it the layered view resolved the env-wide row
39263939
// only, so an author who had just saved an org overlay opened Studio to
@@ -5804,7 +5817,9 @@ export class RestServer {
58045817
const result = await (p as any).getMetaDiagnostics({
58055818
type: diagnosticsType,
58065819
severity,
5807-
packageId: (req.query?.package as string | undefined) || undefined,
5820+
// [#22188] The list's reading ({@link metaItemPackageBinding}):
5821+
// each swept type is a list read, so `all` names no package.
5822+
packageId: metaItemPackageBinding(req.query?.package),
58085823
// SPREAD, never `organizationId: x ?? null` — the
58095824
// implementation declares `organizationId?: string`
58105825
// (optional plain string, not nullable), and a
@@ -6045,7 +6060,9 @@ export class RestServer {
60456060
// `query-multiplicity.ts` for why picking one of two
60466061
// conflicting intents is worse than a 400.
60476062
if (refuseRepeatedQueryParams(req, res, ['package', 'preview', 'object', 'include', 'id'])) return;
6048-
const packageId = req.query?.package || undefined;
6063+
// [#22188] Read through {@link metaItemPackageBinding}:
6064+
// `all`, this list's "show everything" scope, names no package.
6065+
const packageId = metaItemPackageBinding(req.query?.package);
60496066
const environmentId = isScoped ? req.params?.environmentId : undefined;
60506067
const p = await this.resolveProtocol(environmentId, req);
60516068
// [#9488] …and BEFORE any listing work: a `:type` that
@@ -6379,7 +6396,8 @@ export class RestServer {
63796396
const prot = await this.resolveProtocol(environmentId, req);
63806397
// [#6877] One package scopes the book lookup.
63816398
if (refuseRepeatedQueryParams(req, res, ['package'])) return;
6382-
const packageId = req.query?.package || undefined;
6399+
// [#22188] Read through {@link metaItemPackageBinding}: `all` names no package.
6400+
const packageId = metaItemPackageBinding(req.query?.package);
63836401
// [#20408] The route's whole answer is
63846402
// `createMetaBookTreeAnswer` in `./meta-item-read-gate.ts`
63856403
// — the book and doc reads, THE `DocsAudience` (#19790: one

0 commit comments

Comments
 (0)