Skip to content

Commit cd51229

Browse files
os-zhuangclaude
andauthored
feat(meta): expose authoritative create seeds via /meta/types (create-shape contract, Phase 2) (#2276)
Phase 1 (#2270) added the authoritative minimal create seeds in @objectstack/spec/kernel. Phase 2 delivers them to consumers: every /meta/types entry now carries an optional `createSeed`, so the Studio designer / CLI / API derive create defaults from the spec's single source of truth instead of re-inventing `createDefaults` (the drift that produced the dashboard-`layout` and action-`body` create→save 422s). - spec: barrel-export getMetadataCreateSeed / listMetadataCreateSeedTypes from /kernel; add optional `createSeed` to the GetMetaTypesResponse entry schema; regenerate api-surface (purely additive — 2 new exports). - objectql: getMetaTypes() attaches each type's seed to registry + runtime entries. Canvas-create types built interactively (report) stay absent. - dogfood: meta-types-create-seed.dogfood.test.ts asserts /meta exposes the dashboard/action seeds end-to-end and omits report's (4 pass). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 99d41b5 commit cd51229

6 files changed

Lines changed: 86 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
Expose authoritative create seeds via /meta/types (spec-derived create-shape contract, Phase 2)
7+
8+
The minimal valid create seeds added in `@objectstack/spec/kernel` (`getMetadataCreateSeed`) now reach consumers through the real `/meta/types` registry response: each entry carries an optional `createSeed`. The Studio designer / CLI / API clients derive their create defaults from this single source of truth instead of re-inventing them — closing the drift that produced the dashboard-`layout` and action-`body` create→save 422s.
9+
10+
- `@objectstack/spec`: barrel-export `getMetadataCreateSeed` / `listMetadataCreateSeedTypes` from `/kernel`; add optional `createSeed` to the `GetMetaTypesResponse` entry schema.
11+
- `@objectstack/objectql`: `getMetaTypes()` attaches each type's seed (registry + runtime entries). Canvas-create types whose shape is built interactively (report) are intentionally absent.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Phase 2 of the spec-derived create-shape contract: the authoritative minimal
4+
// create seeds (packages/spec/src/kernel/metadata-create-seeds.ts) must reach
5+
// the Studio designer / CLI / API clients through the real `/meta/types`
6+
// registry response, so consumers derive their create defaults from the spec
7+
// instead of re-inventing them (the drift that produced the dashboard-`layout`
8+
// and action-`body` create-save 422s). Exercised end-to-end over real HTTP.
9+
10+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
11+
import showcaseStack from '@objectstack/example-showcase';
12+
import { bootStack, type VerifyStack } from '@objectstack/verify';
13+
import { getMetadataCreateSeed, listMetadataCreateSeedTypes } from '@objectstack/spec/kernel';
14+
15+
describe('dogfood: /meta/types exposes authoritative create seeds (spec-derived create-shape contract)', () => {
16+
let stack: VerifyStack;
17+
let token: string;
18+
let entries: Array<Record<string, unknown>>;
19+
20+
beforeAll(async () => {
21+
stack = await bootStack(showcaseStack);
22+
token = await stack.signIn();
23+
const res = await stack.apiAs(token, 'GET', '/meta'); // GET {prefix} lists all metadata types (entries[])
24+
expect(res.status).toBe(200);
25+
const body = (await res.json()) as { entries?: Array<Record<string, unknown>> };
26+
entries = body.entries ?? [];
27+
expect(entries.length).toBeGreaterThan(0);
28+
}, 90_000);
29+
30+
afterAll(async () => {
31+
await stack?.stop();
32+
});
33+
34+
const entryFor = (type: string) => entries.find((e) => e.type === type);
35+
36+
it('carries the dashboard create seed (widgets: [])', () => {
37+
const seed = entryFor('dashboard')?.createSeed as Record<string, unknown> | undefined;
38+
expect(seed).toBeDefined();
39+
expect(seed).toMatchObject({ widgets: [] });
40+
});
41+
42+
it('carries the action create seed with a valid executable body', () => {
43+
const seed = entryFor('action')?.createSeed as Record<string, unknown> | undefined;
44+
expect(seed).toBeDefined();
45+
expect(seed?.type).toBe('script');
46+
expect((seed?.body as Record<string, unknown>)?.language).toBe('js');
47+
});
48+
49+
it('omits a seed for report (a canvas-create type whose dataset is picked interactively)', () => {
50+
// report IS a registered type but is intentionally absent from the seed
51+
// registry — its minimal valid shape needs a dataset chosen on the canvas,
52+
// not a static literal. The designer falls back / uses canvas create.
53+
expect(entryFor('report')).toBeDefined();
54+
expect(entryFor('report')?.createSeed).toBeUndefined();
55+
});
56+
57+
it('every seeded type that is also a registered /meta/types entry exposes its exact seed', () => {
58+
for (const type of listMetadataCreateSeedTypes()) {
59+
const entry = entryFor(type);
60+
if (!entry) continue; // type may not be registered in this app — skip
61+
expect(entry.createSeed, `${type} entry is missing its create seed`).toEqual(getMetadataCreateSeed(type));
62+
}
63+
});
64+
});

packages/objectql/src/protocol.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { parseFilterAST, isFilterAST } from '@objectstack/spec/data';
2020
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
2121
import { type FormView, isAggregatedViewContainer } from '@objectstack/spec/ui';
2222
import { METADATA_FORM_REGISTRY } from '@objectstack/spec/system';
23-
import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema, getMetadataTypeActions } from '@objectstack/spec/kernel';
23+
import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema, getMetadataTypeActions, getMetadataCreateSeed } from '@objectstack/spec/kernel';
2424
import {
2525
extractProtection,
2626
evaluateLockForWrite,
@@ -992,6 +992,10 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
992992
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : undefined)
993993
?? HAND_CRAFTED_SCHEMAS[singular];
994994
const form = TYPE_TO_FORM[singular];
995+
// Phase 2: the authoritative minimal create seed (single source of
996+
// truth in @objectstack/spec). Studio/CLI derive create defaults
997+
// from this via /meta/types instead of re-inventing them.
998+
const createSeed = getMetadataCreateSeed(singular);
995999

