bonescript-compiler 0.3.0 → 0.5.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.
Files changed (71) hide show
  1. package/dist/commands/compile.js +42 -10
  2. package/dist/commands/compile.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -1
  4. package/dist/commands/init.js +29 -2
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/emit_auth.d.ts +14 -2
  7. package/dist/emit_auth.js +498 -60
  8. package/dist/emit_auth.js.map +1 -1
  9. package/dist/emit_capability.js +61 -7
  10. package/dist/emit_capability.js.map +1 -1
  11. package/dist/emit_composition.js +37 -3
  12. package/dist/emit_composition.js.map +1 -1
  13. package/dist/emit_events.d.ts +1 -0
  14. package/dist/emit_events.js +68 -1
  15. package/dist/emit_events.js.map +1 -1
  16. package/dist/emit_full.js +166 -11
  17. package/dist/emit_full.js.map +1 -1
  18. package/dist/emit_index.js +46 -1
  19. package/dist/emit_index.js.map +1 -1
  20. package/dist/emit_models.d.ts +12 -0
  21. package/dist/emit_models.js +171 -0
  22. package/dist/emit_models.js.map +1 -0
  23. package/dist/emit_openapi.d.ts +9 -0
  24. package/dist/emit_openapi.js +308 -0
  25. package/dist/emit_openapi.js.map +1 -0
  26. package/dist/emit_router.js +19 -4
  27. package/dist/emit_router.js.map +1 -1
  28. package/dist/emit_tests.js +37 -0
  29. package/dist/emit_tests.js.map +1 -1
  30. package/dist/emitter.js +81 -5
  31. package/dist/emitter.js.map +1 -1
  32. package/dist/ir.d.ts +4 -0
  33. package/dist/lowering.js +16 -1
  34. package/dist/lowering.js.map +1 -1
  35. package/dist/lowering_channels.d.ts +1 -1
  36. package/dist/lowering_channels.js +3 -2
  37. package/dist/lowering_channels.js.map +1 -1
  38. package/dist/lowering_entities.js +11 -1
  39. package/dist/lowering_entities.js.map +1 -1
  40. package/dist/optimizer.js +1 -1
  41. package/dist/optimizer.js.map +1 -1
  42. package/dist/scaffold.js +0 -1
  43. package/dist/scaffold.js.map +1 -1
  44. package/dist/typechecker.d.ts +5 -0
  45. package/dist/typechecker.js +68 -13
  46. package/dist/typechecker.js.map +1 -1
  47. package/dist/verifier.d.ts +5 -0
  48. package/dist/verifier.js +140 -2
  49. package/dist/verifier.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/commands/compile.ts +41 -10
  52. package/src/commands/init.ts +28 -2
  53. package/src/emit_auth.ts +513 -67
  54. package/src/emit_capability.ts +61 -6
  55. package/src/emit_composition.ts +36 -3
  56. package/src/emit_events.ts +70 -0
  57. package/src/emit_full.ts +172 -13
  58. package/src/emit_index.ts +210 -161
  59. package/src/emit_models.ts +176 -0
  60. package/src/emit_openapi.ts +318 -0
  61. package/src/emit_router.ts +18 -4
  62. package/src/emit_tests.ts +41 -0
  63. package/src/emitter.ts +81 -5
  64. package/src/ir.ts +1 -0
  65. package/src/lowering.ts +19 -1
  66. package/src/lowering_channels.ts +3 -2
  67. package/src/lowering_entities.ts +258 -248
  68. package/src/optimizer.ts +1 -1
  69. package/src/scaffold.ts +0 -1
  70. package/src/typechecker.ts +81 -15
  71. package/src/verifier.ts +495 -348
