Skip to content

Commit 9610b13

Browse files
committed
feat(spec)!: resolve the MetadataEvent/MetadataBulkRegisterRequest dual source — ./kernel copies removed, ./api keeps the bare names (#4587)
The three #4535-C2 baseline rows were the #4411 trap on the kernel metadata family: MetadataEvent(Schema) and MetadataBulkRegisterRequestSchema each had a second, different declaration in ./kernel, and which shape a consumer got depended only on the import path. Import-statement-level scan across framework, cloud and objectui: - ./kernel copies: zero importers outside their own unit test in all three repos. The kernel MetadataEvent lifecycle vocabulary (metadata.registered/.../exported) has NO producer anywhere; the kernel bulk-register per-item `namespace` field matches no enforced write path (IMetadataService.bulkRegister and MetadataManager.bulkRegister both take {type,name,data} items only). - ./api MetadataEvent is the live realtime contract: MetadataManager publishes metadata.{type}.{created|deleted} events and @objectstack/client / client-react subscribe against the type. - ./api MetadataBulkRegisterRequestSchema is the POST /api/meta/bulk/register contract whose item shape matches the runtime. Disposal (route 1, dead-side delete, v17 major window): both kernel copies removed; ./api is the sole owner of the bare names. Name continuity for the kernel-only type alias: `MetadataBulkRegisterRequest` (z.input) is now exported from ./api beside its schema, per the family convention and the #4570 docs-import concern. Compile-time pin (typeof import conditional type, #4581 pattern) keeps the bare names out of ./kernel; new events.test.ts covers the surviving ./api declarations. dual-source-exports.baseline.json: exactly the 3 named rows removed (31 -> 28). json-schema.manifest: kernel/MetadataEvent and kernel/MetadataBulkRegisterRequest retired deliberately; their 11 authorable-surface rows hand-deleted per the #4458/#4568/#4581 precedent (plugin TS types, schema bodies deleted — no silent-strip path, misuse fails at the import site at compile time). Changeset: @objectstack/spec major with FROM -> TO migration lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
1 parent 355e951 commit 9610b13

9 files changed

Lines changed: 253 additions & 177 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: `@objectstack/spec/kernel` no longer exports `MetadataEvent(Schema)` / `MetadataBulkRegisterRequest(Schema)` — the bare names belong to `./api` alone (#4587)
6+
7+
The names `MetadataEvent` / `MetadataEventSchema` /
8+
`MetadataBulkRegisterRequestSchema` resolved to **two different declarations**
9+
depending on the import path (`./api` vs `./kernel`) — the #4411 dual-source
10+
trap. Resolution (three-repo, import-statement-level consumer scan: framework,
11+
cloud, objectui — the `./kernel` copies had zero importers outside their own
12+
unit test):
13+
14+
- **Removed** `MetadataEventSchema` / `MetadataEvent` from
15+
`@objectstack/spec/kernel`. This was a lifecycle-event envelope
16+
(`event: 'metadata.registered' | … | 'metadata.exported'`, plus
17+
`actor`/`payload`/`namespace`) that **nothing ever emitted or consumed**
18+
the vocabulary appears in no producer in any of the three repos. The live
19+
contract is `./api`'s `MetadataEvent(Schema)`
20+
(`type: 'metadata.{type}.{created|updated|deleted}'`, with `id` / `definition`
21+
/ `userId`): `MetadataManager` publishes those events to the realtime
22+
service and `@objectstack/client` / `@objectstack/client-react` subscribe to
23+
them.
24+
- FROM `import { MetadataEvent } from '@objectstack/spec/kernel'`
25+
TO `import type { MetadataEvent } from '@objectstack/spec/api'`.
26+
**Shape change**: the api event has `id` (uuid), `type`
27+
(`metadata.{type}.{created|updated|deleted}`), `definition`, `userId`; the
28+
removed kernel shape's `event` / `actor` / `payload` / `namespace` fields
29+
do not exist there. If you needed runtime *watch* events, that contract is
30+
`MetadataWatchEvent` in `@objectstack/spec/system`; repository change-log
31+
events are `MetadataEvent` from `@objectstack/metadata-core` (ADR-0008) —
32+
a third, unrelated declaration that is not part of `@objectstack/spec`.
33+
- **Removed** `MetadataBulkRegisterRequestSchema` /
34+
`MetadataBulkRegisterRequest` from `@objectstack/spec/kernel`. It was a dead
35+
near-duplicate of the REST contract that also diverged from the enforced
36+
write path: its per-item `namespace` field exists neither in
37+
`IMetadataService.bulkRegister` (contracts) nor in
38+
`MetadataManager.bulkRegister`, and `namespace` is deprecated platform-wide.
39+
- FROM `import { MetadataBulkRegisterRequestSchema, MetadataBulkRegisterRequest } from '@objectstack/spec/kernel'`
40+
TO `import { MetadataBulkRegisterRequestSchema, type MetadataBulkRegisterRequest } from '@objectstack/spec/api'`
41+
(the `POST /api/meta/bulk/register` contract; the type export is new on
42+
`./api` in this release). **Shape change**: items are strictly
43+
`{ type, name, data }` — a per-item `namespace` no longer parses into the
44+
accepted shape. `MetadataBulkRegisterRequest` is the authoring-side type
45+
(`z.input`): `continueOnError` / `validate` stay optional and carry
46+
defaults, as before.
47+
- `@objectstack/spec/api`'s `MetadataEvent(Schema)` and
48+
`MetadataBulkRegisterRequestSchema` are **unchanged** and are now the sole
49+
owners of the bare names. Imports from `./api` need no migration.
50+
- `@objectstack/spec/kernel`'s `MetadataBulkResultSchema` /
51+
`MetadataBulkResult` are **unchanged** — only the bulk *register request*
52+
pair moved.
53+
54+
`dual-source-exports.baseline.json` shrinks by exactly these 3 rows (31 → 28,
55+
#4535 C2).

packages/spec/authorable-surface.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4639,9 +4639,6 @@
46394639
"kernel/MergeStrategyConfig:alwaysKeepCustom",
46404640
"kernel/MergeStrategyConfig:autoResolveNonConflicting",
46414641
"kernel/MergeStrategyConfig:defaultStrategy",
4642-
"kernel/MetadataBulkRegisterRequest:continueOnError",
4643-
"kernel/MetadataBulkRegisterRequest:items",
4644-
"kernel/MetadataBulkRegisterRequest:validate",
46454642
"kernel/MetadataBulkResult:errors",
46464643
"kernel/MetadataBulkResult:failed",
46474644
"kernel/MetadataBulkResult:succeeded",
@@ -4657,14 +4654,6 @@
46574654
"kernel/MetadataDiffItem:previousName",
46584655
"kernel/MetadataDiffItem:summary",
46594656
"kernel/MetadataDiffItem:type",
4660-
"kernel/MetadataEvent:actor",
4661-
"kernel/MetadataEvent:event",
4662-
"kernel/MetadataEvent:metadataType",
4663-
"kernel/MetadataEvent:name",
4664-
"kernel/MetadataEvent:namespace",
4665-
"kernel/MetadataEvent:packageId",
4666-
"kernel/MetadataEvent:payload",
4667-
"kernel/MetadataEvent:timestamp",
46684657
"kernel/MetadataManagerConfig:cache",
46694658
"kernel/MetadataManagerConfig:datasource",
46704659
"kernel/MetadataManagerConfig:fallback",

packages/spec/dual-source-exports.baseline.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
"FieldMappingSchema — [./data (const)] ≠ [./integration (const)] ≠ [./shared (const)]",
1616
"HttpMethod — [./api, ./shared (type)] ≠ [./ui (type)]",
1717
"HttpRequest — [./shared (type)] ≠ [./ui (type)]",
18-
"MetadataBulkRegisterRequestSchema — [./api (const)] ≠ [./kernel (const)]",
19-
"MetadataEvent — [./api (type)] ≠ [./kernel (type)]",
20-
"MetadataEventSchema — [./api (const)] ≠ [./kernel (const)]",
2118
"Notification — [./api (type)] ≠ [./ui (type)]",
2219
"NotificationConfig — [./system (type)] ≠ [./ui (type)]",
2320
"NotificationConfigSchema — [./system (const)] ≠ [./ui (const)]",

packages/spec/json-schema.manifest.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,14 +966,12 @@
966966
"kernel/MergeConflict",
967967
"kernel/MergeResult",
968968
"kernel/MergeStrategyConfig",
969-
"kernel/MetadataBulkRegisterRequest",
970969
"kernel/MetadataBulkResult",
971970
"kernel/MetadataCategoryEnum",
972971
"kernel/MetadataChangeOperation",
973972
"kernel/MetadataChangeType",
974973
"kernel/MetadataDependency",
975974
"kernel/MetadataDiffItem",
976-
"kernel/MetadataEvent",
977975
"kernel/MetadataFallbackStrategy",
978976
"kernel/MetadataLock",
979977
"kernel/MetadataLockSource",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
MetadataEventType,
4+
DataEventType,
5+
MetadataEventSchema,
6+
DataEventSchema,
7+
} from './events.zod';
8+
9+
// Coverage added with the v17 dual-source cleanup (#4587): ./api is now the
10+
// SOLE owner of the bare names MetadataEvent(Schema). This vocabulary is the
11+
// live realtime contract — `MetadataManager` publishes
12+
// `metadata.{type}.{created|deleted}` events and the client SDK
13+
// (`@objectstack/client` subscribeMetadata / `@objectstack/client-react`)
14+
// subscribes against exactly these names. The kernel-side lifecycle envelope
15+
// (`metadata.registered/…`) that used to share the names was removed — it had
16+
// no producer and no consumer.
17+
18+
describe('MetadataEventType', () => {
19+
it('follows the metadata.{type}.{created|updated|deleted} pattern for every member', () => {
20+
for (const value of MetadataEventType.options) {
21+
expect(value).toMatch(/^metadata\.[a-z]+\.(created|updated|deleted)$/);
22+
}
23+
});
24+
25+
it('covers the created/updated/deleted triple for each covered metadata type', () => {
26+
const byType = new Map<string, string[]>();
27+
for (const value of MetadataEventType.options) {
28+
const [, metadataType, action] = value.split('.');
29+
const actions = byType.get(metadataType) ?? [];
30+
actions.push(action);
31+
byType.set(metadataType, actions);
32+
}
33+
for (const [metadataType, actions] of byType) {
34+
expect(actions, `actions of metadata.${metadataType}.*`).toEqual([
35+
'created',
36+
'updated',
37+
'deleted',
38+
]);
39+
}
40+
});
41+
42+
it('includes the event names the client SDK subscribes to', () => {
43+
// `RealtimeAPI.subscribeMetadata(type)` filters on exactly these three.
44+
for (const name of [
45+
'metadata.object.created',
46+
'metadata.object.updated',
47+
'metadata.object.deleted',
48+
]) {
49+
expect(MetadataEventType.options).toContain(name);
50+
}
51+
});
52+
});
53+
54+
describe('MetadataEventSchema', () => {
55+
const base = {
56+
id: '4b4720e8-97c3-4a12-9b70-b70a3d2314a1',
57+
type: 'metadata.object.created',
58+
metadataType: 'object',
59+
name: 'account',
60+
timestamp: new Date().toISOString(),
61+
};
62+
63+
it('validates a minimal realtime metadata event', () => {
64+
const event = MetadataEventSchema.parse(base);
65+
expect(event.type).toBe('metadata.object.created');
66+
expect(event.name).toBe('account');
67+
});
68+
69+
it('accepts the optional payload fields', () => {
70+
const event = MetadataEventSchema.parse({
71+
...base,
72+
packageId: 'com.acme.crm',
73+
definition: { label: 'Account' },
74+
userId: 'usr_1',
75+
});
76+
expect(event.packageId).toBe('com.acme.crm');
77+
expect(event.userId).toBe('usr_1');
78+
});
79+
80+
it('rejects event types outside the vocabulary', () => {
81+
expect(() => MetadataEventSchema.parse({
82+
...base,
83+
// The retired kernel-side lifecycle vocabulary must NOT parse here —
84+
// it was never produced by anything and is not part of this contract.
85+
type: 'metadata.registered',
86+
})).toThrow();
87+
});
88+
89+
it('rejects a non-uuid id and a non-datetime timestamp', () => {
90+
expect(() => MetadataEventSchema.parse({ ...base, id: 'evt-1' })).toThrow();
91+
expect(() => MetadataEventSchema.parse({ ...base, timestamp: 'yesterday' })).toThrow();
92+
});
93+
});
94+
95+
describe('DataEventSchema', () => {
96+
it('validates a data record event', () => {
97+
const event = DataEventSchema.parse({
98+
id: '4b4720e8-97c3-4a12-9b70-b70a3d2314a2',
99+
type: 'data.record.updated',
100+
object: 'account',
101+
recordId: 'rec_1',
102+
changes: { name: 'New Name' },
103+
timestamp: new Date().toISOString(),
104+
});
105+
expect(event.object).toBe('account');
106+
expect(event.recordId).toBe('rec_1');
107+
});
108+
109+
it('rejects types outside the DataEventType vocabulary', () => {
110+
expect(DataEventType.options).toEqual([
111+
'data.record.created',
112+
'data.record.updated',
113+
'data.record.deleted',
114+
'data.field.changed',
115+
]);
116+
expect(() => DataEventSchema.parse({
117+
id: '4b4720e8-97c3-4a12-9b70-b70a3d2314a3',
118+
type: 'data.record.upserted',
119+
object: 'account',
120+
recordId: 'rec_1',
121+
timestamp: new Date().toISOString(),
122+
})).toThrow();
123+
});
124+
});

packages/spec/src/api/metadata.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
// Dependencies
3636
MetadataDependenciesResponseSchema,
3737
MetadataDependentsResponseSchema,
38+
type MetadataBulkRegisterRequest,
3839
} from './metadata.zod';
3940

4041
// ==========================================
@@ -469,6 +470,24 @@ describe('MetadataBulkRegisterRequestSchema', () => {
469470
expect(result.continueOnError).toBe(true);
470471
expect(result.validate).toBe(false);
471472
});
473+
474+
// v17 (#4587): `MetadataBulkRegisterRequest` moved here from ./kernel,
475+
// which carried a dead near-duplicate of this schema (extra per-item
476+
// `namespace` that no enforced write path reads). The exported type is the
477+
// authoring-side shape (z.input): the defaulted flags stay optional, and a
478+
// per-item `namespace` is NOT part of the contract.
479+
it('exports the authoring-side MetadataBulkRegisterRequest type (#4587)', () => {
480+
const minimal: MetadataBulkRegisterRequest = {
481+
items: [{ type: 'object', name: 'account', data: { label: 'Account' } }],
482+
};
483+
const parsed = MetadataBulkRegisterRequestSchema.parse(minimal);
484+
expect(parsed.continueOnError).toBe(false);
485+
expect(parsed.validate).toBe(true);
486+
487+
type Item = MetadataBulkRegisterRequest['items'][number];
488+
const hasNamespace: 'namespace' extends keyof Item ? true : false = false;
489+
expect(hasNamespace).toBe(false);
490+
});
472491
});
473492

