Skip to content

Commit c6a5c0b

Browse files
Merge branch 'main' into fix/findconflict-orphaned-ledger
2 parents d34cca6 + 03d8c4c commit c6a5c0b

63 files changed

Lines changed: 680 additions & 332 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@tailwindcss/postcss": "^4.3.1",
2828
"@types/mdx": "^2.0.14",
2929
"@types/negotiator": "^0.6.4",
30-
"@types/node": "^26.0.0",
30+
"@types/node": "^25.9.3",
3131
"@types/react": "^19.2.17",
3232
"@types/react-dom": "^19.2.3",
3333
"negotiator": "^1.0.0",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"license": "Apache-2.0",
3939
"devDependencies": {
4040
"@changesets/cli": "^2.31.0",
41-
"@types/node": "^26.0.0",
41+
"@types/node": "^25.9.3",
4242
"@typescript-eslint/parser": "^8.61.1",
4343
"eslint": "^10.5.0",
4444
"tsup": "^8.5.1",

packages/apps/account/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@objectstack/spec": "workspace:*"
2222
},
2323
"devDependencies": {
24-
"@types/node": "^26.0.0",
24+
"@types/node": "^25.9.3",
2525
"tsup": "^8.5.1",
2626
"typescript": "^6.0.3",
2727
"vitest": "^4.1.9"

packages/apps/setup/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@objectstack/spec": "workspace:*"
2222
},
2323
"devDependencies": {
24-
"@types/node": "^26.0.0",
24+
"@types/node": "^25.9.3",
2525
"tsup": "^8.5.1",
2626
"typescript": "^6.0.3",
2727
"vitest": "^4.1.9"

packages/apps/studio/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@objectstack/spec": "workspace:*"
2222
},
2323
"devDependencies": {
24-
"@types/node": "^26.0.0",
24+
"@types/node": "^25.9.3",
2525
"tsup": "^8.5.1",
2626
"typescript": "^6.0.3",
2727
"vitest": "^4.1.9"

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"devDependencies": {
104104
"@oclif/plugin-help": "^6.2.52",
105105
"@oclif/plugin-plugins": "^5.4.78",
106-
"@types/node": "^26.0.0",
106+
"@types/node": "^25.9.3",
107107
"tsup": "^8.5.1",
108108
"typescript": "^6.0.3",
109109
"vitest": "^4.1.9"

packages/cloud-connection/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@objectstack/types": "workspace:*"
2525
},
2626
"devDependencies": {
27-
"@types/node": "^26.0.0",
27+
"@types/node": "^25.9.3",
2828
"typescript": "^6.0.3",
2929
"vitest": "^4.1.9"
3030
},

packages/cloud-connection/src/marketplace-install-local-plugin.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,31 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
562562
}, 400);
563563
}
564564

