bonescript-compiler 0.2.1 → 0.4.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 +215 -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 +96 -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 +292 -128
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_composition.js +37 -3
- package/dist/emit_composition.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.d.ts +1 -0
- package/dist/emit_events.js +342 -275
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_full.js +135 -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_models.d.ts +12 -0
- package/dist/emit_models.js +171 -0
- package/dist/emit_models.js.map +1 -0
- package/dist/emit_openapi.d.ts +9 -0
- package/dist/emit_openapi.js +308 -0
- package/dist/emit_openapi.js.map +1 -0
- 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 +390 -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/emit_tests.js +37 -0
- package/dist/emit_tests.js.map +1 -1
- package/dist/emitter.js +34 -5
- package/dist/emitter.js.map +1 -1
- 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 +47 -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/dist/typechecker.js +32 -13
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.d.ts +5 -0
- package/dist/verifier.js +140 -2
- package/dist/verifier.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 +191 -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 +72 -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 +617 -436
- package/src/emit_composition.ts +229 -196
- package/src/emit_database.ts +75 -0
- package/src/emit_deploy.ts +190 -190
- package/src/emit_events.ts +377 -307
- package/src/emit_extras.ts +240 -240
- package/src/emit_full.ts +351 -309
- package/src/emit_index.ts +161 -0
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_models.ts +176 -0
- package/src/emit_openapi.ts +318 -0
- package/src/emit_package.ts +69 -0
- package/src/emit_router.ts +409 -0
- package/src/emit_runtime.ts +17 -728
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +246 -205
- package/src/emit_websocket.ts +229 -229
- package/src/emitter.ts +31 -5
- 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 +142 -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 +30 -15
- package/src/types.ts +122 -122
- package/src/verifier.ts +151 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript OpenAPI 3.1 Schema Emitter
|
|
3
|
+
* Generates a complete openapi.json for each api_service module.
|
|
4
|
+
* Implements spec/09_CODEGEN.md §2 (ApiService → JSON secondary target).
|
|
5
|
+
*
|
|
6
|
+
* Produces: openapi.json at the project root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as IR from "./ir";
|
|
10
|
+
|
|
11
|
+
// ─── Type mapping ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function irTypeToJsonSchema(irType: string): Record<string, unknown> {
|
|
14
|
+
switch (irType) {
|
|
15
|
+
case "string": return { type: "string" };
|
|
16
|
+
case "uint": return { type: "integer", minimum: 0 };
|
|
17
|
+
case "int": return { type: "integer" };
|
|
18
|
+
case "float": return { type: "number" };
|
|
19
|
+
case "bool": return { type: "boolean" };
|
|
20
|
+
case "timestamp": return { type: "string", format: "date-time" };
|
|
21
|
+
case "uuid": return { type: "string", format: "uuid" };
|
|
22
|
+
case "bytes": return { type: "string", format: "byte" };
|
|
23
|
+
case "json": return {};
|
|
24
|
+
default: {
|
|
25
|
+
const listMatch = irType.match(/^list<(.+)>$/);
|
|
26
|
+
if (listMatch) return { type: "array", items: irTypeToJsonSchema(listMatch[1]) };
|
|
27
|
+
const setMatch = irType.match(/^set<(.+)>$/);
|
|
28
|
+
if (setMatch) return { type: "array", items: irTypeToJsonSchema(setMatch[1]), uniqueItems: true };
|
|
29
|
+
const optMatch = irType.match(/^optional<(.+)>$/);
|
|
30
|
+
if (optMatch) return { oneOf: [irTypeToJsonSchema(optMatch[1]), { type: "null" }] };
|
|
31
|
+
// Entity reference — use $ref
|
|
32
|
+
return { $ref: `#/components/schemas/${irType}` };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function modelToSchema(model: IR.IRModel): Record<string, unknown> {
|
|
38
|
+
const properties: Record<string, unknown> = {};
|
|
39
|
+
const required: string[] = [];
|
|
40
|
+
|
|
41
|
+
for (const field of model.fields) {
|
|
42
|
+
if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
|
|
43
|
+
properties[field.name] = irTypeToJsonSchema(field.type);
|
|
44
|
+
if (!field.nullable && !field.default_value) {
|
|
45
|
+
required.push(field.name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties,
|
|
52
|
+
...(required.length > 0 ? { required } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toSnakeCase(s: string): string {
|
|
57
|
+
return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Main emitter ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export function emitOpenApiSchema(system: IR.IRSystem): string {
|
|
63
|
+
const apiModules = system.modules.filter(m => m.kind === "api_service" && m.models.length > 0);
|
|
64
|
+
if (apiModules.length === 0) return "";
|
|
65
|
+
|
|
66
|
+
// ── Collect all schemas ──────────────────────────────────────────────────
|
|
67
|
+
const schemas: Record<string, unknown> = {};
|
|
68
|
+
|
|
69
|
+
// Standard error schema
|
|
70
|
+
schemas["Error"] = {
|
|
71
|
+
type: "object",
|
|
72
|
+
required: ["error"],
|
|
73
|
+
properties: {
|
|
74
|
+
error: {
|
|
75
|
+
type: "object",
|
|
76
|
+
required: ["code", "message"],
|
|
77
|
+
properties: {
|
|
78
|
+
code: { type: "string" },
|
|
79
|
+
message: { type: "string" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Paginated result wrapper
|
|
86
|
+
schemas["PaginatedResult"] = {
|
|
87
|
+
type: "object",
|
|
88
|
+
required: ["items", "total", "page", "page_size"],
|
|
89
|
+
properties: {
|
|
90
|
+
items: { type: "array", items: {} },
|
|
91
|
+
total: { type: "integer", minimum: 0 },
|
|
92
|
+
page: { type: "integer", minimum: 1 },
|
|
93
|
+
page_size: { type: "integer", minimum: 1 },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
for (const mod of apiModules) {
|
|
98
|
+
for (const model of mod.models) {
|
|
99
|
+
schemas[model.name] = modelToSchema(model);
|
|
100
|
+
|
|
101
|
+
// Create input schema (omit server-set fields)
|
|
102
|
+
const createProps: Record<string, unknown> = {};
|
|
103
|
+
const createRequired: string[] = [];
|
|
104
|
+
for (const field of model.fields) {
|
|
105
|
+
if (["id", "created_at", "updated_at"].includes(field.name)) continue;
|
|
106
|
+
if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
|
|
107
|
+
createProps[field.name] = irTypeToJsonSchema(field.type);
|
|
108
|
+
if (!field.nullable && !field.default_value) createRequired.push(field.name);
|
|
109
|
+
}
|
|
110
|
+
schemas[`Create${model.name}Input`] = {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: createProps,
|
|
113
|
+
...(createRequired.length > 0 ? { required: createRequired } : {}),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Update input schema (all optional)
|
|
117
|
+
const updateProps: Record<string, unknown> = {};
|
|
118
|
+
for (const field of model.fields) {
|
|
119
|
+
if (["id", "created_at", "updated_at"].includes(field.name)) continue;
|
|
120
|
+
if (field.default_value?.startsWith("GENERATED ALWAYS")) continue;
|
|
121
|
+
updateProps[field.name] = irTypeToJsonSchema(field.type);
|
|
122
|
+
}
|
|
123
|
+
schemas[`Update${model.name}Input`] = {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: updateProps,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Event payload schemas
|
|
131
|
+
for (const ev of system.events) {
|
|
132
|
+
const props: Record<string, unknown> = {};
|
|
133
|
+
for (const field of ev.payload) {
|
|
134
|
+
props[field.name] = irTypeToJsonSchema(field.type);
|
|
135
|
+
}
|
|
136
|
+
schemas[`${ev.name}Payload`] = { type: "object", properties: props };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Build paths ──────────────────────────────────────────────────────────
|
|
140
|
+
const paths: Record<string, unknown> = {};
|
|
141
|
+
|
|
142
|
+
for (const mod of apiModules) {
|
|
143
|
+
const model = mod.models[0];
|
|
144
|
+
const basePath = `/${toSnakeCase(model.name)}s`;
|
|
145
|
+
const tag = mod.name;
|
|
146
|
+
|
|
147
|
+
// GET / — list
|
|
148
|
+
paths[basePath] = {
|
|
149
|
+
get: {
|
|
150
|
+
tags: [tag],
|
|
151
|
+
summary: `List ${model.name}s`,
|
|
152
|
+
security: [{ bearerAuth: [] }],
|
|
153
|
+
parameters: [
|
|
154
|
+
{ name: "page", in: "query", schema: { type: "integer", minimum: 1, default: 1 } },
|
|
155
|
+
{ name: "page_size", in: "query", schema: { type: "integer", minimum: 1, maximum: 100, default: 50 } },
|
|
156
|
+
],
|
|
157
|
+
responses: {
|
|
158
|
+
"200": {
|
|
159
|
+
description: `List of ${model.name}s`,
|
|
160
|
+
content: {
|
|
161
|
+
"application/json": {
|
|
162
|
+
schema: {
|
|
163
|
+
allOf: [
|
|
164
|
+
{ $ref: "#/components/schemas/PaginatedResult" },
|
|
165
|
+
{ properties: { items: { type: "array", items: { $ref: `#/components/schemas/${model.name}` } } } },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
post: {
|
|
175
|
+
tags: [tag],
|
|
176
|
+
summary: `Create ${model.name}`,
|
|
177
|
+
security: [{ bearerAuth: [] }],
|
|
178
|
+
requestBody: {
|
|
179
|
+
required: true,
|
|
180
|
+
content: {
|
|
181
|
+
"application/json": {
|
|
182
|
+
schema: { $ref: `#/components/schemas/Create${model.name}Input` },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
responses: {
|
|
187
|
+
"201": {
|
|
188
|
+
description: `${model.name} created`,
|
|
189
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${model.name}` } } },
|
|
190
|
+
},
|
|
191
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
192
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// GET /:id, PUT /:id, DELETE /:id
|
|
198
|
+
const idPath = `${basePath}/{id}`;
|
|
199
|
+
paths[idPath] = {
|
|
200
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }],
|
|
201
|
+
get: {
|
|
202
|
+
tags: [tag],
|
|
203
|
+
summary: `Get ${model.name} by id`,
|
|
204
|
+
security: [{ bearerAuth: [] }],
|
|
205
|
+
responses: {
|
|
206
|
+
"200": { description: `${model.name}`, content: { "application/json": { schema: { $ref: `#/components/schemas/${model.name}` } } } },
|
|
207
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
208
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
put: {
|
|
212
|
+
tags: [tag],
|
|
213
|
+
summary: `Update ${model.name}`,
|
|
214
|
+
security: [{ bearerAuth: [] }],
|
|
215
|
+
requestBody: {
|
|
216
|
+
required: true,
|
|
217
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/Update${model.name}Input` } } },
|
|
218
|
+
},
|
|
219
|
+
responses: {
|
|
220
|
+
"200": { description: `Updated ${model.name}`, content: { "application/json": { schema: { $ref: `#/components/schemas/${model.name}` } } } },
|
|
221
|
+
"400": { $ref: "#/components/responses/BadRequest" },
|
|
222
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
223
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
delete: {
|
|
227
|
+
tags: [tag],
|
|
228
|
+
summary: `Delete ${model.name}`,
|
|
229
|
+
security: [{ bearerAuth: [] }],
|
|
230
|
+
responses: {
|
|
231
|
+
"204": { description: "Deleted" },
|
|
232
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
233
|
+
"404": { $ref: "#/components/responses/NotFound" },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Capability endpoints
|
|
239
|
+
for (const iface of mod.interfaces) {
|
|
240
|
+
for (const method of iface.methods) {
|
|
241
|
+
if (["create", "read", "update", "delete", "list"].includes(method.name)) continue;
|
|
242
|
+
|
|
243
|
+
const capPath = `${basePath}/${method.name.replace(/_/g, "-")}`;
|
|
244
|
+
const inputProps: Record<string, unknown> = {};
|
|
245
|
+
for (const param of method.input) {
|
|
246
|
+
inputProps[param.name] = irTypeToJsonSchema(param.type);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
paths[capPath] = {
|
|
250
|
+
post: {
|
|
251
|
+
tags: [tag],
|
|
252
|
+
summary: method.name.replace(/_/g, " "),
|
|
253
|
+
description: [
|
|
254
|
+
method.preconditions.length > 0 ? `**Preconditions:** ${method.preconditions.map(p => p.description).join("; ")}` : "",
|
|
255
|
+
method.effects.length > 0 ? `**Effects:** ${method.effects.map(e => `${e.target} ${e.op === "assign" ? "=" : e.op === "add" ? "+=" : "-="} ${e.value}`).join("; ")}` : "",
|
|
256
|
+
method.sync ? `**Sync:** ${method.sync}` : "",
|
|
257
|
+
].filter(Boolean).join("\n\n"),
|
|
258
|
+
security: [{ bearerAuth: [] }],
|
|
259
|
+
requestBody: {
|
|
260
|
+
required: true,
|
|
261
|
+
content: {
|
|
262
|
+
"application/json": {
|
|
263
|
+
schema: { type: "object", properties: inputProps },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
responses: {
|
|
268
|
+
"200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { ok: { type: "boolean" }, action: { type: "string" } } } } } },
|
|
269
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
270
|
+
"422": { description: "Precondition failed", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Assemble document ────────────────────────────────────────────────────
|
|
279
|
+
const doc = {
|
|
280
|
+
openapi: "3.1.0",
|
|
281
|
+
info: {
|
|
282
|
+
title: system.name,
|
|
283
|
+
version: system.version,
|
|
284
|
+
description: `Generated by BoneScript compiler. Source hash: ${system.source_hash}`,
|
|
285
|
+
},
|
|
286
|
+
servers: [
|
|
287
|
+
{ url: "http://localhost:3000", description: "Local development" },
|
|
288
|
+
],
|
|
289
|
+
tags: apiModules.map(m => ({ name: m.name, description: `${m.name} endpoints` })),
|
|
290
|
+
paths,
|
|
291
|
+
components: {
|
|
292
|
+
securitySchemes: {
|
|
293
|
+
bearerAuth: {
|
|
294
|
+
type: "http",
|
|
295
|
+
scheme: "bearer",
|
|
296
|
+
bearerFormat: "JWT",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
schemas,
|
|
300
|
+
responses: {
|
|
301
|
+
Unauthorized: {
|
|
302
|
+
description: "Authentication required",
|
|
303
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
304
|
+
},
|
|
305
|
+
NotFound: {
|
|
306
|
+
description: "Resource not found",
|
|
307
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
308
|
+
},
|
|
309
|
+
BadRequest: {
|
|
310
|
+
description: "Invalid request",
|
|
311
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return JSON.stringify(doc, null, 2);
|
|
318
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Package Emitter
|
|
3
|
+
* Generates package.json and tsconfig.json for the output project.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as IR from "./ir";
|
|
7
|
+
|
|
8
|
+
function toSnakeCase(s: string): string {
|
|
9
|
+
return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emitPackageJson(system: IR.IRSystem): string {
|
|
13
|
+
const pkg = {
|
|
14
|
+
name: toSnakeCase(system.name),
|
|
15
|
+
version: system.version,
|
|
16
|
+
private: true,
|
|
17
|
+
scripts: {
|
|
18
|
+
build: "tsc",
|
|
19
|
+
start: "node dist/index.js",
|
|
20
|
+
dev: "ts-node src/index.ts",
|
|
21
|
+
migrate: "ts-node src/migrate.ts",
|
|
22
|
+
},
|
|
23
|
+
dependencies: {
|
|
24
|
+
express: "4.18.2",
|
|
25
|
+
pg: "8.11.3",
|
|
26
|
+
ioredis: "5.3.2",
|
|
27
|
+
ws: "8.16.0",
|
|
28
|
+
uuid: "9.0.0",
|
|
29
|
+
cors: "2.8.5",
|
|
30
|
+
helmet: "7.1.0",
|
|
31
|
+
"express-rate-limit": "7.1.5",
|
|
32
|
+
jsonwebtoken: "9.0.2",
|
|
33
|
+
dotenv: "16.3.1",
|
|
34
|
+
},
|
|
35
|
+
devDependencies: {
|
|
36
|
+
"@types/express": "4.17.21",
|
|
37
|
+
"@types/node": "18.19.0",
|
|
38
|
+
"@types/pg": "8.10.9",
|
|
39
|
+
"@types/ws": "8.5.10",
|
|
40
|
+
"@types/cors": "2.8.17",
|
|
41
|
+
"@types/jsonwebtoken": "9.0.5",
|
|
42
|
+
"@types/uuid": "9.0.7",
|
|
43
|
+
typescript: "5.3.3",
|
|
44
|
+
"ts-node": "10.9.2",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
return JSON.stringify(pkg, null, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function emitTsConfig(): string {
|
|
51
|
+
const cfg = {
|
|
52
|
+
compilerOptions: {
|
|
53
|
+
target: "ES2020",
|
|
54
|
+
module: "commonjs",
|
|
55
|
+
lib: ["ES2020"],
|
|
56
|
+
outDir: "./dist",
|
|
57
|
+
rootDir: "./src",
|
|
58
|
+
strict: true,
|
|
59
|
+
esModuleInterop: true,
|
|
60
|
+
skipLibCheck: true,
|
|
61
|
+
forceConsistentCasingInFileNames: true,
|
|
62
|
+
declaration: true,
|
|
63
|
+
sourceMap: true,
|
|
64
|
+
},
|
|
65
|
+
include: ["src/**/*"],
|
|
66
|
+
exclude: ["node_modules", "dist"],
|
|
67
|
+
};
|
|
68
|
+
return JSON.stringify(cfg, null, 2);
|
|
69
|
+
}
|