diff --git a/examples/app-crm/src/data/index.ts b/examples/app-crm/src/data/index.ts index 6da2f9488..bc378010c 100644 --- a/examples/app-crm/src/data/index.ts +++ b/examples/app-crm/src/data/index.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { defineDataset } from '@objectstack/spec/data'; +import { defineSeed } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; import { Account } from '../objects/account.object.js'; import { Contact } from '../objects/contact.object.js'; @@ -8,7 +8,7 @@ import { Opportunity } from '../objects/opportunity.object.js'; import { Lead } from '../objects/lead.object.js'; import { Activity } from '../objects/activity.object.js'; -const accounts = defineDataset(Account, { +const accounts = defineSeed(Account, { mode: 'upsert', externalId: 'name', records: [ @@ -18,7 +18,7 @@ const accounts = defineDataset(Account, { ], }); -const contacts = defineDataset(Contact, { +const contacts = defineSeed(Contact, { mode: 'upsert', externalId: 'email', records: [ @@ -28,7 +28,7 @@ const contacts = defineDataset(Contact, { ], }); -const opportunities = defineDataset(Opportunity, { +const opportunities = defineSeed(Opportunity, { mode: 'upsert', externalId: 'name', records: [ @@ -56,7 +56,7 @@ const opportunities = defineDataset(Opportunity, { ], }); -const leads = defineDataset(Lead, { +const leads = defineSeed(Lead, { mode: 'upsert', externalId: 'email', records: [ @@ -101,7 +101,7 @@ const leads = defineDataset(Lead, { ], }); -const activities = defineDataset(Activity, { +const activities = defineSeed(Activity, { mode: 'upsert', externalId: 'subject', records: [ diff --git a/examples/app-showcase/src/data/index.ts b/examples/app-showcase/src/data/index.ts index f9b7ab391..63bb8f9e0 100644 --- a/examples/app-showcase/src/data/index.ts +++ b/examples/app-showcase/src/data/index.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { defineDataset } from '@objectstack/spec/data'; +import { defineSeed } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; import { Account } from '../objects/account.object.js'; import { Project } from '../objects/project.object.js'; @@ -14,7 +14,7 @@ import { Team, ProjectMembership } from '../objects/team.object.js'; * work location (map), and projects span every status and health. */ -const accounts = defineDataset(Account, { +const accounts = defineSeed(Account, { mode: 'upsert', externalId: 'name', records: [ @@ -27,7 +27,7 @@ const accounts = defineDataset(Account, { ], }); -const projects = defineDataset(Project, { +const projects = defineSeed(Project, { mode: 'upsert', externalId: 'name', records: [ @@ -40,7 +40,7 @@ const projects = defineDataset(Project, { }); // Tasks across all five board columns, with dates + locations to drive every view. -const tasks = defineDataset(Task, { +const tasks = defineSeed(Task, { mode: 'upsert', externalId: 'title', records: [ @@ -57,7 +57,7 @@ const tasks = defineDataset(Task, { ], }); -const categories = defineDataset(Category, { +const categories = defineSeed(Category, { mode: 'upsert', externalId: 'name', records: [ @@ -68,7 +68,7 @@ const categories = defineDataset(Category, { ], }); -const teams = defineDataset(Team, { +const teams = defineSeed(Team, { mode: 'upsert', externalId: 'name', records: [ @@ -77,7 +77,7 @@ const teams = defineDataset(Team, { ], }); -const memberships = defineDataset(ProjectMembership, { +const memberships = defineSeed(ProjectMembership, { mode: 'insert', records: [ { team: 'Experience', project: 'Website Relaunch', role: 'owner', allocation_percent: 80 }, diff --git a/examples/app-todo/src/data/index.ts b/examples/app-todo/src/data/index.ts index 10775dc06..63f6045a6 100644 --- a/examples/app-todo/src/data/index.ts +++ b/examples/app-todo/src/data/index.ts @@ -1,10 +1,10 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { defineDataset } from '@objectstack/spec/data'; +import { defineSeed } from '@objectstack/spec/data'; import { cel } from '@objectstack/spec'; import { Task } from '../objects/task.object'; -const tasks = defineDataset(Task, { +const tasks = defineSeed(Task, { mode: 'upsert', externalId: 'subject', records: [ diff --git a/packages/formula/src/seed-eval.ts b/packages/formula/src/seed-eval.ts index b24a28e90..abbf17f07 100644 --- a/packages/formula/src/seed-eval.ts +++ b/packages/formula/src/seed-eval.ts @@ -1,7 +1,7 @@ /** * Seed-value resolver. * - * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array + * `Seed.records` accepts {@link SeedValue} = primitive | Expression | array * | object — install-time resolution walks the tree and replaces any * Expression node with its evaluated result. This is what makes * `close_date: cel\`now() + duration("P30D")\`` resolve to *the customer's* diff --git a/packages/plugins/plugin-org-scoping/src/claim-orphan-org-rows.ts b/packages/plugins/plugin-org-scoping/src/claim-orphan-org-rows.ts index 9854c2d9d..08eb36418 100644 --- a/packages/plugins/plugin-org-scoping/src/claim-orphan-org-rows.ts +++ b/packages/plugins/plugin-org-scoping/src/claim-orphan-org-rows.ts @@ -3,7 +3,7 @@ /** * claimOrphanOrgRows — assign seed-loaded records to the first organization. * - * Seeds (`defineDataset`) are inserted by `SeedLoaderService` using + * Seeds (`defineSeed`) are inserted by `SeedLoaderService` using * `{ context: { isSystem: true } }`, which intentionally bypasses * SecurityPlugin's `organization_id` auto-fill. As a result, in * multi-tenant mode every seed row lands with `organization_id = NULL`. diff --git a/packages/runtime/src/app-plugin.ts b/packages/runtime/src/app-plugin.ts index 0115a3793..b67d9ca7e 100644 --- a/packages/runtime/src/app-plugin.ts +++ b/packages/runtime/src/app-plugin.ts @@ -520,7 +520,7 @@ export class AppPlugin implements Plugin { const seedLoader = new SeedLoaderService(ql, md, loggerRef); const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data'); const request = SeedLoaderRequestSchema.parse({ - datasets: datasetsNow, + seeds: datasetsNow, config: { defaultMode: 'upsert', multiPass: true, @@ -540,7 +540,7 @@ export class AppPlugin implements Plugin { }; }; registerSvc('seed-replayer', replayer); - ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total datasets: ${merged.length})`); + ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total seeds: ${merged.length})`); } catch (e: any) { ctx.logger.warn('[Seeder] Failed to register seed-datasets/seed-replayer service', { error: e?.message }); } @@ -571,7 +571,7 @@ export class AppPlugin implements Plugin { const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger); const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data'); const request = SeedLoaderRequestSchema.parse({ - datasets: normalizedDatasets, + seeds: normalizedDatasets, config: { defaultMode: 'upsert', multiPass: true, identity: seedIdentity }, }); const result = await seedLoader.load(request); diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index b141b2e48..cf70f5d00 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -982,6 +982,46 @@ describe('HttpDispatcher', () => { expect(result.handled).toBe(true); expect(result.response?.status).toBe(501); }); + + // Integration: publishing a `seed` draft must LOAD its rows. This + // exercises applyPublishedSeeds end-to-end against the REAL + // SeedLoaderService (only the engine/metadata are mocked), so it pins + // the read-back shape (protocol.getMetaItem returns a WRAPPER whose body + // is under `.item`), the renamed `seeds` request field, and the loader + // invocation — the exact chain that silently loaded 0 rows on staging. + it('POST /packages/:id/publish-drafts applies published `seed` rows', async () => { + const records = [ + { name: 'Apollo', status: 'active', budget_amount: 120000 }, + { name: 'Gemini', status: 'planned', budget_amount: 45000 }, + ]; + const publishPackageDrafts = vi.fn().mockResolvedValue({ + success: true, publishedCount: 1, failedCount: 0, + published: [{ type: 'seed', name: 'project_seed', version: 'h' }], failed: [], + }); + // protocol.getMetaItem returns the WRAPPER shape (body under `.item`). + const getMetaItem = vi.fn().mockResolvedValue({ + type: 'seed', name: 'project_seed', lock: null, editable: true, + item: { object: 'project', externalId: 'name', mode: 'upsert', records }, + }); + const insert = vi.fn().mockImplementation(async (_obj: string, rec: any) => ({ id: `id_${rec.name}` })); + const find = vi.fn().mockResolvedValue([]); // no existing rows → all insert + (kernel as any).getService = vi.fn().mockImplementation((name: string) => { + if (name === 'protocol') return Promise.resolve({ publishPackageDrafts, getMetaItem }); + if (name === 'objectql') return Promise.resolve({ insert, find, update: vi.fn(), registry: { getAllPackages: vi.fn().mockReturnValue([]) } }); + if (name === 'metadata') return Promise.resolve({ getObject: vi.fn().mockResolvedValue({ name: 'project', fields: { name: { type: 'text' }, status: { type: 'select' }, budget_amount: { type: 'currency' } } }) }); + return null; + }); + + const result = await dispatcher.handlePackages('/com.workspace/publish-drafts', 'POST', {}, {}, { request: {} }); + + expect(result.response?.status).toBe(200); + const seedApplied = (result.response as any)?.body?.data?.seedApplied; + expect(seedApplied?.success).toBe(true); + expect(seedApplied?.inserted).toBe(2); + // rows actually went to the engine + expect(insert).toHaveBeenCalledTimes(2); + expect(insert).toHaveBeenCalledWith('project', expect.objectContaining({ name: 'Apollo' }), expect.anything()); + }); }); // ═══════════════════════════════════════════════════════════════ diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index d05e89a36..7a1883c42 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -1775,10 +1775,14 @@ export class HttpDispatcher { readErrors.push(`read ${name}: ${(e as Error)?.message ?? String(e)}`); } } - // getMetaItem returns the item body directly; tolerate a wrapper. + // protocol.getMetaItem (called directly, unlike the HTTP endpoint + // which unwraps) returns a WRAPPER: `{ type, name, item, lock, + // editable, … }` — the seed body (object/records) lives under + // `.item`. Tolerate the wrapper (`.item`) plus the body-direct and + // `.metadata`/`.body` shapes other protocols may return. const seed = item?.object && Array.isArray(item?.records) ? item - : (item?.metadata ?? item?.body); + : (item?.item ?? item?.metadata ?? item?.body); if (seed?.object && Array.isArray(seed?.records)) { datasets.push(seed); } else { @@ -1795,9 +1799,6 @@ export class HttpDispatcher { const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data'); const loader = new SeedLoaderService(ql, metadata, (this as any).logger ?? console); const request = SeedLoaderRequestSchema.parse({ - // ADR field is `seeds` (renamed from `datasets`); this constructor - // was added in the same PR and the rename missed it — passing - // `datasets` left `seeds` undefined and the loader saw nothing. seeds: datasets, config: { defaultMode: 'upsert', diff --git a/packages/runtime/src/seed-loader.test.ts b/packages/runtime/src/seed-loader.test.ts index e364373b4..8b2c46d18 100644 --- a/packages/runtime/src/seed-loader.test.ts +++ b/packages/runtime/src/seed-loader.test.ts @@ -277,7 +277,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme Corp' }, { name: 'Globex' }] }, ], config: { @@ -298,7 +298,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [], + seeds: [], config: { dryRun: false, haltOnError: false, multiPass: true, defaultMode: 'upsert', batchSize: 1000, transaction: false, @@ -318,7 +318,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'demo_data', externalId: 'name', mode: 'upsert', env: ['dev'], records: [{ name: 'Demo' }] }, ], @@ -356,7 +356,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme Corp' }] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'Acme Corp' }] }, ], @@ -393,7 +393,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: null }] }, ], @@ -422,7 +422,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme Corp' }] }, // The previously-masked bug: a `{ externalId: 'X' }` wrapper instead of // the plain natural-key string. Must be rejected, never handed to the driver. @@ -465,7 +465,7 @@ describe('SeedLoaderService', () => { const uuid = '550e8400-e29b-41d4-a716-446655440000'; const result = await loader.load({ - datasets: [ + seeds: [ { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: uuid }] }, ], config: { @@ -500,7 +500,7 @@ describe('SeedLoaderService', () => { // Only load contacts (accounts already exist) const result = await loader.load({ - datasets: [ + seeds: [ { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'Acme Corp' }] }, ], config: { @@ -532,7 +532,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'NonExistent' }] }, { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [] }, ], @@ -576,7 +576,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'department', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Engineering', head_id: 'Alice' }] }, { object: 'employee', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Alice', department_id: 'Engineering' }] }, ], @@ -608,7 +608,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], @@ -634,7 +634,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], @@ -661,7 +661,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'ignore', env: ['prod', 'dev', 'test'], @@ -688,7 +688,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'update', env: ['prod', 'dev', 'test'], @@ -723,7 +723,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { @@ -752,7 +752,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'NonExistent' }] }, ], @@ -782,7 +782,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'Acme' }] }, ], @@ -815,7 +815,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John' }] }, ], @@ -859,7 +859,7 @@ describe('SeedLoaderService', () => { // Deliberately put contact before account await loader.load({ - datasets: [ + seeds: [ { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'Acme' }] }, { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], @@ -894,7 +894,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: 'MissingAccount' }] }, ], @@ -932,7 +932,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [] }, { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [ { name: 'John', account_id: 'Missing1' }, @@ -986,7 +986,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { @@ -1008,7 +1008,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'A' }, { name: 'B' }] }, ], config: { @@ -1039,7 +1039,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { @@ -1079,7 +1079,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'note', externalId: 'name', @@ -1110,7 +1110,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'note', externalId: 'name', @@ -1139,7 +1139,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'note', externalId: 'name', @@ -1170,7 +1170,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', @@ -1198,7 +1198,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'code', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { @@ -1220,7 +1220,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { @@ -1250,7 +1250,7 @@ describe('SeedLoaderService', () => { const loader = new SeedLoaderService(engine, metadata, logger); const result = await loader.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, { object: 'user', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Admin' }] }, { object: 'opportunity', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Deal', account_id: 'Acme', owner_id: 'Admin' }] }, @@ -1281,7 +1281,7 @@ describe('SeedLoaderService', () => { const objectId = '507f1f77bcf86cd799439011'; const result = await loader.load({ - datasets: [ + seeds: [ { object: 'contact', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'John', account_id: objectId }] }, ], config: { diff --git a/packages/runtime/src/seed-loader.ts b/packages/runtime/src/seed-loader.ts index e934f5252..06bad9b2d 100644 --- a/packages/runtime/src/seed-loader.ts +++ b/packages/runtime/src/seed-loader.ts @@ -10,8 +10,8 @@ import type { ObjectDependencyNode, ReferenceResolution, ReferenceResolutionError, - DatasetLoadResult, - Dataset, + SeedLoadResult, + Seed, } from '@objectstack/spec/data'; import { SeedLoaderConfigSchema } from '@objectstack/spec/data'; import { resolveSeedRecord } from '@objectstack/formula'; @@ -34,7 +34,7 @@ const DEFAULT_EXTERNAL_ID_FIELD = 'name'; * - Topological dependency ordering (parents before children) * - Multi-pass loading for circular references * - Dry-run validation mode - * - Upsert support honoring DatasetSchema mode + * - Upsert support honoring SeedSchema mode * - Actionable error reporting */ export class SeedLoaderService implements ISeedLoaderService { @@ -56,10 +56,10 @@ export class SeedLoaderService implements ISeedLoaderService { const startTime = Date.now(); const config = request.config; const allErrors: ReferenceResolutionError[] = []; - const allResults: DatasetLoadResult[] = []; + const allResults: SeedLoadResult[] = []; // 1. Filter datasets by environment - const datasets = this.filterByEnv(request.datasets, config.env); + const datasets = this.filterByEnv(request.seeds, config.env); if (datasets.length === 0) { return this.buildEmptyResult(config, Date.now() - startTime); @@ -153,23 +153,23 @@ export class SeedLoaderService implements ISeedLoaderService { return { nodes, insertOrder, circularDependencies }; } - async validate(datasets: Dataset[], config?: SeedLoaderConfigInput): Promise { + async validate(datasets: Seed[], config?: SeedLoaderConfigInput): Promise { const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true }); - return this.load({ datasets, config: parsedConfig }); + return this.load({ seeds: datasets, config: parsedConfig }); } // ========================================================================== - // Internal: Dataset Loading + // Internal: Seed Loading // ========================================================================== private async loadDataset( - dataset: Dataset, + dataset: Seed, config: SeedLoaderConfig, refMap: Map, insertedRecords: Map>, deferredUpdates: DeferredUpdate[], allErrors: ReferenceResolutionError[], - ): Promise { + ): Promise { const objectName = dataset.object; const mode = dataset.mode || config.defaultMode; const externalId = dataset.externalId || 'name'; @@ -457,7 +457,7 @@ export class SeedLoaderService implements ISeedLoaderService { private async resolveDeferredUpdates( deferredUpdates: DeferredUpdate[], insertedRecords: Map>, - allResults: DatasetLoadResult[], + allResults: SeedLoadResult[], allErrors: ReferenceResolutionError[], organizationId?: string, ): Promise { @@ -709,12 +709,12 @@ export class SeedLoaderService implements ISeedLoaderService { // Internal: Helpers // ========================================================================== - private filterByEnv(datasets: Dataset[], env?: string): Dataset[] { + private filterByEnv(datasets: Seed[], env?: string): Seed[] { if (!env) return datasets; return datasets.filter(d => (d.env as string[]).includes(env)); } - private orderDatasets(datasets: Dataset[], insertOrder: string[]): Dataset[] { + private orderDatasets(datasets: Seed[], insertOrder: string[]): Seed[] { const orderMap = new Map(insertOrder.map((name, i) => [name, i])); return [...datasets].sort((a, b) => { const orderA = orderMap.get(a.object) ?? Number.MAX_SAFE_INTEGER; @@ -803,7 +803,7 @@ export class SeedLoaderService implements ISeedLoaderService { private buildResult( config: SeedLoaderConfig, graph: ObjectDependencyGraph, - results: DatasetLoadResult[], + results: SeedLoadResult[], errors: ReferenceResolutionError[], durationMs: number, ): SeedLoaderResult { diff --git a/packages/spec/src/ai/solution-blueprint.zod.ts b/packages/spec/src/ai/solution-blueprint.zod.ts index 88b8b5568..c0ca5b051 100644 --- a/packages/spec/src/ai/solution-blueprint.zod.ts +++ b/packages/spec/src/ai/solution-blueprint.zod.ts @@ -104,7 +104,7 @@ export const BlueprintAppSchema = lazySchema(() => z.object({ export type BlueprintApp = z.infer; /** - * Seed data the agent suggests. Mirrors {@link DatasetSchema.records}. NOTE: + * Seed data the agent suggests. Mirrors {@link SeedSchema.records}. NOTE: * Phase C does NOT auto-apply seed data — there is no runtime-draftable * `dataset` metadata type (seed = code-loaded `*.seed.ts`). `apply_blueprint` * reports it as "proposed, not applied" so a human can wire it deliberately. diff --git a/packages/spec/src/contracts/seed-loader-service.test.ts b/packages/spec/src/contracts/seed-loader-service.test.ts index dca0f7bb4..a3cc09e57 100644 --- a/packages/spec/src/contracts/seed-loader-service.test.ts +++ b/packages/spec/src/contracts/seed-loader-service.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'; import type { ISeedLoaderService } from './seed-loader-service'; import type { SeedLoaderRequest, SeedLoaderResult, ObjectDependencyGraph } from '../data/seed-loader.zod'; -import type { Dataset } from '../data/dataset.zod'; +import type { Seed } from '../data/seed.zod'; describe('Seed Loader Service Contract', () => { it('should allow a minimal implementation with required methods', () => { @@ -32,7 +32,7 @@ describe('Seed Loader Service Contract', () => { buildDependencyGraph: async (_objectNames: string[]): Promise => { return { nodes: [], insertOrder: [], circularDependencies: [] }; }, - validate: async (_datasets: Dataset[]): Promise => { + validate: async (_datasets: Seed[]): Promise => { return { success: true, dryRun: true, @@ -69,8 +69,8 @@ describe('Seed Loader Service Contract', () => { results: [], errors: [], summary: { - objectsProcessed: request.datasets.length, - totalRecords: request.datasets.reduce((sum, d) => sum + d.records.length, 0), + objectsProcessed: request.seeds.length, + totalRecords: request.seeds.reduce((sum, d) => sum + d.records.length, 0), totalInserted: 0, totalUpdated: 0, totalSkipped: 0, @@ -97,7 +97,7 @@ describe('Seed Loader Service Contract', () => { }; const result = await service.load({ - datasets: [ + seeds: [ { object: 'account', externalId: 'name', mode: 'upsert', env: ['prod', 'dev', 'test'], records: [{ name: 'Acme' }] }, ], config: { diff --git a/packages/spec/src/contracts/seed-loader-service.ts b/packages/spec/src/contracts/seed-loader-service.ts index 72746fc01..6c456d604 100644 --- a/packages/spec/src/contracts/seed-loader-service.ts +++ b/packages/spec/src/contracts/seed-loader-service.ts @@ -7,7 +7,7 @@ import type { ObjectDependencyGraph, } from '../data/seed-loader.zod.js'; -import type { Dataset } from '../data/dataset.zod.js'; +import type { Seed } from '../data/seed.zod.js'; /** * ISeedLoaderService — Metadata-driven Seed Data Loader Contract @@ -54,9 +54,9 @@ export interface ISeedLoaderService { * Validate datasets without writing any data (equivalent to config.dryRun = true). * Checks reference integrity and reports all broken references. * - * @param datasets - Datasets to validate + * @param datasets - Seeds to validate * @param config - Optional loader config overrides * @returns Structured result with validation errors (no data written) */ - validate(datasets: Dataset[], config?: SeedLoaderConfigInput): Promise; + validate(datasets: Seed[], config?: SeedLoaderConfigInput): Promise; } diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts index 76eb8e4e5..54874be87 100644 --- a/packages/spec/src/data/index.ts +++ b/packages/spec/src/data/index.ts @@ -14,7 +14,7 @@ export * from './driver.zod'; export * from './driver-sql.zod'; export * from './driver-nosql.zod'; -export * from './dataset.zod'; +export * from './seed.zod'; // Form Layouts export { objectForm } from './object.form'; diff --git a/packages/spec/src/data/seed-loader.test.ts b/packages/spec/src/data/seed-loader.test.ts index 5038b6a60..4552565cb 100644 --- a/packages/spec/src/data/seed-loader.test.ts +++ b/packages/spec/src/data/seed-loader.test.ts @@ -5,7 +5,7 @@ import { ObjectDependencyGraphSchema, ReferenceResolutionErrorSchema, SeedLoaderConfigSchema, - DatasetLoadResultSchema, + SeedLoadResultSchema, SeedLoaderResultSchema, SeedLoaderRequestSchema, } from './seed-loader.zod'; @@ -269,12 +269,12 @@ describe('SeedLoaderConfigSchema', () => { }); // ========================================================================== -// DatasetLoadResultSchema +// SeedLoadResultSchema // ========================================================================== -describe('DatasetLoadResultSchema', () => { +describe('SeedLoadResultSchema', () => { it('should accept a successful load result', () => { - const result = DatasetLoadResultSchema.parse({ + const result = SeedLoadResultSchema.parse({ object: 'account', mode: 'upsert', inserted: 5, @@ -291,7 +291,7 @@ describe('DatasetLoadResultSchema', () => { }); it('should accept a result with errors', () => { - const result = DatasetLoadResultSchema.parse({ + const result = SeedLoadResultSchema.parse({ object: 'contact', mode: 'upsert', inserted: 3, @@ -318,7 +318,7 @@ describe('DatasetLoadResultSchema', () => { }); it('should accept a result with deferred references', () => { - const result = DatasetLoadResultSchema.parse({ + const result = SeedLoadResultSchema.parse({ object: 'task', mode: 'insert', inserted: 10, @@ -333,7 +333,7 @@ describe('DatasetLoadResultSchema', () => { }); it('should default errors to empty array', () => { - const result = DatasetLoadResultSchema.parse({ + const result = SeedLoadResultSchema.parse({ object: 'product', mode: 'insert', inserted: 1, @@ -348,7 +348,7 @@ describe('DatasetLoadResultSchema', () => { }); it('should reject negative counts', () => { - expect(() => DatasetLoadResultSchema.parse({ + expect(() => SeedLoadResultSchema.parse({ object: 'test', mode: 'upsert', inserted: -1, @@ -576,18 +576,18 @@ describe('SeedLoaderResultSchema', () => { describe('SeedLoaderRequestSchema', () => { it('should accept a minimal request with defaults', () => { const request = SeedLoaderRequestSchema.parse({ - datasets: [ + seeds: [ { object: 'country', records: [{ name: 'United States', code: 'US' }] }, ], }); - expect(request.datasets).toHaveLength(1); + expect(request.seeds).toHaveLength(1); expect(request.config.dryRun).toBe(false); expect(request.config.defaultMode).toBe('upsert'); }); it('should accept a request with full configuration', () => { const request = SeedLoaderRequestSchema.parse({ - datasets: [ + seeds: [ { object: 'account', externalId: 'code', @@ -607,14 +607,14 @@ describe('SeedLoaderRequestSchema', () => { env: 'dev', }, }); - expect(request.datasets).toHaveLength(2); + expect(request.seeds).toHaveLength(2); expect(request.config.dryRun).toBe(true); expect(request.config.env).toBe('dev'); }); it('should reject empty datasets', () => { expect(() => SeedLoaderRequestSchema.parse({ - datasets: [], + seeds: [], })).toThrow(); }); @@ -624,7 +624,7 @@ describe('SeedLoaderRequestSchema', () => { it('should handle CRM seed data scenario', () => { const request = SeedLoaderRequestSchema.parse({ - datasets: [ + seeds: [ { object: 'industry', externalId: 'code', @@ -658,8 +658,8 @@ describe('SeedLoaderRequestSchema', () => { defaultMode: 'upsert', }, }); - expect(request.datasets).toHaveLength(3); - expect(request.datasets[0].externalId).toBe('code'); - expect(request.datasets[2].externalId).toBe('email'); + expect(request.seeds).toHaveLength(3); + expect(request.seeds[0].externalId).toBe('code'); + expect(request.seeds[2].externalId).toBe('email'); }); }); diff --git a/packages/spec/src/data/seed-loader.zod.ts b/packages/spec/src/data/seed-loader.zod.ts index d74fda90b..360e03fbb 100644 --- a/packages/spec/src/data/seed-loader.zod.ts +++ b/packages/spec/src/data/seed-loader.zod.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { DatasetSchema, DatasetMode } from './dataset.zod'; +import { SeedSchema, SeedMode } from './seed.zod'; /** * # Seed Loader Protocol @@ -227,7 +227,7 @@ export const SeedLoaderConfigSchema = lazySchema(() => z.object({ * Default dataset mode when not specified per-dataset. * @default 'upsert' */ - defaultMode: DatasetMode.default('upsert') + defaultMode: SeedMode.default('upsert') .describe('Default conflict resolution strategy'), /** @@ -299,12 +299,12 @@ export type SeedLoaderConfigInput = z.input; /** * Result of loading a single object's dataset. */ -export const DatasetLoadResultSchema = lazySchema(() => z.object({ +export const SeedLoadResultSchema = lazySchema(() => z.object({ /** Target object name */ object: z.string().describe('Object that was loaded'), /** Import mode used */ - mode: DatasetMode.describe('Import mode used'), + mode: SeedMode.describe('Import mode used'), /** Number of records successfully inserted */ inserted: z.number().int().min(0).describe('Records inserted'), @@ -332,7 +332,7 @@ export const DatasetLoadResultSchema = lazySchema(() => z.object({ .describe('Reference resolution errors'), }).describe('Result of loading a single dataset')); -export type DatasetLoadResult = z.infer; +export type SeedLoadResult = z.infer; // ========================================================================== // 7. Seed Loader Result @@ -353,7 +353,7 @@ export const SeedLoaderResultSchema = lazySchema(() => z.object({ dependencyGraph: ObjectDependencyGraphSchema.describe('Object dependency graph'), /** Per-object load results, in the order they were processed */ - results: z.array(DatasetLoadResultSchema).describe('Per-object load results'), + results: z.array(SeedLoadResultSchema).describe('Per-object load results'), /** All reference resolution errors across all objects */ errors: z.array(ReferenceResolutionErrorSchema).describe('All reference resolution errors'), @@ -403,8 +403,8 @@ export type SeedLoaderResult = z.infer; * Combines datasets with loader configuration. */ export const SeedLoaderRequestSchema = lazySchema(() => z.object({ - /** Datasets to load */ - datasets: z.array(DatasetSchema).min(1).describe('Datasets to load'), + /** Seeds to load */ + seeds: z.array(SeedSchema).min(1).describe('Seeds to load'), /** Loader configuration */ config: SeedLoaderConfigSchema.default(() => SeedLoaderConfigSchema.parse({})).describe('Loader configuration'), diff --git a/packages/spec/src/data/dataset.test.ts b/packages/spec/src/data/seed.test.ts similarity index 76% rename from packages/spec/src/data/dataset.test.ts rename to packages/spec/src/data/seed.test.ts index 8daf132ec..7e9c5e803 100644 --- a/packages/spec/src/data/dataset.test.ts +++ b/packages/spec/src/data/seed.test.ts @@ -1,25 +1,25 @@ import { describe, it, expect } from 'vitest'; -import { DatasetSchema, DatasetMode, type Dataset } from './dataset.zod'; +import { SeedSchema, SeedMode, type Seed } from './seed.zod'; -describe('DatasetMode', () => { +describe('SeedMode', () => { it('should accept valid dataset modes', () => { const validModes = ['insert', 'update', 'upsert', 'replace', 'ignore']; validModes.forEach(mode => { - expect(() => DatasetMode.parse(mode)).not.toThrow(); + expect(() => SeedMode.parse(mode)).not.toThrow(); }); }); it('should reject invalid modes', () => { - expect(() => DatasetMode.parse('merge')).toThrow(); - expect(() => DatasetMode.parse('delete')).toThrow(); - expect(() => DatasetMode.parse('')).toThrow(); + expect(() => SeedMode.parse('merge')).toThrow(); + expect(() => SeedMode.parse('delete')).toThrow(); + expect(() => SeedMode.parse('')).toThrow(); }); }); -describe('DatasetSchema', () => { +describe('SeedSchema', () => { it('should accept valid minimal dataset', () => { - const validDataset: Dataset = { + const validDataset: Seed = { object: 'user', records: [ { name: 'John', email: 'john@example.com' }, @@ -27,11 +27,11 @@ describe('DatasetSchema', () => { ] }; - expect(() => DatasetSchema.parse(validDataset)).not.toThrow(); + expect(() => SeedSchema.parse(validDataset)).not.toThrow(); }); it('should accept dataset with all fields', () => { - const fullDataset: Dataset = { + const fullDataset: Seed = { object: 'account', externalId: 'code', mode: 'upsert', @@ -42,11 +42,11 @@ describe('DatasetSchema', () => { ] }; - expect(() => DatasetSchema.parse(fullDataset)).not.toThrow(); + expect(() => SeedSchema.parse(fullDataset)).not.toThrow(); }); it('should apply default values', () => { - const dataset = DatasetSchema.parse({ + const dataset = SeedSchema.parse({ object: 'product', records: [{ name: 'Widget' }] }); @@ -57,27 +57,27 @@ describe('DatasetSchema', () => { }); it('should validate object name format (snake_case)', () => { - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'valid_object_name', records: [] })).not.toThrow(); - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'InvalidObject', records: [] })).toThrow(); - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'invalid-object', records: [] })).toThrow(); }); it('should accept different modes', () => { - const modes: Array = ['insert', 'update', 'upsert', 'replace', 'ignore']; + const modes: Array = ['insert', 'update', 'upsert', 'replace', 'ignore']; modes.forEach(mode => { - const dataset = DatasetSchema.parse({ + const dataset = SeedSchema.parse({ object: 'test_object', mode, records: [] @@ -87,14 +87,14 @@ describe('DatasetSchema', () => { }); it('should accept environment scopes', () => { - const dataset1 = DatasetSchema.parse({ + const dataset1 = SeedSchema.parse({ object: 'test_object', env: ['dev'], records: [] }); expect(dataset1.env).toEqual(['dev']); - const dataset2 = DatasetSchema.parse({ + const dataset2 = SeedSchema.parse({ object: 'test_object', env: ['prod', 'test'], records: [] @@ -103,13 +103,13 @@ describe('DatasetSchema', () => { }); it('should reject invalid environment values', () => { - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'test_object', env: ['production'], records: [] })).toThrow(); - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'test_object', env: ['staging'], records: [] @@ -117,7 +117,7 @@ describe('DatasetSchema', () => { }); it('should accept empty records array', () => { - const dataset = DatasetSchema.parse({ + const dataset = SeedSchema.parse({ object: 'empty_table', records: [] }); @@ -126,7 +126,7 @@ describe('DatasetSchema', () => { }); it('should accept records with various data types', () => { - const dataset = DatasetSchema.parse({ + const dataset = SeedSchema.parse({ object: 'mixed_data', records: [ { @@ -149,7 +149,7 @@ describe('DatasetSchema', () => { const validExternalIds = ['name', 'code', 'external_id', 'username', 'slug']; validExternalIds.forEach(externalId => { - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'test_object', externalId, records: [] @@ -158,7 +158,7 @@ describe('DatasetSchema', () => { }); it('should handle seed data use case', () => { - const seedData = DatasetSchema.parse({ + const seedData = SeedSchema.parse({ object: 'country', externalId: 'code', mode: 'upsert', @@ -175,7 +175,7 @@ describe('DatasetSchema', () => { }); it('should handle demo data use case', () => { - const demoData = DatasetSchema.parse({ + const demoData = SeedSchema.parse({ object: 'project', externalId: 'name', mode: 'replace', @@ -191,7 +191,7 @@ describe('DatasetSchema', () => { }); it('should handle test data use case', () => { - const testData = DatasetSchema.parse({ + const testData = SeedSchema.parse({ object: 'test_user', mode: 'ignore', env: ['test'], @@ -205,17 +205,17 @@ describe('DatasetSchema', () => { }); it('should reject dataset without required fields', () => { - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ records: [] })).toThrow(); - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'test_object' })).toThrow(); }); it('should reject invalid mode value', () => { - expect(() => DatasetSchema.parse({ + expect(() => SeedSchema.parse({ object: 'test_object', mode: 'invalid_mode', records: [] @@ -223,7 +223,7 @@ describe('DatasetSchema', () => { }); it('should handle large datasets', () => { - const largeDataset = DatasetSchema.parse({ + const largeDataset = SeedSchema.parse({ object: 'bulk_data', records: Array.from({ length: 1000 }, (_, i) => ({ id: i, diff --git a/packages/spec/src/data/dataset.zod.ts b/packages/spec/src/data/seed.zod.ts similarity index 70% rename from packages/spec/src/data/dataset.zod.ts rename to packages/spec/src/data/seed.zod.ts index 48bf0036a..1832e7f2d 100644 --- a/packages/spec/src/data/dataset.zod.ts +++ b/packages/spec/src/data/seed.zod.ts @@ -3,11 +3,11 @@ import { z } from 'zod'; /** - * Data Import Strategy - * Defines how the engine handles existing records. + * Seed Import Strategy + * Defines how the engine handles existing records when a seed is applied. */ import { lazySchema } from '../shared/lazy-schema'; -export const DatasetMode = z.enum([ +export const SeedMode = z.enum([ 'insert', // Try to insert, fail on duplicate 'update', // Only update found records, ignore new 'upsert', // Create new or Update existing (Standard) @@ -16,22 +16,26 @@ export const DatasetMode = z.enum([ ]); /** - * Dataset Schema (Seed Data / Fixtures) - * - * Standardized format for transporting data. - * Used for: + * Seed Schema (Seed Data / Fixtures) + * + * Standardized format for transporting initialization data. Used for: * 1. System Bootstrapping (Admin accounts, Standard Roles) * 2. Reference Data (Countries, Currencies) - * 3. Demo/Test Data + * 3. Demo / sample data (incl. AI-authored, applied on publish) + * + * This is the shape of the runtime-draftable `seed` metadata type: an author + * (or the AI metadata assistant) stages a `seed` draft against this schema and + * its rows load when the draft is published. Named `Seed` (not `Dataset`) so + * the `dataset` name stays reserved for the ADR-0021 analytics semantic layer. */ -export const DatasetSchema = lazySchema(() => z.object({ - /** - * Target Object +export const SeedSchema = lazySchema(() => z.object({ + /** + * Target Object * The machine name of the object to populate. */ object: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Target Object Name'), - /** + /** * Idempotency Key (The "Upsert" Key) * The field used to check if a record already exists. * Best Practice: Use a natural key like 'code', 'slug', 'username' or 'external_id'. @@ -39,10 +43,10 @@ export const DatasetSchema = lazySchema(() => z.object({ */ externalId: z.string().default('name').describe('Field match for uniqueness check'), - /** + /** * Import Strategy */ - mode: DatasetMode.default('upsert').describe('Conflict resolution strategy'), + mode: SeedMode.default('upsert').describe('Conflict resolution strategy'), /** * Environment Scope @@ -52,37 +56,20 @@ export const DatasetSchema = lazySchema(() => z.object({ */ env: z.array(z.enum(['prod', 'dev', 'test'])).default(['prod', 'dev', 'test']).describe('Applicable environments'), - /** + /** * The Payload * Array of raw JSON objects matching the Object Schema. */ 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; -export type SeedInput = z.input; - /** Parsed/output type — all defaults are applied (env, mode, externalId always present) */ -export type Dataset = z.infer; +export type Seed = z.infer; /** Input type — fields with defaults (env, mode, externalId) are optional */ -export type DatasetInput = z.input; +export type SeedInput = z.input; -export type DatasetImportMode = z.infer; +export type SeedImportMode = z.infer; /** * Per-field value type for a seed record. @@ -106,7 +93,7 @@ type SeedRecord = { }; /** - * Type-safe factory for creating seed dataset definitions. + * Type-safe factory for creating seed definitions. * Infers valid field keys from the object definition passed in, * so typos in record field names are caught at compile time. Reference * fields (lookup/master_detail) are additionally constrained to the @@ -114,7 +101,7 @@ type SeedRecord = { * * @example * ```ts - * export const leadSeed = defineDataset(Lead, { + * export const leadSeed = defineSeed(Lead, { * externalId: 'email', * records: [ * { first_name: 'Alice', lead_source: 'web' }, // ✅ type-checked @@ -125,13 +112,13 @@ type SeedRecord = { * }); * ``` */ -export function defineDataset< +export function defineSeed< const TObj extends { name: string; fields: Record } >( objectDef: TObj, - config: Omit & { + config: Omit & { records: Array>; } -): Dataset { - return DatasetSchema.parse({ ...config, object: objectDef.name }); +): Seed { + return SeedSchema.parse({ ...config, object: objectDef.name }); } diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index 6f7b76f8d..1164259d0 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { PluginCapabilityManifestSchema } from './plugin-capability.zod'; import { PluginLoadingConfigSchema } from './plugin-loading.zod'; import { CORE_PLUGIN_TYPES } from './plugin.zod'; -import { DatasetSchema } from '../data/dataset.zod'; +import { SeedSchema } from '../data/seed.zod'; import { NavigationContributionSchema } from '../ui/app.zod'; // ───────────────────────────────────────────────────────────────────── @@ -466,7 +466,7 @@ export const ManifestSchema = z.object({ * Initial data seeding configuration. * Defines default records to be inserted when the package is installed. * - * Uses the standard DatasetSchema which supports idempotent upsert via + * Uses the standard SeedSchema which supports idempotent upsert via * `externalId`, environment scoping via `env`, and multiple conflict * resolution modes. * @@ -474,7 +474,7 @@ export const ManifestSchema = z.object({ * (defineStack({ data: [...] })) for better visibility and metadata registration. * This field is retained for backward compatibility with manifest-only packages. */ - data: z.array(DatasetSchema).optional().describe('Initial seed data (prefer top-level data field)'), + data: z.array(SeedSchema).optional().describe('Initial seed data (prefer top-level data field)'), /** * Plugin Capability Manifest. diff --git a/packages/spec/src/kernel/metadata-type-schemas.ts b/packages/spec/src/kernel/metadata-type-schemas.ts index e0a5faaab..82cb6c83a 100644 --- a/packages/spec/src/kernel/metadata-type-schemas.ts +++ b/packages/spec/src/kernel/metadata-type-schemas.ts @@ -32,7 +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 { SeedSchema } from '../data/seed.zod'; import { ViewSchema } from '../ui/view.zod'; import { PageSchema } from '../ui/page.zod'; diff --git a/packages/spec/src/kernel/package-artifact.zod.ts b/packages/spec/src/kernel/package-artifact.zod.ts index 24f6513a4..7e1418b21 100644 --- a/packages/spec/src/kernel/package-artifact.zod.ts +++ b/packages/spec/src/kernel/package-artifact.zod.ts @@ -24,7 +24,7 @@ import { z } from 'zod'; * ├── assets/ ← Static resources * │ ├── icon.svg * │ └── screenshots/ - * ├── data/ ← Seed data (DatasetSchema serialized) + * ├── data/ ← Seed data (SeedSchema serialized) * ├── locales/ ← i18n translation files * ├── checksums.json ← SHA256 checksum per file * └── signature.sig ← RSA-SHA256 package signature diff --git a/packages/spec/src/shared/metadata-collection.zod.ts b/packages/spec/src/shared/metadata-collection.zod.ts index d83097ec8..2cdfc41d0 100644 --- a/packages/spec/src/shared/metadata-collection.zod.ts +++ b/packages/spec/src/shared/metadata-collection.zod.ts @@ -64,7 +64,7 @@ export type MetadataCollectionInput = * Excluded fields: * - `views` — ViewSchema has no `name` field (it's a container with `list`/`form`) * - `objectExtensions` — uses `extend` as its identifier, not `name` - * - `data` — DatasetSchema uses `object` as its identifier + * - `data` — SeedSchema uses `object` as its identifier * - `translations` — TranslationBundleSchema is a record, not a named object * - `plugins` / `devPlugins` — not named metadata schemas */ diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 4f1cd07b0..0984b9d8a 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -11,7 +11,7 @@ import { normalizeStackInput, type MetadataCollectionInput, type MapSupportedFie // Data Protocol import { ObjectSchema, ObjectExtensionSchema } from './data/object.zod'; -import { DatasetSchema } from './data/dataset.zod'; +import { SeedSchema } from './data/seed.zod'; // UI Protocol import { AppSchema } from './ui/app.zod'; @@ -296,7 +296,7 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({ * Each entry targets a specific object and provides records to load * using the specified conflict resolution strategy. * - * Uses the standard DatasetSchema which supports: + * Uses the standard SeedSchema which supports: * - `externalId`: Idempotency key for upsert matching (default: 'name') * - `mode`: Conflict resolution (upsert, insert, ignore, replace) * - `env`: Environment scoping (prod, dev, test) @@ -315,7 +315,7 @@ export const ObjectStackDefinitionSchema = lazySchema(() => z.object({ * ] * ``` */ - data: z.array(DatasetSchema).optional().describe('Seed Data / Fixtures for bootstrapping'), + data: z.array(SeedSchema).optional().describe('Seed Data / Fixtures for bootstrapping'), /** * Plugins: External Capabilities