bonescript-compiler 0.2.1 → 0.3.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/LICENSE +21 -21
- package/dist/algorithm_catalog.js +166 -166
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +75 -543
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.js +34 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/compile.d.ts +5 -0
- package/dist/commands/compile.js +183 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/debug.d.ts +5 -0
- package/dist/commands/debug.js +59 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.js +125 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fmt.d.ts +5 -0
- package/dist/commands/fmt.js +49 -0
- package/dist/commands/fmt.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +69 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ir.d.ts +5 -0
- package/dist/commands/ir.js +27 -0
- package/dist/commands/ir.js.map +1 -0
- package/dist/commands/lex.d.ts +5 -0
- package/dist/commands/lex.js +21 -0
- package/dist/commands/lex.js.map +1 -0
- package/dist/commands/parse.d.ts +5 -0
- package/dist/commands/parse.js +30 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +61 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/verify_determinism.d.ts +5 -0
- package/dist/commands/verify_determinism.js +64 -0
- package/dist/commands/verify_determinism.js.map +1 -0
- package/dist/commands/watch.d.ts +5 -0
- package/dist/commands/watch.js +50 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/emit_auth.d.ts +6 -0
- package/dist/emit_auth.js +69 -0
- package/dist/emit_auth.js.map +1 -0
- package/dist/emit_capability.d.ts +13 -0
- package/dist/emit_capability.js +235 -125
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_database.d.ts +7 -0
- package/dist/emit_database.js +74 -0
- package/dist/emit_database.js.map +1 -0
- package/dist/emit_deploy.js +162 -162
- package/dist/emit_events.js +274 -274
- package/dist/emit_full.js +102 -95
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.d.ts +6 -0
- package/dist/emit_index.js +157 -0
- package/dist/emit_index.js.map +1 -0
- package/dist/emit_maintenance.js +249 -249
- package/dist/emit_package.d.ts +7 -0
- package/dist/emit_package.js +70 -0
- package/dist/emit_package.js.map +1 -0
- package/dist/emit_router.d.ts +12 -0
- package/dist/emit_router.js +375 -0
- package/dist/emit_router.js.map +1 -0
- package/dist/emit_runtime.d.ts +17 -11
- package/dist/emit_runtime.js +29 -686
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_sourcemap.js +66 -66
- package/dist/extension_manager.d.ts +2 -2
- package/dist/extension_manager.js +6 -3
- package/dist/extension_manager.js.map +1 -1
- package/dist/lowering.d.ts +5 -14
- package/dist/lowering.js +32 -417
- package/dist/lowering.js.map +1 -1
- package/dist/lowering_channels.d.ts +11 -0
- package/dist/lowering_channels.js +102 -0
- package/dist/lowering_channels.js.map +1 -0
- package/dist/lowering_entities.d.ts +11 -0
- package/dist/lowering_entities.js +222 -0
- package/dist/lowering_entities.js.map +1 -0
- package/dist/lowering_helpers.d.ts +13 -0
- package/dist/lowering_helpers.js +76 -0
- package/dist/lowering_helpers.js.map +1 -0
- package/dist/module_loader.d.ts +2 -2
- package/dist/module_loader.js +20 -23
- package/dist/module_loader.js.map +1 -1
- package/dist/scaffold.d.ts +2 -2
- package/dist/scaffold.js +316 -319
- package/dist/scaffold.js.map +1 -1
- package/package.json +62 -52
- package/src/algorithm_catalog.ts +345 -345
- package/src/ast.ts +334 -334
- package/src/cli.ts +98 -624
- package/src/commands/check.ts +33 -0
- package/src/commands/compile.ts +160 -0
- package/src/commands/debug.ts +33 -0
- package/src/commands/diff.ts +108 -0
- package/src/commands/fmt.ts +22 -0
- package/src/commands/init.ts +46 -0
- package/src/commands/ir.ts +23 -0
- package/src/commands/lex.ts +17 -0
- package/src/commands/parse.ts +24 -0
- package/src/commands/test.ts +36 -0
- package/src/commands/verify_determinism.ts +66 -0
- package/src/commands/watch.ts +25 -0
- package/src/emit_auth.ts +67 -0
- package/src/emit_batch.ts +140 -140
- package/src/emit_capability.ts +562 -436
- package/src/emit_composition.ts +196 -196
- package/src/emit_database.ts +75 -0
- package/src/emit_deploy.ts +190 -190
- package/src/emit_events.ts +307 -307
- package/src/emit_extras.ts +240 -240
- package/src/emit_full.ts +316 -309
- package/src/emit_index.ts +161 -0
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_package.ts +69 -0
- package/src/emit_router.ts +395 -0
- package/src/emit_runtime.ts +17 -728
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +205 -205
- package/src/emit_websocket.ts +229 -229
- package/src/emitter.ts +566 -566
- package/src/extension_manager.ts +189 -187
- package/src/formatter.ts +297 -297
- package/src/index.ts +88 -88
- package/src/ir.ts +215 -215
- package/src/lexer.ts +630 -630
- package/src/lowering.ts +124 -556
- package/src/lowering_channels.ts +107 -0
- package/src/lowering_entities.ts +248 -0
- package/src/lowering_helpers.ts +75 -0
- package/src/module_loader.ts +112 -114
- package/src/optimizer.ts +196 -196
- package/src/parse_decls.ts +409 -409
- package/src/parse_decls2.ts +244 -244
- package/src/parse_expr.ts +197 -197
- package/src/parse_types.ts +54 -54
- package/src/parser.ts +1 -1
- package/src/parser_base.ts +57 -57
- package/src/parser_recovery.ts +153 -153
- package/src/scaffold.ts +372 -375
- package/src/solver.ts +330 -330
- package/src/typechecker.ts +591 -591
- package/src/types.ts +122 -122
- package/src/verifier.ts +348 -348
package/src/emit_capability.ts
CHANGED
|
@@ -1,436 +1,562 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Capability Body Emitter
|
|
3
|
-
*
|
|
4
|
-
* Translates IR effects and preconditions into real TypeScript + SQL.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let
|
|
72
|
-
for (
|
|
73
|
-
|
|
74
|
-
if (ch === "
|
|
75
|
-
else if (ch === "
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
case "
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Capability Body Emitter
|
|
3
|
+
*
|
|
4
|
+
* Translates IR effects and preconditions into real TypeScript + SQL.
|
|
5
|
+
*
|
|
6
|
+
* Performance strategies applied (PERF-003):
|
|
7
|
+
*
|
|
8
|
+
* 1. Entity fetches — same-table fetches batched into WHERE id = ANY($1::uuid[])
|
|
9
|
+
* and resolved with a Map lookup. Different-table fetches run in parallel via
|
|
10
|
+
* Promise.all rather than sequentially.
|
|
11
|
+
*
|
|
12
|
+
* 2. Effect batching — multiple effects targeting the same entity+table are
|
|
13
|
+
* collapsed into a single UPDATE ... SET a=$1, b=$2 ... WHERE id = $n
|
|
14
|
+
* instead of one UPDATE per field.
|
|
15
|
+
*
|
|
16
|
+
* 3. LIST queries — combined with COUNT(*) OVER() window function to avoid a
|
|
17
|
+
* separate COUNT(*) round-trip (handled in emit_router.ts).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as IR from "./ir";
|
|
21
|
+
|
|
22
|
+
// ─── Expression Parser ────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
type ExprKind =
|
|
25
|
+
| { kind: "literal"; value: string; raw: string }
|
|
26
|
+
| { kind: "field"; path: string[] }
|
|
27
|
+
| { kind: "binop"; op: string; left: Expr; right: Expr }
|
|
28
|
+
| { kind: "call"; name: string; args: Expr[] };
|
|
29
|
+
|
|
30
|
+
type Expr = ExprKind;
|
|
31
|
+
|
|
32
|
+
function parseExprStr(s: string): Expr {
|
|
33
|
+
s = s.trim();
|
|
34
|
+
if (s.startsWith("(") && s.endsWith(")")) return parseExprStr(s.slice(1, -1));
|
|
35
|
+
if (s.startsWith('"') && s.endsWith('"')) return { kind: "literal", value: s.slice(1, -1), raw: s };
|
|
36
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return { kind: "literal", value: s, raw: s };
|
|
37
|
+
if (s === "true" || s === "false") return { kind: "literal", value: s, raw: s };
|
|
38
|
+
|
|
39
|
+
const binOps = [" or ", " and ", " == ", " != ", " >= ", " <= ", " > ", " < ", " in ", " contains ", " + ", " - ", " * ", " / "];
|
|
40
|
+
for (const op of binOps) {
|
|
41
|
+
const idx = findBinOp(s, op);
|
|
42
|
+
if (idx !== -1) {
|
|
43
|
+
return { kind: "binop", op: op.trim(), left: parseExprStr(s.slice(0, idx)), right: parseExprStr(s.slice(idx + op.length)) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const callMatch = s.match(/^(\w+)\((.*)?\)$/);
|
|
48
|
+
if (callMatch) {
|
|
49
|
+
const args = callMatch[2] ? splitArgs(callMatch[2]).map(parseExprStr) : [];
|
|
50
|
+
return { kind: "call", name: callMatch[1], args };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (/^[\w.]+$/.test(s)) return { kind: "field", path: s.split(".") };
|
|
54
|
+
return { kind: "literal", value: s, raw: s };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findBinOp(s: string, op: string): number {
|
|
58
|
+
let depth = 0;
|
|
59
|
+
for (let i = 0; i <= s.length - op.length; i++) {
|
|
60
|
+
const ch = s[i];
|
|
61
|
+
if (ch === "(" || ch === "[") depth++;
|
|
62
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
63
|
+
else if (depth === 0 && s.slice(i, i + op.length) === op) return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function splitArgs(s: string): string[] {
|
|
69
|
+
const args: string[] = [];
|
|
70
|
+
let depth = 0;
|
|
71
|
+
let current = "";
|
|
72
|
+
for (const ch of s) {
|
|
73
|
+
if (ch === "(" || ch === "[") depth++;
|
|
74
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
75
|
+
else if (ch === "," && depth === 0) { args.push(current.trim()); current = ""; continue; }
|
|
76
|
+
current += ch;
|
|
77
|
+
}
|
|
78
|
+
if (current.trim()) args.push(current.trim());
|
|
79
|
+
return args;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Entity Resolution ────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
interface EntityFetch {
|
|
85
|
+
paramName: string;
|
|
86
|
+
entityType: string;
|
|
87
|
+
tableName: string;
|
|
88
|
+
idField: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toSnakeCase(s: string): string {
|
|
92
|
+
return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getEntityFetches(method: IR.IRMethod, mod: IR.IRModule, system: IR.IRSystem): EntityFetch[] {
|
|
96
|
+
const fetches: EntityFetch[] = [];
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
|
|
99
|
+
const allModels = new Map<string, string>();
|
|
100
|
+
for (const m of system.modules) {
|
|
101
|
+
for (const model of m.models) {
|
|
102
|
+
allModels.set(model.name, toSnakeCase(model.name) + "s");
|
|
103
|
+
allModels.set(model.name.toLowerCase(), toSnakeCase(model.name) + "s");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const param of method.input) {
|
|
108
|
+
const tableName = allModels.get(param.type) || allModels.get(param.type.toLowerCase());
|
|
109
|
+
if (tableName && !seen.has(param.name)) {
|
|
110
|
+
seen.add(param.name);
|
|
111
|
+
fetches.push({ paramName: param.name, entityType: param.type, tableName, idField: param.name + "_id" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return fetches;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Precondition Compiler ────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function compilePrecondition(expr: Expr, indent: string): string {
|
|
121
|
+
const condition = exprToTs(expr, true);
|
|
122
|
+
const description = exprToDescription(expr).replace(/"/g, '\\"');
|
|
123
|
+
return [
|
|
124
|
+
`${indent}if (${condition}) {`,
|
|
125
|
+
`${indent} return res.status(422).json({ error: { code: "PRECONDITION_FAILED", message: ${JSON.stringify(description)} } });`,
|
|
126
|
+
`${indent}}`,
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function exprToTs(expr: Expr, negate = false): string {
|
|
131
|
+
const inner = exprToTsInner(expr);
|
|
132
|
+
return negate ? `!(${inner})` : inner;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exprToTsInner(expr: Expr): string {
|
|
136
|
+
switch (expr.kind) {
|
|
137
|
+
case "literal":
|
|
138
|
+
if (expr.value === "true") return "true";
|
|
139
|
+
if (expr.value === "false") return "false";
|
|
140
|
+
if (/^"/.test(expr.raw)) return expr.raw;
|
|
141
|
+
return expr.value;
|
|
142
|
+
case "field":
|
|
143
|
+
return expr.path.join("?.");
|
|
144
|
+
case "binop": {
|
|
145
|
+
const l = exprToTsInner(expr.left);
|
|
146
|
+
const r = exprToTsInner(expr.right);
|
|
147
|
+
switch (expr.op) {
|
|
148
|
+
case "==": return `${l} === ${r}`;
|
|
149
|
+
case "!=": return `${l} !== ${r}`;
|
|
150
|
+
case "and": return `(${l} && ${r})`;
|
|
151
|
+
case "or": return `(${l} || ${r})`;
|
|
152
|
+
case "in": return `[${r}].flat().includes(${l})`;
|
|
153
|
+
case "contains": return `${l}?.includes(${r})`;
|
|
154
|
+
default: return `${l} ${expr.op} ${r}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
case "call":
|
|
158
|
+
if (expr.name === "now") return "new Date()";
|
|
159
|
+
return `${expr.name}(${expr.args.map(exprToTsInner).join(", ")})`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function exprToDescription(expr: Expr): string {
|
|
164
|
+
switch (expr.kind) {
|
|
165
|
+
case "literal": return expr.raw;
|
|
166
|
+
case "field": return expr.path.join(".");
|
|
167
|
+
case "binop": return `${exprToDescription(expr.left)} ${expr.op} ${exprToDescription(expr.right)}`;
|
|
168
|
+
case "call": return `${expr.name}(${expr.args.map(exprToDescription).join(", ")})`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Effect Compiler ──────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
interface CompiledEffect {
|
|
175
|
+
tableName: string;
|
|
176
|
+
entityParam: string;
|
|
177
|
+
idParam: string;
|
|
178
|
+
// For batched UPDATE: list of (column, paramPlaceholder, tsValue) tuples
|
|
179
|
+
assignments: { column: string; placeholder: string; tsValue: string }[];
|
|
180
|
+
description: string;
|
|
181
|
+
// For non-batchable effects (JSONB, array ops) — emitted as standalone query
|
|
182
|
+
standalone?: { sql: string; params: string[] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compile a single effect into a structured form.
|
|
187
|
+
* Returns null if the effect target can't be resolved.
|
|
188
|
+
*/
|
|
189
|
+
function compileEffect(
|
|
190
|
+
effect: IR.IREffect,
|
|
191
|
+
mod: IR.IRModule,
|
|
192
|
+
system: IR.IRSystem,
|
|
193
|
+
paramIdx: { n: number },
|
|
194
|
+
): CompiledEffect | null {
|
|
195
|
+
const targetParts = effect.target.split(".");
|
|
196
|
+
if (targetParts.length < 2) return null;
|
|
197
|
+
|
|
198
|
+
const entityParam = targetParts[0];
|
|
199
|
+
const fieldName = targetParts[1];
|
|
200
|
+
const nestedPath = targetParts.slice(2);
|
|
201
|
+
|
|
202
|
+
const model = (() => {
|
|
203
|
+
for (const m of system.modules) {
|
|
204
|
+
const found = m.models.find(mdl =>
|
|
205
|
+
toSnakeCase(mdl.name) === entityParam || mdl.name.toLowerCase() === entityParam.toLowerCase()
|
|
206
|
+
);
|
|
207
|
+
if (found) return found;
|
|
208
|
+
}
|
|
209
|
+
return mod.models.find(m =>
|
|
210
|
+
toSnakeCase(m.name) === entityParam || m.name.toLowerCase() === entityParam.toLowerCase()
|
|
211
|
+
);
|
|
212
|
+
})();
|
|
213
|
+
if (!model) return null;
|
|
214
|
+
|
|
215
|
+
const tableName = toSnakeCase(model.name) + "s";
|
|
216
|
+
const valueTs = exprToTsInner(parseExprStr(effect.value));
|
|
217
|
+
const idParam = `req.body.${entityParam}_id || req.params.id`;
|
|
218
|
+
|
|
219
|
+
// JSONB nested path — must be standalone (jsonb_set can't be batched cleanly)
|
|
220
|
+
if (nestedPath.length > 0) {
|
|
221
|
+
const p1 = `$${paramIdx.n++}`;
|
|
222
|
+
const p2 = `$${paramIdx.n++}`;
|
|
223
|
+
const jsonbPathLiteral = `'{${nestedPath.join(",")}}'`;
|
|
224
|
+
return {
|
|
225
|
+
tableName, entityParam, idParam,
|
|
226
|
+
assignments: [],
|
|
227
|
+
description: `${effect.target} = ${effect.value}`,
|
|
228
|
+
standalone: {
|
|
229
|
+
sql: `UPDATE ${tableName} SET ${fieldName} = jsonb_set(COALESCE(${fieldName}, '{}'), ${jsonbPathLiteral}, to_jsonb(${p1}::text), true), updated_at = NOW() WHERE id = ${p2} RETURNING *`,
|
|
230
|
+
params: [valueTs, idParam],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const fieldType = model.fields.find(f => f.name === fieldName)?.type || "";
|
|
236
|
+
const isNumeric = ["uint", "int", "float"].includes(fieldType);
|
|
237
|
+
|
|
238
|
+
switch (effect.op) {
|
|
239
|
+
case "assign": {
|
|
240
|
+
const p1 = `$${paramIdx.n++}`;
|
|
241
|
+
return {
|
|
242
|
+
tableName, entityParam, idParam,
|
|
243
|
+
assignments: [{ column: fieldName, placeholder: p1, tsValue: valueTs }],
|
|
244
|
+
description: `${effect.target} = ${effect.value}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case "add": {
|
|
248
|
+
const p1 = `$${paramIdx.n++}`;
|
|
249
|
+
if (isNumeric) {
|
|
250
|
+
return {
|
|
251
|
+
tableName, entityParam, idParam,
|
|
252
|
+
assignments: [{ column: `${fieldName} = ${fieldName} + `, placeholder: p1, tsValue: valueTs }],
|
|
253
|
+
description: `${effect.target} += ${effect.value}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Array append — standalone
|
|
257
|
+
const p2 = `$${paramIdx.n++}`;
|
|
258
|
+
return {
|
|
259
|
+
tableName, entityParam, idParam,
|
|
260
|
+
assignments: [],
|
|
261
|
+
description: `${effect.target} += ${effect.value}`,
|
|
262
|
+
standalone: {
|
|
263
|
+
sql: `UPDATE ${tableName} SET ${fieldName} = ${fieldName} || jsonb_build_array(${p1}::jsonb), updated_at = NOW() WHERE id = ${p2} RETURNING *`,
|
|
264
|
+
params: [valueTs, idParam],
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
case "remove": {
|
|
269
|
+
const p1 = `$${paramIdx.n++}`;
|
|
270
|
+
if (isNumeric) {
|
|
271
|
+
return {
|
|
272
|
+
tableName, entityParam, idParam,
|
|
273
|
+
assignments: [{ column: `${fieldName} = ${fieldName} - `, placeholder: p1, tsValue: valueTs }],
|
|
274
|
+
description: `${effect.target} -= ${effect.value}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Array remove — standalone
|
|
278
|
+
const p2 = `$${paramIdx.n++}`;
|
|
279
|
+
return {
|
|
280
|
+
tableName, entityParam, idParam,
|
|
281
|
+
assignments: [],
|
|
282
|
+
description: `${effect.target} -= ${effect.value}`,
|
|
283
|
+
standalone: {
|
|
284
|
+
sql: `UPDATE ${tableName} SET ${fieldName} = (SELECT jsonb_agg(elem) FROM jsonb_array_elements(${fieldName}) elem WHERE elem != ${p1}::jsonb), updated_at = NOW() WHERE id = ${p2} RETURNING *`,
|
|
285
|
+
params: [valueTs, idParam],
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Effect Batching ──────────────────────────────────────────────────────────
|
|
293
|
+
// Groups effects targeting the same (tableName, entityParam) into a single UPDATE.
|
|
294
|
+
|
|
295
|
+
interface BatchedUpdate {
|
|
296
|
+
tableName: string;
|
|
297
|
+
entityParam: string;
|
|
298
|
+
idParam: string;
|
|
299
|
+
setClauses: string[]; // e.g. ["hp = $1", "xp = xp + $2"]
|
|
300
|
+
paramValues: string[]; // TypeScript expressions for each $n
|
|
301
|
+
descriptions: string[];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function batchEffects(compiled: (CompiledEffect | null)[]): {
|
|
305
|
+
batches: BatchedUpdate[];
|
|
306
|
+
standalones: { sql: string; params: string[]; description: string }[];
|
|
307
|
+
} {
|
|
308
|
+
const batches = new Map<string, BatchedUpdate>(); // key: `${tableName}::${entityParam}::${idParam}`
|
|
309
|
+
const standalones: { sql: string; params: string[]; description: string }[] = [];
|
|
310
|
+
|
|
311
|
+
// Re-number parameters globally across all batches
|
|
312
|
+
let globalParamN = 1;
|
|
313
|
+
|
|
314
|
+
for (const effect of compiled) {
|
|
315
|
+
if (!effect) continue;
|
|
316
|
+
|
|
317
|
+
if (effect.standalone) {
|
|
318
|
+
// Re-number the standalone params
|
|
319
|
+
let sql = effect.standalone.sql;
|
|
320
|
+
const params = effect.standalone.params;
|
|
321
|
+
const renumbered: string[] = [];
|
|
322
|
+
let localN = 1;
|
|
323
|
+
for (const p of params) {
|
|
324
|
+
sql = sql.replace(`$${localN}`, `$${globalParamN}`);
|
|
325
|
+
renumbered.push(p);
|
|
326
|
+
globalParamN++;
|
|
327
|
+
localN++;
|
|
328
|
+
}
|
|
329
|
+
standalones.push({ sql, params: renumbered, description: effect.description });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (effect.assignments.length === 0) continue;
|
|
334
|
+
|
|
335
|
+
const key = `${effect.tableName}::${effect.entityParam}::${effect.idParam}`;
|
|
336
|
+
if (!batches.has(key)) {
|
|
337
|
+
batches.set(key, {
|
|
338
|
+
tableName: effect.tableName,
|
|
339
|
+
entityParam: effect.entityParam,
|
|
340
|
+
idParam: effect.idParam,
|
|
341
|
+
setClauses: [],
|
|
342
|
+
paramValues: [],
|
|
343
|
+
descriptions: [],
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const batch = batches.get(key)!;
|
|
347
|
+
|
|
348
|
+
for (const { column, placeholder, tsValue } of effect.assignments) {
|
|
349
|
+
// column may be "fieldName" (assign) or "fieldName = fieldName + " (numeric add/remove)
|
|
350
|
+
if (column.includes(" = ")) {
|
|
351
|
+
// Numeric add/remove: column is already "field = field + " — append placeholder
|
|
352
|
+
batch.setClauses.push(`${column}$${globalParamN}`);
|
|
353
|
+
} else {
|
|
354
|
+
batch.setClauses.push(`${column} = $${globalParamN}`);
|
|
355
|
+
}
|
|
356
|
+
batch.paramValues.push(tsValue);
|
|
357
|
+
globalParamN++;
|
|
358
|
+
}
|
|
359
|
+
batch.descriptions.push(effect.description);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { batches: Array.from(batches.values()), standalones };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Main Capability Body Emitter ─────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
export function emitCapabilityBody(
|
|
368
|
+
method: IR.IRMethod,
|
|
369
|
+
mod: IR.IRModule,
|
|
370
|
+
system: IR.IRSystem,
|
|
371
|
+
indent: string = " ",
|
|
372
|
+
): string {
|
|
373
|
+
const lines: string[] = [];
|
|
374
|
+
const fetches = getEntityFetches(method, mod, system);
|
|
375
|
+
|
|
376
|
+
// 0. Destructure primitive params
|
|
377
|
+
const primitiveParams = method.input.filter(p => {
|
|
378
|
+
const isPrimitive = ["string", "uint", "int", "float", "bool", "timestamp", "uuid", "bytes", "json"].includes(p.type);
|
|
379
|
+
const isListOrSet = p.type.startsWith("list<") || p.type.startsWith("set<");
|
|
380
|
+
return (isPrimitive || isListOrSet) && !fetches.some(f => f.paramName === p.name);
|
|
381
|
+
});
|
|
382
|
+
if (primitiveParams.length > 0) {
|
|
383
|
+
lines.push(`${indent}const { ${primitiveParams.map(p => p.name).join(", ")} } = req.body;`);
|
|
384
|
+
lines.push(``);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 1. Fetch entities — batch same-table fetches, parallelize different-table fetches
|
|
388
|
+
if (fetches.length > 0) {
|
|
389
|
+
lines.push(`${indent}// Fetch entities`);
|
|
390
|
+
|
|
391
|
+
// Group fetches by table
|
|
392
|
+
const byTable = new Map<string, EntityFetch[]>();
|
|
393
|
+
for (const f of fetches) {
|
|
394
|
+
if (!byTable.has(f.tableName)) byTable.set(f.tableName, []);
|
|
395
|
+
byTable.get(f.tableName)!.push(f);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const fetchGroups = Array.from(byTable.entries());
|
|
399
|
+
|
|
400
|
+
if (fetchGroups.length === 1 && fetchGroups[0][1].length === 1) {
|
|
401
|
+
// Single fetch — simple queryOne
|
|
402
|
+
const f = fetchGroups[0][1][0];
|
|
403
|
+
const idExpr = `req.body.${f.idField} || req.params.id`;
|
|
404
|
+
lines.push(`${indent}const ${f.paramName} = await queryOne(\`SELECT * FROM ${f.tableName} WHERE id = $1\`, [${idExpr}]);`);
|
|
405
|
+
lines.push(`${indent}if (!${f.paramName}) {`);
|
|
406
|
+
lines.push(`${indent} return res.status(404).json({ error: { code: "NOT_FOUND", message: "${f.paramName} not found" } });`);
|
|
407
|
+
lines.push(`${indent}}`);
|
|
408
|
+
|
|
409
|
+
} else if (fetchGroups.length === 1 && fetchGroups[0][1].length > 1) {
|
|
410
|
+
// Multiple fetches from the SAME table — batch into WHERE id = ANY($1::uuid[])
|
|
411
|
+
const [tableName, group] = fetchGroups[0];
|
|
412
|
+
const idExprs = group.map(f => `req.body.${f.idField} || req.params.id`);
|
|
413
|
+
lines.push(`${indent}// Batch fetch: ${group.map(f => f.paramName).join(", ")} from ${tableName} in one query`);
|
|
414
|
+
lines.push(`${indent}const __ids_${tableName} = [${idExprs.join(", ")}];`);
|
|
415
|
+
lines.push(`${indent}const __rows_${tableName} = await query(\`SELECT * FROM ${tableName} WHERE id = ANY($1::uuid[])\`, [__ids_${tableName}]);`);
|
|
416
|
+
lines.push(`${indent}const __map_${tableName} = new Map(__rows_${tableName}.map((r: any) => [r.id, r]));`);
|
|
417
|
+
for (const f of group) {
|
|
418
|
+
const idExpr = `req.body.${f.idField} || req.params.id`;
|
|
419
|
+
lines.push(`${indent}const ${f.paramName} = __map_${tableName}.get(${idExpr}) ?? null;`);
|
|
420
|
+
lines.push(`${indent}if (!${f.paramName}) {`);
|
|
421
|
+
lines.push(`${indent} return res.status(404).json({ error: { code: "NOT_FOUND", message: "${f.paramName} not found" } });`);
|
|
422
|
+
lines.push(`${indent}}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
} else {
|
|
426
|
+
// Multiple fetches from DIFFERENT tables — run in parallel with Promise.all
|
|
427
|
+
lines.push(`${indent}// Parallel fetch from ${fetchGroups.length} tables`);
|
|
428
|
+
const resultVars: string[] = [];
|
|
429
|
+
const fetchExprs: string[] = [];
|
|
430
|
+
|
|
431
|
+
for (const [tableName, group] of fetchGroups) {
|
|
432
|
+
if (group.length === 1) {
|
|
433
|
+
const f = group[0];
|
|
434
|
+
const idExpr = `req.body.${f.idField} || req.params.id`;
|
|
435
|
+
resultVars.push(`__r_${f.paramName}`);
|
|
436
|
+
fetchExprs.push(`queryOne(\`SELECT * FROM ${tableName} WHERE id = $1\`, [${idExpr}])`);
|
|
437
|
+
} else {
|
|
438
|
+
// Same-table batch within a multi-table parallel fetch
|
|
439
|
+
const idExprs = group.map(f => `req.body.${f.idField} || req.params.id`);
|
|
440
|
+
resultVars.push(`__rows_${tableName}`);
|
|
441
|
+
fetchExprs.push(`query(\`SELECT * FROM ${tableName} WHERE id = ANY($1::uuid[])\`, [[${idExprs.join(", ")}]])`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
lines.push(`${indent}const [${resultVars.join(", ")}] = await Promise.all([`);
|
|
446
|
+
for (const expr of fetchExprs) lines.push(`${indent} ${expr},`);
|
|
447
|
+
lines.push(`${indent}]);`);
|
|
448
|
+
|
|
449
|
+
// Unpack results
|
|
450
|
+
let resultIdx = 0;
|
|
451
|
+
for (const [tableName, group] of fetchGroups) {
|
|
452
|
+
if (group.length === 1) {
|
|
453
|
+
const f = group[0];
|
|
454
|
+
lines.push(`${indent}const ${f.paramName} = ${resultVars[resultIdx]};`);
|
|
455
|
+
lines.push(`${indent}if (!${f.paramName}) {`);
|
|
456
|
+
lines.push(`${indent} return res.status(404).json({ error: { code: "NOT_FOUND", message: "${f.paramName} not found" } });`);
|
|
457
|
+
lines.push(`${indent}}`);
|
|
458
|
+
} else {
|
|
459
|
+
const mapVar = `__map_${tableName}`;
|
|
460
|
+
lines.push(`${indent}const ${mapVar} = new Map((${resultVars[resultIdx]} as any[]).map((r: any) => [r.id, r]));`);
|
|
461
|
+
for (const f of group) {
|
|
462
|
+
const idExpr = `req.body.${f.idField} || req.params.id`;
|
|
463
|
+
lines.push(`${indent}const ${f.paramName} = ${mapVar}.get(${idExpr}) ?? null;`);
|
|
464
|
+
lines.push(`${indent}if (!${f.paramName}) {`);
|
|
465
|
+
lines.push(`${indent} return res.status(404).json({ error: { code: "NOT_FOUND", message: "${f.paramName} not found" } });`);
|
|
466
|
+
lines.push(`${indent}}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
resultIdx++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
lines.push(``);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 2. Precondition checks
|
|
476
|
+
if (method.preconditions.length > 0) {
|
|
477
|
+
lines.push(`${indent}// Preconditions`);
|
|
478
|
+
for (const pre of method.preconditions) {
|
|
479
|
+
try {
|
|
480
|
+
lines.push(compilePrecondition(parseExprStr(pre.expression), indent));
|
|
481
|
+
} catch {
|
|
482
|
+
lines.push(`${indent}// CHECK: ${pre.description}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
lines.push(``);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 3. Effects — batch same-entity updates into single UPDATEs
|
|
489
|
+
if (method.effects.length > 0) {
|
|
490
|
+
lines.push(`${indent}// Effects (batched by entity to minimise round-trips)`);
|
|
491
|
+
|
|
492
|
+
const paramIdx = { n: 1 };
|
|
493
|
+
const compiled = method.effects.map(e => compileEffect(e, mod, system, paramIdx));
|
|
494
|
+
const { batches, standalones } = batchEffects(compiled);
|
|
495
|
+
|
|
496
|
+
// Emit batched UPDATEs
|
|
497
|
+
for (const batch of batches) {
|
|
498
|
+
if (batch.setClauses.length === 0) continue;
|
|
499
|
+
const idParamN = paramIdx.n++;
|
|
500
|
+
const setClauses = batch.setClauses.join(", ");
|
|
501
|
+
const sql = `UPDATE ${batch.tableName} SET ${setClauses}, updated_at = NOW() WHERE id = $${idParamN} RETURNING *`;
|
|
502
|
+
const params = [...batch.paramValues, batch.idParam].join(", ");
|
|
503
|
+
const resultVar = `__upd_${batch.entityParam}`;
|
|
504
|
+
lines.push(`${indent}// ${batch.descriptions.join("; ")}`);
|
|
505
|
+
lines.push(`${indent}const ${resultVar} = await query(\`${sql}\`, [${params}]);`);
|
|
506
|
+
lines.push(`${indent}if (!${resultVar} || ${resultVar}.length === 0) {`);
|
|
507
|
+
lines.push(`${indent} throw new Error("Update failed for ${batch.entityParam}");`);
|
|
508
|
+
lines.push(`${indent}}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Emit standalone effects (JSONB, array ops)
|
|
512
|
+
for (const s of standalones) {
|
|
513
|
+
const resultVar = `__eff_${standalones.indexOf(s)}`;
|
|
514
|
+
lines.push(`${indent}// ${s.description}`);
|
|
515
|
+
lines.push(`${indent}const ${resultVar} = await query(\`${s.sql}\`, [${s.params.join(", ")}]);`);
|
|
516
|
+
lines.push(`${indent}if (!${resultVar} || ${resultVar}.length === 0) {`);
|
|
517
|
+
lines.push(`${indent} throw new Error("Effect failed: ${s.description.replace(/"/g, '\\"')}");`);
|
|
518
|
+
lines.push(`${indent}}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Fallback for effects that couldn't be compiled
|
|
522
|
+
for (const effect of method.effects) {
|
|
523
|
+
const paramIdx2 = { n: 1 };
|
|
524
|
+
if (!compileEffect(effect, mod, system, paramIdx2)) {
|
|
525
|
+
lines.push(`${indent}// EFFECT: ${effect.target} ${effect.op === "assign" ? "=" : effect.op === "add" ? "+=" : "-="} ${effect.value}`);
|
|
526
|
+
lines.push(`${indent}// TODO: Implement this effect manually`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
lines.push(``);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 4. Event emissions
|
|
533
|
+
if (method.emissions.length > 0) {
|
|
534
|
+
lines.push(`${indent}// Emit events`);
|
|
535
|
+
for (const ev of method.emissions) {
|
|
536
|
+
const payload = buildEventPayload(method, fetches);
|
|
537
|
+
if (method.sync === "transactional") {
|
|
538
|
+
lines.push(`${indent}await eventBus.publish("${ev}", ${payload}, "${mod.name}", auth.trace_id, __client);`);
|
|
539
|
+
} else {
|
|
540
|
+
lines.push(`${indent}await eventBus.publish("${ev}", ${payload}, "${mod.name}", auth.trace_id);`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
lines.push(``);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 5. Return result
|
|
547
|
+
const resultEntity = fetches[0];
|
|
548
|
+
if (resultEntity) {
|
|
549
|
+
lines.push(`${indent}res.json({ ok: true, action: "${method.name}", entity: ${resultEntity.paramName} });`);
|
|
550
|
+
} else {
|
|
551
|
+
lines.push(`${indent}res.json({ ok: true, action: "${method.name}" });`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return lines.join("\n");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildEventPayload(method: IR.IRMethod, fetches: EntityFetch[]): string {
|
|
558
|
+
const fields: string[] = fetches.map(f => `${f.paramName}_id: ${f.paramName}?.id`);
|
|
559
|
+
fields.push(`timestamp: new Date().toISOString()`);
|
|
560
|
+
fields.push(`actor_id: auth.actor_id`);
|
|
561
|
+
return `{ ${fields.join(", ")} }`;
|
|
562
|
+
}
|