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
10 changes: 10 additions & 0 deletions .changeset/29510-meta-types-named-return.md
Original file line number Diff line number Diff line change
@@ -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<MetaTypesListing>`, 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.
68 changes: 68 additions & 0 deletions packages/metadata-protocol/src/protocol-meta-types-listing.ts
Original file line number Diff line number Diff line change
@@ -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<inferred, MetaTypesListing>`
// 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<T> = { [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<MetadataTypeRegistryEntryParsed, 'type' | 'allowEnvironmentOverlay' | 'actions'> & {
/** 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<string, unknown>;
/** 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<MetadataTypeRegistryEntryParsed['actions']>;
}
>;

/** 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<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
11 changes: 9 additions & 2 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<MetaTypesListing> {
const allTypes = await this.listLiveMetadataTypes();

// Phase 3a-1: enrich response with per-type registry metadata so admin
Expand Down Expand Up @@ -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<typeof listing, MetaTypesListing> = true;
void exact;
return listing;
}

/**
Expand Down
Loading