package/src/emit_index.ts CHANGED
@@ -1,161 +1,210 @@
1
- /**
2
- * BoneScript Server Entry Point Emitter
3
- * Generates src/index.ts — the Express server with all middleware and routes wired.
4
- */
5
-
6
- import * as IR from "./ir";
7
- import { toSnakeCase, toCamelCase } from "./emit_router";
8
-
9
- export function emitIndex(system: IR.IRSystem): string {
10
- const apiModules = system.modules.filter(m => m.kind === "api_service");
11
- const hasWebSocket = system.modules.some(m => m.kind === "realtime_service");
12
- const hasBatch = system.modules.some(m =>
13
- m.interfaces.some(i => i.methods.some(mth => mth.sync === "batch"))
14
- );
15
- const lines: string[] = [];
16
-
17
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
18
- lines.push(`// System: ${system.name}`);
19
- lines.push(`require("dotenv").config();`);
20
- lines.push(`import express from "express";`);
21
- lines.push(`import { createServer } from "http";`);
22
- lines.push(`import cors from "cors";`);
23
- lines.push(`import helmet from "helmet";`);
24
- lines.push(`import rateLimit from "express-rate-limit";`);
25
- lines.push(`import { authMiddleware } from "./auth";`);
26
- lines.push(`import { healthRouter } from "./health";`);
27
- lines.push(`import { logger } from "./logger";`);
28
- lines.push(`import { eventBus } from "./events";`);
29
- lines.push(`import { pool } from "./db";`);
30
- if (hasBatch) lines.push(`import { startBatchWorker } from "./batch";`);
31
- if (hasWebSocket) lines.push(`import { setupWebSocketServer } from "./websocket";`);
32
- lines.push(``);
33
-
34
- for (const mod of apiModules) {
35
- const model = mod.models[0];
36
- if (!model) continue;
37
- const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
38
- lines.push(`import { ${routerName} } from "./routes/${toSnakeCase(model.name)}";`);
39
- }
40
-
41
- lines.push(``);
42
- lines.push(`const app = express();`);
43
- lines.push(`const httpServer = createServer(app);`);
44
- lines.push(`const PORT = parseInt(process.env.PORT || "3000");`);
45
- lines.push(``);
46
-
47
- // Middleware
48
- lines.push(`// Middleware`);
49
- lines.push(`app.use(helmet());`);
50
- lines.push(`// CORS: restrict to ALLOWED_ORIGINS env var (comma-separated). Defaults to same-origin only.`);
51
- lines.push(`const __allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim()).filter(Boolean);`);
52
- lines.push(`app.use(cors({`);
53
- lines.push(` origin: __allowedOrigins.length > 0`);
54
- lines.push(` ? (origin, cb) => { if (!origin || __allowedOrigins.includes(origin)) cb(null, true); else cb(new Error("Not allowed by CORS")); }`);
55
- lines.push(` : false,`);
56
- lines.push(` credentials: true,`);
57
- lines.push(`}));`);
58
- lines.push(`app.use(express.json({ limit: "1mb" }));`);
59
- lines.push(`app.use(express.urlencoded({ extended: false, limit: "1mb" }));`);
60
- lines.push(`app.use(authMiddleware);`);
61
- lines.push(``);
62
-
63
- // Rate limiting
64
- const gw = system.modules.find(m => m.kind === "gateway");
65
- const rateVal = gw?.config["rate_limit"] || 300;
66
- lines.push(`// Global rate limit (default ${rateVal} req/min, override with RATE_LIMIT_MAX env var)`);
67
- lines.push(`const __globalRateLimit = rateLimit({`);
68
- lines.push(` windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000"),`);
69
- lines.push(` max: parseInt(process.env.RATE_LIMIT_MAX || "${rateVal}"),`);
70
- lines.push(` standardHeaders: true,`);
71
- lines.push(` legacyHeaders: false,`);
72
- lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many requests, please slow down." } },`);
73
- lines.push(`});`);
74
- lines.push(`app.use(__globalRateLimit);`);
75
- lines.push(``);
76
- lines.push(`// Strict rate limit on auth endpoints (20 req/min per IP)`);
77
- lines.push(`const __authRateLimit = rateLimit({`);
78
- lines.push(` windowMs: 60000,`);
79
- lines.push(` max: parseInt(process.env.AUTH_RATE_LIMIT_MAX || "20"),`);
80
- lines.push(` standardHeaders: true,`);
81
- lines.push(` legacyHeaders: false,`);
82
- lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many auth attempts." } },`);
83
- lines.push(`});`);
84
- lines.push(``);
85
-
86
- // Request timeout
87
- lines.push(`// Request timeout (default 30s, override per-route)`);
88
- lines.push(`app.use((req: any, res: any, next: any) => {`);
89
- lines.push(` const timeout = parseInt(process.env.REQUEST_TIMEOUT_MS || "30000");`);
90
- lines.push(` const timer = setTimeout(() => {`);
91
- lines.push(` if (!res.headersSent) {`);
92
- lines.push(` res.status(503).json({ error: { code: "REQUEST_TIMEOUT", message: "Request timed out" } });`);
93
- lines.push(` }`);
94
- lines.push(` }, timeout);`);
95
- lines.push(` res.on("finish", () => clearTimeout(timer));`);
96
- lines.push(` next();`);
97
- lines.push(`});`);
98
- lines.push(``);
99
-
100
- // Routes
101
- lines.push(`// Health & metrics`);
102
- lines.push(`app.use("/health", healthRouter);`);
103
- lines.push(``);
104
- lines.push(`// Routes`);
105
- for (const mod of apiModules) {
106
- const model = mod.models[0];
107
- if (!model) continue;
108
- const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
109
- const routePath = `/${toSnakeCase(model.name)}s`;
110
- lines.push(`app.use("${routePath}", ${routerName});`);
111
- lines.push(`app.use("${routePath}/login", __authRateLimit);`);
112
- lines.push(`app.use("${routePath}/register", __authRateLimit);`);
113
- }
114
- lines.push(``);
115
-
116
- if (hasWebSocket) {
117
- lines.push(`// WebSocket`);
118
- lines.push(`setupWebSocketServer(httpServer);`);
119
- lines.push(``);
120
- }
121
-
122
- // Startup
123
- lines.push(`// Start`);
124
- lines.push(`httpServer.listen(PORT, () => {`);
125
- lines.push(` logger.info("server_started", { event: "startup", metadata: { port: PORT } });`);
126
- lines.push(` eventBus.startWorker(parseInt(process.env.EVENT_WORKER_INTERVAL_MS || "1000"));`);
127
- if (hasBatch) lines.push(` startBatchWorker();`);
128
- lines.push(` console.log(\`[${system.name}] Running on port \${PORT}\`);`);
129
- lines.push(` console.log(\` HTTP routes:\`);`);
130
- for (const mod of apiModules) {
131
- const model = mod.models[0];
132
- if (!model) continue;
133
- lines.push(` console.log(\` /${toSnakeCase(model.name)}s\`);`);
134
- }
135
- if (hasWebSocket) lines.push(` console.log(\` WebSocket: /ws?channel=<name>&token=<jwt>\`);`);
136
- lines.push(` console.log(\` Health: /health/live, /health/ready, /health/metrics\`);`);
137
- lines.push(`});`);
138
- lines.push(``);
139
-
140
- // Graceful shutdown
141
- lines.push(`// Graceful shutdown`);
142
- lines.push(`const shutdown = async (signal: string) => {`);
143
- lines.push(` logger.info("server_stopping", { event: "shutdown", metadata: { signal } });`);
144
- lines.push(` httpServer.close(async () => {`);
145
- lines.push(` try {`);
146
- lines.push(` await pool.end();`);
147
- lines.push(` logger.info("server_stopped", { event: "shutdown", status: "success" });`);
148
- lines.push(` } catch (e: any) {`);
149
- lines.push(` logger.error("shutdown_error", { event: "shutdown", metadata: { error: e.message } });`);
150
- lines.push(` }`);
151
- lines.push(` process.exit(0);`);
152
- lines.push(` });`);
153
- lines.push(` setTimeout(() => { logger.error("shutdown_timeout", { event: "shutdown" }); process.exit(1); }, 10000);`);
154
- lines.push(`};`);
155
- lines.push(`process.on("SIGTERM", () => shutdown("SIGTERM"));`);
156
- lines.push(`process.on("SIGINT", () => shutdown("SIGINT"));`);
157
- lines.push(``);
158
- lines.push(`export { app, httpServer };`);
159
-
160
- return lines.join("\n");
161
- }
1
+ /**
2
+ * BoneScript Server Entry Point Emitter
3
+ * Generates src/index.ts — the Express server with all middleware and routes wired.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+ import { toSnakeCase, toCamelCase } from "./emit_router";
8
+
9
+ export function emitIndex(system: IR.IRSystem): string {
10
+ const apiModules = system.modules.filter(m => m.kind === "api_service");
11
+ const hasWebSocket = system.modules.some(m => m.kind === "realtime_service");
12
+ const hasBatch = system.modules.some(m =>
13
+ m.interfaces.some(i => i.methods.some(mth => mth.sync === "batch"))
14
+ );
15
+
16
+ // Collect policy settings from the gateway config (populated by lowering from PolicyDecl)
17
+ const gw = system.modules.find(m => m.kind === "gateway");
18
+ const encryptionMode = (gw?.config["encryption"] as string | undefined) || "none";
19
+ const enforceHttps = encryptionMode === "in_transit" || encryptionMode === "both";
20
+
21
+ const lines: string[] = [];
22
+
23
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
24
+ lines.push(`// System: ${system.name}`);
25
+ lines.push(`require("dotenv").config();`);
26
+ lines.push(`import express from "express";`);
27
+ lines.push(`import { createServer } from "http";`);
28
+ lines.push(`import cors from "cors";`);
29
+ lines.push(`import helmet from "helmet";`);
30
+ lines.push(`import rateLimit from "express-rate-limit";`);
31
+ lines.push(`import { authMiddleware } from "./auth";`);
32
+ lines.push(`import { healthRouter } from "./health";`);
33
+ lines.push(`import { logger } from "./logger";`);
34
+ lines.push(`import { eventBus } from "./events";`);
35
+ lines.push(`import { pool } from "./db";`);
36
+ if (hasBatch) lines.push(`import { startBatchWorker } from "./batch";`);
37
+ if (hasWebSocket) lines.push(`import { setupWebSocketServer } from "./websocket";`);
38
+
39
+ // Auth router import — must be at top-level with other imports
40
+ const authMethod = (() => {
41
+ const direct = system.resolution["implied.auth_method"] || system.resolution["system.auth_method"];
42
+ if (direct === "oauth2" || direct === "apikey" || direct === "jwt") return direct;
43
+ for (const [key, val] of Object.entries(system.resolution)) {
44
+ if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) return val;
45
+ }
46
+ for (const mod of system.modules) {
47
+ const m = mod.config["auth_method"] as string | undefined;
48
+ if (m === "oauth2" || m === "apikey" || m === "jwt") return m;
49
+ }
50
+ return "jwt";
51
+ })();
52
+ if (authMethod === "oauth2" || authMethod === "apikey") {
53
+ lines.push(`import { authRouter } from "./auth";`);
54
+ }
55
+ lines.push(``);
56
+
57
+ for (const mod of apiModules) {
58
+ const model = mod.models[0];
59
+ if (!model) continue;
60
+ const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
61
+ lines.push(`import { ${routerName} } from "./routes/${toSnakeCase(model.name)}";`);
62
+ }
63
+
64
+ lines.push(``);
65
+ lines.push(`const app = express();`);
66
+ lines.push(`const httpServer = createServer(app);`);
67
+ lines.push(`const PORT = parseInt(process.env.PORT || "3000");`);
68
+ lines.push(``);
69
+
70
+ // Middleware
71
+ lines.push(`// Middleware`);
72
+ lines.push(`app.use(helmet());`);
73
+
74
+ // HTTPS enforcement (in_transit or both encryption mode)
75
+ if (enforceHttps) {
76
+ lines.push(`// Encryption policy: in_transit redirect HTTP to HTTPS in production`);
77
+ lines.push(`app.use((req: any, res: any, next: any) => {`);
78
+ lines.push(` if (process.env.NODE_ENV === "production" && req.headers["x-forwarded-proto"] !== "https") {`);
79
+ lines.push(` return res.redirect(301, \`https://\${req.headers.host}\${req.url}\`);`);
80
+ lines.push(` }`);
81
+ lines.push(` next();`);
82
+ lines.push(`});`);
83
+ }
84
+
85
+ // At-rest encryption warning (at_rest or both)
86
+ if (encryptionMode === "at_rest" || encryptionMode === "both") {
87
+ lines.push(`// Encryption policy: at_rest ensure DATABASE_URL uses SSL and disk encryption is enabled`);
88
+ lines.push(`if (process.env.NODE_ENV === "production" && process.env.DATABASE_URL && !process.env.DATABASE_URL.includes("sslmode=require")) {`);
89
+ lines.push(` console.warn("[WARN] Encryption policy requires at-rest encryption. Add ?sslmode=require to DATABASE_URL.");`);
90
+ lines.push(`}`);
91
+ }
92
+ lines.push(`// CORS: restrict to ALLOWED_ORIGINS env var (comma-separated). Defaults to same-origin only.`);
93
+ lines.push(`const __allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(",").map(s => s.trim()).filter(Boolean);`);
94
+ lines.push(`app.use(cors({`);
95
+ lines.push(` origin: __allowedOrigins.length > 0`);
96
+ lines.push(` ? (origin, cb) => { if (!origin || __allowedOrigins.includes(origin)) cb(null, true); else cb(new Error("Not allowed by CORS")); }`);
97
+ lines.push(` : false,`);
98
+ lines.push(` credentials: true,`);
99
+ lines.push(`}));`);
100
+ lines.push(`app.use(express.json({ limit: "1mb" }));`);
101
+ lines.push(`app.use(express.urlencoded({ extended: false, limit: "1mb" }));`);
102
+ lines.push(`app.use(authMiddleware);`);
103
+ lines.push(``);
104
+
105
+ // Rate limiting
106
+ const rateVal = gw?.config["rate_limit"] || 300;
107
+ lines.push(`// Global rate limit (default ${rateVal} req/min, override with RATE_LIMIT_MAX env var)`);
108
+ lines.push(`const __globalRateLimit = rateLimit({`);
109
+ lines.push(` windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || "60000"),`);
110
+ lines.push(` max: parseInt(process.env.RATE_LIMIT_MAX || "${rateVal}"),`);
111
+ lines.push(` standardHeaders: true,`);
112
+ lines.push(` legacyHeaders: false,`);
113
+ lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many requests, please slow down." } },`);
114
+ lines.push(`});`);
115
+ lines.push(`app.use(__globalRateLimit);`);
116
+ lines.push(``);
117
+ lines.push(`// Strict rate limit on auth endpoints (20 req/min per IP)`);
118
+ lines.push(`const __authRateLimit = rateLimit({`);
119
+ lines.push(` windowMs: 60000,`);
120
+ lines.push(` max: parseInt(process.env.AUTH_RATE_LIMIT_MAX || "20"),`);
121
+ lines.push(` standardHeaders: true,`);
122
+ lines.push(` legacyHeaders: false,`);
123
+ lines.push(` message: { error: { code: "RATE_LIMITED", message: "Too many auth attempts." } },`);
124
+ lines.push(`});`);
125
+ lines.push(``);
126
+
127
+ // Request timeout
128
+ lines.push(`// Request timeout (default 30s, override per-route)`);
129
+ lines.push(`app.use((req: any, res: any, next: any) => {`);
130
+ lines.push(` const timeout = parseInt(process.env.REQUEST_TIMEOUT_MS || "30000");`);
131
+ lines.push(` const timer = setTimeout(() => {`);
132
+ lines.push(` if (!res.headersSent) {`);
133
+ lines.push(` res.status(503).json({ error: { code: "REQUEST_TIMEOUT", message: "Request timed out" } });`);
134
+ lines.push(` }`);
135
+ lines.push(` }, timeout);`);
136
+ lines.push(` res.on("finish", () => clearTimeout(timer));`);
137
+ lines.push(` next();`);
138
+ lines.push(`});`);
139
+ lines.push(``);
140
+
141
+ // Routes
142
+ lines.push(`// Health & metrics`);
143
+ lines.push(`app.use("/health", healthRouter);`);
144
+ lines.push(``);
145
+
146
+ // Auth routes for OAuth2 and API key strategies
147
+ if (authMethod === "oauth2" || authMethod === "apikey") {
148
+ lines.push(`// Auth routes (${authMethod})`);
149
+ lines.push(`app.use("/auth", __authRateLimit, authRouter);`);
150
+ lines.push(``);
151
+ }
152
+
153
+ lines.push(`// Routes`);
154
+ for (const mod of apiModules) {
155
+ const model = mod.models[0];
156
+ if (!model) continue;
157
+ const routerName = toCamelCase(toSnakeCase(model.name) + "s") + "Router";
158
+ const routePath = `/${toSnakeCase(model.name)}s`;
159
+ lines.push(`app.use("${routePath}", ${routerName});`);
160
+ lines.push(`app.use("${routePath}/login", __authRateLimit);`);
161
+ lines.push(`app.use("${routePath}/register", __authRateLimit);`);
162
+ }
163
+ lines.push(``);
164
+
165
+ if (hasWebSocket) {
166
+ lines.push(`// WebSocket`);
167
+ lines.push(`setupWebSocketServer(httpServer);`);
168
+ lines.push(``);
169
+ }
170
+
171
+ // Startup
172
+ lines.push(`// Start`);
173
+ lines.push(`httpServer.listen(PORT, () => {`);
174
+ lines.push(` logger.info("server_started", { event: "startup", metadata: { port: PORT } });`);
175
+ lines.push(` eventBus.startWorker(parseInt(process.env.EVENT_WORKER_INTERVAL_MS || "1000"));`);
176
+ if (hasBatch) lines.push(` startBatchWorker();`);
177
+ lines.push(` console.log(\`[${system.name}] Running on port \${PORT}\`);`);
178
+ lines.push(` console.log(\` HTTP routes:\`);`);
179
+ for (const mod of apiModules) {
180
+ const model = mod.models[0];
181
+ if (!model) continue;
182
+ lines.push(` console.log(\` /${toSnakeCase(model.name)}s\`);`);
183
+ }
184
+ if (hasWebSocket) lines.push(` console.log(\` WebSocket: /ws?channel=<name>&token=<jwt>\`);`);
185
+ lines.push(` console.log(\` Health: /health/live, /health/ready, /health/metrics\`);`);
186
+ lines.push(`});`);
187
+ lines.push(``);
188
+
189
+ // Graceful shutdown
190
+ lines.push(`// Graceful shutdown`);
191
+ lines.push(`const shutdown = async (signal: string) => {`);
192
+ lines.push(` logger.info("server_stopping", { event: "shutdown", metadata: { signal } });`);
193
+ lines.push(` httpServer.close(async () => {`);
194
+ lines.push(` try {`);
195
+ lines.push(` await pool.end();`);
196
+ lines.push(` logger.info("server_stopped", { event: "shutdown", status: "success" });`);
197
+ lines.push(` } catch (e: any) {`);
198
+ lines.push(` logger.error("shutdown_error", { event: "shutdown", metadata: { error: e.message } });`);
199
+ lines.push(` }`);
200
+ lines.push(` process.exit(0);`);
201
+ lines.push(` });`);
202
+ lines.push(` setTimeout(() => { logger.error("shutdown_timeout", { event: "shutdown" }); process.exit(1); }, 10000);`);
203
+ lines.push(`};`);
204
+ lines.push(`process.on("SIGTERM", () => shutdown("SIGTERM"));`);
205
+ lines.push(`process.on("SIGINT", () => shutdown("SIGINT"));`);
206
+ lines.push(``);
207
+ lines.push(`export { app, httpServer };`);
208
+
209
+ return lines.join("\n");
210
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * BoneScript Model Emitter
3
+ * Generates per-model TypeScript artifacts as specified in spec/09_CODEGEN.md §5.1:
4
+ * - XxxInterface — typed interface
5
+ * - XxxSchema — const with field metadata (type, nullable, unique, indexed)
6
+ * - validateXxx() — runtime validation function derived from IR constraints
7
+ *
8
+ * These are emitted to src/models/<entity_name>.ts
9
+ */
10
+
11
+ import * as IR from "./ir";
12
+ import { toTsType, toSnakeCase } from "./emit_router";
13
+
14
+ // ─── Per-model file ───────────────────────────────────────────────────────────
15
+
16
+ export function emitModelFile(model: IR.IRModel, mod: IR.IRModule, system: IR.IRSystem): string {
17
+ const lines: string[] = [];
18
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
19
+ lines.push(`// Source hash: ${system.source_hash}`);
20
+ lines.push(`// Module: ${mod.name}`);
21
+ lines.push(``);
22
+
23
+ // ── Interface ──────────────────────────────────────────────────────────────
24
+ lines.push(`export interface ${model.name} {`);
25
+ for (const field of model.fields) {
26
+ // Skip generated-always fields (derived columns)
27
+ if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
28
+ const nullable = field.nullable ? " | null" : "";
29
+ lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
30
+ }
31
+ lines.push(`}`);
32
+ lines.push(``);
33
+
34
+ // ── Schema const ───────────────────────────────────────────────────────────
35
+ lines.push(`export const ${model.name}Schema = {`);
36
+ for (const field of model.fields) {
37
+ if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
38
+ const meta: string[] = [
39
+ `type: ${JSON.stringify(field.type)}`,
40
+ `nullable: ${field.nullable}`,
41
+ `unique: ${field.unique}`,
42
+ `indexed: ${field.indexed}`,
43
+ ];
44
+ if (field.default_value && !field.default_value.startsWith("GENERATED")) {
45
+ meta.push(`default: ${JSON.stringify(field.default_value)}`);
46
+ }
47
+ lines.push(` ${field.name}: { ${meta.join(", ")} },`);
48
+ }
49
+ lines.push(`} as const;`);
50
+ lines.push(``);
51
+
52
+ // ── Input type (omits server-set fields) ──────────────────────────────────
53
+ const inputFields = model.fields.filter(f =>
54
+ !["id", "created_at", "updated_at"].includes(f.name) &&
55
+ !f.default_value?.startsWith("GENERATED ALWAYS")
56
+ );
57
+ lines.push(`export interface Create${model.name}Input {`);
58
+ for (const field of inputFields) {
59
+ const nullable = field.nullable ? "?" : "";
60
+ lines.push(` ${field.name}${nullable}: ${toTsType(field.type)};`);
61
+ }
62
+ lines.push(`}`);
63
+ lines.push(``);
64
+
65
+ lines.push(`export interface Update${model.name}Input {`);
66
+ for (const field of inputFields) {
67
+ lines.push(` ${field.name}?: ${toTsType(field.type)};`);
68
+ }
69
+ lines.push(`}`);
70
+ lines.push(``);
71
+
72
+ // ── Validation function ────────────────────────────────────────────────────
73
+ lines.push(`export interface ValidationError {`);
74
+ lines.push(` field: string;`);
75
+ lines.push(` code: string;`);
76
+ lines.push(` message: string;`);
77
+ lines.push(`}`);
78
+ lines.push(``);
79
+ lines.push(`export function validate${model.name}(input: unknown): { ok: true; value: ${model.name} } | { ok: false; errors: ValidationError[] } {`);
80
+ lines.push(` const errors: ValidationError[] = [];`);
81
+ lines.push(` const data = input as Record<string, unknown>;`);
82
+ lines.push(``);
83
+
84
+ // Required field presence checks
85
+ for (const field of model.fields) {
86
+ if (field.nullable || field.default_value || ["id", "created_at", "updated_at"].includes(field.name)) continue;
87
+ if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
88
+ lines.push(` if (data.${field.name} === undefined || data.${field.name} === null) {`);
89
+ lines.push(` errors.push({ field: "${field.name}", code: "REQUIRED", message: "${field.name} is required" });`);
90
+ lines.push(` }`);
91
+ }
92
+
93
+ // Type checks for primitive fields
94
+ for (const field of model.fields) {
95
+ if (["id", "created_at", "updated_at"].includes(field.name)) continue;
96
+ if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
97
+ const check = typeCheckExpr(field.type, `data.${field.name}`);
98
+ if (check) {
99
+ lines.push(` if (data.${field.name} !== undefined && data.${field.name} !== null && !(${check})) {`);
100
+ lines.push(` errors.push({ field: "${field.name}", code: "INVALID_TYPE", message: "${field.name} must be ${field.type}" });`);
101
+ lines.push(` }`);
102
+ }
103
+ }
104
+
105
+ // Constraint-derived checks from model constraints
106
+ for (const c of model.constraints) {
107
+ if (c.kind === "unique") continue; // enforced by DB
108
+ if (c.kind === "range" && c.params["min"] !== undefined && c.params["max"] !== undefined) {
109
+ lines.push(` if (typeof data.${c.target} === "number" && (data.${c.target} < ${c.params["min"]} || data.${c.target} > ${c.params["max"]})) {`);
110
+ lines.push(` errors.push({ field: "${c.target}", code: "OUT_OF_RANGE", message: "${c.target} must be between ${c.params["min"]} and ${c.params["max"]}" });`);
111
+ lines.push(` }`);
112
+ }
113
+ if (c.kind === "enum" && Array.isArray(c.params["values"])) {
114
+ const vals = (c.params["values"] as string[]).map(v => JSON.stringify(v)).join(", ");
115
+ lines.push(` if (data.${c.target} !== undefined && ![${vals}].includes(data.${c.target} as string)) {`);
116
+ lines.push(` errors.push({ field: "${c.target}", code: "INVALID_ENUM", message: "${c.target} must be one of: ${(c.params["values"] as string[]).join(", ")}" });`);
117
+ lines.push(` }`);
118
+ }
119
+ if (c.kind === "check" && typeof c.params["expression"] === "string") {
120
+ // Parse length-in-range constraints like "username.length in 3..32"
121
+ const lenMatch = (c.params["expression"] as string).match(/^(\w+)\.length\s+in\s+(\d+)\.\.(\d+)$/);
122
+ if (lenMatch) {
123
+ const [, fieldName, minLen, maxLen] = lenMatch;
124
+ lines.push(` if (typeof data.${fieldName} === "string" && (data.${fieldName}.length < ${minLen} || data.${fieldName}.length > ${maxLen})) {`);
125
+ lines.push(` errors.push({ field: "${fieldName}", code: "INVALID_LENGTH", message: "${fieldName} must be between ${minLen} and ${maxLen} characters" });`);
126
+ lines.push(` }`);
127
+ }
128
+ // Parse non-negative constraints like "score >= 0"
129
+ const nonNegMatch = (c.params["expression"] as string).match(/^(\w+)\s*>=\s*0$/);
130
+ if (nonNegMatch) {
131
+ const [, fieldName] = nonNegMatch;
132
+ lines.push(` if (typeof data.${fieldName} === "number" && data.${fieldName} < 0) {`);
133
+ lines.push(` errors.push({ field: "${fieldName}", code: "OUT_OF_RANGE", message: "${fieldName} must be >= 0" });`);
134
+ lines.push(` }`);
135
+ }
136
+ }
137
+ }
138
+
139
+ lines.push(``);
140
+ lines.push(` if (errors.length > 0) return { ok: false, errors };`);
141
+ lines.push(` return { ok: true, value: data as unknown as ${model.name} };`);
142
+ lines.push(`}`);
143
+ lines.push(``);
144
+
145
+ return lines.join("\n");
146
+ }
147
+
148
+ function typeCheckExpr(irType: string, expr: string): string | null {
149
+ switch (irType) {
150
+ case "string": return `typeof ${expr} === "string"`;
151
+ case "uint": return `typeof ${expr} === "number" && Number.isInteger(${expr}) && ${expr} >= 0`;
152
+ case "int": return `typeof ${expr} === "number" && Number.isInteger(${expr})`;
153
+ case "float": return `typeof ${expr} === "number"`;
154
+ case "bool": return `typeof ${expr} === "boolean"`;
155
+ case "uuid": return `typeof ${expr} === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${expr})`;
156
+ case "timestamp": return `${expr} instanceof Date || typeof ${expr} === "string"`;
157
+ default:
158
+ if (irType.startsWith("list<") || irType.startsWith("set<")) return `Array.isArray(${expr})`;
159
+ return null;
160
+ }
161
+ }
162
+
163
+ // ─── Barrel index for all models ─────────────────────────────────────────────
164
+
165
+ export function emitModelsIndex(system: IR.IRSystem): string {
166
+ const lines: string[] = [];
167
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
168
+ lines.push(`// Re-exports all model interfaces, schemas, and validators.`);
169
+ lines.push(``);
170
+ for (const mod of system.modules) {
171
+ for (const model of mod.models) {
172
+ lines.push(`export * from "./${toSnakeCase(model.name)}";`);
173
+ }
174
+ }
175
+ return lines.join("\n");
176
+ }