db-model-router 1.0.4 → 1.0.5

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 (92) hide show
  1. package/README.md +110 -16
  2. package/TODO.md +14 -0
  3. package/dbmr.schema.json +333 -0
  4. package/demo/.dockerignore +7 -0
  5. package/demo/.env.example +13 -0
  6. package/demo/Dockerfile +20 -0
  7. package/demo/app.js +37 -0
  8. package/demo/commons/add_migration.js +43 -0
  9. package/demo/commons/db.js +17 -0
  10. package/demo/commons/migrate.js +65 -0
  11. package/demo/commons/security.js +30 -0
  12. package/demo/commons/session.js +13 -0
  13. package/demo/dbmr.schema.json +362 -0
  14. package/demo/docs/llm.md +197 -0
  15. package/demo/llms.txt +70 -0
  16. package/demo/middleware/logger.js +67 -0
  17. package/demo/migrations/20260430155808_create_migrations_table.sql +6 -0
  18. package/demo/migrations/20260430155809_create_tables.sql +207 -0
  19. package/demo/models/addresses.js +22 -0
  20. package/demo/models/cart_items.js +18 -0
  21. package/demo/models/carts.js +16 -0
  22. package/demo/models/categories.js +20 -0
  23. package/demo/models/coupons.js +23 -0
  24. package/demo/models/order_items.js +21 -0
  25. package/demo/models/orders.js +25 -0
  26. package/demo/models/payments.js +21 -0
  27. package/demo/models/product_images.js +18 -0
  28. package/demo/models/product_reviews.js +20 -0
  29. package/demo/models/product_variants.js +20 -0
  30. package/demo/models/products.js +30 -0
  31. package/demo/models/shipments.js +19 -0
  32. package/demo/models/users.js +19 -0
  33. package/demo/models/wishlists.js +15 -0
  34. package/demo/openapi.json +5872 -0
  35. package/demo/package-lock.json +2810 -0
  36. package/demo/package.json +34 -0
  37. package/demo/routes/addresses.js +6 -0
  38. package/demo/routes/carts/cart_items.js +7 -0
  39. package/demo/routes/carts.js +6 -0
  40. package/demo/routes/categories.js +6 -0
  41. package/demo/routes/coupons.js +6 -0
  42. package/demo/routes/docs.js +18 -0
  43. package/demo/routes/health.js +35 -0
  44. package/demo/routes/index.js +39 -0
  45. package/demo/routes/orders/order_items.js +7 -0
  46. package/demo/routes/orders/payments.js +7 -0
  47. package/demo/routes/orders/shipments.js +7 -0
  48. package/demo/routes/orders.js +6 -0
  49. package/demo/routes/products/product_images.js +7 -0
  50. package/demo/routes/products/product_reviews.js +7 -0
  51. package/demo/routes/products/product_variants.js +7 -0
  52. package/demo/routes/products.js +6 -0
  53. package/demo/routes/users.js +6 -0
  54. package/demo/routes/wishlists.js +6 -0
  55. package/docker-compose.yml +1 -1
  56. package/package.json +8 -7
  57. package/scripts/demo-create.js +47 -0
  58. package/skill/SKILL.md +464 -0
  59. package/skill/references/cockroachdb.md +49 -0
  60. package/skill/references/dynamodb.md +53 -0
  61. package/skill/references/mongodb.md +56 -0
  62. package/skill/references/mssql.md +55 -0
  63. package/skill/references/oracle.md +52 -0
  64. package/skill/references/postgres.md +50 -0
  65. package/skill/references/redis.md +53 -0
  66. package/skill/references/sqlite3.md +43 -0
  67. package/src/cli/commands/generate.js +58 -17
  68. package/src/cli/commands/help.js +11 -6
  69. package/src/cli/commands/init.js +2 -2
  70. package/src/cli/commands/inspect.js +1 -0
  71. package/src/cli/diff-engine.js +52 -22
  72. package/src/cli/generate-docs-route.js +31 -0
  73. package/src/cli/generate-migration.js +356 -0
  74. package/src/cli/generate-route.js +52 -24
  75. package/src/cli/init/dependencies.js +3 -0
  76. package/src/cli/init/generators.js +1 -1
  77. package/src/cli/init.js +8 -8
  78. package/src/cockroachdb/db.js +90 -59
  79. package/src/commons/route.js +20 -20
  80. package/src/commons/validator.js +58 -1
  81. package/src/dynamodb/db.js +50 -27
  82. package/src/mongodb/db.js +1 -0
  83. package/src/mssql/db.js +89 -61
  84. package/src/mysql/db.js +1 -0
  85. package/src/oracle/db.js +1 -0
  86. package/src/postgres/db.js +61 -41
  87. package/src/redis/db.js +1 -0
  88. package/src/schema/schema-parser.js +43 -1
  89. package/src/schema/schema-printer.js +7 -0
  90. package/src/schema/schema-validator.js +17 -0
  91. package/src/sqlite3/db.js +1 -0
  92. package/docs/SKILL.md +0 -419
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Generate database migration SQL/JS from parsed schema tables.
5
+ *
6
+ * Produces CREATE TABLE statements (or equivalent) for each table,
7
+ * with proper column types, constraints, and indexes per adapter.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Column type mapping per adapter
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Map a dbmr column rule to a SQL column type for the given adapter.
16
+ *
17
+ * @param {string} rule - e.g. "required|string", "auto_increment", "integer"
18
+ * @param {string} adapter - e.g. "postgres", "mysql", "sqlite3"
19
+ * @returns {{ sqlType: string, nullable: boolean, isAutoIncrement: boolean }}
20
+ */
21
+ function mapColumnType(rule, adapter) {
22
+ const parts = rule.split("|");
23
+ const isRequired = parts.includes("required");
24
+ const baseType = parts.filter((p) => p !== "required")[0] || "string";
25
+
26
+ let sqlType;
27
+ let isAutoIncrement = false;
28
+
29
+ switch (baseType) {
30
+ case "auto_increment":
31
+ isAutoIncrement = true;
32
+ sqlType = autoIncrementType(adapter);
33
+ break;
34
+ case "string":
35
+ sqlType = stringType(adapter);
36
+ break;
37
+ case "integer":
38
+ sqlType = integerType(adapter);
39
+ break;
40
+ case "numeric":
41
+ sqlType = numericType(adapter);
42
+ break;
43
+ case "boolean":
44
+ sqlType = booleanType(adapter);
45
+ break;
46
+ case "datetime":
47
+ sqlType = datetimeType(adapter);
48
+ break;
49
+ case "object":
50
+ sqlType = jsonType(adapter);
51
+ break;
52
+ default:
53
+ sqlType = stringType(adapter);
54
+ }
55
+
56
+ return {
57
+ sqlType,
58
+ nullable: !isRequired && !isAutoIncrement,
59
+ isAutoIncrement,
60
+ };
61
+ }
62
+
63
+ function autoIncrementType(adapter) {
64
+ switch (adapter) {
65
+ case "postgres":
66
+ case "cockroachdb":
67
+ return "SERIAL";
68
+ case "mssql":
69
+ return "INT IDENTITY(1,1)";
70
+ case "oracle":
71
+ return "NUMBER GENERATED BY DEFAULT AS IDENTITY";
72
+ case "sqlite3":
73
+ return "INTEGER";
74
+ default:
75
+ // mysql, mariadb
76
+ return "INT AUTO_INCREMENT";
77
+ }
78
+ }
79
+
80
+ function stringType(adapter) {
81
+ if (adapter === "oracle") return "VARCHAR2(255)";
82
+ return "VARCHAR(255)";
83
+ }
84
+
85
+ function integerType(adapter) {
86
+ if (adapter === "oracle") return "NUMBER(10)";
87
+ return "INTEGER";
88
+ }
89
+
90
+ function numericType(adapter) {
91
+ if (adapter === "oracle") return "NUMBER(12,2)";
92
+ if (adapter === "mssql") return "DECIMAL(12,2)";
93
+ return "DECIMAL(12,2)";
94
+ }
95
+
96
+ function booleanType(adapter) {
97
+ if (adapter === "oracle") return "NUMBER(1)";
98
+ if (adapter === "mssql") return "BIT";
99
+ if (adapter === "mysql" || adapter === "mariadb") return "TINYINT(1)";
100
+ return "BOOLEAN";
101
+ }
102
+
103
+ function datetimeType(adapter) {
104
+ if (adapter === "mssql") return "DATETIME";
105
+ return "TIMESTAMP";
106
+ }
107
+
108
+ function jsonType(adapter) {
109
+ if (adapter === "postgres" || adapter === "cockroachdb") return "JSONB";
110
+ if (adapter === "oracle") return "CLOB";
111
+ if (adapter === "mssql") return "NVARCHAR(MAX)";
112
+ if (adapter === "sqlite3") return "TEXT";
113
+ return "JSON";
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // SQL migration generators
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Generate a CREATE TABLE SQL statement for a single table.
122
+ *
123
+ * @param {string} tableName
124
+ * @param {object} tableDef - parsed table definition from schema
125
+ * @param {string} adapter
126
+ * @returns {string}
127
+ */
128
+ function generateCreateTableSQL(tableName, tableDef, adapter) {
129
+ const lines = [];
130
+ const pk = tableDef.pk;
131
+
132
+ for (const [colName, rule] of Object.entries(tableDef.columns)) {
133
+ const { sqlType, nullable, isAutoIncrement } = mapColumnType(rule, adapter);
134
+ let line = ` ${quoteIdent(colName, adapter)} ${sqlType}`;
135
+
136
+ if (colName === pk) {
137
+ line += " PRIMARY KEY";
138
+ if (isAutoIncrement && adapter === "sqlite3") {
139
+ // SQLite needs AUTOINCREMENT after PRIMARY KEY for INTEGER type
140
+ line = ` ${quoteIdent(colName, adapter)} INTEGER PRIMARY KEY AUTOINCREMENT`;
141
+ }
142
+ } else {
143
+ if (!nullable) line += " NOT NULL";
144
+ }
145
+
146
+ // Default values for timestamps
147
+ if (tableDef.timestamps) {
148
+ if (
149
+ colName === tableDef.timestamps.created_at ||
150
+ colName === tableDef.timestamps.modified_at
151
+ ) {
152
+ line += defaultTimestamp(adapter);
153
+ }
154
+ }
155
+
156
+ // Default for boolean softDelete column
157
+ if (tableDef.softDelete && colName === tableDef.softDelete) {
158
+ line += defaultBoolean(adapter);
159
+ }
160
+
161
+ lines.push(line);
162
+ }
163
+
164
+ // Unique constraints (excluding PK which is already PRIMARY KEY)
165
+ if (tableDef.unique && tableDef.unique.length > 0) {
166
+ const uniqueCols = tableDef.unique.filter((c) => c !== pk);
167
+ for (const col of uniqueCols) {
168
+ lines.push(` UNIQUE (${quoteIdent(col, adapter)})`);
169
+ }
170
+ }
171
+
172
+ const createPrefix =
173
+ adapter === "oracle" || adapter === "mssql"
174
+ ? `CREATE TABLE ${quoteIdent(tableName, adapter)}`
175
+ : `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableName, adapter)}`;
176
+
177
+ return `${createPrefix} (\n${lines.join(",\n")}\n);\n`;
178
+ }
179
+
180
+ /**
181
+ * Quote an identifier based on adapter.
182
+ */
183
+ function quoteIdent(name, adapter) {
184
+ // Most adapters are fine without quoting for simple names
185
+ // but we keep it safe for reserved words
186
+ if (adapter === "mssql") return `[${name}]`;
187
+ if (adapter === "oracle") return `"${name.toUpperCase()}"`;
188
+ return name;
189
+ }
190
+
191
+ /**
192
+ * Default timestamp expression per adapter.
193
+ */
194
+ function defaultTimestamp(adapter) {
195
+ switch (adapter) {
196
+ case "mssql":
197
+ return " DEFAULT GETDATE()";
198
+ case "oracle":
199
+ return " DEFAULT CURRENT_TIMESTAMP";
200
+ case "sqlite3":
201
+ return " DEFAULT CURRENT_TIMESTAMP";
202
+ default:
203
+ return " DEFAULT CURRENT_TIMESTAMP";
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Default boolean false expression per adapter.
209
+ */
210
+ function defaultBoolean(adapter) {
211
+ if (adapter === "oracle" || adapter === "mssql") return " DEFAULT 0";
212
+ if (adapter === "mysql" || adapter === "mariadb") return " DEFAULT 0";
213
+ return " DEFAULT FALSE";
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // NoSQL migration generators
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * Generate a MongoDB migration JS file for a single table (collection).
222
+ */
223
+ function generateMongoDBMigration(tableName, tableDef) {
224
+ const uniqueCols = (tableDef.unique || []).filter((c) => c !== tableDef.pk);
225
+ const indexLines = uniqueCols
226
+ .map(
227
+ (col) =>
228
+ ` await db.collection("${tableName}").createIndex({ ${col}: 1 }, { unique: true });`,
229
+ )
230
+ .join("\n");
231
+
232
+ return `"use strict";
233
+
234
+ module.exports = {
235
+ async up(db) {
236
+ await db.createCollection("${tableName}");
237
+ ${indexLines ? indexLines + "\n" : ""} },
238
+
239
+ async down(db) {
240
+ await db.collection("${tableName}").drop();
241
+ },
242
+ };
243
+ `;
244
+ }
245
+
246
+ /**
247
+ * Generate a DynamoDB migration JS file for a single table.
248
+ */
249
+ function generateDynamoDBMigration(tableName, tableDef) {
250
+ const pk = tableDef.pk;
251
+ return `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
252
+
253
+ export async function up(db) {
254
+ await db.send(new CreateTableCommand({
255
+ TableName: "${tableName}",
256
+ KeySchema: [{ AttributeName: "${pk}", KeyType: "HASH" }],
257
+ AttributeDefinitions: [{ AttributeName: "${pk}", AttributeType: "N" }],
258
+ BillingMode: "PAY_PER_REQUEST",
259
+ }));
260
+ }
261
+
262
+ export async function down(db) {
263
+ await db.send(new DeleteTableCommand({ TableName: "${tableName}" }));
264
+ }
265
+ `;
266
+ }
267
+
268
+ /**
269
+ * Generate a Redis migration JS file (Redis doesn't need table creation,
270
+ * but we create a placeholder for consistency).
271
+ */
272
+ function generateRedisMigration(tableName) {
273
+ return `"use strict";
274
+
275
+ module.exports = {
276
+ async up(db) {
277
+ // Redis is schema-less. This migration is a placeholder.
278
+ // Data for "${tableName}" will be stored as hash keys: ${tableName}:<id>
279
+ console.log("Redis: ${tableName} collection ready (schema-less).");
280
+ },
281
+
282
+ async down(db) {
283
+ // Warning: this deletes ALL keys matching the pattern
284
+ // In production, use SCAN instead of KEYS
285
+ const keys = await db.keys("${tableName}:*");
286
+ if (keys.length > 0) {
287
+ await db.del(...keys);
288
+ }
289
+ },
290
+ };
291
+ `;
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Public API
296
+ // ---------------------------------------------------------------------------
297
+
298
+ const SQL_ADAPTERS = [
299
+ "mysql",
300
+ "mariadb",
301
+ "postgres",
302
+ "sqlite3",
303
+ "mssql",
304
+ "cockroachdb",
305
+ "oracle",
306
+ ];
307
+
308
+ /**
309
+ * Generate migration file content for all tables in the schema.
310
+ *
311
+ * For SQL adapters: produces a single .sql file with all CREATE TABLE statements.
312
+ * For NoSQL adapters: produces one .js file per table.
313
+ *
314
+ * @param {object} schema - parsed schema from parseSchema()
315
+ * @returns {Array<{ filename: string, content: string }>}
316
+ */
317
+ function generateMigrationFiles(schema) {
318
+ const adapter = schema.adapter;
319
+ const tables = schema.tables;
320
+ const tableNames = Object.keys(tables).sort();
321
+
322
+ if (SQL_ADAPTERS.includes(adapter)) {
323
+ // Single SQL file with all CREATE TABLE statements
324
+ const statements = [];
325
+ for (const name of tableNames) {
326
+ statements.push(generateCreateTableSQL(name, tables[name], adapter));
327
+ }
328
+ const content = statements.join("\n");
329
+ return [{ filename: `create_tables.sql`, content }];
330
+ }
331
+
332
+ // NoSQL: one file per table
333
+ const files = [];
334
+ for (const name of tableNames) {
335
+ let content;
336
+ if (adapter === "mongodb") {
337
+ content = generateMongoDBMigration(name, tables[name]);
338
+ } else if (adapter === "dynamodb") {
339
+ content = generateDynamoDBMigration(name, tables[name]);
340
+ } else if (adapter === "redis") {
341
+ content = generateRedisMigration(name);
342
+ } else {
343
+ // Fallback
344
+ content = `// Migration for ${name}\n`;
345
+ }
346
+ files.push({ filename: `create_${name}.js`, content });
347
+ }
348
+
349
+ return files;
350
+ }
351
+
352
+ module.exports = {
353
+ generateMigrationFiles,
354
+ generateCreateTableSQL,
355
+ mapColumnType,
356
+ };
@@ -58,8 +58,15 @@ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
58
58
  /**
59
59
  * Generate the routes index file that mounts all routes on an express Router.
60
60
  * Supports parent-child nesting: parent/:pk/child
61
+ *
62
+ * Child routes are placed in subfolders: routes/<parent>/<child>.js
63
+ * Children are only mounted under their parent path (no duplicate top-level route).
64
+ *
65
+ * @param {string[]} tableNames
66
+ * @param {Array<{parent, child, foreignKey}>} relationships
67
+ * @param {{ includeDocs?: boolean }} [options]
61
68
  */
62
- function generateRoutesIndexFile(tableNames, relationships = []) {
69
+ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
63
70
  let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
64
71
 
65
72
  // Collect child tables that are nested under parents
@@ -68,35 +75,42 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
68
75
  nestedChildren.add(rel.child);
69
76
  }
70
77
 
78
+ // Import top-level routes only (not children)
71
79
  for (const table of tableNames) {
80
+ if (nestedChildren.has(table)) continue;
72
81
  const varName = safeVarName(table);
73
82
  imports += `import ${varName}Route from "./${table}.js";\n`;
74
83
  }
75
- // Import child routes with _child suffix for nested ones
84
+
85
+ // Import child routes from subfolders
76
86
  for (const rel of relationships) {
77
87
  const varName = safeVarName(rel.child);
78
- imports += `import ${varName}ChildRoute from "./${rel.child}_child_of_${rel.parent}.js";\n`;
88
+ imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
89
+ }
90
+
91
+ // Import docs route if openapi is generated
92
+ if (options.includeDocs) {
93
+ imports += `import docsRoute from "./docs.js";\n`;
79
94
  }
80
95
 
81
96
  imports += "\n";
82
97
 
83
- // Mount top-level routes (skip tables that are ONLY children)
98
+ // Mount docs route first
99
+ if (options.includeDocs) {
100
+ imports += `router.use("/docs", docsRoute);\n`;
101
+ }
102
+
103
+ // Mount top-level routes
84
104
  for (const table of tableNames) {
85
105
  if (nestedChildren.has(table)) continue;
86
106
  const varName = safeVarName(table);
87
107
  imports += `router.use("/${table}", ${varName}Route);\n`;
88
108
  }
89
109
 
90
- // Mount nested child routes under parent
110
+ // Mount child routes under parent path
91
111
  for (const rel of relationships) {
92
112
  const childVar = safeVarName(rel.child);
93
- imports += `router.use("/${rel.parent}/:${rel.fkColumn}/${rel.child}", ${childVar}ChildRoute);\n`;
94
- }
95
-
96
- // Also mount children as top-level for direct access
97
- for (const rel of relationships) {
98
- const varName = safeVarName(rel.child);
99
- imports += `router.use("/${rel.child}", ${varName}Route);\n`;
113
+ imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
100
114
  }
101
115
 
102
116
  imports += "\nexport default router;\n";
@@ -387,7 +401,7 @@ async function main() {
387
401
  const fkColumn = parent.replace(/s$/, "") + "_id";
388
402
  // Only add if both tables exist in our model set
389
403
  if (tableNames.includes(parent) && tableNames.includes(child)) {
390
- relationships.push({ parent, child, fkColumn });
404
+ relationships.push({ parent, child, foreignKey: fkColumn });
391
405
  }
392
406
  }
393
407
  }
@@ -398,23 +412,36 @@ async function main() {
398
412
  fs.mkdirSync(routesDir, { recursive: true });
399
413
  }
400
414
 
415
+ // Collect child tables to skip top-level route files
416
+ const nestedChildren = new Set();
417
+ for (const rel of relationships) {
418
+ nestedChildren.add(rel.child);
419
+ }
420
+
401
421
  for (const table of tableNames) {
422
+ if (nestedChildren.has(table)) continue;
402
423
  const filePath = path.join(routesDir, table + ".js");
403
424
  fs.writeFileSync(filePath, generateRouteFile(table, modelsRelPath));
404
425
  console.log(` Created ${filePath}`);
405
426
  }
406
427
 
407
- // Write child route files for parent-child relationships
428
+ // Write child route files in subfolders: routes/<parent>/<child>.js
408
429
  for (const rel of relationships) {
409
- const fileName = `${rel.child}_child_of_${rel.parent}.js`;
410
- const filePath = path.join(routesDir, fileName);
430
+ const parentDir = path.join(routesDir, rel.parent);
431
+ if (!fs.existsSync(parentDir)) {
432
+ fs.mkdirSync(parentDir, { recursive: true });
433
+ }
434
+ const filePath = path.join(parentDir, `${rel.child}.js`);
435
+ const childModelsRelPath = path
436
+ .relative(parentDir, path.resolve(modelsDir))
437
+ .replace(/\\/g, "/");
411
438
  fs.writeFileSync(
412
439
  filePath,
413
440
  generateChildRouteFile(
414
441
  rel.child,
415
442
  rel.parent,
416
- rel.fkColumn,
417
- modelsRelPath,
443
+ rel.foreignKey,
444
+ childModelsRelPath,
418
445
  ),
419
446
  );
420
447
  console.log(` Created ${filePath}`);
@@ -473,7 +500,7 @@ async function main() {
473
500
  console.log(` Created ${testPath}`);
474
501
  }
475
502
 
476
- // Generate child route test files
503
+ // Generate child route test files in subfolders
477
504
  for (const rel of relationships) {
478
505
  let pk = "id";
479
506
  const modelPath = path.join(modelsDir, rel.child + ".js");
@@ -484,13 +511,14 @@ async function main() {
484
511
  );
485
512
  if (meta && meta.primary_key) pk = meta.primary_key;
486
513
  }
487
- const testPath = path.join(
488
- testsDir,
489
- `${rel.child}_child_of_${rel.parent}.test.js`,
490
- );
514
+ const parentTestDir = path.join(testsDir, rel.parent);
515
+ if (!fs.existsSync(parentTestDir)) {
516
+ fs.mkdirSync(parentTestDir, { recursive: true });
517
+ }
518
+ const testPath = path.join(parentTestDir, `${rel.child}.test.js`);
491
519
  fs.writeFileSync(
492
520
  testPath,
493
- generateChildTestFile(rel.child, rel.parent, rel.fkColumn, pk),
521
+ generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
494
522
  );
495
523
  console.log(` Created ${testPath}`);
496
524
  }
@@ -63,6 +63,9 @@ function collectDependencies(answers) {
63
63
  // Dev dependencies
64
64
  devDependencies["nodemon"] = "latest";
65
65
 
66
+ // Swagger UI for API documentation
67
+ dependencies["swagger-ui-express"] = "latest";
68
+
66
69
  return { dependencies, devDependencies };
67
70
  }
68
71
 
@@ -1715,7 +1715,7 @@ function generateAppJsV2(answers, outputDir) {
1715
1715
  answers.framework === "ultimate-express" ? "ultimate-express" : "express";
1716
1716
 
1717
1717
  const commonsPrefix = outputDir ? `./${outputDir}/commons` : "./commons";
1718
- const routePrefix = outputDir ? `./${outputDir}/route` : "./route";
1718
+ const routePrefix = outputDir ? `./${outputDir}/routes` : "./routes";
1719
1719
  const middlewarePrefix = outputDir
1720
1720
  ? `./${outputDir}/middleware`
1721
1721
  : "./middleware";
package/src/cli/init.js CHANGED
@@ -95,7 +95,7 @@ function generateFiles(answers, outputDir) {
95
95
  path.join(srcBase, "middleware"),
96
96
  path.join(srcBase, "migrations"),
97
97
  path.join(srcBase, "commons"),
98
- path.join(srcBase, "route"),
98
+ path.join(srcBase, "routes"),
99
99
  ];
100
100
  // SQLite3 needs a data/ folder for the database file
101
101
  if (answers.database === "sqlite3") {
@@ -189,15 +189,15 @@ function generateFiles(answers, outputDir) {
189
189
  if (safeWriteFile(dbPath, generateDbModule(answers)))
190
190
  files.push(path.join(srcBase, "commons/db.js"));
191
191
 
192
- // route/health.js
193
- const healthPath = path.join(srcBase, "route", "health.js");
192
+ // routes/health.js
193
+ const healthPath = path.join(srcBase, "routes", "health.js");
194
194
  if (safeWriteFile(healthPath, generateHealthRoute()))
195
- files.push(path.join(srcBase, "route/health.js"));
195
+ files.push(path.join(srcBase, "routes/health.js"));
196
196
 
197
- // route/index.js
198
- const routeIndexPath = path.join(srcBase, "route", "index.js");
197
+ // routes/index.js
198
+ const routeIndexPath = path.join(srcBase, "routes", "index.js");
199
199
  if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
200
- files.push(path.join(srcBase, "route/index.js"));
200
+ files.push(path.join(srcBase, "routes/index.js"));
201
201
 
202
202
  // Initial migration (inside outputDir/migrations)
203
203
  const initialMigration = generateInitialMigration(answers);
@@ -365,7 +365,7 @@ Options:
365
365
  --db <name> Alias for --database
366
366
  --session <type> Session store: memory, redis, database
367
367
  --output <dir> Directory for backend source files (e.g. --output backend).
368
- package.json stays in root; index.js, commons/, route/,
368
+ package.json stays in root; index.js, commons/, routes/,
369
369
  middleware/, and migrations/ go inside the output folder.
370
370
  --rateLimiting Enable rate limiting (express-rate-limit)
371
371
  --helmet Enable Helmet security headers