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 d0f308c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(rest): the app metadata bundle serves its objects in the object list's order (#29752)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ88ufNEZsxoeAzp4WfYk5fTh)_Fixes#29680
Clause-②: no
Metadata-protocol: none
Claim: 6593195648 (seat `domain:server#1`). Triage designation
6593258467: one cross-domain PR in `domain:server` covering the rest fix
and the two `app-bundle.zod.ts` order sentences. **This unblocks #29552
leg 1**, whose PR waits on this one (`Blocked-by`).
## 维护者速读
- 改了什么:应用元数据包(`GET /meta/app/{appId}/bundle`)里的 `objects`
改按对象列表的顺序返回,不再是"导航先、反向关联后"。集合不变,只是顺序。spec 里该响应的文档注释和 `objects`
描述同步改成对象列表顺序,参考页按生成器重新生成。
- 为什么改:#29552
第一步让控制台的记录页从包里取对象范围。记录页的相关列表按拿到的对象顺序排,一个导航对象又引用另一个导航对象时(联系人→客户),页签顺序和从对象列表取时不同。
- 风险与回滚:导航顺序与对象列表顺序不一致的应用,包里 `objects`
的顺序会变;今天没有读者依赖这个顺序(界面按名字建索引)。导航顺序本就与列表一致的包逐字节不变。回滚即 revert。
- 先看:`packages/rest/src/meta-app-bundle-routes.ts` 的 `inObjectListOrder`
和调用处;`packages/spec/src/api/app-bundle.zod.ts`
的两句契约文字;`meta-app-bundle-routes.test.ts` 新增的 #29680 用例。
- 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run
src/meta-app-bundle-routes.test.ts`,预期 1 个文件、19 个用例全部通过。
- 需要你做的:无,席位落地(spec 路径,先过契约复审)。
## What changes
- **`packages/rest/src/meta-app-bundle-routes.ts`.** The bundle built
its objects as `members('object', [...reach.objects, ...inbound])`.
`inboundRelationObjects` skips the reach targets, so an inbound object
that is also a navigation object (`contact` referencing `account`) kept
its navigation place. The route now serves the same names through a new
exported `inObjectListOrder(list, names)`: every name the object list
holds, at its first position in that list (the list `objectList()`
already reads), then the names the list does not hold, in their own
(navigation) order. The set is unchanged, so views and pages, which
derive from the served set, do not move. Nothing else in the bundle
changes.
- **`packages/rest/src/rest-route-ledger.ts`.** The route's ledger note
now reads "objects: navigation plus one hop of inbound relationships, in
object-list order".
- **`packages/spec/src/api/app-bundle.zod.ts`.** Docs only, no key and
no shape change. The docblock's `objects` bullet said "in navigation
order; then ... in registry order"; it now says the navigation objects
and their inbound relations come "all in the object list's order; a name
the object list does not hold follows the listed ones, in navigation
order". The `objects` `.describe()` says "in the object list's order".
`content/docs/references/api/app-bundle.mdx` was regenerated with
`gen:schema` + `gen:docs`; no other tracked artefact moved.
- **Changeset** `.changeset/29680-app-bundle-object-list-order.md`:
`@objectstack/rest` patch, `@objectstack/spec` patch (the docblock ships
in `dist`).
**Readers of the bundle's `objects` array** (checked before editing):
`ui/packages/data-objectstack/src/app-bundle.ts` `indexAppBundle`
indexes every member by type and name into a `Map`; `packages/client`
`meta.getAppBundle` passes the body through. No reader relies on
reach-first order. Navigation reads the app's own entries, not this
array.
## Tests
`packages/rest/src/meta-app-bundle-routes.test.ts`:
- The three reach-first pins on the shared CRM fixture (admin bundle,
undetermined-visibility bundle, `enableCache: false` bundle) now expect
`account, contact, secret_deal, note, task, deal_line`. That fixture is
the card's case: `contact` is a navigation object that references
`account`.
- `inObjectListOrder` unit case, including names the list does not hold
(kept after, in their own order).
- An app whose navigation names `contact` then `account` (one references
the other): objects come back `account, contact, note, task`; views and
pages are the same set as before.
- CONTROL: an app whose navigation already follows the list (`account`
only): the served `objects` are byte-equal to the reach-first
derivation's item reads (`account, contact, note`).
<details><summary>Ablation: reach-first again, once, restored</summary>
Through `scripts/ablation-replace.mjs` (wrap mode, restore trap) under
`os-verify-lock`, on commit 8c59a53. The test imports the route from
`src` (relative import), so no build leg applies.
```
ablation-replace: anchor "members('object', inObjectListOrder(listed, [...reach.objects, ...inbound]))" x1 (before)
ablation-replace: replace "members('object', [...reach.objects, ...inbound])" x0 -> x1
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 76947ca -> ca6f86bfc9db
× admin: every reached member, objects in the object list's order (#29680), the body parsing against the declared shape
× two navigation objects where one references the other come back in object-list order
× a member served under an undetermined field visibility serves the bundle no-store with no validator
× `metadata.enableCache: false` sends no validator and answers a matching If-None-Match in full, as the item reads do
AssertionError: expected [ 'account', 'secret_deal', …(4) ] to deeply equal [ 'account', 'contact', …(4) ]
Tests 4 failed | 15 passed (19)
ablation-replace: ok restored: blob == HEAD (76947ca) and `git diff HEAD` is empty
```
The CONTROL stayed green under the mutation: reach-first and list order
agree on it.
</details>
## Acceptance notes
- The order sentence in the spec contract is the only contract text that
pinned reach-first; it changed in this PR with triage's designation, so
the server and its declared contract agree.
- `check:dev-prereqs` (local-only) exits 1 on this container because 35
workspace packages (mostly `ui/**`) are not built; `packages/spec/dist`
was rebuilt from this head before the gates.
## Verification
All on head 03d2096 (after merging `origin/main` 0eb97fa), under
`os-verify-lock` where heavy:
<details><summary>Local gates</summary>
```
node scripts/pm/dispatch-gates.mjs --ran FILE --repo objectstack-ai/objectstack
Run reconciliation — 103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 103 derived famil(ies) accounted for — 103 run, 0 NOT-MEASURED
pnpm lint :: exit 0
pnpm --filter @objectstack/rest typecheck :: exit 0
pnpm --filter @objectstack/rest test :: exit 0 Test Files 381 passed (381) · Tests 7133 passed | 332 skipped
pnpm --filter @objectstack/spec typecheck :: exit 0
pnpm --filter @objectstack/spec test :: exit 0 Test Files 803 passed (803) · Tests 23792 passed | 1 todo
pnpm check:dev-prereqs :: exit 1 workspace not fully built (local state, see Acceptance notes)
```
An earlier gate run on 8c59a53 caught an apostrophe that broke the
single-quoted ledger note (`check-comment-mask-corpus`,
`check:doc-authoring` and two siblings refused to parse the file);
commit 092ed98 fixed it.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind
(`inObjectListOrder`).
- [x] 3. No formatter ran; `pnpm lint` passes.
- [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: its generated artifacts are
regenerated and committed.
- [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 brief
is Chinese by the dispatch contract).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbWHJEZNQyk4aybUZm1
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbWHJEZNQyk4aybUZm1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(rest): the app metadata bundle serves its objects in the object list's order
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
`GET /meta/app/{appId}/bundle` listed its objects navigation-first, then the objects that reference them. An inbound object that is also a navigation object kept its navigation place, so a console that derives related lists from the bundle showed them in a different order than over the object list (contact before activities on an account record). The bundle now serves the same objects in the object list's order. Objects the list does not name keep their navigation order after the listed ones.
12
+
13
+
The bundle's contract in `@objectstack/spec` (`AppMetadataBundleResponseSchema`: the docblock and the `objects` description, and the reference page generated from them) now states the object list's order. The schema's shape does not change.
14
+
15
+
-**Unchanged.** Which objects, views, pages and dashboards the bundle holds, each member's envelope, the validator and the cache posture. A bundle whose navigation already follows the object list is served byte for byte as before.
Copy file name to clipboardExpand all lines: content/docs/references/api/app-bundle.mdx
+6-5Lines changed: 6 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,10 +29,11 @@ Which items the bundle holds is derived on the server, from the app the
29
29
caller is served (navigation already pruned to that caller):
30
30
31
31
-`objects` — every object an `object` navigation entry names (top-level
32
-
`navigation`, each area's `navigation`, and `children` at any depth), in
33
-
navigation order; then every object with a `lookup` or `master_detail`
34
-
field whose `reference` is one of those, in registry order. One hop: an
35
-
object related only to a related object is not included.
32
+
`navigation`, each area's `navigation`, and `children` at any depth), and
33
+
every object with a `lookup` or `master_detail` field whose `reference` is
34
+
one of those, all in the object list's order; a name the object list does
35
+
not hold follows the listed ones, in navigation order. One hop: an object
36
+
related only to a related object is not included.
36
37
-`views` — every view a navigation entry names by `viewName`, and every view
37
38
bound to an object in `objects`.
38
39
-`pages` — every page a `page` navigation entry names, and every page bound
@@ -73,7 +74,7 @@ const result = AppMetadataBundleResponseSchema.parse(data);
73
74
| Property | Type | Required | Description |
74
75
| :--- | :--- | :--- | :--- |
75
76
|**app**|`{ type: string; name: string; item: any; sortability?: object; … }`| ✅ | The app, as `GET /meta/app/{appId}` answers it to this caller (its navigation pruned to what the caller may open). |
76
-
|**objects**|`{ type: string; name: string; item: any; sortability?: object; … }[]`| ✅ | The objects the app's navigation reaches, then the objects whose relationship fields reference them, each as `GET /meta/object/{name}` answers it. An object the caller may not read is absent. |
77
+
|**objects**|`{ type: string; name: string; item: any; sortability?: object; … }[]`| ✅ | The objects the app's navigation reaches and the objects whose relationship fields reference them, in the object list's order, each as `GET /meta/object/{name}` answers it. An object the caller may not read is absent. |
77
78
|**views**|`{ type: string; name: string; item: any; sortability?: object; … }[]`| ✅ | The views the navigation names and the views bound to the bundled objects, each as `GET /meta/view/{name}` answers it. A view the caller may not read is absent. |
78
79
|**pages**|`{ type: string; name: string; item: any; sortability?: object; … }[]`| ✅ | The pages the navigation names and the pages bound to the bundled objects, each as `GET /meta/page/{name}` answers it. A page the caller may not read is absent. |
79
80
|**dashboards**|`{ type: string; name: string; item: any; sortability?: object; … }[]`| ✅ | The dashboards the navigation names, each as `GET /meta/dashboard/{name}` answers it. A dashboard the caller may not read is absent. |
note: 'The app metadata bundle: the app and the objects, views, pages and dashboards its navigation reaches (objects: navigation, then one hop of inbound relationships), each member the item chain\'s answer of its own GET /meta/:type/:name as the same caller (createMetaItemAnswer, the uncached arm\'s fetch), a refused member absent, a refused app the whole answer. Answers BARE, so the named schema is the whole body; per-caller weak ETag over the served body (the /meta reads\' shared validator), private, no-cache, 304 on If-None-Match. meta-app-bundle-routes.test.ts parses the body against the named schema on the mount'},
238
+
note: 'The app metadata bundle: the app and the objects, views, pages and dashboards its navigation reaches (objects: navigation plus one hop of inbound relationships, in object-list order), each member the item chain\'s answer of its own GET /meta/:type/:name as the same caller (createMetaItemAnswer, the uncached arm\'s fetch), a refused member absent, a refused app the whole answer. Answers BARE, so the named schema is the whole body; per-caller weak ETag over the served body (the /meta reads\' shared validator), private, no-cache, 304 on If-None-Match. meta-app-bundle-routes.test.ts parses the body against the named schema on the mount'},
239
239
// [#5882] The three-layer diagnostic projection, promoted from the
240
240
// `?layers=true` flag on the row below to a path of its own so that one path
241
241
// answers one response shape. `responseSchema` is filled because this mount
0 commit comments