Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/rest-publish-meta-item-cast-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
---
"@objectstack/rest": patch
---

refactor(rest): the `publishMetaItem` call site is compiled against the declared contract (#11145)

The `POST /meta/:type/:name/publish` door in `packages/rest/src/rest-server.ts`
reached its protocol method through `(p as any).publishMetaItem` — once for the
501 feature-detection guard, once for the call — so the compiler checked nothing
about the request literal it built. The cast was load-bearing on **member
existence**, not on request shape: #10350 measured that deleting it answered
`TS2339: Property 'publishMetaItem' does not exist on type 'RestProtocol'`, not
a `TS2353` about an unknown key. `publishMetaItem` was an ADR-0076 D9
server-only extension, so no amount of widening the implementation's own
parameter type in `@objectstack/metadata-protocol` (which this package
deliberately does not depend on) could have retired it.

#11006 (maintainer ruling 2026-08-22, option B) declared the member on
`MetadataProtocol` with a `PublishMetaItemRequest`, which is what removes the
prop. The guard is now `if (!p.publishMetaItem)` and the request is a named
const typed `TransportScopedMetaRequest<PublishMetaItemRequest>` — the same
shape #9741 gave the meta-read doors and #9805 gave the non-door helpers.

**No behaviour change of any kind, and nothing about the wire moves.** The
outgoing payload is byte-identical (same keys, same conditional spreads); the
edit hoists the literal into a const and drops a type-level cast. Two things
deliberately survive:

- the 501 feature-detection guard, because the declared member is **optional**
(a kernel may not implement the promotion door at all) — and it is also what
narrows the member to callable at the call site;
- the transport-level `environmentId`, which stays layered on by the
`TransportScopedMetaRequest` envelope rather than becoming a protocol key, per
the #9741 ruling (2026-08-18).

What the typing buys, measured rather than asserted: an undeclared key in this
request literal is now `TS2353` at compile time instead of a payload member no
contract has ever seen. The docblock that existed only to explain why the cast
had to stay is replaced rather than left behind — a rationale for a prop that no
longer exists is a declaration that outlived its subject.
60 changes: 33 additions & 27 deletions packages/rest/src/rest-server.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ import type {
GetMetaItemRequest,
GetMetaItemCachedRequest,
GetMetaItemLayeredRequest,
PublishMetaItemRequest,
} from '@objectstack/spec/api';
// [#8073] The closed ADR-0112 error vocabulary, so the explain family's single
// refusal emitter types its `code` parameter as the vocabulary rather than as
Expand DownExpand Up@@ -5937,7 +5938,7 @@ export class RestServer {
return;
}
const p = await this.resolveProtocol(environmentId, req);
if (!(p as any).publishMetaItem) {
if (!p.publishMetaItem) {
res.status(501).json({
error: 'Publish operation not supported by protocol implementation',
});
Expand DownExpand Up@@ -6021,42 +6022,47 @@ export class RestServer {
// org-scope comment for the measurement.
canonicalMetaUrlType(req.params.type), ctx?.tenantId,
);
// [#10350] The cast stays, and what it is load-bearing
// FOR is worth stating so this is not re-filed as a missing
// contract. It is NOT hiding the request shape: measured by
// deleting it and running `pnpm --filter @objectstack/rest
// typecheck`, the compiler answers
// [#11145] The `(p as any)` cast this call carried came off
// when `MetadataProtocol` declared `publishMetaItem` (#11006,
// maintainer ruling 2026-08-22, option B). What the cast was
// load-bearing FOR is recorded because it is counter-intuitive
// and was measured, not assumed: deleting it while the member
// was undeclared answered
// `TS2339: Property 'publishMetaItem' does not exist on
// type 'RestProtocol'`
// — not a TS2353 about an unknown key. `publishMetaItem` is
// an ADR-0076 D9 SERVER-ONLY extension: `RestProtocol` is
// `DataProtocol & MetadataProtocol`, and `MetadataProtocol`
// (`packages/spec`) declares no such member — only
// `PublishMetaItemResponseSchema` (#7294) exists there, with
// no request schema and no interface entry. So the cast is
// feature detection, exactly like the `auditMetaItem` twin
// a few hundred lines up, and the same measurement holds
// AFTER #10350 declared `packageId` on the implementation's
// request type: that type lives in
// `@objectstack/metadata-protocol`, which `packages/rest`
// deliberately does not depend on.
// — NOT a `TS2353` about an unknown key. The cast was feature
// detection for an ADR-0076 D9 server-only extension, so only
// declaring the member could retire it; widening the
// implementation's own request type in
// `@objectstack/metadata-protocol` (which this package
// deliberately does not depend on) never could, and #10350
// measured exactly that.
//
// Removing it therefore needs `MetadataProtocol` to declare
// the member (plus a `PublishMetaItemRequest` to hang the
// #9741 `TransportScopedMetaRequest` typing off) — a
// `packages/spec` contract decision, promoting an undeclared
// optional extension into a declared one, which is the same
// call the 501 refusal above declines to pre-empt. Filed
// rather than taken here.
const result = await (p as any).publishMetaItem({
// What replaces it is the point of the exercise, not a
// side effect: the literal below is compiled against the spec
// contract through the #9741 `TransportScopedMetaRequest`
// wrapper, so an undeclared key here is a COMPILE ERROR
// (`TS2353`, measured) instead of a payload member no contract
// has ever seen. `environmentId` is the transport-level
// routing key that wrapper layers on — ⛔ never a protocol
// key; a key that belongs on the request belongs in the spec
// schema.
//
// The 501 feature-detection guard above STAYS. The member is
// declared OPTIONAL (ADR-0076 D9 promotion is additive to a
// shipped contract, and a kernel may not implement the
// promotion door at all), and that same guard is what narrows
// it to callable here.
const publishRequest: TransportScopedMetaRequest<PublishMetaItemRequest> = {
type: req.params.type,
name: req.params.name,
organizationId,
...(environmentId ? { environmentId } : {}),
...(actor ? { actor } : {}),
...(message ? { message } : {}),
...(packageId ? { packageId } : {}),
});
};
const result = await p.publishMetaItem(publishRequest);
res.json(result);
} catch (error: any) {
handleRouteError(res, error);
Expand Down
Loading