565-
// Persist flag flip
565+
const inserted = summary.seeded.inserted ?? 0;
566+
const updated = summary.seeded.updated ?? 0;
567+
const errors = summary.seeded.errors ?? 0;
568+
const wrote = inserted + updated > 0;
569+
570+
// HONEST RESULT: the loader runs row-by-row and counts write failures
571+
// (locked DB, missing table, validation reject) into `errors` rather
572+
// than throwing. Previously this handler returned success — and flipped
573+
// `withSampleData` to true — even when every row failed, so the UI said
574+
// "done" while the database stayed empty. Treat a run that landed no
575+
// rows as a failure and report why.
576+
if (!wrote) {
577+
return c.json({
578+
success: false,
579+
error: {
580+
code: 'reseed_no_rows',
581+
message: errors > 0
582+
? `Reseed wrote no rows (${errors} error${errors === 1 ? '' : 's'}).${summary.seeded.errorSample ? ` First error: ${summary.seeded.errorSample}` : ''}`
583+
: 'Reseed wrote no rows. The package declares no seedable records for this runtime.',
584+
details: { inserted, updated, errors },
585+
},
586+
}, 422);
587+
}
588+
589+
// Only mark the install as carrying sample data once rows actually landed.
566590
try {
567591
entry.withSampleData = true;
568592
this.ledger.write(entry);
@@ -572,9 +596,9 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
572596
success: true,
573597
data: {
574598
manifestId,
575-
inserted: summary.seeded.inserted ?? 0,
576-
updated: summary.seeded.updated ?? 0,
577-
errors: summary.seeded.errors ?? 0,
599+
inserted,
600+
updated,
601+
errors,
578602
withSampleData: true,
579603
},
580604
}, 200);
@@ -689,7 +713,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
689713
ctx: PluginContext,
690714
manifest: any,
691715
opts: { seedNow: boolean; c?: any },
692-
): Promise<{ translationsLoaded: number; seeded: { mode: 'inline' | 'replayer' | 'skipped'; inserted?: number; updated?: number; errors?: number; reason?: string } }> => {
716+
): Promise<{ translationsLoaded: number; seeded: { mode: 'inline' | 'replayer' | 'skipped'; inserted?: number; updated?: number; errors?: number; reason?: string; errorSample?: string } }> => {
693717
const appId = String(manifest?.id ?? 'unknown');
694718
let translationsLoaded = 0;
695719
let seedSummary: any = { mode: 'skipped', reason: 'no-datasets' };
@@ -803,6 +827,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
803827
inserted: result.summary.totalInserted,
804828
updated: result.summary.totalUpdated,
805829
errors: result.errors.length,
830+
// Surface the first write/resolution failure so the
831+
// caller can report WHY nothing landed (e.g. a locked
832+
// DB, a missing table, a failed validation) instead of
833+
// a bare "0 rows".
834+
errorSample: result.errors[0]?.message,
806835
};
807836
ctx.logger?.info?.(`[MarketplaceInstallLocal] inline seed for ${appId}${organizationId ? ` (org=${organizationId})` : ''}: inserted=${seedSummary.inserted} updated=${seedSummary.updated} errors=${seedSummary.errors}`);
808837
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* HONEST RESULT for the reseed endpoint.
5+
*
6+
* The SeedLoaderService runs row-by-row and counts write failures (a locked
7+
* DB, a missing table, a rejected validation) into `result.errors` rather than
8+
* throwing. The reseed handler used to return `success: true` — AND flip the
9+
* install's `withSampleData` flag to true — whenever the loader didn't outright
10+
* skip, so a run that wrote ZERO rows still reported success while the database
11+
* stayed empty (the "提示成功但没有数据" bug). These tests pin the corrected
12+
* behaviour: no rows written => failure + flag stays false; rows written =>
13+
* success + flag flips.
14+
*/
15+
16+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
17+
import { mkdtempSync, rmSync } from 'node:fs';
18+
import { join } from 'node:path';
19+
import { tmpdir } from 'node:os';
20+
21+
// Controls what the (mocked) seed loader reports back. The handler under test
22+
// only cares about result.summary.total{Inserted,Updated} + result.errors.
23+
let seedResult: any = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] };
24+
25+
vi.mock('@objectstack/runtime', () => ({
26+
SeedLoaderService: class {
27+
async load() { return seedResult; }
28+
},
29+
}));
30+
vi.mock('@objectstack/spec/data', () => ({
31+
SeedLoaderRequestSchema: { parse: (x: any) => x },
32+
}));
33+
34+
import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js';
35+
36+
type Handler = (c: any) => Promise<any>;
37+
38+
function makeRawApp() {
39+
const routes = new Map<string, Handler>();
40+
return {
41+
routes,
42+
get: (p: string, h: Handler) => routes.set(`GET ${p}`, h),
43+
post: (p: string, h: Handler) => routes.set(`POST ${p}`, h),
44+
delete: (p: string, h: Handler) => routes.set(`DELETE ${p}`, h),
45+
};
46+
}
47+
48+
function makeCtx(rawApp: any, services: Record<string, any>) {
49+
const hooks = new Map<string, any>();
50+
return {
51+
ctx: {
52+
hook: (e: string, h: any) => hooks.set(e, h),
53+
getService: (name: string) => {
54+
if (name === 'http-server') return { getRawApp: () => rawApp };
55+
const svc = services[name];
56+
if (svc === undefined) throw new Error(`no ${name}`);
57+
return svc;
58+
},
59+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
60+
},
61+
fire: async () => { await hooks.get('kernel:ready')?.(); },
62+
};
63+
}
64+
65+
/** A Hono-ish context. `param` carries the :manifestId route value. */
66+
function makeC(body: any, manifestId?: string) {
67+
const json = vi.fn((payload: any, status?: number) => ({ payload, status: status ?? 200 }));
68+
return {
69+
req: {
70+
url: 'http://localhost:3000/api/v1/marketplace/install-local',
71+
raw: new Request('http://localhost:3000/x'),
72+
json: async () => body,
73+
param: (k: string) => (k === 'manifestId' ? manifestId : undefined),
74+
header: () => undefined,
75+
},
76+
json,
77+
};
78+
}
79+
80+
const SERVICES = () => ({
81+
manifest: { register: vi.fn() },
82+
auth: { api: { getSession: async () => ({ user: { id: 'admin' } }) } },
83+
objectql: { syncSchemas: async () => undefined },
84+
metadata: {},
85+
});
86+
87+
const MANIFEST = {
88+
id: 'app.test.proj',
89+
version: '1.0.0',
90+
objects: [{ name: 'pm_x', fields: { name: { type: 'text' } } }],
91+
data: [{ object: 'pm_x', records: [{ name: 'a' }, { name: 'b' }] }],
92+
};
93+
94+
let dir: string;
95+
beforeEach(() => {
96+
dir = mkdtempSync(join(tmpdir(), 'mil-reseed-'));
97+
seedResult = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] };
98+
});
99+
afterEach(() => { rmSync(dir, { recursive: true, force: true }); vi.restoreAllMocks(); });
100+
101+
async function installAndGetRoutes() {
102+
const rawApp = makeRawApp();
103+
const { ctx, fire } = makeCtx(rawApp, SERVICES());
104+
const plugin = new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir: dir });
105+
await plugin.start(ctx as any);
106+
await fire();
107+
// Install with seed loader reporting nothing — install must still succeed.
108+
const installRes = await rawApp.routes.get('POST /api/v1/marketplace/install-local')!(
109+
makeC({ manifest: MANIFEST }),
110+
);
111+
expect(installRes.payload?.success).toBe(true);
112+
return rawApp;
113+
}
114+
115+
describe('reseed honest result', () => {
116+
it('FAILS (422) when the seed run wrote zero rows but errored', async () => {
117+
const rawApp = await installAndGetRoutes();
118+
seedResult = {
119+
summary: { totalInserted: 0, totalUpdated: 0 },
120+
errors: [{ message: 'database is locked' }, { message: 'database is locked' }],
121+
};
122+
const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!(
123+
makeC({}, 'app.test.proj'),
124+
);
125+
expect(res.status).toBe(422);
126+
expect(res.payload?.success).toBe(false);
127+
expect(res.payload?.error?.code).toBe('reseed_no_rows');
128+
// The real failure reason is surfaced, not swallowed.
129+
expect(res.payload?.error?.message).toContain('database is locked');
130+
expect(res.payload?.error?.details).toMatchObject({ inserted: 0, updated: 0, errors: 2 });
131+
});
132+
133+
it('FAILS (422) when the package seeds nothing (0 rows, 0 errors)', async () => {
134+
const rawApp = await installAndGetRoutes();
135+
seedResult = { summary: { totalInserted: 0, totalUpdated: 0 }, errors: [] };
136+
const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!(
137+
makeC({}, 'app.test.proj'),
138+
);
139+
expect(res.status).toBe(422);
140+
expect(res.payload?.error?.code).toBe('reseed_no_rows');
141+
});
142+
143+
it('SUCCEEDS and flips withSampleData when rows actually land', async () => {
144+
const rawApp = await installAndGetRoutes();
145+
seedResult = { summary: { totalInserted: 2, totalUpdated: 0 }, errors: [] };
146+
const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!(
147+
makeC({}, 'app.test.proj'),
148+
);
149+
expect(res.status).toBe(200);
150+
expect(res.payload?.success).toBe(true);
151+
expect(res.payload?.data).toMatchObject({ inserted: 2, updated: 0, withSampleData: true });
152+
153+
// The ledger now reflects that sample data is present.
154+
const listRes = await rawApp.routes.get('GET /api/v1/marketplace/install-local')!(makeC({}));
155+
const entry = listRes.payload.data.items.find((i: any) => i.manifestId === 'app.test.proj');
156+
expect(entry?.withSampleData).toBe(true);
157+
});
158+
159+
it('partial success (some rows + some errors) still reports the error count', async () => {
160+
const rawApp = await installAndGetRoutes();
161+
seedResult = {
162+
summary: { totalInserted: 1, totalUpdated: 0 },
163+
errors: [{ message: 'one row rejected' }],
164+
};
165+
const res = await rawApp.routes.get('POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data')!(
166+
makeC({}, 'app.test.proj'),
167+
);
168+
expect(res.status).toBe(200);
169+
expect(res.payload?.success).toBe(true);
170+
expect(res.payload?.data?.errors).toBe(1);
171+
});
172+
});

packages/connectors/connector-mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"devDependencies": {
2525
"@objectstack/service-automation": "workspace:*",
26-
"@types/node": "^26.0.0",
26+
"@types/node": "^25.9.3",
2727
"typescript": "^6.0.3",
2828
"vitest": "^4.1.9"
2929
},

0 commit comments

Comments
 (0)