diff --git a/.changeset/29510-meta-types-named-return.md b/.changeset/29510-meta-types-named-return.md new file mode 100644 index 00000000000..a7d401ec459 --- /dev/null +++ b/.changeset/29510-meta-types-named-return.md @@ -0,0 +1,10 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +refactor(metadata-protocol): `getMetaTypes()` declares a named return type, held equal to the type it returned before + +Clause-②: no +Metadata-protocol: none + +No behaviour change and no type change. `ObjectStackProtocolImplementation.getMetaTypes()` now declares `Promise`, composed from `@objectstack/spec`'s `MetadataTypeRegistryEntryParsed`, `Action` and `FormView`, in place of an inferred type that the published `index.d.ts` printed inline over 793 lines. A compile-time equality check in the method holds the name equal to what the body builds, so the two cannot drift. The `.d.ts` text is shorter; the type a consumer reads is the same one, checked equal against the declaration file `main` built before this change. `MetaTypesListing` is not exported from the package. Nothing to migrate. diff --git a/packages/metadata-protocol/src/protocol-meta-types-listing.ts b/packages/metadata-protocol/src/protocol-meta-types-listing.ts new file mode 100644 index 00000000000..81483bc18bc --- /dev/null +++ b/packages/metadata-protocol/src/protocol-meta-types-listing.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// [#29510] The NAMED return type of `ObjectStackProtocolImplementation.getMetaTypes()` +// (`GET /meta/types`), composed from types `@objectstack/spec` already exports. +// +// Why a name: inferred, that return type rolled up into ~790 lines of inline +// `.d.ts`, and every `protocol.ts` cut of #25467 had to read that raw block by +// hand, because a byte shift in `protocol.ts` can surface a class comment +// inside an inferred type. Named, the `.d.ts` prints these aliases instead. +// +// What holds it to the implementation: `getMetaTypes()` builds its answer with +// an inferred type, and a `const` typed `Equal` +// is initialised with `true` there: it only compiles when the inferred type is +// EQUAL (not merely assignable) to the one declared here. A member added, +// dropped or retyped on either side fails `tsc` at that line. The type is the +// one `main` emitted before the name existed; only its `.d.ts` spelling changed. +// +// Not exported from the package index: no new public name. + +import type { MetadataTypeRegistryEntryParsed } from '@objectstack/spec/kernel'; +import type { Action, FormView } from '@objectstack/spec/ui'; + +/** Flattens an intersection into one object type, so it compares EQUAL to an inferred object literal. */ +type Flatten = { [K in keyof T]: T[K] }; + +/** + * One `entries` row: the registry entry for the type, with the per-type + * fields the listing adds or overrides. A type with no registry entry gets a + * synthesised descriptor that is a subtype of this row. + */ +export type MetaTypesListingEntry = Flatten< + Omit & { + /** Singular spelling, as `PLURAL_TO_SINGULAR` resolves it; plugin types are not in the registry enum. */ + type: string; + /** Same value as `type`; the API client reads this field. */ + schemaId: string; + /** The registry value, raised by the `OS_METADATA_WRITABLE` env hatch. */ + allowEnvironmentOverlay: boolean; + /** `'env'` when only the env hatch opened the overlay. */ + overrideSource: 'registry' | 'env'; + /** JSON Schema of the type, for the generic admin form. */ + schema: Record; + /** Form layout of the type's editor. */ + form: FormView; + /** The minimal create seed, when the spec declares one. */ + createSeed?: {} | null; + /** + * Declarative plus plugin-registered type actions (`Action[]`); absent + * when there are none. The registry entry's own parsed `actions` stay + * in the type because the row spreads the entry before overriding. + */ + actions?: Action[] | NonNullable; + } +>; + +/** What `getMetaTypes()` answers. */ +export type MetaTypesListing = { + /** Every live type name, spelled as its source stores it. */ + types: string[]; + /** One row per live type, sorted by `domain`, then `type`. */ + entries: MetaTypesListingEntry[]; +}; + +/** + * Strict type equality: no `any` leak, no one-sided assignability. `true` + * only when `A` and `B` are the same type. + */ +export type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index acae72bf506..19132e6b03c 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -466,6 +466,8 @@ import { datasourceCredentialsRefSaveRefusal } from './protocol-datasource-crede import { withRefusalAudit } from './protocol-refusal-audit.js'; // [#28665] The null-master rows a `controlled_by_parent` detail stops serving, counted into the finding that makes its master required. import { withNullMasterRowCounts } from './protocol-destructive-null-master.js'; +// [#29510] `getMetaTypes()`'s named return type and the equality its pin reads. +import type { Equal, MetaTypesListing } from './protocol-meta-types-listing.js'; // [#25467 E0] The module preamble that stood here moved, verbatim, into the themed // `protocol-*.ts` modules below. What this file exported from it, it still exports; // the module order below is the order the rolled-up `.d.ts` keeps their declarations in. @@ -3136,7 +3138,8 @@ export class ObjectStackProtocolImplementation implements return listLiveMetadataTypes.call(host); } - async getMetaTypes() { + // [#29510] Named return type; the `exact` pin below holds it EQUAL to what this body infers. + async getMetaTypes(): Promise { const allTypes = await this.listLiveMetadataTypes(); // Phase 3a-1: enrich response with per-type registry metadata so admin @@ -3289,7 +3292,11 @@ export class ObjectStackProtocolImplementation implements return a.type.localeCompare(b.type); }); - return { types: allTypes, entries }; + const listing = { types: allTypes, entries }; + // [#29510] Compile-time pin: `false` (a type error) the moment the body and the name drift apart. + const exact: Equal = true; + void exact; + return listing; } /**