bonescript-compiler 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli.js +17 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/emit_admin.d.ts +7 -0
  4. package/dist/emit_admin.js +130 -0
  5. package/dist/emit_admin.js.map +1 -0
  6. package/dist/emit_audit.d.ts +7 -0
  7. package/dist/emit_audit.js +89 -0
  8. package/dist/emit_audit.js.map +1 -0
  9. package/dist/emit_full.js +22 -0
  10. package/dist/emit_full.js.map +1 -1
  11. package/dist/emit_openapi.d.ts +7 -0
  12. package/dist/emit_openapi.js +333 -0
  13. package/dist/emit_openapi.js.map +1 -0
  14. package/dist/emit_postman.d.ts +6 -0
  15. package/dist/emit_postman.js +126 -0
  16. package/dist/emit_postman.js.map +1 -0
  17. package/dist/emit_runtime.js +30 -6
  18. package/dist/emit_runtime.js.map +1 -1
  19. package/dist/emit_sdk.d.ts +7 -0
  20. package/dist/emit_sdk.js +162 -0
  21. package/dist/emit_sdk.js.map +1 -0
  22. package/dist/emit_seed.d.ts +6 -0
  23. package/dist/emit_seed.js +88 -0
  24. package/dist/emit_seed.js.map +1 -0
  25. package/dist/emit_zod.d.ts +7 -0
  26. package/dist/emit_zod.js +115 -0
  27. package/dist/emit_zod.js.map +1 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +19 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/lowering.js +5 -2
  32. package/dist/lowering.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/cli.ts +14 -1
  35. package/src/emit_admin.ts +131 -0
  36. package/src/emit_audit.ts +112 -0
  37. package/src/emit_full.ts +29 -0
  38. package/src/emit_openapi.ts +344 -0
  39. package/src/emit_postman.ts +145 -0
  40. package/src/emit_runtime.ts +31 -6
  41. package/src/emit_sdk.ts +195 -0
  42. package/src/emit_seed.ts +91 -0
  43. package/src/emit_zod.ts +111 -0
  44. package/src/index.ts +9 -0
  45. package/src/lowering.ts +5 -2
