diff --git a/.changeset/metadata-type-registration-names-a-real-hook.md b/.changeset/metadata-type-registration-names-a-real-hook.md new file mode 100644 index 0000000000..d760bbc1af --- /dev/null +++ b/.changeset/metadata-type-registration-names-a-real-hook.md @@ -0,0 +1,41 @@ +--- +'@objectstack/spec': patch +--- + +**The metadata-type registry told plugins to use a hook that does not exist (#4212).** + +`registerMetadataTypeSchema` and `registerMetadataTypeActions` both documented +themselves as things a plugin calls "from its `onInstall` hook", and named +`/api/v1/meta/types/:type` as the endpoint that would then serve the result. +Neither is real: + +- The kernel's plugin contract is **`init` / `start` / `destroy`** + (`packages/core/src/types.ts`). The `onInstall` / `onEnable` / `onDisable` / + `onUninstall` / `onUpgrade` family on `PluginLifecycleSchema` has no + invocation site anywhere in the runtime — `kernel.use()` validates and + stores, `bootstrap()` calls `init` then `start`. A plugin that followed the + documented advice registered nothing, and got no error saying so. +- **`/api/v1/meta/types/:type` is not a registered route.** What exists is + `GET /api/v1/meta` and the server-only `GET /meta/types`, both served from + `getMetaTypes()`. + +Both TSDoc blocks and `content/docs/plugins/adding-a-metadata-type.mdx` now +name the hook that runs and the endpoint that exists. The example is written as +a real `Plugin` with an `init(ctx)`, matching `DatasourceAdminServicePlugin` — +the one production caller of `registerMetadataTypeActions`, which has always +used `init` rather than the documented `onInstall`. + +Two facts worth knowing that the old text obscured, now stated: + +- `getMetaTypes()` reads these registries **at request time**, not from a boot + snapshot, so a type registered during `init` is served from the first call. +- Registering a schema does **not** by itself put a type in the listing. + `getMetaTypes()` enumerates types from the engine registry unioned with the + metadata service and then decorates each with its schema, so a type present + only in the schema registry is never reached. Declare the type via + `additionalTypes` as well. + +Documentation only — no behaviour change. The broader question of whether the +five declared-but-uninvoked lifecycle hooks should be implemented or retired +(ADR-0049 enforce-or-remove) is tracked in #4212; this change stops the +registry's own docs from sending authors at the dead one in the meantime. diff --git a/content/docs/plugins/adding-a-metadata-type.mdx b/content/docs/plugins/adding-a-metadata-type.mdx index 7d895b5bc2..b04705d459 100644 --- a/content/docs/plugins/adding-a-metadata-type.mdx +++ b/content/docs/plugins/adding-a-metadata-type.mdx @@ -70,8 +70,8 @@ use, not pinned at process start. platform. A third-party package contributes its own types instead through the **`additionalTypes`** array on `MetadataPluginConfig`, and registers the matching Zod schema with `registerMetadataTypeSchema(type, schema)` from its -`onInstall` hook so the `/api/v1/meta/types/:type` endpoint emits a real JSON -Schema. The registry entry shape is the same in both cases. +plugin's `init(ctx)` so `GET /api/v1/meta` emits a real JSON Schema. The +registry entry shape is the same in both cases. ## 2. Define the Zod schema @@ -112,13 +112,26 @@ editor consumes from the registered Zod schema. ``` - **Plugin-contributed types:** call `registerMetadataTypeSchema` from your - plugin's `onInstall` hook (see *Plugin lifecycle*, below): + plugin's `init(ctx)` — the kernel's plugin contract is `init` / `start` / + `destroy`: ```ts import { registerMetadataTypeSchema } from '@objectstack/spec/kernel'; - registerMetadataTypeSchema('my_widget', MyWidgetSchema); + + export class MyWidgetPlugin implements Plugin { + name = 'plugin.my-widget'; + async init(_ctx: PluginContext) { + registerMetadataTypeSchema('my_widget', MyWidgetSchema); + } + } ``` + `getMetaTypes()` reads this registry on every request, so a type registered + during `init` is served from the first call onward. Registering the schema + does not by itself put the type in the listing — `getMetaTypes()` enumerates + types from the engine registry and the metadata service and then decorates + each with its schema, so declare the type (via `additionalTypes`) as well. + The Metadata Admin **SchemaForm** consumes the JSON Schema derived from this Zod schema and produces: @@ -221,7 +234,7 @@ If you omit them, the directory falls back to the registry `label`. - [ ] Registry entry added (`type`, `label`, `domain`, required `filePatterns`, flags) — for plugins, via `additionalTypes` on `MetadataPluginConfig` - [ ] Zod schema authored under `packages/spec/src//.zod.ts` -- [ ] Zod schema wired up: built-in → `BUILTIN_METADATA_TYPE_SCHEMAS`; plugin → `registerMetadataTypeSchema()` in `onInstall` +- [ ] Zod schema wired up: built-in → `BUILTIN_METADATA_TYPE_SCHEMAS`; plugin → `registerMetadataTypeSchema()` in the plugin's `init()` - [ ] (Optional) Custom editor registered in `builtinComponents.tsx` - [ ] (Optional) i18n labels added to `metadata-admin/i18n.ts` - [ ] `pnpm test` passes (framework) and `pnpm --filter @object-ui/app-shell build` passes (objectui) diff --git a/packages/spec/src/kernel/metadata-type-schemas.ts b/packages/spec/src/kernel/metadata-type-schemas.ts index 86ea94e565..68a587c129 100644 --- a/packages/spec/src/kernel/metadata-type-schemas.ts +++ b/packages/spec/src/kernel/metadata-type-schemas.ts @@ -7,11 +7,15 @@ * * 1. Runtime validators (`MetadataManager.validate`) — already wired * through the domain-specific overlay validator in `objectql/protocol`. - * 2. The `/api/v1/meta/types/:type` endpoint, which converts each - * registered schema to JSON Schema (`z.toJSONSchema()`) and exposes - * it as `MetadataTypeInfo.schema`. Studio's metadata-admin engine - * renders the result with its generic `SchemaForm`, so adding a new - * writable metadata type now requires **zero** Studio-side code. + * 2. `GET /api/v1/meta` (and the richer server-only `GET /meta/types`), + * which converts each registered schema to JSON Schema + * (`z.toJSONSchema()`) and exposes it as `MetadataTypeInfo.schema`. + * Studio's metadata-admin engine renders the result with its generic + * `SchemaForm`, so adding a new writable metadata type now requires + * **zero** Studio-side code. Both are served from + * `ObjectStackProtocolImplementation.getMetaTypes()`, which reads this + * registry at REQUEST time — a type registered after boot is picked up + * on the next call. * * The map intentionally only contains types that meaningfully round-trip * through the runtime metadata API. (The former code-only placeholder kinds @@ -130,9 +134,25 @@ export function getMetadataTypeSchema(type: string): z.ZodType | undefined { * Register (or replace) the canonical Zod schema for a metadata type. * * Plugins that introduce custom metadata types — declared through - * `additionalTypes` on `MetadataPluginConfig` — should call this from - * their `onInstall` hook so the engine's `/meta/types/:type` endpoint - * starts emitting a real JSON Schema for them. Idempotent. + * `additionalTypes` on `MetadataPluginConfig` — should call this from their + * plugin's **`init(ctx)`**, so `GET /api/v1/meta` starts emitting a real JSON + * Schema for them. Idempotent. + * + * This used to say "from their `onInstall` hook", pointing at a hook that does + * not run (#4212). The kernel's `Plugin` contract is `init` / `start` / + * `destroy` (`packages/core/src/types.ts`); the `onInstall` / `onEnable` / + * `onDisable` / `onUninstall` / `onUpgrade` family declared on + * `PluginLifecycleSchema` has no invocation site anywhere in the runtime, so a + * plugin that followed the old advice registered nothing and got no error. + * `init` is what the one real caller of the sibling + * {@link registerMetadataTypeActions} uses — see + * `DatasourceAdminServicePlugin.init`. + * + * NOTE — registering a schema alone does not make a type appear in the + * listing. `getMetaTypes()` enumerates types from the engine registry and the + * metadata service, then decorates each with its schema; a type present here + * but in neither of those is not reached. Register the type as well as its + * schema. */ export function registerMetadataTypeSchema(type: string, schema: z.ZodType): void { EXTRA_METADATA_TYPE_SCHEMAS.set(type, schema); @@ -154,18 +174,21 @@ export function listMetadataTypeSchemaTypes(): string[] { * keyed by metadata type. Mirrors `EXTRA_METADATA_TYPE_SCHEMAS` above. * * The merged view (built-in declarative actions from - * `DEFAULT_METADATA_TYPE_REGISTRY` + these registered ones) is what the - * `/api/v1/meta/types/:type` endpoint emits, so the Studio metadata-admin - * engine renders one button mechanism — the same `ActionSchema` business - * objects already use — for every metadata type. + * `DEFAULT_METADATA_TYPE_REGISTRY` + these registered ones) is what + * `GET /api/v1/meta` emits, so the Studio metadata-admin engine renders one + * button mechanism — the same `ActionSchema` business objects already use — + * for every metadata type. */ const EXTRA_METADATA_TYPE_ACTIONS = new Map(); /** * Register (or extend) the type-level actions for a metadata type. * - * Plugins call this from `onInstall` to layer actions onto any type — - * built-in or custom — without forking the registry. Actions merge by + * Plugins call this from their `init(ctx)` to layer actions onto any type — + * built-in or custom — without forking the registry. `DatasourceAdminServicePlugin` + * is the worked example: it registers the datasource "Test connection" button + * as the first statement of `init`, co-located with the route that serves it. + * (This said `onInstall` until #4212 — a hook nothing calls.) Actions merge by * `name`: a later registration with the same `name` replaces the earlier * one; new names append. Idempotent for identical input. *