Skip to content

Commit 90c2b15

Browse files
os-zhuangclaude
andauthored
docs(spec): the metadata-type registry named a hook that does not exist (#4212) (#4216)
`registerMetadataTypeSchema` and `registerMetadataTypeActions` both told plugins to call them "from their `onInstall` hook", and named `/api/v1/meta/types/:type` as the endpoint that would serve the result. Neither is real. The kernel's plugin contract is `init` / `start` / `destroy` (`packages/core/src/types.ts`): `kernel.use()` validates and stores, `bootstrap()` calls `init` then `start`. The `onInstall` / `onEnable` / `onDisable` / `onUninstall` / `onUpgrade` family declared on `PluginLifecycleSchema` has no invocation site anywhere in the runtime — its only importer repo-wide is its own test. So a plugin following the documented advice registered nothing and got no error saying so, and its custom metadata type silently served no JSON Schema. `/api/v1/meta/types/:type` is likewise not a registered route; the route ledger carries `GET /meta/types` and the public surface is `GET /api/v1/meta`. Both TSDoc blocks and `content/docs/plugins/adding-a-metadata-type.mdx` now name the hook that runs and the endpoint that exists, with the example written as a real `Plugin` with an `init(ctx)`. That matches `DatasourceAdminServicePlugin` — the single production caller of `registerMetadataTypeActions` — which has always used `init` rather than the `onInstall` the docs prescribed. Two facts the old text obscured, now stated because both change how an author writes the code: - `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 `engine.registry.getRegisteredTypes()` unioned with the metadata service's, then decorates each with its schema — a type present only in the schema registry is never reached. Documentation only; no behaviour change. Whether the five uninvoked lifecycle hooks get implemented or retired (ADR-0049) is #4212 — this stops the registry's own docs from sending authors at the dead one meanwhile. Claude-Session: https://claude.ai/code/session_01HrRNgrWaRtggzmrHpbomyh Co-authored-by: Claude <noreply@anthropic.com>
1 parent 698cbc2 commit 90c2b15

3 files changed

Lines changed: 96 additions & 19 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
**The metadata-type registry told plugins to use a hook that does not exist (#4212).**
6+
7+
`registerMetadataTypeSchema` and `registerMetadataTypeActions` both documented
8+
themselves as things a plugin calls "from its `onInstall` hook", and named
9+
`/api/v1/meta/types/:type` as the endpoint that would then serve the result.
10+
Neither is real:
11+
12+
- The kernel's plugin contract is **`init` / `start` / `destroy`**
13+
(`packages/core/src/types.ts`). The `onInstall` / `onEnable` / `onDisable` /
14+
`onUninstall` / `onUpgrade` family on `PluginLifecycleSchema` has no
15+
invocation site anywhere in the runtime — `kernel.use()` validates and
16+
stores, `bootstrap()` calls `init` then `start`. A plugin that followed the
17+
documented advice registered nothing, and got no error saying so.
18+
- **`/api/v1/meta/types/:type` is not a registered route.** What exists is
19+
`GET /api/v1/meta` and the server-only `GET /meta/types`, both served from
20+
`getMetaTypes()`.
21+
22+
Both TSDoc blocks and `content/docs/plugins/adding-a-metadata-type.mdx` now
23+
name the hook that runs and the endpoint that exists. The example is written as
24+
a real `Plugin` with an `init(ctx)`, matching `DatasourceAdminServicePlugin`
25+
the one production caller of `registerMetadataTypeActions`, which has always
26+
used `init` rather than the documented `onInstall`.
27+
28+
Two facts worth knowing that the old text obscured, now stated:
29+
30+
- `getMetaTypes()` reads these registries **at request time**, not from a boot
31+
snapshot, so a type registered during `init` is served from the first call.
32+
- Registering a schema does **not** by itself put a type in the listing.
33+
`getMetaTypes()` enumerates types from the engine registry unioned with the
34+
metadata service and then decorates each with its schema, so a type present
35+
only in the schema registry is never reached. Declare the type via
36+
`additionalTypes` as well.
37+
38+
Documentation only — no behaviour change. The broader question of whether the
39+
five declared-but-uninvoked lifecycle hooks should be implemented or retired
40+
(ADR-0049 enforce-or-remove) is tracked in #4212; this change stops the
41+
registry's own docs from sending authors at the dead one in the meantime.

content/docs/plugins/adding-a-metadata-type.mdx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ use, not pinned at process start.
7070
platform. A third-party package contributes its own types instead through
7171
the **`additionalTypes`** array on `MetadataPluginConfig`, and registers the
7272
matching Zod schema with `registerMetadataTypeSchema(type, schema)` from its
73-
`onInstall` hook so the `/api/v1/meta/types/:type` endpoint emits a real JSON
74-
Schema. The registry entry shape is the same in both cases.
73+
plugin's `init(ctx)` so `GET /api/v1/meta` emits a real JSON Schema. The
74+
registry entry shape is the same in both cases.
7575

7676
## 2. Define the Zod schema
7777

@@ -112,13 +112,26 @@ editor consumes from the registered Zod schema.
112112
```
113113

114114
- **Plugin-contributed types:** call `registerMetadataTypeSchema` from your
115-
plugin's `onInstall` hook (see *Plugin lifecycle*, below):
115+
plugin's `init(ctx)` — the kernel's plugin contract is `init` / `start` /
116+
`destroy`:
116117

117118
```ts
118119
import { registerMetadataTypeSchema } from '@objectstack/spec/kernel';
119-
registerMetadataTypeSchema('my_widget', MyWidgetSchema);
120+
121+
export class MyWidgetPlugin implements Plugin {
122+
name = 'plugin.my-widget';
123+
async init(_ctx: PluginContext) {
124+
registerMetadataTypeSchema('my_widget', MyWidgetSchema);
125+
}
126+
}
120127
```
121128

129+
`getMetaTypes()` reads this registry on every request, so a type registered
130+
during `init` is served from the first call onward. Registering the schema
131+
does not by itself put the type in the listing — `getMetaTypes()` enumerates
132+
types from the engine registry and the metadata service and then decorates
133+
each with its schema, so declare the type (via `additionalTypes`) as well.
134+
122135
The Metadata Admin **SchemaForm** consumes the JSON Schema derived from this
123136
Zod schema and produces:
124137

@@ -221,7 +234,7 @@ If you omit them, the directory falls back to the registry `label`.
221234

222235
- [ ] Registry entry added (`type`, `label`, `domain`, required `filePatterns`, flags) — for plugins, via `additionalTypes` on `MetadataPluginConfig`
223236
- [ ] Zod schema authored under `packages/spec/src/<domain>/<name>.zod.ts`
224-
- [ ] Zod schema wired up: built-in → `BUILTIN_METADATA_TYPE_SCHEMAS`; plugin → `registerMetadataTypeSchema()` in `onInstall`
237+
- [ ] Zod schema wired up: built-in → `BUILTIN_METADATA_TYPE_SCHEMAS`; plugin → `registerMetadataTypeSchema()` in the plugin's `init()`
225238
- [ ] (Optional) Custom editor registered in `builtinComponents.tsx`
226239
- [ ] (Optional) i18n labels added to `metadata-admin/i18n.ts`
227240
- [ ] `pnpm test` passes (framework) and `pnpm --filter @object-ui/app-shell build` passes (objectui)

packages/spec/src/kernel/metadata-type-schemas.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
*
88
* 1. Runtime validators (`MetadataManager.validate`) — already wired
99
* through the domain-specific overlay validator in `objectql/protocol`.
10-
* 2. The `/api/v1/meta/types/:type` endpoint, which converts each
11-
* registered schema to JSON Schema (`z.toJSONSchema()`) and exposes
12-
* it as `MetadataTypeInfo.schema`. Studio's metadata-admin engine
13-
* renders the result with its generic `SchemaForm`, so adding a new
14-
* writable metadata type now requires **zero** Studio-side code.
10+
* 2. `GET /api/v1/meta` (and the richer server-only `GET /meta/types`),
11+
* which converts each registered schema to JSON Schema
12+
* (`z.toJSONSchema()`) and exposes it as `MetadataTypeInfo.schema`.
13+
* Studio's metadata-admin engine renders the result with its generic
14+
* `SchemaForm`, so adding a new writable metadata type now requires
15+
* **zero** Studio-side code. Both are served from
16+
* `ObjectStackProtocolImplementation.getMetaTypes()`, which reads this
17+
* registry at REQUEST time — a type registered after boot is picked up
18+
* on the next call.
1519
*
1620
* The map intentionally only contains types that meaningfully round-trip
1721
* through the runtime metadata API. (The former code-only placeholder kinds
@@ -130,9 +134,25 @@ export function getMetadataTypeSchema(type: string): z.ZodType | undefined {
130134
* Register (or replace) the canonical Zod schema for a metadata type.
131135
*
132136
* Plugins that introduce custom metadata types — declared through
133-
* `additionalTypes` on `MetadataPluginConfig` — should call this from
134-
* their `onInstall` hook so the engine's `/meta/types/:type` endpoint
135-
* starts emitting a real JSON Schema for them. Idempotent.
137+
* `additionalTypes` on `MetadataPluginConfig` — should call this from their
138+
* plugin's **`init(ctx)`**, so `GET /api/v1/meta` starts emitting a real JSON
139+
* Schema for them. Idempotent.
140+
*
141+
* This used to say "from their `onInstall` hook", pointing at a hook that does
142+
* not run (#4212). The kernel's `Plugin` contract is `init` / `start` /
143+
* `destroy` (`packages/core/src/types.ts`); the `onInstall` / `onEnable` /
144+
* `onDisable` / `onUninstall` / `onUpgrade` family declared on
145+
* `PluginLifecycleSchema` has no invocation site anywhere in the runtime, so a
146+
* plugin that followed the old advice registered nothing and got no error.
147+
* `init` is what the one real caller of the sibling
148+
* {@link registerMetadataTypeActions} uses — see
149+
* `DatasourceAdminServicePlugin.init`.
150+
*
151+
* NOTE — registering a schema alone does not make a type appear in the
152+
* listing. `getMetaTypes()` enumerates types from the engine registry and the
153+
* metadata service, then decorates each with its schema; a type present here
154+
* but in neither of those is not reached. Register the type as well as its
155+
* schema.
136156
*/
137157
export function registerMetadataTypeSchema(type: string, schema: z.ZodType): void {
138158
EXTRA_METADATA_TYPE_SCHEMAS.set(type, schema);
@@ -154,18 +174,21 @@ export function listMetadataTypeSchemaTypes(): string[] {
154174
* keyed by metadata type. Mirrors `EXTRA_METADATA_TYPE_SCHEMAS` above.
155175
*
156176
* The merged view (built-in declarative actions from
157-
* `DEFAULT_METADATA_TYPE_REGISTRY` + these registered ones) is what the
158-
* `/api/v1/meta/types/:type` endpoint emits, so the Studio metadata-admin
159-
* engine renders one button mechanism — the same `ActionSchema` business
160-
* objects already use — for every metadata type.
177+
* `DEFAULT_METADATA_TYPE_REGISTRY` + these registered ones) is what
178+
* `GET /api/v1/meta` emits, so the Studio metadata-admin engine renders one
179+
* button mechanism — the same `ActionSchema` business objects already use —
180+
* for every metadata type.
161181
*/
162182
const EXTRA_METADATA_TYPE_ACTIONS = new Map<string, Action[]>();
163183

164184
/**
165185
* Register (or extend) the type-level actions for a metadata type.
166186
*
167-
* Plugins call this from `onInstall` to layer actions onto any type —
168-
* built-in or custom — without forking the registry. Actions merge by
187+
* Plugins call this from their `init(ctx)` to layer actions onto any type —
188+
* built-in or custom — without forking the registry. `DatasourceAdminServicePlugin`
189+
* is the worked example: it registers the datasource "Test connection" button
190+
* as the first statement of `init`, co-located with the route that serves it.
191+
* (This said `onInstall` until #4212 — a hook nothing calls.) Actions merge by
169192
* `name`: a later registration with the same `name` replaces the earlier
170193
* one; new names append. Idempotent for identical input.
171194
*

0 commit comments

Comments
 (0)