bonescript-compiler 0.5.3 → 0.5.4

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 (194) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +1 -2
  4. package/dist/cli.js +543 -75
  5. package/dist/cli.js.map +1 -1
  6. package/dist/emit_capability.d.ts +0 -13
  7. package/dist/emit_capability.js +134 -296
  8. package/dist/emit_capability.js.map +1 -1
  9. package/dist/emit_composition.js +3 -37
  10. package/dist/emit_composition.js.map +1 -1
  11. package/dist/emit_deploy.js +167 -165
  12. package/dist/emit_deploy.js.map +1 -1
  13. package/dist/emit_events.d.ts +0 -1
  14. package/dist/emit_events.js +275 -325
  15. package/dist/emit_events.js.map +1 -1
  16. package/dist/emit_extras.js +5 -3
  17. package/dist/emit_extras.js.map +1 -1
  18. package/dist/emit_full.js +112 -272
  19. package/dist/emit_full.js.map +1 -1
  20. package/dist/emit_maintenance.js +249 -249
  21. package/dist/emit_runtime.d.ts +11 -17
  22. package/dist/emit_runtime.js +688 -29
  23. package/dist/emit_runtime.js.map +1 -1
  24. package/dist/emit_sourcemap.js +66 -66
  25. package/dist/emit_tests.js +12 -47
  26. package/dist/emit_tests.js.map +1 -1
  27. package/dist/emit_websocket.js +3 -0
  28. package/dist/emit_websocket.js.map +1 -1
  29. package/dist/emitter.js +49 -94
  30. package/dist/emitter.js.map +1 -1
  31. package/dist/extension_manager.d.ts +2 -2
  32. package/dist/extension_manager.js +20 -9
  33. package/dist/extension_manager.js.map +1 -1
  34. package/dist/ir.d.ts +0 -4
  35. package/dist/lowering.d.ts +14 -5
  36. package/dist/lowering.js +417 -66
  37. package/dist/lowering.js.map +1 -1
  38. package/dist/module_loader.d.ts +2 -2
  39. package/dist/module_loader.js +23 -20
  40. package/dist/module_loader.js.map +1 -1
  41. package/dist/optimizer.js +3 -6
  42. package/dist/optimizer.js.map +1 -1
  43. package/dist/scaffold.d.ts +2 -2
  44. package/dist/scaffold.js +319 -315
  45. package/dist/scaffold.js.map +1 -1
  46. package/dist/solver.js +1 -1
  47. package/dist/solver.js.map +1 -1
  48. package/dist/source_map.js.map +1 -0
  49. package/dist/test.js.map +1 -0
  50. package/dist/test_typechecker.d.ts +5 -0
  51. package/dist/test_typechecker.js +126 -0
  52. package/dist/test_typechecker.js.map +1 -0
  53. package/dist/typechecker.d.ts +0 -7
  54. package/dist/typechecker.js +16 -103
  55. package/dist/typechecker.js.map +1 -1
  56. package/dist/verifier.d.ts +1 -5
  57. package/dist/verifier.js +38 -142
  58. package/dist/verifier.js.map +1 -1
  59. package/package.json +52 -62
  60. package/src/algorithm_catalog.ts +345 -345
  61. package/src/ast.d.ts +244 -0
  62. package/src/ast.ts +334 -334
  63. package/src/cli.ts +624 -98
  64. package/src/emit_batch.ts +140 -140
  65. package/src/emit_capability.ts +436 -613
  66. package/src/emit_composition.ts +196 -229
  67. package/src/emit_deploy.ts +190 -187
  68. package/src/emit_events.ts +307 -362
  69. package/src/emit_extras.ts +240 -237
  70. package/src/emit_full.ts +309 -472
  71. package/src/emit_maintenance.ts +459 -459
  72. package/src/emit_runtime.ts +730 -17
  73. package/src/emit_sourcemap.ts +140 -140
  74. package/src/emit_tests.ts +205 -243
  75. package/src/emit_websocket.ts +229 -226
  76. package/src/emitter.ts +578 -626
  77. package/src/extension_manager.ts +187 -177
  78. package/src/formatter.ts +297 -297
  79. package/src/index.ts +88 -88
  80. package/src/ir.ts +215 -216
  81. package/src/lexer.d.ts +195 -0
  82. package/src/lexer.ts +630 -630
  83. package/src/lowering.ts +556 -168
  84. package/src/module_loader.ts +114 -112
  85. package/src/optimizer.ts +196 -199
  86. package/src/parse_decls.d.ts +13 -0
  87. package/src/parse_decls.ts +409 -409
  88. package/src/parse_decls2.d.ts +13 -0
  89. package/src/parse_decls2.ts +244 -244
  90. package/src/parse_expr.d.ts +7 -0
  91. package/src/parse_expr.ts +197 -197
  92. package/src/parse_types.d.ts +6 -0
  93. package/src/parse_types.ts +54 -54
  94. package/src/parser.d.ts +10 -0
  95. package/src/parser.ts +1 -1
  96. package/src/parser_base.d.ts +19 -0
  97. package/src/parser_base.ts +57 -57
  98. package/src/parser_recovery.ts +153 -153
  99. package/src/scaffold.ts +375 -371
  100. package/src/solver.ts +330 -330
  101. package/src/typechecker.d.ts +52 -0
  102. package/src/typechecker.ts +591 -700
  103. package/src/types.d.ts +38 -0
  104. package/src/types.ts +122 -122
  105. package/src/verifier.ts +49 -154
  106. package/README.md +0 -382
  107. package/dist/commands/check.d.ts +0 -5
  108. package/dist/commands/check.js +0 -34
  109. package/dist/commands/check.js.map +0 -1
  110. package/dist/commands/compile.d.ts +0 -5
  111. package/dist/commands/compile.js +0 -215
  112. package/dist/commands/compile.js.map +0 -1
  113. package/dist/commands/debug.d.ts +0 -5
  114. package/dist/commands/debug.js +0 -59
  115. package/dist/commands/debug.js.map +0 -1
  116. package/dist/commands/diff.d.ts +0 -5
  117. package/dist/commands/diff.js +0 -123
  118. package/dist/commands/diff.js.map +0 -1
  119. package/dist/commands/fmt.d.ts +0 -5
  120. package/dist/commands/fmt.js +0 -49
  121. package/dist/commands/fmt.js.map +0 -1
  122. package/dist/commands/init.d.ts +0 -5
  123. package/dist/commands/init.js +0 -96
  124. package/dist/commands/init.js.map +0 -1
  125. package/dist/commands/ir.d.ts +0 -5
  126. package/dist/commands/ir.js +0 -27
  127. package/dist/commands/ir.js.map +0 -1
  128. package/dist/commands/lex.d.ts +0 -5
  129. package/dist/commands/lex.js +0 -21
  130. package/dist/commands/lex.js.map +0 -1
  131. package/dist/commands/parse.d.ts +0 -5
  132. package/dist/commands/parse.js +0 -30
  133. package/dist/commands/parse.js.map +0 -1
  134. package/dist/commands/test.d.ts +0 -5
  135. package/dist/commands/test.js +0 -61
  136. package/dist/commands/test.js.map +0 -1
  137. package/dist/commands/verify_determinism.d.ts +0 -5
  138. package/dist/commands/verify_determinism.js +0 -64
  139. package/dist/commands/verify_determinism.js.map +0 -1
  140. package/dist/commands/watch.d.ts +0 -5
  141. package/dist/commands/watch.js +0 -50
  142. package/dist/commands/watch.js.map +0 -1
  143. package/dist/emit_auth.d.ts +0 -18
  144. package/dist/emit_auth.js +0 -507
  145. package/dist/emit_auth.js.map +0 -1
  146. package/dist/emit_database.d.ts +0 -7
  147. package/dist/emit_database.js +0 -72
  148. package/dist/emit_database.js.map +0 -1
  149. package/dist/emit_index.d.ts +0 -6
  150. package/dist/emit_index.js +0 -202
  151. package/dist/emit_index.js.map +0 -1
  152. package/dist/emit_models.d.ts +0 -12
  153. package/dist/emit_models.js +0 -171
  154. package/dist/emit_models.js.map +0 -1
  155. package/dist/emit_openapi.d.ts +0 -9
  156. package/dist/emit_openapi.js +0 -306
  157. package/dist/emit_openapi.js.map +0 -1
  158. package/dist/emit_package.d.ts +0 -7
  159. package/dist/emit_package.js +0 -68
  160. package/dist/emit_package.js.map +0 -1
  161. package/dist/emit_router.d.ts +0 -12
  162. package/dist/emit_router.js +0 -389
  163. package/dist/emit_router.js.map +0 -1
  164. package/dist/lowering_channels.d.ts +0 -11
  165. package/dist/lowering_channels.js +0 -103
  166. package/dist/lowering_channels.js.map +0 -1
  167. package/dist/lowering_entities.d.ts +0 -11
  168. package/dist/lowering_entities.js +0 -232
  169. package/dist/lowering_entities.js.map +0 -1
  170. package/dist/lowering_helpers.d.ts +0 -13
  171. package/dist/lowering_helpers.js +0 -76
  172. package/dist/lowering_helpers.js.map +0 -1
  173. package/src/commands/check.ts +0 -33
  174. package/src/commands/compile.ts +0 -191
  175. package/src/commands/debug.ts +0 -33
  176. package/src/commands/diff.ts +0 -105
  177. package/src/commands/fmt.ts +0 -22
  178. package/src/commands/init.ts +0 -72
  179. package/src/commands/ir.ts +0 -23
  180. package/src/commands/lex.ts +0 -17
  181. package/src/commands/parse.ts +0 -24
  182. package/src/commands/test.ts +0 -36
  183. package/src/commands/verify_determinism.ts +0 -66
  184. package/src/commands/watch.ts +0 -25
  185. package/src/emit_auth.ts +0 -513
  186. package/src/emit_database.ts +0 -72
  187. package/src/emit_index.ts +0 -210
  188. package/src/emit_models.ts +0 -176
  189. package/src/emit_openapi.ts +0 -315
  190. package/src/emit_package.ts +0 -66
  191. package/src/emit_router.ts +0 -408
  192. package/src/lowering_channels.ts +0 -108
  193. package/src/lowering_entities.ts +0 -258
  194. package/src/lowering_helpers.ts +0 -75
package/src/emit_index.ts DELETED
@@ -1,210 +0,0 @@
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
- }
@@ -1,176 +0,0 @@
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
- }