9961000
// Type-level actions: merge the registry's declarative actions
9971001
// with any plugin-registered overlay (`registerMetadataTypeActions`).
@@ -1014,6 +1018,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
10141018
: 'registry' as const,
10151019
schema,
10161020
form,
1021+
...(createSeed !== undefined ? { createSeed } : {}),
10171022
// Override the spread `base.actions` with the merged view
10181023
// (declarative + plugin-registered). Omit when empty to
10191024
// preserve the prior "no actions key" response shape.
@@ -1038,6 +1043,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
10381043
overrideSource: writableOverrides.has(singular) ? 'env' as const : 'registry' as const,
10391044
schema,
10401045
form,
1046+
...(createSeed !== undefined ? { createSeed } : {}),
10411047
// Plugin-registered actions on a type with no registry entry.
10421048
...(typeActions.length ? { actions: typeActions } : {}),
10431049
};

packages/spec/api-surface.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,9 +1685,11 @@
16851685
"evaluateLockForDelete (function)",
16861686
"evaluateLockForWrite (function)",
16871687
"extractProtection (function)",
1688+
"getMetadataCreateSeed (function)",
16881689
"getMetadataTypeActions (function)",
16891690
"getMetadataTypeSchema (function)",
16901691
"isConsumerInstallable (function)",
1692+
"listMetadataCreateSeedTypes (function)",
16911693
"listMetadataTypeSchemaTypes (function)",
16921694
"registerMetadataTypeActions (function)",
16931695
"registerMetadataTypeSchema (function)",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export const GetMetaTypesResponseSchema = lazySchema(() => z.object({
180180
loadOrder: z.number().int().describe('Loading priority (lower = earlier)'),
181181
domain: z.enum(['data', 'ui', 'automation', 'system', 'security', 'ai']).describe('Protocol domain'),
182182
overrideSource: z.enum(['registry', 'env']).describe('Whether allowOrgOverride is set in the static registry or via OBJECTSTACK_METADATA_WRITABLE env var'),
183+
createSeed: z.unknown().optional().describe('Authoritative minimal valid create seed for this type — Studio/CLI/API derive create defaults from it (single source of truth in @objectstack/spec). Absent for canvas-create types whose shape is built interactively.'),
183184
})).optional().describe('Enriched per-type registry entries (Phase 3a)'),
184185
}));
185186

packages/spec/src/kernel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export * from './startup-orchestrator.zod';
3131
export * from './plugin-registry.zod';
3232
export * from './plugin-security.zod';
3333
export * from './execution-context.zod';
34+
export * from './metadata-create-seeds';

0 commit comments

Comments
 (0)