fix(plugin-sdk): dev db binds an args array like the real host, and a failed onLoad stops the routes

This commit is contained in:
Maurice
2026-07-04 20:13:14 +02:00
committed by Maurice
parent 91217e83fa
commit 6ae47cbdf5
2 changed files with 38 additions and 4 deletions
+18 -4
View File
@@ -26,8 +26,17 @@ interface PluginLike { onLoad?: (ctx: unknown) => unknown; routes?: PluginRouteL
class PermissionDenied extends Error {}
/**
* Accept both bind shapes the real host accepts: spread positional args AND a
* single array of them (better-sqlite3 binds an array; node:sqlite does not,
* so a plugin using the array form would fail only in dev).
*/
function flatBind(args: unknown[]): unknown[] {
return args.length === 1 && Array.isArray(args[0]) ? (args[0] as unknown[]) : args;
}
/** A dev db backed by node:sqlite if available, else an in-memory recorder that returns []. */
function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close: () => void } {
export function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close: () => void } {
try {
// node:sqlite is available on Node 22.5+ (experimental). Fall back gracefully if not.
const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite') as { DatabaseSync: new (p: string) => SqliteDb };
@@ -38,9 +47,9 @@ function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close
note: `db:own → real SQLite at ${dbFile}`,
close: () => { try { sq.close(); } catch { /* ignore */ } },
db: {
async query(sql: string, ...args: unknown[]) { return sq.prepare(sql).all(...args) as unknown[]; },
async query(sql: string, ...args: unknown[]) { return sq.prepare(sql).all(...flatBind(args)) as unknown[]; },
async exec(sql: string, ...args: unknown[]) {
if (args.length) { const r = sq.prepare(sql).run(...args); return { changes: Number(r.changes ?? 0) }; }
if (args.length) { const r = sq.prepare(sql).run(...flatBind(args)); return { changes: Number(r.changes ?? 0) }; }
sq.exec(sql); return { changes: 0 };
},
async migrate(id: string, sql: string) { if (applied.has(id)) return { applied: false }; sq.exec(sql); applied.add(id); return { applied: true }; },
@@ -167,7 +176,12 @@ export async function runDev(dir: string, opts: { port?: number } = {}): Promise
await plugin.onLoad?.(ctx);
version++;
console.log(` ↻ loaded ${plugin.routes?.length ?? 0} route(s)`);
} catch (e) { console.error(' ✗ plugin failed to load:', e instanceof Error ? e.message : e); }
} catch (e) {
// A plugin whose load/onLoad throws would fail activation in TREK — don't
// keep serving its routes here, or dev would hide exactly that failure.
plugin = {};
console.error(' ✗ plugin failed to load:', e instanceof Error ? e.message : e);
}
};
await load();
+20
View File
@@ -164,6 +164,26 @@ describe('dev-server SDK injection', () => {
});
});
describe('dev db bind shapes', () => {
let tmp: string;
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'devdb-')); });
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
it('accepts spread args AND a single array of them, like the real host', async () => {
const { createDevDb } = await import('../src/cli/dev.js');
const { db, close } = createDevDb(path.join(tmp, 'db.sqlite'));
try {
await db.migrate('001', 'CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)');
await db.exec('INSERT INTO kv (k, v) VALUES (?, ?)', ['a', '1']); // array form (better-sqlite3 style)
await db.exec('INSERT INTO kv (k, v) VALUES (?, ?)', 'b', '2'); // spread form
expect(await db.query('SELECT v FROM kv WHERE k = ?', ['a'])).toEqual([{ v: '1' }]);
expect(await db.query('SELECT v FROM kv WHERE k = ?', 'b')).toEqual([{ v: '2' }]);
} finally {
close();
}
});
});
describe('reference plugin (examples/koffi)', () => {
const dir = path.resolve(import.meta.dirname, '..', 'examples', 'koffi');