bonescript-compiler 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +64 -15
- package/dist/cli.js.map +1 -1
- package/dist/emit_graphql.js +19 -7
- package/dist/emit_graphql.js.map +1 -1
- package/dist/emit_notify.js +36 -1
- package/dist/emit_notify.js.map +1 -1
- package/dist/emit_prisma.js +10 -2
- package/dist/emit_prisma.js.map +1 -1
- package/dist/emit_sqlite.d.ts +42 -1
- package/dist/emit_sqlite.js +375 -51
- package/dist/emit_sqlite.js.map +1 -1
- package/dist/lowering.js +27 -0
- package/dist/lowering.js.map +1 -1
- package/package.json +9 -2
- package/src/cli.ts +64 -15
- package/src/emit_graphql.ts +18 -7
- package/src/emit_notify.ts +36 -1
- package/src/emit_prisma.ts +10 -2
- package/src/emit_sqlite.ts +388 -52
- package/src/lowering.ts +26 -0
- package/dist/test_notify.d.ts +0 -11
- package/dist/test_notify.js +0 -220
- package/dist/test_notify.js.map +0 -1
- package/dist/test_react.d.ts +0 -10
- package/dist/test_react.js +0 -177
- package/dist/test_react.js.map +0 -1
- package/dist/test_sqlite.d.ts +0 -13
- package/dist/test_sqlite.js +0 -262
- package/dist/test_sqlite.js.map +0 -1
package/dist/emit_sqlite.d.ts
CHANGED
|
@@ -25,8 +25,49 @@ export declare function emitSqliteAuditSchema(): string;
|
|
|
25
25
|
export declare function emitSqliteDbClient(system: IR.IRSystem): string;
|
|
26
26
|
export declare function emitSqliteMigration(_system: IR.IRSystem, schemas: string[]): string;
|
|
27
27
|
export declare function emitSqlitePackageJson(system: IR.IRSystem): string;
|
|
28
|
+
import { FullEmitterOptions } from "./emit_full";
|
|
28
29
|
export declare class SqliteEmitter {
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Emit a complete SQLite-backed Express project.
|
|
32
|
+
*
|
|
33
|
+
* Strategy: delegate the bulk of generation to FullEmitter (routes, auth,
|
|
34
|
+
* websocket, etc), then swap out the Postgres-specific pieces for SQLite
|
|
35
|
+
* equivalents. The generated routes use a Postgres-shaped SQL surface that
|
|
36
|
+
* our SQLite db.ts client translates at runtime (NOW() → datetime('now'),
|
|
37
|
+
* $N → ?, RETURNING * → re-select by id).
|
|
38
|
+
*
|
|
39
|
+
* What's swapped:
|
|
40
|
+
* - package.json SQLite deps (better-sqlite3) instead of pg
|
|
41
|
+
* - .env.example SQLite-shaped env vars
|
|
42
|
+
* - src/db.ts better-sqlite3 client with SQL translation
|
|
43
|
+
* - src/migrate.ts SQLite migration runner
|
|
44
|
+
* - migrations/*.sql SQLite-flavored DDL (TEXT, no JSONB, etc.)
|
|
45
|
+
* - README.md SQLite-aware quick start
|
|
46
|
+
*
|
|
47
|
+
* What's removed:
|
|
48
|
+
* - docker-compose.yaml, Dockerfile, k8s/deployment.yaml — SQLite is
|
|
49
|
+
* single-file; no service orchestration needed
|
|
50
|
+
*
|
|
51
|
+
* Known limitations:
|
|
52
|
+
* - Capability bodies that use jsonb_set / jsonb_agg / jsonb_array_elements
|
|
53
|
+
* fail at runtime. Refactor those capabilities or use the Express target.
|
|
54
|
+
* - Durable event delivery (EVENT_MODE=durable) requires Postgres-only
|
|
55
|
+
* features (FOR UPDATE SKIP LOCKED). Default is in_process which works
|
|
56
|
+
* fine on SQLite.
|
|
57
|
+
* - row_to_json(t.*) in LIST joins degrades to t.id (no full join object).
|
|
58
|
+
*/
|
|
59
|
+
emit(system: IR.IRSystem, options?: FullEmitterOptions): EmittedFile[];
|
|
60
|
+
private buildReplacements;
|
|
61
|
+
/**
|
|
62
|
+
* In-process-only event bus for SQLite.
|
|
63
|
+
*
|
|
64
|
+
* The default Express target's events.ts has a durable mode backed by a
|
|
65
|
+
* Postgres outbox with FOR UPDATE SKIP LOCKED. SQLite has no equivalent,
|
|
66
|
+
* so we ship a simpler bus that only does in-memory delivery. Callers
|
|
67
|
+
* can still use the same eventBus.publish() / .subscribe() API; only
|
|
68
|
+
* `EVENT_MODE=durable` is unsupported.
|
|
69
|
+
*/
|
|
70
|
+
private emitInProcessEventBus;
|
|
30
71
|
private emitTsConfig;
|
|
31
72
|
private emitEnvExample;
|
|
32
73
|
private emitReadme;
|
package/dist/emit_sqlite.js
CHANGED
|
@@ -209,11 +209,35 @@ function emitSqliteDbClient(system) {
|
|
|
209
209
|
` return _db;`,
|
|
210
210
|
`}`,
|
|
211
211
|
``,
|
|
212
|
-
`// Translate Postgres-style
|
|
213
|
-
|
|
214
|
-
`//
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
`// Translate Postgres-style SQL to SQLite-compatible SQL.`,
|
|
213
|
+
`//`,
|
|
214
|
+
`// Handles:`,
|
|
215
|
+
`// - $1, $2, ... → ? (and rebuilds params for out-of-order references)`,
|
|
216
|
+
`// - NOW() → datetime('now')`,
|
|
217
|
+
`// - row_to_json(t.*) → json_object(...) — best-effort, see comment below`,
|
|
218
|
+
`//`,
|
|
219
|
+
`// We can't translate every Postgres-ism. Capability effects that use`,
|
|
220
|
+
`// jsonb_set / jsonb_agg / jsonb_array_elements still fail at runtime; if`,
|
|
221
|
+
`// you hit those, refactor the capability or use the Express target.`,
|
|
222
|
+
`function translateSql(sql: string, params: any[]): { sql: string; params: any[] } {`,
|
|
223
|
+
` let out = sql.replace(/\\bNOW\\s*\\(\\s*\\)/gi, "datetime('now')");`,
|
|
224
|
+
``,
|
|
225
|
+
` // row_to_json(t.*) → json_object('col', t.col, ...) is hard to do without`,
|
|
226
|
+
` // knowing the columns. As a degraded fallback, replace it with the table`,
|
|
227
|
+
` // alias so the LIST join still returns *something* (the joined table's id).`,
|
|
228
|
+
` // The "as <name>" still works; consumers will see a string instead of an`,
|
|
229
|
+
` // object. This is a known limitation.`,
|
|
230
|
+
` out = out.replace(/row_to_json\\((\\w+)\\.\\*\\)/gi, "$1.id");`,
|
|
231
|
+
``,
|
|
232
|
+
` // Postgres-style placeholders → SQLite ?, with param rewrite for`,
|
|
233
|
+
` // out-of-order references (e.g. "WHERE id = $1 AND ... = $2" vs`,
|
|
234
|
+
` // "SET col = $2 WHERE id = $1").`,
|
|
235
|
+
` const newParams: any[] = [];`,
|
|
236
|
+
` out = out.replace(/\\$(\\d+)/g, (_m, n) => {`,
|
|
237
|
+
` newParams.push(params[parseInt(n, 10) - 1]);`,
|
|
238
|
+
` return "?";`,
|
|
239
|
+
` });`,
|
|
240
|
+
` return { sql: out, params: newParams };`,
|
|
217
241
|
`}`,
|
|
218
242
|
``,
|
|
219
243
|
`// Strip Postgres-only RETURNING * — better-sqlite3 returns the inserted/updated`,
|
|
@@ -233,29 +257,48 @@ function emitSqliteDbClient(system) {
|
|
|
233
257
|
` return null;`,
|
|
234
258
|
`}`,
|
|
235
259
|
``,
|
|
260
|
+
// Parse the WHERE clause to find the param index that holds the id. The
|
|
261
|
+
// generated SQL uses two shapes:
|
|
262
|
+
// - emit_runtime PUT: "UPDATE t SET ... WHERE id = $1" → idIdx = 1
|
|
263
|
+
// - emit_capability: "UPDATE t SET col = $1 WHERE id = $2" → idIdx = 2
|
|
264
|
+
// For INSERT the row id is always $1 by convention.
|
|
265
|
+
`function findIdParamIndex(sql: string, params: any[]): any {`,
|
|
266
|
+
` // Look for "WHERE id = $N" or "WHERE id = ?" (after translation).`,
|
|
267
|
+
` const m = sql.match(/WHERE\\s+id\\s*=\\s*\\$(\\d+)/i);`,
|
|
268
|
+
` if (m) {`,
|
|
269
|
+
` const idx = parseInt(m[1], 10) - 1;`,
|
|
270
|
+
` return params[idx];`,
|
|
271
|
+
` }`,
|
|
272
|
+
` // Translated form: count which "?" corresponds to the WHERE id.`,
|
|
273
|
+
` const beforeWhere = sql.split(/WHERE\\s+id\\s*=\\s*\\?/i)[0] || "";`,
|
|
274
|
+
` const placeholdersBeforeId = (beforeWhere.match(/\\?/g) || []).length;`,
|
|
275
|
+
` return params[placeholdersBeforeId];`,
|
|
276
|
+
`}`,
|
|
277
|
+
``,
|
|
236
278
|
`export async function query<T = any>(text: string, params: any[] = []): Promise<T[]> {`,
|
|
237
279
|
` const db = getDb();`,
|
|
238
280
|
` const { sql: stripped, hadReturning } = stripReturning(text);`,
|
|
239
|
-
` const sql = translateSql(stripped);`,
|
|
281
|
+
` const { sql, params: translatedParams } = translateSql(stripped, params);`,
|
|
240
282
|
` const trimmed = sql.trim().toUpperCase();`,
|
|
241
283
|
` if (trimmed.startsWith("SELECT") || trimmed.startsWith("WITH")) {`,
|
|
242
|
-
` return db.prepare(sql).all(...
|
|
284
|
+
` return db.prepare(sql).all(...translatedParams) as T[];`,
|
|
243
285
|
` }`,
|
|
244
286
|
` // INSERT / UPDATE / DELETE`,
|
|
245
287
|
` const stmt = db.prepare(sql);`,
|
|
246
|
-
` const info = stmt.run(...
|
|
288
|
+
` const info = stmt.run(...translatedParams);`,
|
|
247
289
|
` if (hadReturning) {`,
|
|
248
290
|
` const table = extractTable(sql);`,
|
|
249
291
|
` if (!table) return [];`,
|
|
250
292
|
` if (trimmed.startsWith("INSERT")) {`,
|
|
251
|
-
` //
|
|
293
|
+
` // Generated INSERTs have id as the first column → params[0].`,
|
|
252
294
|
` const id = params[0];`,
|
|
253
295
|
` const row = db.prepare(\`SELECT * FROM \${table} WHERE id = ?\`).get(id);`,
|
|
254
296
|
` return row ? [row as T] : [];`,
|
|
255
297
|
` }`,
|
|
256
298
|
` if (trimmed.startsWith("UPDATE")) {`,
|
|
257
|
-
` //
|
|
258
|
-
` const id = params
|
|
299
|
+
` // Find the id from the WHERE clause regardless of param order.`,
|
|
300
|
+
` const id = findIdParamIndex(stripped, params);`,
|
|
301
|
+
` if (id === undefined) return [];`,
|
|
259
302
|
` const row = db.prepare(\`SELECT * FROM \${table} WHERE id = ?\`).get(id);`,
|
|
260
303
|
` return row ? [row as T] : [];`,
|
|
261
304
|
` }`,
|
|
@@ -272,17 +315,43 @@ function emitSqliteDbClient(system) {
|
|
|
272
315
|
`export async function execute(text: string, params: any[] = []): Promise<number> {`,
|
|
273
316
|
` const db = getDb();`,
|
|
274
317
|
` const { sql: stripped } = stripReturning(text);`,
|
|
275
|
-
` const sql = translateSql(stripped);`,
|
|
276
|
-
` const info = db.prepare(sql).run(...
|
|
318
|
+
` const { sql, params: translatedParams } = translateSql(stripped, params);`,
|
|
319
|
+
` const info = db.prepare(sql).run(...translatedParams);`,
|
|
277
320
|
` return info.changes;`,
|
|
278
321
|
`}`,
|
|
279
322
|
``,
|
|
280
|
-
|
|
323
|
+
`// Run a function inside a SQLite transaction.`,
|
|
324
|
+
`//`,
|
|
325
|
+
`// IMPORTANT: better-sqlite3 transactions are synchronous. The callback runs`,
|
|
326
|
+
`// to completion before COMMIT — but if you do "await" inside it for a Promise`,
|
|
327
|
+
`// other than another query()/execute(), the transaction will commit before`,
|
|
328
|
+
`// the awaited work finishes.`,
|
|
329
|
+
`//`,
|
|
330
|
+
`// In practice this is fine: query()/execute() above are async only at the`,
|
|
331
|
+
`// type level; their bodies are synchronous because better-sqlite3 is.`,
|
|
332
|
+
`// The transaction will be safely committed once the callback returns, and`,
|
|
333
|
+
`// any chained query/execute calls inside it run on the same database in the`,
|
|
334
|
+
`// same transaction.`,
|
|
335
|
+
`//`,
|
|
336
|
+
`// Don't put fetch(), setTimeout, or other genuinely-async work inside fn.`,
|
|
337
|
+
`export async function transaction<T>(fn: (client: { query: typeof query; execute: typeof execute }) => Promise<T> | T): Promise<T> {`,
|
|
281
338
|
` const db = getDb();`,
|
|
282
|
-
`
|
|
283
|
-
`
|
|
284
|
-
` const txn = db.transaction(
|
|
285
|
-
`
|
|
339
|
+
` let result!: T;`,
|
|
340
|
+
` let promise: Promise<T> | null = null;`,
|
|
341
|
+
` const txn = db.transaction(() => {`,
|
|
342
|
+
` const r = fn({ query, execute });`,
|
|
343
|
+
` if (r instanceof Promise) {`,
|
|
344
|
+
` // Capture the promise; await outside the synchronous transaction.`,
|
|
345
|
+
` // Note: this means async work inside fn runs OUTSIDE the transaction,`,
|
|
346
|
+
` // which may be a bug in caller code. Better to keep fn synchronous.`,
|
|
347
|
+
` promise = r;`,
|
|
348
|
+
` return;`,
|
|
349
|
+
` }`,
|
|
350
|
+
` result = r as T;`,
|
|
351
|
+
` });`,
|
|
352
|
+
` txn();`,
|
|
353
|
+
` if (promise) return await promise;`,
|
|
354
|
+
` return result;`,
|
|
286
355
|
`}`,
|
|
287
356
|
``,
|
|
288
357
|
`// Compatibility shim: the Postgres path uses pool.connect()/release() for nested`,
|
|
@@ -317,6 +386,7 @@ function emitSqliteDbClient(system) {
|
|
|
317
386
|
exports.emitSqliteDbClient = emitSqliteDbClient;
|
|
318
387
|
// ─── Migration runner (SQLite flavor) ─────────────────────────────────────────
|
|
319
388
|
function emitSqliteMigration(_system, schemas) {
|
|
389
|
+
const system = _system; // alias for use below — kept underscore for backwards compat
|
|
320
390
|
// Build deterministic blocks just like the Postgres path does.
|
|
321
391
|
const lines = [];
|
|
322
392
|
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
@@ -354,7 +424,7 @@ function emitSqliteMigration(_system, schemas) {
|
|
|
354
424
|
lines.push(`}`);
|
|
355
425
|
lines.push(``);
|
|
356
426
|
lines.push(`async function migrate() {`);
|
|
357
|
-
lines.push(` const dbPath = process.env.SQLITE_PATH || path.resolve(process.cwd(), "
|
|
427
|
+
lines.push(` const dbPath = process.env.SQLITE_PATH || path.resolve(process.cwd(), "${toSnakeCase(_system.name)}.db");`);
|
|
358
428
|
lines.push(` const db = new Database(dbPath);`);
|
|
359
429
|
lines.push(` db.pragma("foreign_keys = ON");`);
|
|
360
430
|
lines.push(``);
|
|
@@ -400,6 +470,7 @@ function emitSqliteMigration(_system, schemas) {
|
|
|
400
470
|
exports.emitSqliteMigration = emitSqliteMigration;
|
|
401
471
|
// ─── Package.json (SQLite flavor) ─────────────────────────────────────────────
|
|
402
472
|
function emitSqlitePackageJson(system) {
|
|
473
|
+
// Full backend deps, but with better-sqlite3 instead of pg.
|
|
403
474
|
const pkg = {
|
|
404
475
|
name: toSnakeCase(system.name),
|
|
405
476
|
version: system.version,
|
|
@@ -409,10 +480,13 @@ function emitSqlitePackageJson(system) {
|
|
|
409
480
|
start: "node dist/index.js",
|
|
410
481
|
dev: "ts-node src/index.ts",
|
|
411
482
|
migrate: "ts-node src/migrate.ts",
|
|
483
|
+
seed: "ts-node src/seed.ts",
|
|
412
484
|
},
|
|
413
485
|
dependencies: {
|
|
486
|
+
// Pinned versions match the Express target except pg → better-sqlite3.
|
|
414
487
|
express: "4.22.2",
|
|
415
488
|
"better-sqlite3": "11.5.0",
|
|
489
|
+
ws: "8.18.0",
|
|
416
490
|
uuid: "10.0.0",
|
|
417
491
|
cors: "2.8.5",
|
|
418
492
|
helmet: "8.0.0",
|
|
@@ -426,6 +500,7 @@ function emitSqlitePackageJson(system) {
|
|
|
426
500
|
"@types/express": "4.17.21",
|
|
427
501
|
"@types/node": "20.14.0",
|
|
428
502
|
"@types/better-sqlite3": "7.6.11",
|
|
503
|
+
"@types/ws": "8.5.13",
|
|
429
504
|
"@types/cors": "2.8.17",
|
|
430
505
|
"@types/jsonwebtoken": "9.0.7",
|
|
431
506
|
"@types/uuid": "10.0.0",
|
|
@@ -438,15 +513,70 @@ function emitSqlitePackageJson(system) {
|
|
|
438
513
|
}
|
|
439
514
|
exports.emitSqlitePackageJson = emitSqlitePackageJson;
|
|
440
515
|
// ─── Top-level emitter ────────────────────────────────────────────────────────
|
|
516
|
+
const emit_full_1 = require("./emit_full");
|
|
441
517
|
class SqliteEmitter {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Emit a complete SQLite-backed Express project.
|
|
520
|
+
*
|
|
521
|
+
* Strategy: delegate the bulk of generation to FullEmitter (routes, auth,
|
|
522
|
+
* websocket, etc), then swap out the Postgres-specific pieces for SQLite
|
|
523
|
+
* equivalents. The generated routes use a Postgres-shaped SQL surface that
|
|
524
|
+
* our SQLite db.ts client translates at runtime (NOW() → datetime('now'),
|
|
525
|
+
* $N → ?, RETURNING * → re-select by id).
|
|
526
|
+
*
|
|
527
|
+
* What's swapped:
|
|
528
|
+
* - package.json SQLite deps (better-sqlite3) instead of pg
|
|
529
|
+
* - .env.example SQLite-shaped env vars
|
|
530
|
+
* - src/db.ts better-sqlite3 client with SQL translation
|
|
531
|
+
* - src/migrate.ts SQLite migration runner
|
|
532
|
+
* - migrations/*.sql SQLite-flavored DDL (TEXT, no JSONB, etc.)
|
|
533
|
+
* - README.md SQLite-aware quick start
|
|
534
|
+
*
|
|
535
|
+
* What's removed:
|
|
536
|
+
* - docker-compose.yaml, Dockerfile, k8s/deployment.yaml — SQLite is
|
|
537
|
+
* single-file; no service orchestration needed
|
|
538
|
+
*
|
|
539
|
+
* Known limitations:
|
|
540
|
+
* - Capability bodies that use jsonb_set / jsonb_agg / jsonb_array_elements
|
|
541
|
+
* fail at runtime. Refactor those capabilities or use the Express target.
|
|
542
|
+
* - Durable event delivery (EVENT_MODE=durable) requires Postgres-only
|
|
543
|
+
* features (FOR UPDATE SKIP LOCKED). Default is in_process which works
|
|
544
|
+
* fine on SQLite.
|
|
545
|
+
* - row_to_json(t.*) in LIST joins degrades to t.id (no full join object).
|
|
546
|
+
*/
|
|
547
|
+
emit(system, options = {}) {
|
|
548
|
+
// 1. Generate the full Express project
|
|
549
|
+
const fullFiles = new emit_full_1.FullEmitter().emit(system, options);
|
|
550
|
+
// 2. Files we replace with SQLite equivalents (keyed by path)
|
|
551
|
+
const replacements = this.buildReplacements(system);
|
|
552
|
+
// 3. Files we drop entirely (Postgres-specific infra)
|
|
553
|
+
const drops = new Set([
|
|
554
|
+
"docker-compose.yaml",
|
|
555
|
+
"Dockerfile",
|
|
556
|
+
".dockerignore",
|
|
557
|
+
"k8s/deployment.yaml",
|
|
558
|
+
"src/migration_diff.ts", // Postgres-specific schema diff
|
|
559
|
+
]);
|
|
560
|
+
const out = [];
|
|
561
|
+
for (const f of fullFiles) {
|
|
562
|
+
if (drops.has(f.path))
|
|
563
|
+
continue;
|
|
564
|
+
// Migrations need wholesale replacement: regenerate per-entity SQLite SQL.
|
|
565
|
+
if (f.path.startsWith("migrations/") && f.path.endsWith(".sql") &&
|
|
566
|
+
f.path !== "migrations/event_outbox.sql" &&
|
|
567
|
+
f.path !== "migrations/audit_log.sql") {
|
|
568
|
+
// We re-emit these from scratch below.
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const replacement = replacements.get(f.path);
|
|
572
|
+
if (replacement) {
|
|
573
|
+
out.push(replacement);
|
|
574
|
+
replacements.delete(f.path);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
out.push(f);
|
|
578
|
+
}
|
|
579
|
+
// 4. Append per-entity SQLite migrations
|
|
450
580
|
const seenPaths = new Set();
|
|
451
581
|
for (const mod of system.modules) {
|
|
452
582
|
if (mod.kind === "data_store" || mod.kind === "api_service") {
|
|
@@ -455,23 +585,157 @@ class SqliteEmitter {
|
|
|
455
585
|
if (seenPaths.has(file.path))
|
|
456
586
|
continue;
|
|
457
587
|
seenPaths.add(file.path);
|
|
458
|
-
|
|
459
|
-
schemas.push(file.content);
|
|
588
|
+
out.push(file);
|
|
460
589
|
}
|
|
461
590
|
}
|
|
462
591
|
}
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
592
|
+
// 5. Add the SQLite migration runner (rebuilt with all schemas in scope)
|
|
593
|
+
const allSchemas = out
|
|
594
|
+
.filter(f => f.path.startsWith("migrations/") && f.path.endsWith(".sql"))
|
|
595
|
+
.map(f => f.content);
|
|
596
|
+
out.push({
|
|
597
|
+
path: "src/migrate.ts",
|
|
598
|
+
content: emitSqliteMigration(system, allSchemas),
|
|
599
|
+
language: "typescript",
|
|
600
|
+
source_module: "infra",
|
|
601
|
+
});
|
|
602
|
+
// 6. Add any replacements that weren't already handled (i.e. files unique to SQLite)
|
|
603
|
+
for (const r of replacements.values()) {
|
|
604
|
+
out.push(r);
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
buildReplacements(system) {
|
|
609
|
+
const map = new Map();
|
|
610
|
+
map.set("package.json", {
|
|
611
|
+
path: "package.json",
|
|
612
|
+
content: emitSqlitePackageJson(system),
|
|
613
|
+
language: "json",
|
|
614
|
+
source_module: "root",
|
|
615
|
+
});
|
|
616
|
+
map.set(".env.example", {
|
|
617
|
+
path: ".env.example",
|
|
618
|
+
content: this.emitEnvExample(system),
|
|
619
|
+
language: "yaml",
|
|
620
|
+
source_module: "root",
|
|
621
|
+
});
|
|
622
|
+
map.set("src/db.ts", {
|
|
623
|
+
path: "src/db.ts",
|
|
624
|
+
content: emitSqliteDbClient(system),
|
|
625
|
+
language: "typescript",
|
|
626
|
+
source_module: "infra",
|
|
627
|
+
});
|
|
628
|
+
map.set("src/events.ts", {
|
|
629
|
+
path: "src/events.ts",
|
|
630
|
+
content: this.emitInProcessEventBus(system),
|
|
631
|
+
language: "typescript",
|
|
632
|
+
source_module: "infra",
|
|
633
|
+
});
|
|
634
|
+
map.set("migrations/event_outbox.sql", {
|
|
635
|
+
path: "migrations/event_outbox.sql",
|
|
636
|
+
content: emitSqliteOutboxSchema(),
|
|
637
|
+
language: "sql",
|
|
638
|
+
source_module: "infra",
|
|
639
|
+
});
|
|
640
|
+
map.set("migrations/audit_log.sql", {
|
|
641
|
+
path: "migrations/audit_log.sql",
|
|
642
|
+
content: emitSqliteAuditSchema(),
|
|
643
|
+
language: "sql",
|
|
644
|
+
source_module: "infra",
|
|
645
|
+
});
|
|
646
|
+
map.set("README.md", {
|
|
647
|
+
path: "README.md",
|
|
648
|
+
content: this.emitReadme(system),
|
|
649
|
+
language: "yaml",
|
|
650
|
+
source_module: "root",
|
|
651
|
+
});
|
|
652
|
+
return map;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* In-process-only event bus for SQLite.
|
|
656
|
+
*
|
|
657
|
+
* The default Express target's events.ts has a durable mode backed by a
|
|
658
|
+
* Postgres outbox with FOR UPDATE SKIP LOCKED. SQLite has no equivalent,
|
|
659
|
+
* so we ship a simpler bus that only does in-memory delivery. Callers
|
|
660
|
+
* can still use the same eventBus.publish() / .subscribe() API; only
|
|
661
|
+
* `EVENT_MODE=durable` is unsupported.
|
|
662
|
+
*/
|
|
663
|
+
emitInProcessEventBus(system) {
|
|
664
|
+
const lines = [];
|
|
665
|
+
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
666
|
+
lines.push(`// In-process event bus (SQLite target — durable mode is Postgres-only).`);
|
|
667
|
+
lines.push(``);
|
|
668
|
+
lines.push(`import { v4 as uuid } from "uuid";`);
|
|
669
|
+
lines.push(`import { logger } from "./logger";`);
|
|
670
|
+
lines.push(`import { counter } from "./metrics";`);
|
|
671
|
+
lines.push(``);
|
|
672
|
+
lines.push(`export interface EventMetadata {`);
|
|
673
|
+
lines.push(` source: string;`);
|
|
674
|
+
lines.push(` timestamp: Date;`);
|
|
675
|
+
lines.push(` correlation_id: string;`);
|
|
676
|
+
lines.push(` causation_id: string;`);
|
|
677
|
+
lines.push(`}`);
|
|
678
|
+
lines.push(``);
|
|
679
|
+
lines.push(`export interface SystemEvent {`);
|
|
680
|
+
lines.push(` type: string;`);
|
|
681
|
+
lines.push(` payload: Record<string, unknown>;`);
|
|
682
|
+
lines.push(` metadata: EventMetadata;`);
|
|
683
|
+
lines.push(`}`);
|
|
684
|
+
lines.push(``);
|
|
685
|
+
lines.push(`type Handler = (event: SystemEvent) => Promise<void>;`);
|
|
686
|
+
lines.push(``);
|
|
687
|
+
lines.push(`class InProcessBus {`);
|
|
688
|
+
lines.push(` private handlers: Map<string, Handler[]> = new Map();`);
|
|
689
|
+
lines.push(``);
|
|
690
|
+
lines.push(` subscribe(type: string, handler: Handler): void {`);
|
|
691
|
+
lines.push(` const existing = this.handlers.get(type) || [];`);
|
|
692
|
+
lines.push(` existing.push(handler);`);
|
|
693
|
+
lines.push(` this.handlers.set(type, existing);`);
|
|
694
|
+
lines.push(` }`);
|
|
695
|
+
lines.push(``);
|
|
696
|
+
lines.push(` async publish(`);
|
|
697
|
+
lines.push(` type: string,`);
|
|
698
|
+
lines.push(` payload: Record<string, unknown>,`);
|
|
699
|
+
lines.push(` source: string,`);
|
|
700
|
+
lines.push(` correlationId?: string,`);
|
|
701
|
+
lines.push(` _client?: unknown,`);
|
|
702
|
+
lines.push(` ): Promise<void> {`);
|
|
703
|
+
lines.push(` const event: SystemEvent = {`);
|
|
704
|
+
lines.push(` type,`);
|
|
705
|
+
lines.push(` payload,`);
|
|
706
|
+
lines.push(` metadata: {`);
|
|
707
|
+
lines.push(` source,`);
|
|
708
|
+
lines.push(` timestamp: new Date(),`);
|
|
709
|
+
lines.push(` correlation_id: correlationId || uuid(),`);
|
|
710
|
+
lines.push(` causation_id: uuid(),`);
|
|
711
|
+
lines.push(` },`);
|
|
712
|
+
lines.push(` };`);
|
|
713
|
+
lines.push(` counter("event.published", { type, mode: "in_process" });`);
|
|
714
|
+
lines.push(` const handlers = this.handlers.get(type) || [];`);
|
|
715
|
+
lines.push(` for (const handler of handlers) {`);
|
|
716
|
+
lines.push(` try {`);
|
|
717
|
+
lines.push(` await handler(event);`);
|
|
718
|
+
lines.push(` counter("event.delivered", { type, mode: "in_process" });`);
|
|
719
|
+
lines.push(` } catch (e: any) {`);
|
|
720
|
+
lines.push(` counter("event.delivery_failed", { type, mode: "in_process" });`);
|
|
721
|
+
lines.push(` logger.error("event_handler_failed", { event: type, metadata: { error: e.message } });`);
|
|
722
|
+
lines.push(` }`);
|
|
723
|
+
lines.push(` }`);
|
|
724
|
+
lines.push(` }`);
|
|
725
|
+
lines.push(``);
|
|
726
|
+
lines.push(` startWorker(_intervalMs?: number): null {`);
|
|
727
|
+
lines.push(` // No-op for in-process mode.`);
|
|
728
|
+
lines.push(` return null;`);
|
|
729
|
+
lines.push(` }`);
|
|
730
|
+
lines.push(`}`);
|
|
731
|
+
lines.push(``);
|
|
732
|
+
lines.push(`if (process.env.EVENT_MODE === "durable") {`);
|
|
733
|
+
lines.push(` console.warn("[events] EVENT_MODE=durable is not supported on the SQLite target. Falling back to in_process.");`);
|
|
734
|
+
lines.push(`}`);
|
|
735
|
+
lines.push(``);
|
|
736
|
+
lines.push(`export const eventBus = new InProcessBus();`);
|
|
737
|
+
lines.push(``);
|
|
738
|
+
return lines.join("\n");
|
|
475
739
|
}
|
|
476
740
|
emitTsConfig() {
|
|
477
741
|
const cfg = {
|
|
@@ -495,15 +759,41 @@ class SqliteEmitter {
|
|
|
495
759
|
}
|
|
496
760
|
emitEnvExample(system) {
|
|
497
761
|
return `# ${system.name} (SQLite target)
|
|
762
|
+
# Copy to .env and customize.
|
|
498
763
|
|
|
764
|
+
# --- Database ---
|
|
499
765
|
# Path to the SQLite database file. Defaults to ./<system_name>.db
|
|
500
766
|
SQLITE_PATH=./${toSnakeCase(system.name)}.db
|
|
501
767
|
|
|
768
|
+
# --- Required in production ---
|
|
769
|
+
# Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
|
770
|
+
JWT_SECRET=
|
|
771
|
+
|
|
772
|
+
# --- Server ---
|
|
502
773
|
PORT=3000
|
|
503
774
|
NODE_ENV=development
|
|
504
775
|
|
|
505
|
-
|
|
776
|
+
# --- CORS ---
|
|
777
|
+
# Comma-separated list of allowed origins. Leave empty to disallow cross-origin requests.
|
|
778
|
+
ALLOWED_ORIGINS=
|
|
779
|
+
|
|
780
|
+
# --- Event delivery ---
|
|
781
|
+
# in_process: in-memory bus, no durability — recommended for SQLite.
|
|
782
|
+
# durable mode (Postgres outbox + FOR UPDATE SKIP LOCKED) is not supported.
|
|
506
783
|
EVENT_MODE=in_process
|
|
784
|
+
|
|
785
|
+
# --- Request timeout ---
|
|
786
|
+
REQUEST_TIMEOUT_MS=30000
|
|
787
|
+
|
|
788
|
+
# --- Notifications ---
|
|
789
|
+
# NOTIFY_PROVIDER=log|resend|sendgrid|webhook (default: log)
|
|
790
|
+
NOTIFY_PROVIDER=log
|
|
791
|
+
NOTIFY_API_KEY=
|
|
792
|
+
NOTIFY_FROM_EMAIL=noreply@example.com
|
|
793
|
+
|
|
794
|
+
# --- Webhook delivery ---
|
|
795
|
+
NOTIFY_WEBHOOK_URL=
|
|
796
|
+
NOTIFY_WEBHOOK_SECRET=
|
|
507
797
|
`;
|
|
508
798
|
}
|
|
509
799
|
emitReadme(system) {
|
|
@@ -519,19 +809,53 @@ npm run migrate
|
|
|
519
809
|
npm run dev
|
|
520
810
|
\`\`\`
|
|
521
811
|
|
|
522
|
-
The database file will be created at \`./${toSnakeCase(system.name)}.db\` by
|
|
523
|
-
Override with the \`SQLITE_PATH\` environment variable.
|
|
812
|
+
The database file will be created at \`./${toSnakeCase(system.name)}.db\` by
|
|
813
|
+
default. Override with the \`SQLITE_PATH\` environment variable.
|
|
814
|
+
|
|
815
|
+
The server starts at http://localhost:3000 with all routes wired:
|
|
816
|
+
|
|
817
|
+
- CRUD endpoints for every entity
|
|
818
|
+
- Capability endpoints (\`POST /<entity>s/<capability-name>\`)
|
|
819
|
+
- Health checks at \`/health/live\`, \`/health/ready\`, \`/health/metrics\`
|
|
820
|
+
- Admin panel at \`/admin\`
|
|
524
821
|
|
|
525
822
|
## Why SQLite
|
|
526
823
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
824
|
+
Self-contained, single-file database. No external services to start, no
|
|
825
|
+
Docker, no Postgres. The whole database is one file you can copy, version,
|
|
826
|
+
or back up.
|
|
827
|
+
|
|
828
|
+
Ideal for: local development, demos, integration tests, single-node
|
|
829
|
+
deployments. For production multi-writer workloads, use the default Express
|
|
830
|
+
target (Postgres-backed).
|
|
831
|
+
|
|
832
|
+
## Auth
|
|
833
|
+
|
|
834
|
+
Send a Bearer token in the Authorization header:
|
|
835
|
+
|
|
836
|
+
\`\`\`
|
|
837
|
+
Authorization: Bearer <jwt-token>
|
|
838
|
+
\`\`\`
|
|
839
|
+
|
|
840
|
+
Generate a token by signing with the same \`JWT_SECRET\` from your \`.env\`.
|
|
841
|
+
|
|
842
|
+
## Differences from the Postgres target
|
|
843
|
+
|
|
844
|
+
- Schema uses \`TEXT\`/\`INTEGER\` instead of \`VARCHAR\`/\`BIGINT\`/\`JSONB\`
|
|
845
|
+
- The DB client (\`src/db.ts\`) translates Postgres-flavored SQL at runtime:
|
|
846
|
+
- \`$N\` placeholders → \`?\` (with param array reordering)
|
|
847
|
+
- \`NOW()\` → \`datetime('now')\`
|
|
848
|
+
- \`RETURNING *\` → re-select by id
|
|
849
|
+
- \`row_to_json(t.*)\` in LIST joins degrades to \`t.id\` only
|
|
850
|
+
- Durable event mode (\`EVENT_MODE=durable\`) is not supported on SQLite —
|
|
851
|
+
it requires Postgres-only \`FOR UPDATE SKIP LOCKED\`. Use \`in_process\`.
|
|
852
|
+
- Capability bodies that reference \`jsonb_set\`, \`jsonb_agg\`, or
|
|
853
|
+
\`jsonb_array_elements\` will fail at runtime. Refactor those capabilities
|
|
854
|
+
or compile to the Express target.
|
|
855
|
+
|
|
856
|
+
## Environment
|
|
531
857
|
|
|
532
|
-
|
|
533
|
-
single-node deployments. For production multi-writer workloads, use the
|
|
534
|
-
default Express target which generates a Postgres-backed project.
|
|
858
|
+
Copy \`.env.example\` to \`.env\` and configure.
|
|
535
859
|
`;
|
|
536
860
|
}
|
|
537
861
|
}
|