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
41 changes: 41 additions & 0 deletions .changeset/metadata-type-registration-names-a-real-hook.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 18 additions & 5 deletions content/docs/plugins/adding-a-metadata-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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/<domain>/<name>.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)
Expand Down
51 changes: 37 additions & 14 deletions packages/spec/src/kernel/metadata-type-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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<string, Action[]>();

/**
* 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.
*
Expand Down
Loading