Skip to content

Commit 594002d

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(seed): complete Dataset->Seed rename + load published seed rows (locally verified) (#1620)
* fix(runtime): seed-apply read-back reads body from wrapper .item field The diagnostic added in the prior fix revealed the exact shape: a direct protocol.getMetaItem() call returns a WRAPPER `{ type, name, item, lock, editable, deletable, resettable }` — the seed body (object/records) is under `.item`, not top-level and not `.metadata`/`.body` (the HTTP endpoint unwraps it, which is why a manual GET showed the body directly and misled the first guess). Add `.item` to the extraction chain so the published seed body is actually read → its rows load on publish. This completes the magic-moment loop: AI builds → publishes → sample seed rows load into the object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(seed): complete Dataset->Seed rename + load published seed rows Locally verified together (106 spec + 163 runtime tests green) BEFORE deploying. Prior split attempts failed because the rename never landed on main (auto-merge raced ahead of the rename commit) and the seed-apply hook was patched blind through the slow deploy loop. 1. Finish the lost Dataset->Seed rename: - data/dataset.zod.ts -> data/seed.zod.ts (+ dataset.test.ts -> seed.test.ts) - DatasetSchema->SeedSchema (canonical), Dataset->Seed, DatasetInput->SeedInput, DatasetMode->SeedMode, DatasetImportMode->SeedImportMode, DatasetLoadResult(Schema)->SeedLoadResult(Schema), defineDataset->defineSeed - SeedLoaderRequest field datasets->seeds (+ all constructors/readers/tests) - callers: 3 examples, plugin-org-scoping, runtime seed-loader + app-plugin, spec stack/manifest/package-artifact/metadata-collection/solution-blueprint Frees the dataset name for the ADR-0021 analytics layer. NOT renamed (other concepts): env-artifact datasets bucket, seed-datasets service key, manifest data, RAGFlow datasets. 2. Fix publish-drafts seed-apply so published seed rows actually load: protocol.getMetaItem (direct) returns a WRAPPER {type,name,item,...} - the seed body is under .item (the HTTP endpoint unwraps it, which misled earlier guesses). Read .item; try active-org then env-wide; never report success while loading 0 rows. New integration test (http-dispatcher.test.ts) exercises the real SeedLoaderService end-to-end and pins this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d2ffa9a commit 594002d

23 files changed

Lines changed: 215 additions & 187 deletions

‎examples/app-crm/src/data/index.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { defineDataset } from '@objectstack/spec/data';
3+
import { defineSeed } from '@objectstack/spec/data';
44
import { cel } from '@objectstack/spec';
55
import { Account } from '../objects/account.object.js';
66
import { Contact } from '../objects/contact.object.js';
77
import { Opportunity } from '../objects/opportunity.object.js';
88
import { Lead } from '../objects/lead.object.js';
99
import { Activity } from '../objects/activity.object.js';
1010

11-
const accounts = defineDataset(Account, {
11+
const accounts = defineSeed(Account, {
1212
mode: 'upsert',
1313
externalId: 'name',
1414
records: [
@@ -18,7 +18,7 @@ const accounts = defineDataset(Account, {
1818
],
1919
});
2020

21-
const contacts = defineDataset(Contact, {
21+
const contacts = defineSeed(Contact, {
2222
mode: 'upsert',
2323
externalId: 'email',
2424
records: [
@@ -28,7 +28,7 @@ const contacts = defineDataset(Contact, {
2828
],
2929
});
3030

31-
const opportunities = defineDataset(Opportunity, {
31+
const opportunities = defineSeed(Opportunity, {
3232
mode: 'upsert',
3333
externalId: 'name',
3434
records: [
@@ -56,7 +56,7 @@ const opportunities = defineDataset(Opportunity, {
5656
],
5757
});
5858

59-
const leads = defineDataset(Lead, {
59+
const leads = defineSeed(Lead, {
6060
mode: 'upsert',
6161
externalId: 'email',
6262
records: [
@@ -101,7 +101,7 @@ const leads = defineDataset(Lead, {
101101
],
102102
});
103103

104-
const activities = defineDataset(Activity, {
104+
const activities = defineSeed(Activity, {
105105
mode: 'upsert',
106106
externalId: 'subject',
107107
records: [

‎examples/app-showcase/src/data/index.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { defineDataset } from '@objectstack/spec/data';
3+
import { defineSeed } from '@objectstack/spec/data';
44
import { cel } from '@objectstack/spec';
55
import { Account } from '../objects/account.object.js';
66
import { Project } from '../objects/project.object.js';
@@ -14,7 +14,7 @@ import { Team, ProjectMembership } from '../objects/team.object.js';
1414
* work location (map), and projects span every status and health.
1515
*/
1616

17-
const accounts = defineDataset(Account, {
17+
const accounts = defineSeed(Account, {
1818
mode: 'upsert',
1919
externalId: 'name',
2020
records: [
@@ -27,7 +27,7 @@ const accounts = defineDataset(Account, {
2727
],
2828
});
2929

30-
const projects = defineDataset(Project, {
30+
const projects = defineSeed(Project, {
3131
mode: 'upsert',
3232
externalId: 'name',
3333
records: [
@@ -40,7 +40,7 @@ const projects = defineDataset(Project, {
4040
});
4141

4242
// Tasks across all five board columns, with dates + locations to drive every view.
43-
const tasks = defineDataset(Task, {
43+
const tasks = defineSeed(Task, {
4444
mode: 'upsert',
4545
externalId: 'title',
4646
records: [
@@ -57,7 +57,7 @@ const tasks = defineDataset(Task, {
5757
],
5858
});
5959

60-
const categories = defineDataset(Category, {
60+
const categories = defineSeed(Category, {
6161
mode: 'upsert',
6262
externalId: 'name',
6363
records: [
@@ -68,7 +68,7 @@ const categories = defineDataset(Category, {
6868
],
6969
});
7070

71-
const teams = defineDataset(Team, {
71+
const teams = defineSeed(Team, {
7272
mode: 'upsert',
7373
externalId: 'name',
7474
records: [
@@ -77,7 +77,7 @@ const teams = defineDataset(Team, {
7777
],
7878
});
7979

80-
const memberships = defineDataset(ProjectMembership, {
80+
const memberships = defineSeed(ProjectMembership, {
8181
mode: 'insert',
8282
records: [
8383
{ team: 'Experience', project: 'Website Relaunch', role: 'owner', allocation_percent: 80 },

‎examples/app-todo/src/data/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { defineDataset } from '@objectstack/spec/data';
3+
import { defineSeed } from '@objectstack/spec/data';
44
import { cel } from '@objectstack/spec';
55
import { Task } from '../objects/task.object';
66

7-
const tasks = defineDataset(Task, {
7+
const tasks = defineSeed(Task, {
88
mode: 'upsert',
99
externalId: 'subject',
1010
records: [

‎packages/formula/src/seed-eval.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Seed-value resolver.
33
*
4-
* `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array
4+
* `Seed.records` accepts {@link SeedValue} = primitive | Expression | array
55
* | object — install-time resolution walks the tree and replaces any
66
* Expression node with its evaluated result. This is what makes
77
* `close_date: cel\`now() + duration("P30D")\`` resolve to *the customer's*

‎packages/plugins/plugin-org-scoping/src/claim-orphan-org-rows.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* claimOrphanOrgRows — assign seed-loaded records to the first organization.
55
*
6-
* Seeds (`defineDataset`) are inserted by `SeedLoaderService` using
6+
* Seeds (`defineSeed`) are inserted by `SeedLoaderService` using
77
* `{ context: { isSystem: true } }`, which intentionally bypasses
88
* SecurityPlugin's `organization_id` auto-fill. As a result, in
99
* multi-tenant mode every seed row lands with `organization_id = NULL`.

‎packages/runtime/src/app-plugin.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export class AppPlugin implements Plugin {
520520
const seedLoader = new SeedLoaderService(ql, md, loggerRef);
521521
const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data');
522522
const request = SeedLoaderRequestSchema.parse({
523-
datasets: datasetsNow,
523+
seeds: datasetsNow,
524524
config: {
525525
defaultMode: 'upsert',
526526
multiPass: true,
@@ -540,7 +540,7 @@ export class AppPlugin implements Plugin {
540540
};
541541
};
542542
registerSvc('seed-replayer', replayer);
543-
ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total datasets: ${merged.length})`);
543+
ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total seeds: ${merged.length})`);
544544
} catch (e: any) {
545545
ctx.logger.warn('[Seeder] Failed to register seed-datasets/seed-replayer service', { error: e?.message });
546546
}
@@ -571,7 +571,7 @@ export class AppPlugin implements Plugin {
571571
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
572572
const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data');
573573
const request = SeedLoaderRequestSchema.parse({
574-
datasets: normalizedDatasets,
574+
seeds: normalizedDatasets,
575575
config: { defaultMode: 'upsert', multiPass: true, identity: seedIdentity },
576576
});
577577
const result = await seedLoader.load(request);

‎packages/runtime/src/http-dispatcher.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,46 @@ describe('HttpDispatcher', () => {
982982
expect(result.handled).toBe(true);
983983
expect(result.response?.status).toBe(501);
984984
});
985+
986+
// Integration: publishing a `seed` draft must LOAD its rows. This
987+
// exercises applyPublishedSeeds end-to-end against the REAL
988+
// SeedLoaderService (only the engine/metadata are mocked), so it pins
989+
// the read-back shape (protocol.getMetaItem returns a WRAPPER whose body
990+
// is under `.item`), the renamed `seeds` request field, and the loader
991+
// invocation — the exact chain that silently loaded 0 rows on staging.
992+
it('POST /packages/:id/publish-drafts applies published `seed` rows', async () => {
993+
const records = [
994+
{ name: 'Apollo', status: 'active', budget_amount: 120000 },
995+
{ name: 'Gemini', status: 'planned', budget_amount: 45000 },
996+
];
997+
const publishPackageDrafts = vi.fn().mockResolvedValue({
998+
success: true, publishedCount: 1, failedCount: 0,
999+
published: [{ type: 'seed', name: 'project_seed', version: 'h' }], failed: [],
1000+
});
1001+
// protocol.getMetaItem returns the WRAPPER shape (body under `.item`).
1002+
const getMetaItem = vi.fn().mockResolvedValue({
1003+
type: 'seed', name: 'project_seed', lock: null, editable: true,
1004+
item: { object: 'project', externalId: 'name', mode: 'upsert', records },
1005+
});
1006+
const insert = vi.fn().mockImplementation(async (_obj: string, rec: any) => ({ id: `id_${rec.name}` }));
1007+
const find = vi.fn().mockResolvedValue([]); // no existing rows → all insert
1008+
(kernel as any).getService = vi.fn().mockImplementation((name: string) => {
1009+
if (name === 'protocol') return Promise.resolve({ publishPackageDrafts, getMetaItem });
1010+
if (name === 'objectql') return Promise.resolve({ insert, find, update: vi.fn(), registry: { getAllPackages: vi.fn().mockReturnValue([]) } });
1011+
if (name === 'metadata') return Promise.resolve({ getObject: vi.fn().mockResolvedValue({ name: 'project', fields: { name: { type: 'text' }, status: { type: 'select' }, budget_amount: { type: 'currency' } } }) });
1012+
return null;
1013+
});
1014+
1015+
const result = await dispatcher.handlePackages('/com.workspace/publish-drafts', 'POST', {}, {}, { request: {} });
1016+
1017+
expect(result.response?.status).toBe(200);
1018+
const seedApplied = (result.response as any)?.body?.data?.seedApplied;
1019+
expect(seedApplied?.success).toBe(true);
1020+
expect(seedApplied?.inserted).toBe(2);
1021+
// rows actually went to the engine
1022+
expect(insert).toHaveBeenCalledTimes(2);
1023+
expect(insert).toHaveBeenCalledWith('project', expect.objectContaining({ name: 'Apollo' }), expect.anything());
1024+
});
9851025
});
9861026

9871027
// ═══════════════════════════════════════════════════════════════

‎packages/runtime/src/http-dispatcher.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,10 +1775,14 @@ export class HttpDispatcher {
17751775
readErrors.push(`read ${name}: ${(e as Error)?.message ?? String(e)}`);
17761776
}
17771777
}
1778-
// getMetaItem returns the item body directly; tolerate a wrapper.
1778+
// protocol.getMetaItem (called directly, unlike the HTTP endpoint
1779+
// which unwraps) returns a WRAPPER: `{ type, name, item, lock,
1780+
// editable, … }` — the seed body (object/records) lives under
1781+
// `.item`. Tolerate the wrapper (`.item`) plus the body-direct and
1782+
// `.metadata`/`.body` shapes other protocols may return.
17791783
const seed = item?.object && Array.isArray(item?.records)
17801784
? item
1781-
: (item?.metadata ?? item?.body);
1785+
: (item?.item ?? item?.metadata ?? item?.body);
17821786
if (seed?.object && Array.isArray(seed?.records)) {
17831787
datasets.push(seed);
17841788
} else {
@@ -1795,9 +1799,6 @@ export class HttpDispatcher {
17951799
const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data');
17961800
const loader = new SeedLoaderService(ql, metadata, (this as any).logger ?? console);
17971801
const request = SeedLoaderRequestSchema.parse({
1798-
// ADR field is `seeds` (renamed from `datasets`); this constructor
1799-
// was added in the same PR and the rename missed it — passing
1800-
// `datasets` left `seeds` undefined and the loader saw nothing.
18011802
seeds: datasets,
18021803
config: {
18031804
defaultMode: 'upsert',

0 commit comments

Comments
 (0)