bonescript-compiler 0.4.0 → 0.5.1
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/README.md +382 -0
- package/dist/emit_auth.d.ts +14 -2
- package/dist/emit_auth.js +498 -60
- package/dist/emit_auth.js.map +1 -1
- package/dist/emit_full.js +133 -11
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.js +46 -1
- package/dist/emit_index.js.map +1 -1
- package/dist/emitter.js +47 -0
- package/dist/emitter.js.map +1 -1
- package/dist/ir.d.ts +4 -0
- package/dist/lowering_channels.js +1 -0
- package/dist/lowering_channels.js.map +1 -1
- package/dist/lowering_entities.js +11 -1
- package/dist/lowering_entities.js.map +1 -1
- package/dist/optimizer.js +1 -1
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.js +0 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.d.ts +5 -0
- package/dist/typechecker.js +36 -0
- package/dist/typechecker.js.map +1 -1
- package/package.json +1 -1
- package/src/emit_auth.ts +513 -67
- package/src/emit_full.ts +136 -12
- package/src/emit_index.ts +210 -161
- package/src/emitter.ts +642 -592
- package/src/ir.ts +1 -0
- package/src/lowering_channels.ts +1 -0
- package/src/lowering_entities.ts +258 -248
- package/src/optimizer.ts +1 -1
- package/src/scaffold.ts +0 -1
- package/src/typechecker.ts +657 -606
package/src/ir.ts
CHANGED
|
@@ -169,6 +169,7 @@ export interface IRRelation {
|
|
|
169
169
|
to_table: string;
|
|
170
170
|
foreign_key: string; // column name on the owning side
|
|
171
171
|
junction_table?: string; // only for many_to_many
|
|
172
|
+
cardinality?: { min: number; max: number | "*" }; // optional explicit bounds
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// ââ€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬ Invariants ââ€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬Ã¢â€â‚¬
|
package/src/lowering_channels.ts
CHANGED
|
@@ -73,6 +73,7 @@ export function lowerChannel(systemName: string, channel: AST.ChannelDeclNode):
|
|
|
73
73
|
ordering: channel.ordering || "fifo",
|
|
74
74
|
persistence: channel.persistence || "none",
|
|
75
75
|
max_size: channel.maxSize || 10000,
|
|
76
|
+
...(channel.filter ? { filter: serializeExpr(channel.filter) } : {}),
|
|
76
77
|
},
|
|
77
78
|
};
|
|
78
79
|
}
|
package/src/lowering_entities.ts
CHANGED
|
@@ -1,248 +1,258 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Entity Lowering
|
|
3
|
-
* Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
|
|
4
|
-
* Also handles field lowering and CRUD method generation.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as AST from "./ast";
|
|
8
|
-
import * as IR from "./ir";
|
|
9
|
-
import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
|
|
10
|
-
|
|
11
|
-
// ─── Field Lowering ───────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
export function lowerField(f: AST.FieldNode): IR.IRField {
|
|
14
|
-
const type = serializeType(f.type);
|
|
15
|
-
// defaultValue is an ExprNode | null — serialize it to a string if present
|
|
16
|
-
const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
|
|
17
|
-
return {
|
|
18
|
-
name: f.name,
|
|
19
|
-
type,
|
|
20
|
-
nullable: false,
|
|
21
|
-
unique: false,
|
|
22
|
-
indexed: false,
|
|
23
|
-
default_value: defaultValue,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── CRUD Method Generation ───────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
|
|
30
|
-
const input: IR.IRField[] =
|
|
31
|
-
op === "create" || op === "update"
|
|
32
|
-
? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
|
|
33
|
-
: op === "list"
|
|
34
|
-
? [
|
|
35
|
-
{ name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
|
|
36
|
-
{ name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
|
|
37
|
-
]
|
|
38
|
-
: [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
name: op,
|
|
42
|
-
input,
|
|
43
|
-
output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
|
|
44
|
-
preconditions: [],
|
|
45
|
-
effects: [],
|
|
46
|
-
emissions: [],
|
|
47
|
-
idempotent: op === "read" || op === "list",
|
|
48
|
-
authenticated: true,
|
|
49
|
-
timeout_ms: 30000,
|
|
50
|
-
retry: null,
|
|
51
|
-
pipeline: null,
|
|
52
|
-
algorithm: null,
|
|
53
|
-
sync: null,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── Capability Lowering ──────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
|
|
60
|
-
const input: IR.IRField[] = cap.params.map(p => ({
|
|
61
|
-
name: p.name,
|
|
62
|
-
type: serializeType(p.type),
|
|
63
|
-
nullable: false,
|
|
64
|
-
unique: false,
|
|
65
|
-
indexed: false,
|
|
66
|
-
default_value: null,
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
|
|
70
|
-
expression: serializeExpr(r),
|
|
71
|
-
description: serializeExpr(r),
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
const effects: IR.IREffect[] = cap.effects.map(e => ({
|
|
75
|
-
target: e.target.path.join("."),
|
|
76
|
-
op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
|
|
77
|
-
value: serializeExpr(e.value),
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
let pipeline: IR.IRPipeline | null = null;
|
|
81
|
-
if (cap.pipeline) {
|
|
82
|
-
pipeline = {
|
|
83
|
-
parallel: cap.pipeline.parallel,
|
|
84
|
-
steps: cap.pipeline.steps.map(step => ({
|
|
85
|
-
call_name: step.call.name,
|
|
86
|
-
call_args: step.call.args.map(a => serializeExpr(a)),
|
|
87
|
-
bind_as: step.bindAs,
|
|
88
|
-
})),
|
|
89
|
-
on_error: cap.pipeline.onError ? {
|
|
90
|
-
action: cap.pipeline.onError.action,
|
|
91
|
-
call_name: cap.pipeline.onError.call?.name || null,
|
|
92
|
-
call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
|
|
93
|
-
} : null,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let algorithm: IR.IRAlgorithm | null = null;
|
|
98
|
-
if (cap.algorithm) {
|
|
99
|
-
algorithm = {
|
|
100
|
-
catalog_name: cap.algorithm.name,
|
|
101
|
-
bindings: cap.algorithm.using.map(b => ({
|
|
102
|
-
param: b.param,
|
|
103
|
-
value: serializeExpr(b.value),
|
|
104
|
-
})),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
name: cap.name,
|
|
110
|
-
input,
|
|
111
|
-
output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
|
|
112
|
-
preconditions,
|
|
113
|
-
effects,
|
|
114
|
-
emissions: cap.emits.map(e => e.eventName),
|
|
115
|
-
idempotent: cap.idempotent || false,
|
|
116
|
-
authenticated: true,
|
|
117
|
-
timeout_ms: parseDurationMs(cap.timeout) || 30000,
|
|
118
|
-
retry: cap.retry ? {
|
|
119
|
-
max_attempts: cap.retry.maxAttempts || 3,
|
|
120
|
-
backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
|
|
121
|
-
interval_ms: parseDurationMs(cap.retry.interval) || 1000,
|
|
122
|
-
} : null,
|
|
123
|
-
pipeline,
|
|
124
|
-
algorithm,
|
|
125
|
-
sync: cap.sync,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── Entity Lowering ──────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
export function lowerEntity(
|
|
132
|
-
systemName: string,
|
|
133
|
-
entity: AST.EntityDeclNode,
|
|
134
|
-
capabilities: AST.CapabilityDeclNode[],
|
|
135
|
-
stores: AST.StoreDeclNode[],
|
|
136
|
-
): IR.IRModule {
|
|
137
|
-
const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
|
|
138
|
-
|
|
139
|
-
// Ontology-entailed fields + declared fields
|
|
140
|
-
const fields: IR.IRField[] = [
|
|
141
|
-
{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
|
|
142
|
-
{ name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
|
|
143
|
-
{ name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
|
|
144
|
-
...entity.owns.map(lowerField),
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const derivedFields: IR.IRField[] = entity.derived.map(d => ({
|
|
148
|
-
name: d.name,
|
|
149
|
-
type: "json",
|
|
150
|
-
nullable: true,
|
|
151
|
-
unique: false,
|
|
152
|
-
indexed: false,
|
|
153
|
-
default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
|
|
157
|
-
|
|
158
|
-
const modelConstraints: IR.IRModelConstraint[] = [];
|
|
159
|
-
for (const c of entity.constraints) {
|
|
160
|
-
const serialized = serializeExpr(c);
|
|
161
|
-
if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
|
|
162
|
-
const field = c.path.slice(0, -1).join(".");
|
|
163
|
-
modelConstraints.push({ kind: "unique", target: field, params: {} });
|
|
164
|
-
indexes.push({ fields: [field], unique: true });
|
|
165
|
-
} else {
|
|
166
|
-
modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const model: IR.IRModel = {
|
|
171
|
-
name: entity.name,
|
|
172
|
-
fields: [...fields, ...derivedFields],
|
|
173
|
-
primary_key: "id",
|
|
174
|
-
indexes,
|
|
175
|
-
constraints: modelConstraints,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// State machine
|
|
179
|
-
const stateMachines: IR.IRStateMachine[] = [];
|
|
180
|
-
if (entity.states) {
|
|
181
|
-
const states = entity.states.nodes.map(n => n.name);
|
|
182
|
-
const transitions: IR.IRTransition[] = [];
|
|
183
|
-
for (const node of entity.states.nodes) {
|
|
184
|
-
for (const target of node.transitions) {
|
|
185
|
-
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
186
|
-
}
|
|
187
|
-
for (const target of node.branches) {
|
|
188
|
-
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Methods: CRUD + capabilities
|
|
195
|
-
const methods: IR.IRMethod[] = [
|
|
196
|
-
makeCrudMethod("create", entity.name, fields),
|
|
197
|
-
makeCrudMethod("read", entity.name, fields),
|
|
198
|
-
makeCrudMethod("update", entity.name, fields),
|
|
199
|
-
makeCrudMethod("delete", entity.name, fields),
|
|
200
|
-
makeCrudMethod("list", entity.name, fields),
|
|
201
|
-
...capabilities.map(lowerCapability),
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
// Relations
|
|
205
|
-
const relations: IR.IRRelation[] = entity.relations.map(rel => {
|
|
206
|
-
const fromTable = toSnakeCase(entity.name) + "s";
|
|
207
|
-
const toTable = toSnakeCase(rel.target) + "s";
|
|
208
|
-
let foreignKey: string;
|
|
209
|
-
let junctionTable: string | undefined;
|
|
210
|
-
|
|
211
|
-
switch (rel.relationType) {
|
|
212
|
-
case "belongs_to":
|
|
213
|
-
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
214
|
-
break;
|
|
215
|
-
case "has_one":
|
|
216
|
-
case "has_many":
|
|
217
|
-
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
218
|
-
break;
|
|
219
|
-
case "many_to_many":
|
|
220
|
-
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
221
|
-
junctionTable = [fromTable, toTable].sort().join("_");
|
|
222
|
-
break;
|
|
223
|
-
default:
|
|
224
|
-
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Entity Lowering
|
|
3
|
+
* Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
|
|
4
|
+
* Also handles field lowering and CRUD method generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as AST from "./ast";
|
|
8
|
+
import * as IR from "./ir";
|
|
9
|
+
import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
|
|
10
|
+
|
|
11
|
+
// ─── Field Lowering ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export function lowerField(f: AST.FieldNode): IR.IRField {
|
|
14
|
+
const type = serializeType(f.type);
|
|
15
|
+
// defaultValue is an ExprNode | null — serialize it to a string if present
|
|
16
|
+
const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
|
|
17
|
+
return {
|
|
18
|
+
name: f.name,
|
|
19
|
+
type,
|
|
20
|
+
nullable: false,
|
|
21
|
+
unique: false,
|
|
22
|
+
indexed: false,
|
|
23
|
+
default_value: defaultValue,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── CRUD Method Generation ───────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
|
|
30
|
+
const input: IR.IRField[] =
|
|
31
|
+
op === "create" || op === "update"
|
|
32
|
+
? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
|
|
33
|
+
: op === "list"
|
|
34
|
+
? [
|
|
35
|
+
{ name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
|
|
36
|
+
{ name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
|
|
37
|
+
]
|
|
38
|
+
: [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name: op,
|
|
42
|
+
input,
|
|
43
|
+
output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
|
|
44
|
+
preconditions: [],
|
|
45
|
+
effects: [],
|
|
46
|
+
emissions: [],
|
|
47
|
+
idempotent: op === "read" || op === "list",
|
|
48
|
+
authenticated: true,
|
|
49
|
+
timeout_ms: 30000,
|
|
50
|
+
retry: null,
|
|
51
|
+
pipeline: null,
|
|
52
|
+
algorithm: null,
|
|
53
|
+
sync: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Capability Lowering ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
|
|
60
|
+
const input: IR.IRField[] = cap.params.map(p => ({
|
|
61
|
+
name: p.name,
|
|
62
|
+
type: serializeType(p.type),
|
|
63
|
+
nullable: false,
|
|
64
|
+
unique: false,
|
|
65
|
+
indexed: false,
|
|
66
|
+
default_value: null,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
|
|
70
|
+
expression: serializeExpr(r),
|
|
71
|
+
description: serializeExpr(r),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const effects: IR.IREffect[] = cap.effects.map(e => ({
|
|
75
|
+
target: e.target.path.join("."),
|
|
76
|
+
op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
|
|
77
|
+
value: serializeExpr(e.value),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
let pipeline: IR.IRPipeline | null = null;
|
|
81
|
+
if (cap.pipeline) {
|
|
82
|
+
pipeline = {
|
|
83
|
+
parallel: cap.pipeline.parallel,
|
|
84
|
+
steps: cap.pipeline.steps.map(step => ({
|
|
85
|
+
call_name: step.call.name,
|
|
86
|
+
call_args: step.call.args.map(a => serializeExpr(a)),
|
|
87
|
+
bind_as: step.bindAs,
|
|
88
|
+
})),
|
|
89
|
+
on_error: cap.pipeline.onError ? {
|
|
90
|
+
action: cap.pipeline.onError.action,
|
|
91
|
+
call_name: cap.pipeline.onError.call?.name || null,
|
|
92
|
+
call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
|
|
93
|
+
} : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let algorithm: IR.IRAlgorithm | null = null;
|
|
98
|
+
if (cap.algorithm) {
|
|
99
|
+
algorithm = {
|
|
100
|
+
catalog_name: cap.algorithm.name,
|
|
101
|
+
bindings: cap.algorithm.using.map(b => ({
|
|
102
|
+
param: b.param,
|
|
103
|
+
value: serializeExpr(b.value),
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: cap.name,
|
|
110
|
+
input,
|
|
111
|
+
output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
|
|
112
|
+
preconditions,
|
|
113
|
+
effects,
|
|
114
|
+
emissions: cap.emits.map(e => e.eventName),
|
|
115
|
+
idempotent: cap.idempotent || false,
|
|
116
|
+
authenticated: true,
|
|
117
|
+
timeout_ms: parseDurationMs(cap.timeout) || 30000,
|
|
118
|
+
retry: cap.retry ? {
|
|
119
|
+
max_attempts: cap.retry.maxAttempts || 3,
|
|
120
|
+
backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
|
|
121
|
+
interval_ms: parseDurationMs(cap.retry.interval) || 1000,
|
|
122
|
+
} : null,
|
|
123
|
+
pipeline,
|
|
124
|
+
algorithm,
|
|
125
|
+
sync: cap.sync,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Entity Lowering ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function lowerEntity(
|
|
132
|
+
systemName: string,
|
|
133
|
+
entity: AST.EntityDeclNode,
|
|
134
|
+
capabilities: AST.CapabilityDeclNode[],
|
|
135
|
+
stores: AST.StoreDeclNode[],
|
|
136
|
+
): IR.IRModule {
|
|
137
|
+
const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
|
|
138
|
+
|
|
139
|
+
// Ontology-entailed fields + declared fields
|
|
140
|
+
const fields: IR.IRField[] = [
|
|
141
|
+
{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
|
|
142
|
+
{ name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
|
|
143
|
+
{ name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
|
|
144
|
+
...entity.owns.map(lowerField),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const derivedFields: IR.IRField[] = entity.derived.map(d => ({
|
|
148
|
+
name: d.name,
|
|
149
|
+
type: "json",
|
|
150
|
+
nullable: true,
|
|
151
|
+
unique: false,
|
|
152
|
+
indexed: false,
|
|
153
|
+
default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
|
|
157
|
+
|
|
158
|
+
const modelConstraints: IR.IRModelConstraint[] = [];
|
|
159
|
+
for (const c of entity.constraints) {
|
|
160
|
+
const serialized = serializeExpr(c);
|
|
161
|
+
if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
|
|
162
|
+
const field = c.path.slice(0, -1).join(".");
|
|
163
|
+
modelConstraints.push({ kind: "unique", target: field, params: {} });
|
|
164
|
+
indexes.push({ fields: [field], unique: true });
|
|
165
|
+
} else {
|
|
166
|
+
modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const model: IR.IRModel = {
|
|
171
|
+
name: entity.name,
|
|
172
|
+
fields: [...fields, ...derivedFields],
|
|
173
|
+
primary_key: "id",
|
|
174
|
+
indexes,
|
|
175
|
+
constraints: modelConstraints,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// State machine
|
|
179
|
+
const stateMachines: IR.IRStateMachine[] = [];
|
|
180
|
+
if (entity.states) {
|
|
181
|
+
const states = entity.states.nodes.map(n => n.name);
|
|
182
|
+
const transitions: IR.IRTransition[] = [];
|
|
183
|
+
for (const node of entity.states.nodes) {
|
|
184
|
+
for (const target of node.transitions) {
|
|
185
|
+
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
186
|
+
}
|
|
187
|
+
for (const target of node.branches) {
|
|
188
|
+
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Methods: CRUD + capabilities
|
|
195
|
+
const methods: IR.IRMethod[] = [
|
|
196
|
+
makeCrudMethod("create", entity.name, fields),
|
|
197
|
+
makeCrudMethod("read", entity.name, fields),
|
|
198
|
+
makeCrudMethod("update", entity.name, fields),
|
|
199
|
+
makeCrudMethod("delete", entity.name, fields),
|
|
200
|
+
makeCrudMethod("list", entity.name, fields),
|
|
201
|
+
...capabilities.map(lowerCapability),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// Relations
|
|
205
|
+
const relations: IR.IRRelation[] = entity.relations.map(rel => {
|
|
206
|
+
const fromTable = toSnakeCase(entity.name) + "s";
|
|
207
|
+
const toTable = toSnakeCase(rel.target) + "s";
|
|
208
|
+
let foreignKey: string;
|
|
209
|
+
let junctionTable: string | undefined;
|
|
210
|
+
|
|
211
|
+
switch (rel.relationType) {
|
|
212
|
+
case "belongs_to":
|
|
213
|
+
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
214
|
+
break;
|
|
215
|
+
case "has_one":
|
|
216
|
+
case "has_many":
|
|
217
|
+
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
218
|
+
break;
|
|
219
|
+
case "many_to_many":
|
|
220
|
+
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
221
|
+
junctionTable = [fromTable, toTable].sort().join("_");
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
name: rel.name,
|
|
229
|
+
kind: rel.relationType,
|
|
230
|
+
from_entity: entity.name,
|
|
231
|
+
to_entity: rel.target,
|
|
232
|
+
from_table: fromTable,
|
|
233
|
+
to_table: toTable,
|
|
234
|
+
foreign_key: foreignKey,
|
|
235
|
+
junction_table: junctionTable,
|
|
236
|
+
cardinality: rel.cardinality ?? undefined,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const relatedStore = stores.find(s => s.name.toLowerCase().includes(entity.name.toLowerCase()));
|
|
241
|
+
const deps = relatedStore ? [makeId(systemName, "data_store", relatedStore.name)] : [];
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
id: moduleId,
|
|
245
|
+
kind: "api_service",
|
|
246
|
+
name: `${entity.name}Service`,
|
|
247
|
+
interfaces: [{ name: `I${entity.name}Service`, methods }],
|
|
248
|
+
models: [model],
|
|
249
|
+
events: [],
|
|
250
|
+
state_machines: stateMachines,
|
|
251
|
+
relations,
|
|
252
|
+
dependencies: deps,
|
|
253
|
+
config: {
|
|
254
|
+
authenticated: entity.auth !== null && entity.auth !== "none",
|
|
255
|
+
auth_method: entity.auth || "none",
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
package/src/optimizer.ts
CHANGED
|
@@ -52,7 +52,7 @@ function deadModuleElimination(s: IR.IRSystem, log: OptimizationLog[]): IR.IRSys
|
|
|
52
52
|
|
|
53
53
|
// Seed: always-reachable kinds
|
|
54
54
|
for (const m of s.modules) {
|
|
55
|
-
if (["gateway", "frontend", "auth_service", "api_service", "realtime_service"].includes(m.kind)) {
|
|
55
|
+
if (["gateway", "frontend", "auth_service", "api_service", "realtime_service", "data_store", "event_bus", "cache"].includes(m.kind)) {
|
|
56
56
|
reachable.add(m.id);
|
|
57
57
|
}
|
|
58
58
|
}
|