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
81 changes: 81 additions & 0 deletions packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,25 @@ export class HttpDispatcher {
...(organizationId ? { organizationId } : {}),
...(body?.actor ? { actor: body.actor } : {}),
});
// Publishing a `seed` draft is what actually loads its
// rows. Best-effort + idempotent (upsert): apply every
// just-published seed now so the data is live the moment
// the user clicks publish. A seed-load failure NEVER
// fails the publish — it is surfaced under `seedApplied`.
try {
const seedNames = ((result as any)?.published ?? [])
.filter((p: any) => p?.type === 'seed')
.map((p: any) => p.name as string);
if (seedNames.length > 0) {
(result as any).seedApplied = await this.applyPublishedSeeds(
seedNames,
organizationId,
_context,
);
}
} catch (e: any) {
(result as any).seedApplied = { success: false, error: e?.message ?? 'seed apply failed' };
}
return { handled: true, response: this.success(result) };
} catch (e: any) {
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
Expand Down Expand Up @@ -1715,6 +1734,68 @@ export class HttpDispatcher {
* Physical database addressing (database_url, database_driver, etc.)
* is stored directly on the sys_environment row.
*/
/**
* Apply just-published `seed` metadata: load each seed's rows into its
* target object so publishing a seed draft makes the data live (the runtime
* counterpart to staging it). Reads each seed body via the protocol, then
* runs the {@link SeedLoaderService} for the active org. Best-effort and
* idempotent (upsert) — callers must never let this fail the publish.
*
* Lives at the runtime layer (not in the objectql publish primitive)
* because the seed loader needs the data engine + metadata service, which
* objectql cannot depend on without a layering cycle.
*/
private async applyPublishedSeeds(
names: string[],
organizationId: string | undefined,
_context: HttpProtocolContext,
): Promise<{ success: boolean; inserted?: number; updated?: number; errors?: unknown[]; error?: string }> {
const protocol: any = await this.resolveService('protocol');
const metadata: any = await this.getService(CoreServiceName.enum.metadata);
const ql: any = await this.resolveService('objectql');
if (!protocol || typeof protocol.getMetaItem !== 'function' || !ql || !metadata) {
return { success: false, error: 'seed apply: required services unavailable' };
}
const datasets: any[] = [];
for (const name of names) {
try {
const item: any = await protocol.getMetaItem({
type: 'seed',
name,
...(organizationId ? { organizationId } : {}),
});
// getMetaItem returns the item body directly; tolerate a
// wrapper ({metadata|body}) just in case.
const seed = item?.object && Array.isArray(item?.records)
? item
: (item?.metadata ?? item?.body);
if (seed?.object && Array.isArray(seed?.records)) datasets.push(seed);
} catch {
/* skip an unreadable seed; keep applying the rest */
}
}
if (datasets.length === 0) return { success: true, inserted: 0, updated: 0 };

const { SeedLoaderService } = await import('./seed-loader.js');
const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data');
const loader = new SeedLoaderService(ql, metadata, (this as any).logger ?? console);
const request = SeedLoaderRequestSchema.parse({
datasets,
config: {
defaultMode: 'upsert',
multiPass: true,
...(organizationId ? { organizationId } : {}),
},
});
const r = await loader.load(request);
return {
success: r.success,
inserted: r.summary.totalInserted,
updated: r.summary.totalUpdated,
errors: r.errors,
};
}

/**
* Resolve the calling user id from the request session, if any.
* Returns `undefined` for anonymous calls or when auth is not wired up.
Expand Down
17 changes: 17 additions & 0 deletions packages/spec/src/data/dataset.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ export const DatasetSchema = lazySchema(() => z.object({
records: z.array(z.record(z.string(), z.unknown())).describe('Data records'),
}));

/**
* Seed metadata-type schema — the runtime-draftable, publishable form of
* fixture / initialization data (the `seed` metadata type).
*
* It shares the {@link DatasetSchema} shape today (object + externalId + mode +
* env + records), but is exported under its own name so the `seed` metadata
* type can evolve independently of the `dataset` name, which ADR reserves for a
* future data-analysis capability. Authoring tools (incl. the AI metadata
* assistant) stage `type: 'seed'` drafts against this schema; publishing the
* draft is what applies the rows (runtime SeedLoaderService).
*/
export const SeedSchema = DatasetSchema;

/** A seed metadata item (same shape as {@link Dataset}). */
export type Seed = z.infer<typeof SeedSchema>;
export type SeedInput = z.input<typeof SeedSchema>;

/** Parsed/output type — all defaults are applied (env, mode, externalId always present) */
export type Dataset = z.infer<typeof DatasetSchema>;

Expand Down
15 changes: 14 additions & 1 deletion packages/spec/src/kernel/metadata-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('MetadataPluginProtocol', () => {
describe('MetadataTypeSchema', () => {
it('should accept all built-in metadata types', () => {
const types = [
'object', 'field', 'trigger', 'validation', 'hook',
'object', 'field', 'trigger', 'validation', 'hook', 'seed',
'view', 'page', 'dashboard', 'app', 'action', 'report',
'flow', // ADR-0020: `workflow` retired as a metadata type
'datasource', 'translation', 'router', 'function', 'service',
Expand All @@ -40,6 +40,19 @@ describe('MetadataPluginProtocol', () => {
it('should reject `approval` as a metadata type (ADR-0019: approval is a flow node)', () => {
expect(() => MetadataTypeSchema.parse('approval')).toThrow();
});

it('registers `seed` as a runtime-draftable data type applied on publish', () => {
// accepted by the type enum
expect(MetadataTypeSchema.parse('seed')).toBe('seed');
// present in the default registry with the right capabilities
const entry = DEFAULT_METADATA_TYPE_REGISTRY.find((e) => e.type === 'seed');
expect(entry).toBeDefined();
expect(entry?.domain).toBe('data');
expect(entry?.allowRuntimeCreate).toBe(true); // AI/authors can stage seed drafts
// loads after objects/fields so referenced schema already exists
const objectOrder = DEFAULT_METADATA_TYPE_REGISTRY.find((e) => e.type === 'object')?.loadOrder ?? 0;
expect((entry?.loadOrder ?? 0)).toBeGreaterThan(objectOrder);
});
});

describe('MetadataTypeRegistryEntrySchema', () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/spec/src/kernel/metadata-plugin.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const MetadataTypeSchema = lazySchema(() => z.enum([
'trigger', // Data-layer event triggers (TriggerSchema)
'validation', // Validation rules (ValidationSchema)
'hook', // Data hooks (HookSchema)
'seed', // Seed/fixture data — runtime-draftable; publishing applies it (SeedSchema)

// UI Protocol
'view', // List/form views (ViewSchema)
Expand Down Expand Up @@ -594,6 +595,14 @@ export const DEFAULT_METADATA_TYPE_REGISTRY: MetadataTypeRegistryEntry[] = [
{ type: 'trigger', label: 'Trigger', filePatterns: ['**/*.trigger.ts', '**/*.trigger.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' },
{ type: 'validation', label: 'Validation Rule', filePatterns: ['**/*.validation.ts', '**/*.validation.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' },
{ type: 'hook', label: 'Hook', filePatterns: ['**/*.hook.ts', '**/*.hook.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' },
// `seed`: fixture / initialization data (SeedSchema = object + records + mode +
// externalId). Runtime-draftable so the AI (and any author) can stage seed
// rows as a DRAFT (ADR-0033) and PUBLISH them like any other artifact;
// publishing a seed draft is what actually loads the rows (runtime applies it
// via SeedLoaderService — see runtime publish-drafts handler). loadOrder is
// last in the data domain so every referenced object/field already exists.
// NOTE: distinct from the (analytics-bound) `dataset` name — see ADR.
{ type: 'seed', label: 'Seed Data', description: 'Fixture / initialization data applied on publish', filePatterns: ['**/*.seed.ts', '**/*.seed.yml', '**/*.seed.json'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 95, domain: 'data' },

// UI Protocol
// `view/page/dashboard/action/report`: UI artifacts benefit from version
Expand Down
2 changes: 2 additions & 0 deletions packages/spec/src/kernel/metadata-type-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ObjectSchema } from '../data/object.zod';
import { HookSchema } from '../data/hook.zod';
import { ValidationRuleSchema } from '../data/validation.zod';
import { DatasourceSchema } from '../data/datasource.zod';
import { SeedSchema } from '../data/dataset.zod';

import { ViewSchema } from '../ui/view.zod';
import { PageSchema } from '../ui/page.zod';
Expand Down Expand Up @@ -68,6 +69,7 @@ const BUILTIN_METADATA_TYPE_SCHEMAS: Partial<Record<MetadataType, z.ZodType>> =
field: FieldSchema,
hook: HookSchema,
validation: ValidationRuleSchema,
seed: SeedSchema, // fixture/init data; runtime-draftable, applied on publish
// `trigger` — no standalone Zod schema yet; falls back to raw-JSON
// editor until the data-trigger spec lands.

Expand Down
Loading