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
11 changes: 11 additions & 0 deletions .changeset/meta-types-create-seed.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 64 additions & 0 deletions packages/dogfood/test/meta-types-create-seed.dogfood.test.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>>;

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<Record<string, unknown>> };
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<string, unknown> | 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<string, unknown> | undefined;
expect(seed).toBeDefined();
expect(seed?.type).toBe('script');
expect((seed?.body as Record<string, unknown>)?.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));
}
});
});
8 changes: 7 additions & 1 deletion packages/objectql/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`).
Expand All @@ -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.
Expand All @@ -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 } : {}),
};
Expand Down
2 changes: 2 additions & 0 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/src/api/protocol.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)'),
}));

Expand Down
1 change: 1 addition & 0 deletions packages/spec/src/kernel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';