474493
describe('MetadataBulkUnregisterRequestSchema', () => {

packages/spec/src/api/metadata.zod.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,16 @@ export type MetadataNamesResponse = z.infer<typeof MetadataNamesResponseSchema>;
353353
export type MetadataExistsResponse = z.infer<typeof MetadataExistsResponseSchema>;
354354
export type MetadataDeleteResponse = z.infer<typeof MetadataDeleteResponseSchema>;
355355
export type MetadataQueryResponse = z.infer<typeof MetadataQueryResponseSchema>;
356+
/**
357+
* Authoring-side shape of the bulk register request (`continueOnError` /
358+
* `validate` stay optional — they carry defaults).
359+
*
360+
* v17 (#4587): this name moved here from `@objectstack/spec/kernel`, whose
361+
* copy of the schema was a dead duplicate (its extra per-item `namespace`
362+
* field matched no enforced write path). `./api` is the single owner of
363+
* `MetadataBulkRegisterRequest(Schema)` now.
364+
*/
365+
export type MetadataBulkRegisterRequest = z.input<typeof MetadataBulkRegisterRequestSchema>;
356366
export type MetadataBulkResponse = z.infer<typeof MetadataBulkResponseSchema>;
357367
export type MetadataOverlayResponse = z.infer<typeof MetadataOverlayResponseSchema>;
358368
export type MetadataEffectiveResponse = z.infer<typeof MetadataEffectiveResponseSchema>;

0 commit comments

Comments
 (0)