Skip to content

Commit d0f308c

Browse 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>
1 parent 6f09464 commit d0f308c

6 files changed

Lines changed: 109 additions & 19 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
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.

‎content/docs/references/api/app-bundle.mdx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ Which items the bundle holds is derived on the server, from the app the
2929
caller is served (navigation already pruned to that caller):
3030

3131
- `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.
3637
- `views` — every view a navigation entry names by `viewName`, and every view
3738
bound to an object in `objects`.
3839
- `pages` — every page a `page` navigation entry names, and every page bound
@@ -73,7 +74,7 @@ const result = AppMetadataBundleResponseSchema.parse(data);
7374
| Property | Type | Required | Description |
7475
| :--- | :--- | :--- | :--- |
7576
| **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. |
7778
| **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. |
7879
| **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. |
7980
| **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. |

‎packages/rest/src/meta-app-bundle-routes.test.ts‎

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { PerfTiming, runWithPerfTiming } from '@objectstack/observability';
2020
import { representationEtag } from '@objectstack/types';
2121
// Explicit `.js` extension: NodeNext resolution (see the sibling meta tests).
2222
import { RestServer } from './rest-server.js';
23-
import { appNavigationReach, inboundRelationObjects, META_APP_BUNDLE_PARAMS } from './meta-app-bundle-routes.js';
23+
import { appNavigationReach, inboundRelationObjects, inObjectListOrder, META_APP_BUNDLE_PARAMS } from './meta-app-bundle-routes.js';
2424

2525
// ── Fixtures ──────────────────────────────────────────────────────────────────
2626

@@ -176,6 +176,12 @@ describe('[#27627] which items an app reaches', () => {
176176
expect(inboundRelationObjects(OBJECTS, [])).toEqual([]);
177177
});
178178

179+
it('[#29680] orders names as the object list does, names it does not list after, in their own order', () => {
180+
expect(inObjectListOrder(OBJECTS, ['secret_deal', 'contact', 'account', 'note'])).toEqual(['account', 'contact', 'secret_deal', 'note']);
181+
expect(inObjectListOrder(OBJECTS, ['ghost', 'contact', 'phantom', 'account'])).toEqual(['account', 'contact', 'ghost', 'phantom']);
182+
expect(inObjectListOrder(OBJECTS, [])).toEqual([]);
183+
});
184+
179185
it('declares an EMPTY closed query set', () => {
180186
expect(META_APP_BUNDLE_PARAMS).toEqual([]);
181187
});
@@ -184,12 +190,12 @@ describe('[#27627] which items an app reaches', () => {
184190
// ── The bundle ────────────────────────────────────────────────────────────────
185191

186192
describe('[#27627] one request answers the app, its reached objects, views, pages and dashboards', () => {
187-
it('admin: every reached member, in derivation order, the body parsing against the declared shape', async () => {
193+
it('admin: every reached member, objects in the object list\'s order (#29680), the body parsing against the declared shape', async () => {
188194
const { res } = await bundle('admin');
189195
expect(res.statusCode).toBe(200);
190196
expect(AppMetadataBundleResponseSchema.safeParse(res.body).success).toBe(true);
191197
expect(res.body.app.name).toBe('crm');
192-
expect(namesOf(res.body.objects)).toEqual(['account', 'secret_deal', 'contact', 'note', 'task', 'deal_line']);
198+
expect(namesOf(res.body.objects)).toEqual(['account', 'contact', 'secret_deal', 'note', 'task', 'deal_line']);
193199
expect(namesOf(res.body.views)).toEqual(['account.all', 'account.legal', 'contact.mine']);
194200
expect(namesOf(res.body.pages)).toEqual(['home_page', 'account_record']);
195201
expect(namesOf(res.body.dashboards)).toEqual(['pipeline', 'legal_board']);
@@ -240,6 +246,53 @@ describe('[#27627] one request answers the app, its reached objects, views, page
240246
});
241247
});
242248

249+
// ── [#29680] The objects, in the object list's order ──────────────────────────
250+
251+
describe('[#29680] the bundle serves its objects in the object list\'s order, not navigation-reach first', () => {
252+
// `contact` references `account`; the navigation names `contact` first.
253+
const REVERSED_APP = {
254+
name: 'service',
255+
label: 'Service',
256+
navigation: [
257+
{ id: 'nav_contacts', type: 'object', objectName: 'contact' },
258+
{ id: 'nav_accounts', type: 'object', objectName: 'account' },
259+
],
260+
};
261+
// CONTROL: the navigation already follows the object list, so reach-first and list order agree.
262+
const IN_ORDER_APP = {
263+
name: 'accounts',
264+
label: 'Accounts',
265+
navigation: [{ id: 'nav_accounts', type: 'object', objectName: 'account' }],
266+
};
267+
const data = () => ({ ...store(), app: [...store().app, REVERSED_APP, IN_ORDER_APP] });
268+
/** The order the bundle served before #29680: navigation reach, then inbound relations. */
269+
const reachFirst = (app: unknown) => {
270+
const reach = appNavigationReach(app).objects;
271+
return [...reach, ...inboundRelationObjects(OBJECTS, reach)];
272+
};
273+
274+
it('two navigation objects where one references the other come back in object-list order', async () => {
275+
const { res } = await bundle('admin', 'service', { data: data() });
276+
expect(res.statusCode).toBe(200);
277+
expect(AppMetadataBundleResponseSchema.safeParse(res.body).success).toBe(true);
278+
expect(reachFirst(REVERSED_APP)).toEqual(['contact', 'account', 'note', 'task']);
279+
expect(namesOf(res.body.objects)).toEqual(['account', 'contact', 'note', 'task']);
280+
// The same set, and only the order moved: views and pages derive from that set.
281+
expect(namesOf(res.body.views)).toEqual(['account.all', 'account.legal', 'contact.mine']);
282+
expect(namesOf(res.body.pages)).toEqual(['account_record']);
283+
});
284+
285+
it('CONTROL: a bundle whose navigation already follows the list serves exactly the reach-first body', async () => {
286+
const { res } = await bundle('admin', 'accounts', { data: data() });
287+
expect(res.statusCode).toBe(200);
288+
const expected = reachFirst(IN_ORDER_APP);
289+
expect(expected).toEqual(['account', 'contact', 'note']);
290+
expect(namesOf(res.body.objects)).toEqual(expected);
291+
const singles = await Promise.all(expected.map(async (name) => (await item('admin', 'object', name)).body));
292+
expect(JSON.stringify(res.body.objects)).toBe(JSON.stringify(singles));
293+
});
294+
});
295+
243296
// ── The validator ─────────────────────────────────────────────────────────────
244297

245298
describe('[#27627] a per-caller validator: ETag, private, no-cache, and 304', () => {
@@ -290,7 +343,7 @@ describe('[#27627] a per-caller validator: ETag, private, no-cache, and 304', ()
290343
expect(res.statusCode).toBe(200);
291344
expect(res.headers['Cache-Control']).toBe('private, no-store');
292345
expect(res.headers.ETag).toBeUndefined();
293-
expect(namesOf(res.body.objects)).toEqual(['account', 'secret_deal', 'contact', 'note', 'task', 'deal_line']);
346+
expect(namesOf(res.body.objects)).toEqual(['account', 'contact', 'secret_deal', 'note', 'task', 'deal_line']);
294347
});
295348

296349
it('`metadata.enableCache: false` sends no validator and answers a matching If-None-Match in full, as the item reads do', async () => {
@@ -302,7 +355,7 @@ describe('[#27627] a per-caller validator: ETag, private, no-cache, and 304', ()
302355
expect(res.statusCode).toBe(200);
303356
expect(res.headers.ETag).toBeUndefined();
304357
expect(res.headers['Cache-Control']).toBeUndefined();
305-
expect(namesOf(res.body.objects)).toEqual(['account', 'secret_deal', 'contact', 'note', 'task', 'deal_line']);
358+
expect(namesOf(res.body.objects)).toEqual(['account', 'contact', 'secret_deal', 'note', 'task', 'deal_line']);
306359
});
307360
});
308361

‎packages/rest/src/meta-app-bundle-routes.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,22 @@ export function inboundRelationObjects(objects: readonly unknown[], targets: rea
180180
return found;
181181
}
182182

183+
/**
184+
* `names` in the object list's order: every name `objects` lists, at its first
185+
* position there, then the names it does not list, in `names` order. The same
186+
* set as `names`, each name once.
187+
*/
188+
export function inObjectListOrder(objects: readonly unknown[], names: readonly string[]): string[] {
189+
const wanted = new Set(names);
190+
const ordered: string[] = [];
191+
for (const item of objects) {
192+
const name = nameOf((item as { name?: unknown } | null)?.name);
193+
if (name !== undefined && wanted.has(name)) addOnce(ordered, name);
194+
}
195+
for (const name of names) addOnce(ordered, name);
196+
return ordered;
197+
}
198+
183199
/** A member the chain could not judge: the whole request answers it. */
184200
class BundleMemberFault extends Error {
185201
constructor(readonly object: string) {
@@ -273,8 +289,12 @@ export function registerMetaAppBundleRoute(this: MetaAppBundleRouteHost, scope:
273289
if (!app || app.kind !== 'serve') return { kind: 'refused', app: undefined };
274290

275291
const reach = appNavigationReach((app.envelope as { item?: unknown } | null)?.item);
276-
const inbound = inboundRelationObjects(await objectList(), reach.objects);
277-
const objects = await members('object', [...reach.objects, ...inbound]);
292+
// The navigation's objects and their inbound relations, served
293+
// in the object list's order: a navigation object that also
294+
// points at another keeps its place in that list (#29680).
295+
const listed = await objectList();
296+
const inbound = inboundRelationObjects(listed, reach.objects);
297+
const objects = await members('object', inObjectListOrder(listed, [...reach.objects, ...inbound]));
278298
const served = new Set(objects.map((o) => String((o.envelope as { name?: unknown }).name)));
279299

280300
const viewNames = [...reach.views];

‎packages/rest/src/rest-route-ledger.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export const REST_ROUTE_LEDGER: readonly RestRouteLedgerEntry[] = [
235235
// [#27627] The app metadata bundle — see meta-app-bundle-routes.ts.
236236
{ route: 'GET /api/v1/meta/app/:appId/bundle', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getAppBundle',
237237
responseSchema: 'AppMetadataBundleResponseSchema',
238-
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' },
239239
// [#5882] The three-layer diagnostic projection, promoted from the
240240
// `?layers=true` flag on the row below to a path of its own so that one path
241241
// answers one response shape. `responseSchema` is filled because this mount

‎packages/spec/src/api/app-bundle.zod.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ import { GetMetaItemResponseSchema } from './protocol.zod';
2828
* caller is served (navigation already pruned to that caller):
2929
*
3030
* - `objects` — every object an `object` navigation entry names (top-level
31-
* `navigation`, each area's `navigation`, and `children` at any depth), in
32-
* navigation order; then every object with a `lookup` or `master_detail`
33-
* field whose `reference` is one of those, in registry order. One hop: an
34-
* object related only to a related object is not included.
31+
* `navigation`, each area's `navigation`, and `children` at any depth), and
32+
* every object with a `lookup` or `master_detail` field whose `reference` is
33+
* one of those, all in the object list's order; a name the object list does
34+
* not hold follows the listed ones, in navigation order. One hop: an object
35+
* related only to a related object is not included.
3536
* - `views` — every view a navigation entry names by `viewName`, and every view
3637
* bound to an object in `objects`.
3738
* - `pages` — every page a `page` navigation entry names, and every page bound
@@ -56,8 +57,8 @@ export const AppMetadataBundleResponseSchema = lazySchema(() => z.object({
5657
'The app, as `GET /meta/app/{appId}` answers it to this caller (its navigation pruned to what the caller may open).',
5758
),
5859
objects: z.array(GetMetaItemResponseSchema).describe(
59-
'The objects the app\'s navigation reaches, then the objects whose relationship fields reference them, each as '
60-
+ '`GET /meta/object/{name}` answers it. An object the caller may not read is absent.',
60+
'The objects the app\'s navigation reaches and the objects whose relationship fields reference them, in the '
61+
+ 'object list\'s order, each as `GET /meta/object/{name}` answers it. An object the caller may not read is absent.',
6162
),
6263
views: z.array(GetMetaItemResponseSchema).describe(
6364
'The views the navigation names and the views bound to the bundled objects, each as `GET /meta/view/{name}` '

0 commit comments

Comments
 (0)