diff --git a/.changeset/meta-types-create-seed.md b/.changeset/meta-types-create-seed.md new file mode 100644 index 0000000000..06ea23745e --- /dev/null +++ b/.changeset/meta-types-create-seed.md @@ -0,0 +1,11 @@ +--- +'@objectstack/spec': minor +'@objectstack/objectql': minor +--- + +Expose authoritative create seeds via /meta/types (spec-derived create-shape contract, Phase 2) + +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. + +- `@objectstack/spec`: barrel-export `getMetadataCreateSeed` / `listMetadataCreateSeedTypes` from `/kernel`; add optional `createSeed` to the `GetMetaTypesResponse` entry schema. +- `@objectstack/objectql`: `getMetaTypes()` attaches each type's seed (registry + runtime entries). Canvas-create types whose shape is built interactively (report) are intentionally absent. diff --git a/packages/dogfood/test/meta-types-create-seed.dogfood.test.ts b/packages/dogfood/test/meta-types-create-seed.dogfood.test.ts new file mode 100644 index 0000000000..a281a9d078 --- /dev/null +++ b/packages/dogfood/test/meta-types-create-seed.dogfood.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Phase 2 of the spec-derived create-shape contract: the authoritative minimal +// create seeds (packages/spec/src/kernel/metadata-create-seeds.ts) must reach +// the Studio designer / CLI / API clients through the real `/meta/types` +// registry response, so consumers derive their create defaults from the spec +// instead of re-inventing them (the drift that produced the dashboard-`layout` +// and action-`body` create-save 422s). Exercised end-to-end over real HTTP. + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import showcaseStack from '@objectstack/example-showcase'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { getMetadataCreateSeed, listMetadataCreateSeedTypes } from '@objectstack/spec/kernel'; + +describe('dogfood: /meta/types exposes authoritative create seeds (spec-derived create-shape contract)', () => { + let stack: VerifyStack; + let token: string; + let entries: Array>; + + beforeAll(async () => { + stack = await bootStack(showcaseStack); + token = await stack.signIn(); + const res = await stack.apiAs(token, 'GET', '/meta'); // GET {prefix} lists all metadata types (entries[]) + expect(res.status).toBe(200); + const body = (await res.json()) as { entries?: Array> }; + entries = body.entries ?? []; + expect(entries.length).toBeGreaterThan(0); + }, 90_000); + + afterAll(async () => { + await stack?.stop(); + }); + + const entryFor = (type: string) => entries.find((e) => e.type === type); + + it('carries the dashboard create seed (widgets: [])', () => { + const seed = entryFor('dashboard')?.createSeed as Record | undefined; + expect(seed).toBeDefined(); + expect(seed).toMatchObject({ widgets: [] }); + }); + + it('carries the action create seed with a valid executable body', () => { + const seed = entryFor('action')?.createSeed as Record | undefined; + expect(seed).toBeDefined(); + expect(seed?.type).toBe('script'); + expect((seed?.body as Record)?.language).toBe('js'); + }); + + it('omits a seed for report (a canvas-create type whose dataset is picked interactively)', () => { + // report IS a registered type but is intentionally absent from the seed + // registry — its minimal valid shape needs a dataset chosen on the canvas, + // not a static literal. The designer falls back / uses canvas create. + expect(entryFor('report')).toBeDefined(); + expect(entryFor('report')?.createSeed).toBeUndefined(); + }); + + it('every seeded type that is also a registered /meta/types entry exposes its exact seed', () => { + for (const type of listMetadataCreateSeedTypes()) { + const entry = entryFor(type); + if (!entry) continue; // type may not be registered in this app — skip + expect(entry.createSeed, `${type} entry is missing its create seed`).toEqual(getMetadataCreateSeed(type)); + } + }); +}); diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index d8fca81a7d..2b454a8fe7 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -20,7 +20,7 @@ import { parseFilterAST, isFilterAST } from '@objectstack/spec/data'; import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared'; import { type FormView, isAggregatedViewContainer } from '@objectstack/spec/ui'; import { METADATA_FORM_REGISTRY } from '@objectstack/spec/system'; -import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema, getMetadataTypeActions } from '@objectstack/spec/kernel'; +import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema, getMetadataTypeActions, getMetadataCreateSeed } from '@objectstack/spec/kernel'; import { extractProtection, evaluateLockForWrite, @@ -992,6 +992,10 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : undefined) ?? HAND_CRAFTED_SCHEMAS[singular]; const form = TYPE_TO_FORM[singular]; + // Phase 2: the authoritative minimal create seed (single source of + // truth in @objectstack/spec). Studio/CLI derive create defaults + // from this via /meta/types instead of re-inventing them. + const createSeed = getMetadataCreateSeed(singular); // Type-level actions: merge the registry's declarative actions // with any plugin-registered overlay (`registerMetadataTypeActions`). @@ -1014,6 +1018,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { : 'registry' as const, schema, form, + ...(createSeed !== undefined ? { createSeed } : {}), // Override the spread `base.actions` with the merged view // (declarative + plugin-registered). Omit when empty to // preserve the prior "no actions key" response shape. @@ -1038,6 +1043,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { overrideSource: writableOverrides.has(singular) ? 'env' as const : 'registry' as const, schema, form, + ...(createSeed !== undefined ? { createSeed } : {}), // Plugin-registered actions on a type with no registry entry. ...(typeActions.length ? { actions: typeActions } : {}), }; diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index b58eca03a6..1fbe8892a2 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1685,9 +1685,11 @@ "evaluateLockForDelete (function)", "evaluateLockForWrite (function)", "extractProtection (function)", + "getMetadataCreateSeed (function)", "getMetadataTypeActions (function)", "getMetadataTypeSchema (function)", "isConsumerInstallable (function)", + "listMetadataCreateSeedTypes (function)", "listMetadataTypeSchemaTypes (function)", "registerMetadataTypeActions (function)", "registerMetadataTypeSchema (function)", diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index be60b01aa0..6d658b3a9b 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -180,6 +180,7 @@ export const GetMetaTypesResponseSchema = lazySchema(() => z.object({ loadOrder: z.number().int().describe('Loading priority (lower = earlier)'), domain: z.enum(['data', 'ui', 'automation', 'system', 'security', 'ai']).describe('Protocol domain'), overrideSource: z.enum(['registry', 'env']).describe('Whether allowOrgOverride is set in the static registry or via OBJECTSTACK_METADATA_WRITABLE env var'), + 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.'), })).optional().describe('Enriched per-type registry entries (Phase 3a)'), })); diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts index 44e9dcb11f..18fc48cbe8 100644 --- a/packages/spec/src/kernel/index.ts +++ b/packages/spec/src/kernel/index.ts @@ -31,3 +31,4 @@ export * from './startup-orchestrator.zod'; export * from './plugin-registry.zod'; export * from './plugin-security.zod'; export * from './execution-context.zod'; +export * from './metadata-create-seeds';