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