Skip to content

Commit 45862a5

Browse files
huangyiireneclaude
andauthored
refactor(rest): compile the non-door getMetaItems request literals against the declared contract (#9805) (#9845)
Nine getMetaItems call sites in packages/rest/src/rest-server.ts outside the four meta-read doors passed their request through `as any` (or through a `p: any` parameter), so the compiler checked nothing about them. Every member they thread has been expressible in declared types since #9741 landed `previewDrafts` on GetMetaItemsRequest and the TransportScopedMetaRequest envelope for the transport-level environmentId. Each literal is now a named const typed TransportScopedMetaRequest of GetMetaItemsRequest (or plain GetMetaItemsRequest where the site threads no environmentId), the same shape #9741 gave the doors. No behaviour change: the outgoing payloads are byte-identical (same keys, same conditional spreads). The optional-call spelling, the typeof-function guards and the runtime-shaped result handling all deliberately survive — retiring any of them would change behaviour rather than typing — and the envelope alias now documents why. Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent b2789ad commit 45862a5

2 files changed

Lines changed: 93 additions & 18 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
refactor(rest): the non-door `getMetaItems` request literals are compiled against the declared contract (#9805)
6+
7+
Nine `getMetaItems` call sites in `packages/rest/src/rest-server.ts` outside the
8+
four meta-read doors still passed their request through `as any` (or through a
9+
`p: any` parameter), so the compiler checked nothing about them. Every member
10+
they thread has been expressible in declared types since #9741 landed
11+
`previewDrafts` on `GetMetaItemsRequest` and the `TransportScopedMetaRequest`
12+
envelope for the transport-level `environmentId` — the casts were pure
13+
blindness, and an un-typed request literal is exactly the class that lets a
14+
future key drift silently.
15+
16+
Each literal is now a named const typed
17+
`TransportScopedMetaRequest<GetMetaItemsRequest>` (or plain
18+
`GetMetaItemsRequest` where the site threads no `environmentId`), the same shape
19+
#9741 gave the doors: the object-metadata read behind the API-exposure gate, the
20+
audience book fetch, the book-tree book and doc listings, the doc corpus behind
21+
the audience resolver, the public-form view lookup, the public-form object
22+
schema, the public-lookup reference resolution, and the dataset listing.
23+
24+
**No behaviour change of any kind, and nothing about the wire moves.** The
25+
outgoing payloads are byte-identical (same keys, same conditional spreads); the
26+
edit hoists each literal into a const and drops a type-level cast. Two spellings
27+
at these sites deliberately SURVIVE, because retiring either would change
28+
behaviour rather than typing, and both are now documented on the envelope alias:
29+
30+
- the optional call (`getMetaItems?.(…)`) and the `typeof … === 'function'`
31+
guards — `getMetaItems` is a required `MetadataProtocol` member, so these are
32+
not feature detection in the type sense, but a host may occupy the protocol
33+
slot with an object that does not implement the whole surface (the reason
34+
`metaTypeIsLive` documents the same spelling for `getMetaTypes`). Retiring one
35+
turns a tolerated absence into a `TypeError`;
36+
- the result handling — the verb is declared to return `{ type, items }` while
37+
these sites also tolerate the bare-array shape older hosts and stubs return,
38+
so the response stays runtime-shaped on purpose.
39+
40+
Genuinely feature-detected server-only verbs (`getMetaDiagnostics`,
41+
`listDrafts`, `migrateStoredMetadata`, …) are untouched — runtime casts are the
42+
documented convention there, and tightening one would turn optional capability
43+
detection into a hard dependency.

packages/rest/src/rest-server.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ export type RestProtocol = DataProtocol & MetadataProtocol;
134134
* is now checked against the spec contract — an undeclared member is a compile
135135
* error at the call site, not a cast-and-hope. Never add protocol members
136136
* here; a key that belongs to the request belongs in the spec schema.
137+
*
138+
* [#9805] The same typing now covers the NON-door `getMetaItems` helper call
139+
* sites in this file (the object, book, doc, view and dataset listings). Two
140+
* spellings those sites carry deliberately SURVIVE the tightening, because
141+
* retiring either would change behaviour rather than typing:
142+
*
143+
* - The OPTIONAL CALL (`getMetaItems?.(…)`) and the
144+
* `typeof … === 'function'` guards. `getMetaItems` is a REQUIRED
145+
* `MetadataProtocol` member, so these are not feature detection in the
146+
* type sense — but a host may occupy the protocol slot with an object
147+
* that does not implement the whole surface, which is the measured reason
148+
* they exist (`metaTypeIsLive` documents the same deliberate spelling for
149+
* `getMetaTypes`). Retiring one turns a tolerated absence into a
150+
* `TypeError`.
151+
* - The RESULT handling. `getMetaItems` is declared to return
152+
* `{ type, items }`, while these sites also tolerate the bare-array shape
153+
* older hosts and stubs return (see `metaItemsArray`), so the response
154+
* stays runtime-shaped on purpose.
155+
*
156+
* The REQUEST is what is fully typeable today, and the request is what is
157+
* typed — which is the whole point: an undeclared key in one of these
158+
* literals is now a compile error instead of a cast-and-hope.
137159
*/
138160
type TransportScopedMetaRequest<R> = R & { environmentId?: string };
139161
import {
@@ -1329,10 +1351,11 @@ export class RestServer {
13291351
*/
13301352
private async loadObjectItems(p: RestProtocol, environmentId: string | undefined): Promise<any[]> {
13311353
try {
1332-
const r: any = await (p as any).getMetaItems?.({
1354+
const objectsRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
13331355
type: 'object',
13341356
...(environmentId ? { environmentId } : {}),
1335-
});
1357+
};
1358+
const r: any = await p.getMetaItems?.(objectsRequest);
13361359
return Array.isArray(r?.items) ? r.items : Array.isArray(r) ? r : [];
13371360
} catch (err) {
13381361
// [#3545] The API-exposure gate fails OPEN when object metadata can't
@@ -1711,11 +1734,12 @@ export class RestServer {
17111734
}
17121735

17131736
/** Fetch every book of the environment, shaped for the audience resolver. */
1714-
private async fetchAudienceBooks(p: any, environmentId: string | undefined): Promise<any[]> {
1715-
const raw = await p.getMetaItems({
1737+
private async fetchAudienceBooks(p: RestProtocol, environmentId: string | undefined): Promise<any[]> {
1738+
const booksRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
17161739
type: 'book',
17171740
...(environmentId ? { environmentId } : {}),
1718-
} as any).catch(() => []);
1741+
};
1742+
const raw = await p.getMetaItems(booksRequest).catch(() => []);
17191743
return RestServer.metaItemsArray(raw).map((b: any) =>
17201744
b && typeof b === 'object' ? { ...b, packageId: b._packageId } : b,
17211745
);
@@ -4554,11 +4578,12 @@ export class RestServer {
45544578
const norm = (raw: any): any[] =>
45554579
Array.isArray(raw) ? raw : (raw && Array.isArray(raw.items) ? raw.items : []);
45564580

4557-
const books = norm(await prot.getMetaItems({
4581+
const booksRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
45584582
type: 'book',
45594583
...(packageId ? { packageId } : {}),
45604584
...(environmentId ? { environmentId } : {}),
4561-
} as any));
4585+
};
4586+
const books = norm(await prot.getMetaItems(booksRequest));
45624587
let book = books.find((b: any) => b && b.name === req.params.name);
45634588
if (!book) {
45644589
// Unknown name → the implicit per-package book (§6.4).
@@ -4583,11 +4608,12 @@ export class RestServer {
45834608
return;
45844609
}
45854610

4586-
const docs = norm(await prot.getMetaItems({
4611+
const docsRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
45874612
type: 'doc',
45884613
...(packageId ? { packageId } : {}),
45894614
...(environmentId ? { environmentId } : {}),
4590-
} as any))
4615+
};
4616+
const docs = norm(await prot.getMetaItems(docsRequest))
45914617
.map((d: any) => (d && typeof d === 'object' ? resolveDocLocale(d, locale) : d))
45924618
.map((d: any) => ({
45934619
name: d.name,
@@ -5135,10 +5161,12 @@ export class RestServer {
51355161
if (caller.authenticated && !RestServer.anyPermissionSetAudience(books)) {
51365162
allowed = true; // no gated book anywhere → org suffices
51375163
} else {
5138-
const corpus = RestServer.metaItemsArray(await p.getMetaItems({
5164+
const docCorpusRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
51395165
type: 'doc',
51405166
...(environmentId ? { environmentId } : {}),
5141-
} as any).catch(() => []))
5167+
};
5168+
const corpus = RestServer.metaItemsArray(
5169+
await p.getMetaItems(docCorpusRequest).catch(() => []))
51425170
.filter((d: any) => d && typeof d === 'object')
51435171
.map((d: any) => ({
51445172
name: d.name,
@@ -8028,10 +8056,11 @@ export class RestServer {
80288056
): Promise<{ view: any; form: any; object: string } | null> => {
80298057
const p = await this.resolveProtocol(environmentId, req);
80308058
if (typeof (p as any).getMetaItems !== 'function') return null;
8031-
const result: any = await (p as any).getMetaItems({
8059+
const viewsRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
80328060
type: 'view',
80338061
...(environmentId ? { environmentId } : {}),
8034-
});
8062+
};
8063+
const result: any = await p.getMetaItems(viewsRequest);
80358064
const items: any[] = Array.isArray(result?.items)
80368065
? result.items
80378066
: Array.isArray(result)
@@ -8092,10 +8121,11 @@ export class RestServer {
80928121
try {
80938122
const p = await this.resolveProtocol(environmentId, req);
80948123
if (typeof (p as any).getMetaItems === 'function') {
8095-
const r: any = await (p as any).getMetaItems({
8124+
const objectsRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
80968125
type: 'object',
80978126
...(environmentId ? { environmentId } : {}),
8098-
});
8127+
};
8128+
const r: any = await p.getMetaItems(objectsRequest);
80998129
const items: any[] = Array.isArray(r?.items) ? r.items : Array.isArray(r) ? r : [];
81008130
const obj = items.find((o: any) => o?.name === match.object);
81018131
if (obj && obj.fields && typeof obj.fields === 'object') {
@@ -8408,10 +8438,11 @@ export class RestServer {
84088438
let referenceTo: string | undefined = picker.object;
84098439
if (!referenceTo && typeof (p as any).getMetaItems === 'function') {
84108440
try {
8411-
const r: any = await (p as any).getMetaItems({
8441+
const objectsRequest: TransportScopedMetaRequest<GetMetaItemsRequest> = {
84128442
type: 'object',
84138443
...(environmentId ? { environmentId } : {}),
8414-
});
8444+
};
8445+
const r: any = await p.getMetaItems(objectsRequest);
84158446
const items: any[] = Array.isArray(r?.items) ? r.items : Array.isArray(r) ? r : [];
84168447
const obj = items.find((o: any) => o?.name === match.object);
84178448
const def = obj?.fields?.[fieldName];
@@ -8611,7 +8642,8 @@ export class RestServer {
86118642
let dataset = body.dataset;
86128643
if (!dataset && body.datasetName) {
86138644
const p = await this.resolveProtocol(environmentId, req);
8614-
const items = await (p as any).getMetaItems?.({ type: 'dataset', previewDrafts }).catch(() => null);
8645+
const datasetRequest: GetMetaItemsRequest = { type: 'dataset', previewDrafts };
8646+
const items: any = await p.getMetaItems?.(datasetRequest).catch(() => null);
86158647
const list = Array.isArray(items?.items) ? items.items : (Array.isArray(items) ? items : []);
86168648
dataset = list.find((d: any) => d?.name === body.datasetName);
86178649
if (!dataset) {

0 commit comments

Comments
 (0)