@@ -0,0 +1,344 @@
1
+ /**
2
+ * BoneScript OpenAPI Emitter
3
+ * Generates OpenAPI 3.0.3 YAML and JSON specs from an IRSystem.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ function toSnakeCase(s: string): string {
11
+ return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
12
+ }
13
+
14
+ function toDashCase(s: string): string {
15
+ return toSnakeCase(s).replace(/_/g, "-");
16
+ }
17
+
18
+ function toPascalCase(s: string): string {
19
+ return s.replace(/(^|_)([a-z])/g, (_: string, _p: string, c: string) => c.toUpperCase());
20
+ }
21
+
22
+ function irTypeToOpenApi(irType: string): Record<string, unknown> {
23
+ if (irType === "string") return { type: "string" };
24
+ if (irType === "uint" || irType === "int") return { type: "integer" };
25
+ if (irType === "float") return { type: "number" };
26
+ if (irType === "bool") return { type: "boolean" };
27
+ if (irType === "timestamp") return { type: "string", format: "date-time" };
28
+ if (irType === "uuid") return { type: "string", format: "uuid" };
29
+ if (irType === "bytes") return { type: "string", format: "byte" };
30
+ if (irType === "json") return { type: "object" };
31
+ const listMatch = irType.match(/^list<(.+)>$/);
32
+ if (listMatch) return { type: "array", items: irTypeToOpenApi(listMatch[1]) };
33
+ const setMatch = irType.match(/^set<(.+)>$/);
34
+ if (setMatch) return { type: "array", items: irTypeToOpenApi(setMatch[1]) };
35
+ const optMatch = irType.match(/^optional<(.+)>$/);
36
+ if (optMatch) return { ...irTypeToOpenApi(optMatch[1]), nullable: true };
37
+ return { type: "string" };
38
+ }
39
+
40
+ function ind(n: number): string {
41
+ return " ".repeat(n);
42
+ }
43
+
44
+ function yamlValue(v: unknown, depth: number): string {
45
+ if (v === null || v === undefined) return "null";
46
+ if (typeof v === "boolean") return String(v);
47
+ if (typeof v === "number") return String(v);
48
+ if (typeof v === "string") {
49
+ if (
50
+ v.includes(":") ||
51
+ v.includes("#") ||
52
+ v.includes("'") ||
53
+ v.startsWith("{") ||
54
+ v.startsWith("[")
55
+ ) {
56
+ return JSON.stringify(v);
57
+ }
58
+ return v;
59
+ }
60
+ if (Array.isArray(v)) {
61
+ if (v.length === 0) return "[]";
62
+ return (
63
+ "\n" +
64
+ v
65
+ .map((item) => ind(depth) + "- " + yamlValue(item, depth + 1))
66
+ .join("\n")
67
+ );
68
+ }
69
+ if (typeof v === "object") {
70
+ const entries = Object.entries(v as Record<string, unknown>);
71
+ if (entries.length === 0) return "{}";
72
+ return (
73
+ "\n" +
74
+ entries
75
+ .map(([k, val]) => {
76
+ const valStr = yamlValue(val, depth + 1);
77
+ if (valStr.startsWith("\n")) {
78
+ return ind(depth) + k + ":" + valStr;
79
+ }
80
+ return ind(depth) + k + ": " + valStr;
81
+ })
82
+ .join("\n")
83
+ );
84
+ }
85
+ return String(v);
86
+ }
87
+
88
+ function objToYaml(obj: Record<string, unknown>, depth = 0): string {
89
+ const lines: string[] = [];
90
+ for (const [k, v] of Object.entries(obj)) {
91
+ const valStr = yamlValue(v, depth + 1);
92
+ if (valStr.startsWith("\n")) {
93
+ lines.push(ind(depth) + k + ":" + valStr);
94
+ } else {
95
+ lines.push(ind(depth) + k + ": " + valStr);
96
+ }
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+
101
+ // ─── Spec builder ─────────────────────────────────────────────────────────────
102
+
103
+ function buildSpec(system: IR.IRSystem): Record<string, unknown> {
104
+ const paths: Record<string, unknown> = {};
105
+ const schemas: Record<string, unknown> = {};
106
+
107
+ for (const mod of system.modules) {
108
+ if (mod.kind !== "api_service" || mod.models.length === 0) continue;
109
+
110
+ const model = mod.models[0];
111
+ const tableName = toSnakeCase(model.name);
112
+ const modelName = toPascalCase(model.name);
113
+ const collectionPath = "/" + tableName + "s";
114
+ const itemPath = "/" + tableName + "s/{id}";
115
+
116
+ const allMethods: IR.IRMethod[] = mod.interfaces.flatMap((i) => i.methods);
117
+ const crudNames = new Set(["create", "read", "update", "delete", "list"]);
118
+ const capabilityMethods = allMethods.filter(
119
+ (m) => !crudNames.has(m.name.toLowerCase())
120
+ );
121
+
122
+ const securityRef = [{ BearerAuth: [] }];
123
+
124
+ const listOp: Record<string, unknown> = {
125
+ summary: "List " + modelName,
126
+ operationId: "list" + modelName,
127
+ tags: [modelName],
128
+ parameters: [
129
+ { name: "page", in: "query", schema: { type: "integer", default: 1 } },
130
+ {
131
+ name: "page_size",
132
+ in: "query",
133
+ schema: { type: "integer", default: 50 },
134
+ },
135
+ ],
136
+ responses: {
137
+ "200": {
138
+ description: "List of " + modelName,
139
+ content: {
140
+ "application/json": {
141
+ schema: {
142
+ type: "object",
143
+ properties: {
144
+ items: {
145
+ type: "array",
146
+ items: { $ref: "#/components/schemas/" + modelName },
147
+ },
148
+ total: { type: "integer" },
149
+ page: { type: "integer" },
150
+ page_size: { type: "integer" },
151
+ },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ "401": { description: "Unauthorized" },
157
+ },
158
+ };
159
+
160
+ const createOp: Record<string, unknown> = {
161
+ summary: "Create " + modelName,
162
+ operationId: "create" + modelName,
163
+ tags: [modelName],
164
+ security: securityRef,
165
+ requestBody: {
166
+ required: true,
167
+ content: {
168
+ "application/json": {
169
+ schema: { $ref: "#/components/schemas/" + modelName },
170
+ },
171
+ },
172
+ },
173
+ responses: {
174
+ "200": {
175
+ description: "Created",
176
+ content: {
177
+ "application/json": {
178
+ schema: { $ref: "#/components/schemas/" + modelName },
179
+ },
180
+ },
181
+ },
182
+ "401": { description: "Unauthorized" },
183
+ "422": { description: "Precondition failed" },
184
+ "400": { description: "Bad request" },
185
+ },
186
+ };
187
+
188
+ paths[collectionPath] = { get: listOp, post: createOp };
189
+
190
+ const idParam = [
191
+ {
192
+ name: "id",
193
+ in: "path",
194
+ required: true,
195
+ schema: { type: "string", format: "uuid" },
196
+ },
197
+ ];
198
+
199
+ paths[itemPath] = {
200
+ get: {
201
+ summary: "Get " + modelName,
202
+ operationId: "get" + modelName,
203
+ tags: [modelName],
204
+ parameters: idParam,
205
+ security: securityRef,
206
+ responses: {
207
+ "200": {
208
+ description: "Found",
209
+ content: {
210
+ "application/json": {
211
+ schema: { $ref: "#/components/schemas/" + modelName },
212
+ },
213
+ },
214
+ },
215
+ "401": { description: "Unauthorized" },
216
+ "400": { description: "Not found" },
217
+ },
218
+ },
219
+ put: {
220
+ summary: "Update " + modelName,
221
+ operationId: "update" + modelName,
222
+ tags: [modelName],
223
+ parameters: idParam,
224
+ security: securityRef,
225
+ requestBody: {
226
+ required: true,
227
+ content: {
228
+ "application/json": {
229
+ schema: { $ref: "#/components/schemas/" + modelName },
230
+ },
231
+ },
232
+ },
233
+ responses: {
234
+ "200": {
235
+ description: "Updated",
236
+ content: {
237
+ "application/json": {
238
+ schema: { $ref: "#/components/schemas/" + modelName },
239
+ },
240
+ },
241
+ },
242
+ "401": { description: "Unauthorized" },
243
+ "422": { description: "Precondition failed" },
244
+ "400": { description: "Bad request" },
245
+ },
246
+ },
247
+ delete: {
248
+ summary: "Delete " + modelName,
249
+ operationId: "delete" + modelName,
250
+ tags: [modelName],
251
+ parameters: idParam,
252
+ security: securityRef,
253
+ responses: {
254
+ "200": { description: "Deleted" },
255
+ "401": { description: "Unauthorized" },
256
+ "400": { description: "Not found" },
257
+ },
258
+ },
259
+ };
260
+
261
+ for (const method of capabilityMethods) {
262
+ const capPath = collectionPath + "/" + toDashCase(method.name);
263
+ const capOp: Record<string, unknown> = {
264
+ summary: method.name + " on " + modelName,
265
+ operationId: method.name + modelName,
266
+ tags: [modelName],
267
+ requestBody: {
268
+ required: true,
269
+ content: {
270
+ "application/json": {
271
+ schema: { $ref: "#/components/schemas/" + modelName },
272
+ },
273
+ },
274
+ },
275
+ responses: {
276
+ "200": {
277
+ description: "Success",
278
+ content: {
279
+ "application/json": {
280
+ schema: {
281
+ type: "object",
282
+ properties: {
283
+ ok: { type: "boolean" },
284
+ action: { type: "string" },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ },
290
+ "401": { description: "Unauthorized" },
291
+ "422": { description: "Precondition failed" },
292
+ "400": { description: "Bad request" },
293
+ },
294
+ };
295
+ if (method.authenticated) {
296
+ capOp.security = securityRef;
297
+ }
298
+ paths[capPath] = { post: capOp };
299
+ }
300
+
301
+ const properties: Record<string, unknown> = {};
302
+ for (const field of model.fields) {
303
+ properties[field.name] = irTypeToOpenApi(field.type);
304
+ }
305
+ schemas[modelName] = {
306
+ type: "object",
307
+ properties,
308
+ };
309
+ }
310
+
311
+ return {
312
+ openapi: "3.0.3",
313
+ info: {
314
+ title: system.name,
315
+ version: system.version,
316
+ description: "Generated by BoneScript compiler",
317
+ },
318
+ servers: [{ url: "http://localhost:3000" }],
319
+ paths,
320
+ components: {
321
+ securitySchemes: {
322
+ BearerAuth: {
323
+ type: "http",
324
+ scheme: "bearer",
325
+ bearerFormat: "JWT",
326
+ },
327
+ },
328
+ schemas,
329
+ },
330
+ };
331
+ }
332
+
333
+ // ─── Public API ───────────────────────────────────────────────────────────────
334
+
335
+ export function emitOpenApiSpec(system: IR.IRSystem): string {
336
+ const spec = buildSpec(system);
337
+ const lines: string[] = ["# Generated by BoneScript compiler"];
338
+ lines.push(objToYaml(spec));
339
+ return lines.join("\n") + "\n";
340
+ }
341
+
342
+ export function emitOpenApiJson(system: IR.IRSystem): string {
343
+ return JSON.stringify(buildSpec(system), null, 2);
344
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * BoneScript Postman Collection Emitter
3
+ * Generates a Postman Collection v2.1 JSON from an IRSystem.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ function toSnakeCase(s: string): string {
11
+ return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
12
+ }
13
+
14
+ function toDashCase(s: string): string {
15
+ return toSnakeCase(s).replace(/_/g, "-");
16
+ }
17
+
18
+ function sampleValue(irType: string): unknown {
19
+ if (irType === "string") return "example";
20
+ if (irType === "uint" || irType === "int") return 1;
21
+ if (irType === "float") return 1.0;
22
+ if (irType === "bool") return true;
23
+ if (irType === "uuid") return "00000000-0000-0000-0000-000000000001";
24
+ if (irType === "timestamp") return "2024-01-01T00:00:00.000Z";
25
+ if (irType === "bytes") return "";
26
+ if (irType === "json") return {};
27
+ const listMatch = irType.match(/^list<(.+)>$/);
28
+ if (listMatch) return [];
29
+ const setMatch = irType.match(/^set<(.+)>$/);
30
+ if (setMatch) return [];
31
+ const optMatch = irType.match(/^optional<(.+)>$/);
32
+ if (optMatch) return null;
33
+ return "example";
34
+ }
35
+
36
+ function buildSampleBody(model: IR.IRModel): Record<string, unknown> {
37
+ const body: Record<string, unknown> = {};
38
+ for (const field of model.fields) {
39
+ body[field.name] = sampleValue(field.type);
40
+ }
41
+ return body;
42
+ }
43
+
44
+ function makeRequest(
45
+ name: string,
46
+ method: string,
47
+ url: string,
48
+ body?: Record<string, unknown>
49
+ ): Record<string, unknown> {
50
+ const headers = [
51
+ { key: "Content-Type", value: "application/json" },
52
+ { key: "Authorization", value: "Bearer {{token}}" },
53
+ ];
54
+
55
+ const req: Record<string, unknown> = {
56
+ name,
57
+ request: {
58
+ method,
59
+ header: headers,
60
+ url: {
61
+ raw: url,
62
+ host: ["{{baseUrl}}"],
63
+ path: url
64
+ .replace("{{baseUrl}}/", "")
65
+ .split("/")
66
+ .filter(Boolean),
67
+ },
68
+ },
69
+ };
70
+
71
+ if (body !== undefined) {
72
+ (req.request as Record<string, unknown>).body = {
73
+ mode: "raw",
74
+ raw: JSON.stringify(body, null, 2),
75
+ options: { raw: { language: "json" } },
76
+ };
77
+ }
78
+
79
+ return req;
80
+ }
81
+
82
+ // ─── Public API ───────────────────────────────────────────────────────────────
83
+
84
+ export function emitPostmanCollection(system: IR.IRSystem): string {
85
+ const folders: unknown[] = [];
86
+
87
+ for (const mod of system.modules) {
88
+ if (mod.kind !== "api_service" || mod.models.length === 0) continue;
89
+
90
+ const model = mod.models[0];
91
+ const tableName = toSnakeCase(model.name);
92
+ const baseUrl = `{{baseUrl}}/${tableName}s`;
93
+ const sampleBody = buildSampleBody(model);
94
+
95
+ const items: unknown[] = [
96
+ makeRequest(`List ${model.name}s`, "GET", baseUrl),
97
+ makeRequest(`Create ${model.name}`, "POST", baseUrl, sampleBody),
98
+ makeRequest(`Get ${model.name}`, "GET", `${baseUrl}/:id`),
99
+ makeRequest(`Update ${model.name}`, "PUT", `${baseUrl}/:id`, sampleBody),
100
+ makeRequest(`Delete ${model.name}`, "DELETE", `${baseUrl}/:id`),
101
+ ];
102
+
103
+ const crudNames = new Set(["create", "read", "update", "delete", "list"]);
104
+ const allMethods: IR.IRMethod[] = mod.interfaces.flatMap((i) => i.methods);
105
+ const capabilityMethods = allMethods.filter(
106
+ (m) => !crudNames.has(m.name.toLowerCase())
107
+ );
108
+
109
+ for (const method of capabilityMethods) {
110
+ const dashName = toDashCase(method.name);
111
+ items.push(
112
+ makeRequest(
113
+ method.name + " " + model.name,
114
+ "POST",
115
+ `${baseUrl}/${dashName}`,
116
+ sampleBody
117
+ )
118
+ );
119
+ }
120
+
121
+ folders.push({
122
+ name: mod.name,
123
+ item: items,
124
+ });
125
+ }
126
+
127
+ const collection = {
128
+ info: {
129
+ name: system.name,
130
+ schema:
131
+ "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
132
+ },
133
+ auth: {
134
+ type: "bearer",
135
+ bearer: [{ key: "token", value: "{{token}}", type: "string" }],
136
+ },
137
+ variable: [
138
+ { key: "baseUrl", value: "http://localhost:3000" },
139
+ { key: "token", value: "" },
140
+ ],
141
+ item: folders,
142
+ };
143
+
144
+ return JSON.stringify(collection, null, 2);
145
+ }
@@ -217,6 +217,11 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
217
217
  lines.push(`import { query, queryOne, execute, pool } from "../db";`);
218
218
  lines.push(`import { eventBus } from "../events";`);
219
219
  lines.push(`import { requireAuth, AuthContext } from "../auth";`);
220
+ lines.push(`import rateLimit from "express-rate-limit";`);
221
+ // Only import audit if module has audit: true
222
+ if (mod.config["audit"]) {
223
+ lines.push(`import { auditLog } from "../audit";`);
224
+ }
220
225
  lines.push(`import { logger } from "../logger";`);
221
226
  lines.push(`import { counter } from "../metrics";`);
222
227
  lines.push(`import * as __algorithms from "../algorithms";`);
@@ -242,10 +247,22 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
242
247
  lines.push(`export const ${toCamelCase(routeBase)}Router = Router();`);
243
248
  lines.push(``);
244
249
 
250
+ // Per-module rate limiter (from policy declaration)
251
+ const modRateLimit = typeof mod.config["rate_limit"] === "number" && (mod.config["rate_limit"] as number) > 0
252
+ ? mod.config["rate_limit"] as number : 0;
253
+ const modRateLimitWindowMs = typeof mod.config["rate_limit_window_ms"] === "number"
254
+ ? mod.config["rate_limit_window_ms"] as number : 60000;
255
+ if (modRateLimit > 0) {
256
+ lines.push(`// Rate limiter from policy declaration`);
257
+ lines.push(`const __routeRateLimit = rateLimit({ windowMs: ${modRateLimitWindowMs}, max: ${modRateLimit}, standardHeaders: true, legacyHeaders: false });`);
258
+ lines.push(``);
259
+ }
260
+
245
261
  // CREATE
246
262
  const insertFields = entityModel.fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at");
247
263
  lines.push(`// CREATE`);
248
- lines.push(`${toCamelCase(routeBase)}Router.post("/", requireAuth, async (req: Request, res: Response) => {`);
264
+ const __crudMiddlewares = modRateLimit > 0 ? "__routeRateLimit, requireAuth" : "requireAuth";
265
+ lines.push(`${toCamelCase(routeBase)}Router.post("/", ${__crudMiddlewares}, async (req: Request, res: Response) => {`);
249
266
  lines.push(` try {`);
250
267
  lines.push(` const id = uuid();`);
251
268
  lines.push(` const { ${insertFields.map(f => f.name).join(", ")} } = req.body;`);
@@ -270,7 +287,7 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
270
287
 
271
288
  // READ
272
289
  lines.push(`// READ`);
273
- lines.push(`${toCamelCase(routeBase)}Router.get("/:id", requireAuth, async (req: Request, res: Response) => {`);
290
+ lines.push(`${toCamelCase(routeBase)}Router.get("/:id", ${__crudMiddlewares}, async (req: Request, res: Response) => {`);
274
291
  lines.push(` try {`);
275
292
  lines.push(` const row = await queryOne(\`SELECT * FROM ${tableName} WHERE id = $1\`, [req.params.id]);`);
276
293
  lines.push(` if (!row) return res.status(404).json({ error: { code: "NOT_FOUND", message: "Not found" } });`);
@@ -284,7 +301,7 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
284
301
  // LIST — with optional JOINs for has_one/belongs_to relations
285
302
  const joinRelations = mod.relations.filter(r => r.kind === "has_one" || r.kind === "belongs_to");
286
303
  lines.push(`// LIST`);
287
- lines.push(`${toCamelCase(routeBase)}Router.get("/", requireAuth, async (req: Request, res: Response) => {`);
304
+ lines.push(`${toCamelCase(routeBase)}Router.get("/", ${__crudMiddlewares}, async (req: Request, res: Response) => {`);
288
305
  lines.push(` try {`);
289
306
  lines.push(` const page = parseInt(req.query.page as string) || 1;`);
290
307
  lines.push(` const pageSize = Math.min(parseInt(req.query.page_size as string) || 50, 100);`);
@@ -314,7 +331,7 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
314
331
 
315
332
  // UPDATE — with state machine enforcement
316
333
  lines.push(`// UPDATE`);
317
- lines.push(`${toCamelCase(routeBase)}Router.put("/:id", requireAuth, async (req: Request, res: Response) => {`);
334
+ lines.push(`${toCamelCase(routeBase)}Router.put("/:id", ${__crudMiddlewares}, async (req: Request, res: Response) => {`);
318
335
  lines.push(` const fields = { ...req.body };`);
319
336
  if (mod.state_machines.length > 0) {
320
337
  const sm = mod.state_machines[0];
@@ -339,7 +356,7 @@ export function emitEntityRouter(mod: IR.IRModule, system: IR.IRSystem): string
339
356
 
340
357
  // DELETE
341
358
  lines.push(`// DELETE`);
342
- lines.push(`${toCamelCase(routeBase)}Router.delete("/:id", requireAuth, async (req: Request, res: Response) => {`);
359
+ lines.push(`${toCamelCase(routeBase)}Router.delete("/:id", ${__crudMiddlewares}, async (req: Request, res: Response) => {`);
343
360
  lines.push(` try {`);
344
361
  lines.push(` const count = await execute(\`DELETE FROM ${tableName} WHERE id = $1\`, [req.params.id]);`);
345
362
  lines.push(` if (count === 0) return res.status(404).json({ error: { code: "NOT_FOUND", message: "Not found" } });`);
@@ -400,7 +417,15 @@ function emitCapabilityEndpoint(method: IR.IRMethod, mod: IR.IRModule, tableName
400
417
  const isTransactional = method.sync === "transactional";
401
418
 
402
419
  lines.push(`// CAPABILITY: ${method.name}${isTransactional ? " [transactional]" : ""}${method.retry ? ` [retry: ${method.retry.max_attempts}x ${method.retry.backoff}]` : ""}`);
403
- lines.push(`${routerName}.post("${endpoint}", requireAuth, async (req: Request, res: Response) => {`);
420
+ // Build middleware chain: optional rate limiter + requireAuth + optional audit
421
+ const capMiddlewares: string[] = ["requireAuth"];
422
+ if (typeof mod.config["rate_limit"] === "number" && (mod.config["rate_limit"] as number) > 0) {
423
+ capMiddlewares.unshift("__routeRateLimit");
424
+ }
425
+ if (mod.config["audit"]) {
426
+ capMiddlewares.push(`auditLog("${method.name}", "${mod.models[0]?.name ?? ""}")`);
427
+ }
428
+ lines.push(`${routerName}.post("${endpoint}", ${capMiddlewares.join(", ")}, async (req: Request, res: Response) => {`);
404
429
  lines.push(` const auth: AuthContext = (req as any).auth;`);
405
430
 
406
431
  // Wrap in retry logic if declared