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
12 changes: 6 additions & 6 deletions examples/app-crm/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// 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';
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: [
Expand All @@ -18,7 +18,7 @@ const accounts = defineDataset(Account, {
],
});

const contacts = defineDataset(Contact, {
const contacts = defineSeed(Contact, {
mode: 'upsert',
externalId: 'email',
records: [
Expand All @@ -28,7 +28,7 @@ const contacts = defineDataset(Contact, {
],
});

const opportunities = defineDataset(Opportunity, {
const opportunities = defineSeed(Opportunity, {
mode: 'upsert',
externalId: 'name',
records: [
Expand Down Expand Up @@ -56,7 +56,7 @@ const opportunities = defineDataset(Opportunity, {
],
});

const leads = defineDataset(Lead, {
const leads = defineSeed(Lead, {
mode: 'upsert',
externalId: 'email',
records: [
Expand Down Expand Up @@ -101,7 +101,7 @@ const leads = defineDataset(Lead, {
],
});

const activities = defineDataset(Activity, {
const activities = defineSeed(Activity, {
mode: 'upsert',
externalId: 'subject',
records: [
Expand Down
14 changes: 7 additions & 7 deletions examples/app-showcase/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: [
Expand All @@ -27,7 +27,7 @@ const accounts = defineDataset(Account, {
],
});

const projects = defineDataset(Project, {
const projects = defineSeed(Project, {
mode: 'upsert',
externalId: 'name',
records: [
Expand All @@ -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: [
Expand All @@ -57,7 +57,7 @@ const tasks = defineDataset(Task, {
],
});

const categories = defineDataset(Category, {
const categories = defineSeed(Category, {
mode: 'upsert',
externalId: 'name',
records: [
Expand All @@ -68,7 +68,7 @@ const categories = defineDataset(Category, {
],
});

const teams = defineDataset(Team, {
const teams = defineSeed(Team, {
mode: 'upsert',
externalId: 'name',
records: [
Expand All @@ -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 },
Expand Down
4 changes: 2 additions & 2 deletions examples/app-todo/src/data/index.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion packages/formula/src/seed-eval.ts
Original file line number Diff line number Diff line change
@@ -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*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
6 changes: 3 additions & 3 deletions packages/runtime/src/app-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 });
}
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions packages/runtime/src/http-dispatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
});

// ═══════════════════════════════════════════════════════════════
Expand Down
11 changes: 6 additions & 5 deletions packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
Expand Down
Loading