db-model-router 1.0.6 → 1.0.8

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 (140) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +15 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +17 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +19 -0
  62. package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260510092159_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2827 -0
  88. package/demo/package.json +42 -0
  89. package/demo/routes/addresses/index.js +10 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +11 -0
  92. package/demo/routes/carts/index.js +14 -0
  93. package/demo/routes/categories/index.js +10 -0
  94. package/demo/routes/coupons/index.js +10 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +18 -0
  99. package/demo/routes/orders/order_items/index.js +11 -0
  100. package/demo/routes/orders/payments/index.js +11 -0
  101. package/demo/routes/orders/shipments/index.js +11 -0
  102. package/demo/routes/products/index.js +18 -0
  103. package/demo/routes/products/product_images/index.js +11 -0
  104. package/demo/routes/products/product_reviews/index.js +11 -0
  105. package/demo/routes/products/product_variants/index.js +11 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +10 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +66 -27
  120. package/src/cli/generate-saas-structure.js +129 -0
  121. package/src/cli/init/dependencies.js +1 -1
  122. package/src/cli/init/generators.js +6 -77
  123. package/src/cli/init.js +9 -2
  124. package/src/cli/main.js +8 -1
  125. package/src/cli/saas/generate-saas-middleware.js +110 -0
  126. package/src/cli/saas/generate-saas-migrations.js +480 -0
  127. package/src/cli/saas/generate-saas-models.js +211 -0
  128. package/src/cli/saas/generate-saas-openapi.js +419 -0
  129. package/src/cli/saas/generate-saas-routes.js +435 -0
  130. package/src/cli/saas/generate-saas-seeds.js +243 -0
  131. package/src/cli/saas/generate-saas-tests.js +473 -0
  132. package/src/cli/saas/generate-saas-utils.js +176 -0
  133. package/src/commons/kafka.js +139 -0
  134. package/src/commons/model.js +29 -9
  135. package/src/commons/route.js +6 -6
  136. package/src/index.js +2 -0
  137. package/src/mssql/db.js +41 -3
  138. package/src/mysql/db.js +3 -0
  139. package/src/postgres/db.js +6 -0
  140. package/src/cli/generate-db-manager.js +0 -1573
@@ -26,17 +26,63 @@ function safeVarName(name) {
26
26
  function generateRouteFile(tableName, modelsRelPath) {
27
27
  const varName = safeVarName(tableName);
28
28
  return `import dbModelRouter from "db-model-router";
29
- import ${varName} from "${modelsRelPath}/${tableName}.js";
29
+ import express from "express";
30
+ import { ${varName} } from "#models";
31
+
32
+ const router = express.Router({ mergeParams: true });
33
+ const { route } = dbModelRouter;
34
+
35
+ router.use("/", route(${varName}));
36
+
37
+ export default router;
38
+ `;
39
+ }
40
+
41
+ /**
42
+ * Generate a parent route file that includes its own CRUD and mounts child routes.
43
+ * e.g., routes/orders/index.js mounts order_items under /:order_id/items
44
+ *
45
+ * @param {string} tableName - Parent table name
46
+ * @param {Array<{child, foreignKey}>} children - Child relationships for this parent
47
+ * @returns {string}
48
+ */
49
+ function generateParentRouteFile(tableName, children) {
50
+ const varName = safeVarName(tableName);
51
+ let code = `import dbModelRouter from "db-model-router";
52
+ import express from "express";
53
+ import { ${varName} } from "#models";
54
+ `;
55
+
56
+ // Import child routes
57
+ for (const child of children) {
58
+ const childVar = safeVarName(child.child);
59
+ code += `import ${childVar}Route from "./${child.child}/index.js";\n`;
60
+ }
30
61
 
62
+ code += `
63
+ const router = express.Router({ mergeParams: true });
31
64
  const { route } = dbModelRouter;
32
65
 
33
- export default route(${varName});
34
66
  `;
67
+
68
+ // Mount child routes BEFORE own CRUD to prevent path clashing
69
+ for (const child of children) {
70
+ const childVar = safeVarName(child.child);
71
+ code += `router.use("/:${child.foreignKey}/${child.child}", ${childVar}Route);\n`;
72
+ }
73
+
74
+ code += `
75
+ // CRUD routes for ${tableName}
76
+ router.use("/", route(${varName}));
77
+
78
+ export default router;
79
+ `;
80
+ return code;
35
81
  }
36
82
 
37
83
  /**
38
84
  * Generate a child route file that scopes queries by parent FK.
39
- * e.g., posts/:post_id/comments — filters comments where post_id = :post_id
85
+ * e.g., routes/orders/items/index.js — filters items where order_id = :order_id
40
86
  */
41
87
  function generateChildRouteFile(
42
88
  childTable,
@@ -46,12 +92,16 @@ function generateChildRouteFile(
46
92
  ) {
47
93
  const varName = safeVarName(childTable);
48
94
  return `import dbModelRouter from "db-model-router";
49
- import ${varName} from "${modelsRelPath}/${childTable}.js";
95
+ import express from "express";
96
+ import { ${varName} } from "#models";
50
97
 
98
+ const router = express.Router({ mergeParams: true });
51
99
  const { route } = dbModelRouter;
52
100
 
53
101
  // Child route: scoped by parent ${parentTable} via ${fkColumn}
54
- export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
102
+ router.use("/", route(${varName}, { ${fkColumn}: "params.${fkColumn}" }));
103
+
104
+ export default router;
55
105
  `;
56
106
  }
57
107
 
@@ -67,7 +117,7 @@ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
67
117
  * @param {{ includeDocs?: boolean }} [options]
68
118
  */
69
119
  function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
70
- let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
120
+ let imports = `import express from "express";\n\nconst router = express.Router({ mergeParams: true });\n\n`;
71
121
 
72
122
  // Collect child tables that are nested under parents
73
123
  const nestedChildren = new Set();
@@ -75,17 +125,11 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
75
125
  nestedChildren.add(rel.child);
76
126
  }
77
127
 
78
- // Import top-level routes only (not children)
128
+ // Import top-level routes only (children are mounted inside parent folders)
79
129
  for (const table of tableNames) {
80
130
  if (nestedChildren.has(table)) continue;
81
131
  const varName = safeVarName(table);
82
- imports += `import ${varName}Route from "./${table}.js";\n`;
83
- }
84
-
85
- // Import child routes from subfolders
86
- for (const rel of relationships) {
87
- const varName = safeVarName(rel.child);
88
- imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
132
+ imports += `import ${varName}Route from "./${table}/index.js";\n`;
89
133
  }
90
134
 
91
135
  // Import docs route if openapi is generated
@@ -100,13 +144,7 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
100
144
  imports += `router.use("/docs", docsRoute);\n`;
101
145
  }
102
146
 
103
- // Mount child routes BEFORE parent routes to prevent path clashing
104
- for (const rel of relationships) {
105
- const childVar = safeVarName(rel.child);
106
- imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
107
- }
108
-
109
- // Mount top-level routes
147
+ // Mount top-level routes (children are already mounted inside their parent's index.js)
110
148
  for (const table of tableNames) {
111
149
  if (nestedChildren.has(table)) continue;
112
150
  const varName = safeVarName(table);
@@ -133,13 +171,13 @@ function generateTestFile(tableName, pk) {
133
171
  return `import assert from "assert";
134
172
  import express from "express";
135
173
  import request from "supertest";
174
+ import "dotenv/config";
175
+ import "#commons/db.js";
136
176
  import dbModelRouter from "db-model-router";
177
+ import { ${varName} } from "#models";
137
178
 
138
179
  const { route } = dbModelRouter;
139
180
 
140
- // Adjust the path to your model file as needed
141
- import ${varName} from "../models/${tableName}.js";
142
-
143
181
  function createApp() {
144
182
  const app = express();
145
183
  app.use(express.json());
@@ -167,7 +205,7 @@ describe("${tableName} routes", function () {
167
205
  const res = await request(app)
168
206
  .post("/${tableName}/add")
169
207
  .send({});
170
- assert.ok([200, 201, 400].includes(res.status));
208
+ assert.ok([200, 201, 400, 422, 500].includes(res.status));
171
209
  });
172
210
  });
173
211
 
@@ -176,7 +214,7 @@ describe("${tableName} routes", function () {
176
214
  const res = await request(app)
177
215
  .post("/${tableName}/")
178
216
  .send({ data: [] });
179
- assert.ok([200, 201, 400].includes(res.status));
217
+ assert.ok([200, 201, 400, 422, 500].includes(res.status));
180
218
  });
181
219
  });
182
220
 
@@ -217,7 +255,7 @@ describe("${tableName} routes", function () {
217
255
  const res = await request(app)
218
256
  .put("/${tableName}/")
219
257
  .send({ data: [] });
220
- assert.ok([200, 400].includes(res.status));
258
+ assert.ok([200, 400, 422, 500].includes(res.status));
221
259
  });
222
260
  });
223
261
 
@@ -606,6 +644,7 @@ if (require.main === module) {
606
644
 
607
645
  module.exports = {
608
646
  generateRouteFile,
647
+ generateParentRouteFile,
609
648
  generateChildRouteFile,
610
649
  generateRoutesIndexFile,
611
650
  generateTestFile,
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const { generateSaasMigrations } = require("./saas/generate-saas-migrations");
7
+ const { generateSaasModels } = require("./saas/generate-saas-models");
8
+ const {
9
+ generateAuthenticateMiddleware,
10
+ generateTenantIsolationMiddleware,
11
+ generateHasPermissionMiddleware,
12
+ } = require("./saas/generate-saas-middleware");
13
+ const {
14
+ generateCrudRoutes,
15
+ generateAuthRoutes,
16
+ generateRoutesIndex,
17
+ } = require("./saas/generate-saas-routes");
18
+ const { generateSaasSeeds } = require("./saas/generate-saas-seeds");
19
+ const {
20
+ generatePasswordUtil,
21
+ generateModulesUtil,
22
+ generateWebhookUtil,
23
+ } = require("./saas/generate-saas-utils");
24
+ const { generateSaasTests } = require("./saas/generate-saas-tests");
25
+
26
+ /**
27
+ * Read the existing .gitignore file and append `credentials.md` if not already present.
28
+ * Returns the full .gitignore content to be written.
29
+ *
30
+ * @returns {string} Updated .gitignore content
31
+ */
32
+ function getGitignoreContent() {
33
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
34
+ let content = "";
35
+ if (fs.existsSync(gitignorePath)) {
36
+ content = fs.readFileSync(gitignorePath, "utf8");
37
+ }
38
+ if (!content.includes("credentials.md")) {
39
+ content = content.trimEnd() + "\ncredentials.md\n";
40
+ }
41
+ return content;
42
+ }
43
+
44
+ /**
45
+ * Main SaaS structure generator orchestrator.
46
+ *
47
+ * Calls all sub-generators and aggregates their output into a single
48
+ * planned[] array of { relPath, content } objects compatible with the
49
+ * existing generate command's file write loop.
50
+ *
51
+ * @param {string} adapter - Database adapter name (e.g. "postgres", "mysql", "sqlite3")
52
+ * @param {object} [options] - Generation options
53
+ * @param {boolean} [options.dryRun] - If true, files will not be written (handled by caller)
54
+ * @param {boolean} [options.json] - If true, output JSON format (handled by caller)
55
+ * @param {Date|number} [options.timestamp] - Base timestamp for migration files
56
+ * @param {string[]} [options.tableNames] - Schema-generated table names for routes index
57
+ * @param {Array<{parent, child, foreignKey}>} [options.relationships] - Schema relationships for routes index
58
+ * @param {{ includeDocs?: boolean }} [options.routeOptions] - Options for routes index generation
59
+ * @returns {Array<{ relPath: string, content: string }>} Combined planned file array
60
+ */
61
+ function generateSaasStructure(adapter, options) {
62
+ const opts = options || {};
63
+ const planned = [];
64
+
65
+ // 1. Migrations
66
+ const timestamp = opts.timestamp || new Date();
67
+ const migrations = generateSaasMigrations(adapter, timestamp);
68
+ for (const entry of migrations) {
69
+ planned.push(entry);
70
+ }
71
+
72
+ // 2. Models (includes models/index.js barrel with both SaaS + dbmr tables)
73
+ const models = generateSaasModels(adapter, opts.tableNames || []);
74
+ for (const entry of models) {
75
+ planned.push(entry);
76
+ }
77
+
78
+ // 3. Middleware
79
+ planned.push(generateAuthenticateMiddleware());
80
+ planned.push(generateTenantIsolationMiddleware());
81
+ planned.push(generateHasPermissionMiddleware());
82
+
83
+ // 4. Routes (CRUD + auth + combined index with dbmr routes)
84
+ const crudRoutes = generateCrudRoutes();
85
+ for (const entry of crudRoutes) {
86
+ planned.push(entry);
87
+ }
88
+ planned.push(generateAuthRoutes());
89
+ planned.push(
90
+ generateRoutesIndex(
91
+ opts.tableNames || [],
92
+ opts.relationships || [],
93
+ opts.routeOptions || {},
94
+ ),
95
+ );
96
+
97
+ // 5. Seeds
98
+ const seeds = generateSaasSeeds(adapter);
99
+ for (const entry of seeds) {
100
+ planned.push(entry);
101
+ }
102
+
103
+ // 6. Utilities
104
+ planned.push({
105
+ relPath: "commons/password.js",
106
+ content: generatePasswordUtil(),
107
+ });
108
+ planned.push({
109
+ relPath: "commons/modules.js",
110
+ content: generateModulesUtil(),
111
+ });
112
+ planned.push({
113
+ relPath: "commons/webhook.js",
114
+ content: generateWebhookUtil(),
115
+ });
116
+
117
+ // 7. Tests
118
+ const tests = generateSaasTests();
119
+ for (const entry of tests) {
120
+ planned.push(entry);
121
+ }
122
+
123
+ // 8. .gitignore update (add credentials.md)
124
+ planned.push({ relPath: ".gitignore", content: getGitignoreContent() });
125
+
126
+ return planned;
127
+ }
128
+
129
+ module.exports = { generateSaasStructure, getGitignoreContent };
@@ -79,7 +79,7 @@ function getScripts(outputDir) {
79
79
  return {
80
80
  start: "node app.js",
81
81
  dev: "nodemon app.js",
82
- test: 'echo "Error: no test specified" && exit 1',
82
+ test: "dotenv -- mocha --exit",
83
83
  migrate: `node ${prefix}commons/migrate.js`,
84
84
  add_migration: `node ${prefix}commons/add_migration.js`,
85
85
  "docker:build": "docker build -t app .",
@@ -169,8 +169,10 @@ function buildEnvContent(answers, mode, secrets) {
169
169
  const lines = [];
170
170
  lines.push("# Server");
171
171
  lines.push("PORT=3000");
172
+ lines.push("API_BASE_PATH=/api");
172
173
  lines.push("");
173
174
  lines.push("# Database");
175
+ lines.push(`DB_TYPE=${answers.database}`);
174
176
 
175
177
  const vars = DB_ENV_MAP[answers.database] || [];
176
178
  for (const v of vars) {
@@ -357,79 +359,6 @@ app.use(session({
357
359
  }));`;
358
360
  }
359
361
 
360
- /**
361
- * Generate the app.js file content.
362
- * @param {import('./types').InitAnswers} answers
363
- * @returns {string}
364
- */
365
- function generateAppJs(answers) {
366
- const frameworkPkg =
367
- answers.framework === "ultimate-express" ? "ultimate-express" : "express";
368
-
369
- // Imports
370
- let imports = `import express from "${frameworkPkg}";
371
- import { init, db } from "db-model-router";
372
- import session from "express-session";`;
373
-
374
- if (answers.session === "redis") {
375
- imports += `\nimport RedisStore from "connect-redis";
376
- import { Redis } from "ioredis";`;
377
- }
378
- if (answers.rateLimiting) {
379
- imports += `\nimport rateLimit from "express-rate-limit";`;
380
- }
381
- if (answers.helmet) {
382
- imports += `\nimport helmet from "helmet";`;
383
- }
384
- imports += `\nimport logger from "./middleware/logger.js";`;
385
-
386
- // Rate limiting block
387
- const rateLimitBlock = answers.rateLimiting
388
- ? `app.use(rateLimit({
389
- windowMs: 15 * 60 * 1000,
390
- max: 100,
391
- standardHeaders: true,
392
- legacyHeaders: false,
393
- }));`
394
- : "";
395
-
396
- const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
397
-
398
- return `${imports}
399
- import "dotenv/config";
400
-
401
- // Initialize database adapter
402
- init("${answers.database}");
403
- ${dbConnectBlock(answers.database)}
404
-
405
- const app = express();
406
- const PORT = process.env.PORT || 3000;
407
-
408
- // Middleware
409
- app.use(express.json());
410
- app.use(express.urlencoded({ extended: true }));
411
- ${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
412
- app.use(logger);
413
-
414
- // Health check
415
- app.get("/health", (req, res) => {
416
- res.json({ status: "ok", timestamp: new Date().toISOString() });
417
- });
418
-
419
- // Error handler
420
- app.use((err, req, res, next) => {
421
- console.error(err.stack);
422
- res.status(500).json({ type: "danger", message: "Internal Server Error" });
423
- });
424
-
425
- app.listen(PORT, () => {
426
- console.log(\`Server running on port \${PORT}\`);
427
- });
428
-
429
- export default app;
430
- `;
431
- }
432
-
433
362
  // ---------------------------------------------------------------------------
434
363
  // Logger middleware generator
435
364
  // ---------------------------------------------------------------------------
@@ -1715,7 +1644,7 @@ export default db;
1715
1644
  * @param {string} [outputDir] - relative output directory for source files (e.g. "backend")
1716
1645
  * @returns {string}
1717
1646
  */
1718
- function generateAppJsV2(answers, outputDir) {
1647
+ function generateAppJs(answers, outputDir) {
1719
1648
  const frameworkPkg =
1720
1649
  answers.framework === "ultimate-express" ? "ultimate-express" : "express";
1721
1650
 
@@ -1730,8 +1659,9 @@ import "${commonsPrefix}/db.js";
1730
1659
  import configureSession from "${commonsPrefix}/session.js";
1731
1660
  import applySecurity from "${commonsPrefix}/security.js";
1732
1661
  import logger from "${middlewarePrefix}/logger.js";
1733
- import route from "${routePrefix}/index.js";
1662
+ import routes from "${routePrefix}/index.js";
1734
1663
  import { fileURLToPath } from 'node:url';
1664
+ import path from "path";
1735
1665
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
1736
1666
  const app = express();
1737
1667
  const PORT = process.env.PORT || 3000;
@@ -1750,7 +1680,7 @@ app.use(configureSession());
1750
1680
  app.use(logger);
1751
1681
 
1752
1682
  // Routes
1753
- app.use(route);
1683
+ app.use(process.env.API_BASE_PATH || "/api", routes);
1754
1684
 
1755
1685
  // Error handler
1756
1686
  app.use((err, req, res, next) => {
@@ -1771,7 +1701,6 @@ module.exports = {
1771
1701
  isSql,
1772
1702
  randomPassword,
1773
1703
  generateAppJs,
1774
- generateAppJsV2,
1775
1704
  generateEnvFile,
1776
1705
  generateEnvExample,
1777
1706
  generateLoggerMiddleware,
package/src/cli/init.js CHANGED
@@ -7,7 +7,6 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const {
9
9
  generateAppJs,
10
- generateAppJsV2,
11
10
  generateEnvFile,
12
11
  generateEnvExample,
13
12
  generateLoggerMiddleware,
@@ -109,7 +108,7 @@ function generateFiles(answers, outputDir) {
109
108
 
110
109
  // Root-level files (always in cwd, not in outputDir)
111
110
  // app.js uses the v2 generator that links commons/route modules
112
- if (safeWriteFile("app.js", generateAppJsV2(answers, outputDir || "")))
111
+ if (safeWriteFile("app.js", generateAppJs(answers, outputDir || "")))
113
112
  files.push("app.js");
114
113
  if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
115
114
  files.push(".env");
@@ -254,6 +253,14 @@ function updatePackageJson(answers, outputDir) {
254
253
  const scripts = getScripts(outputDir);
255
254
 
256
255
  pkg.type = "module";
256
+ pkg.imports = {
257
+ "#root/*.js": "./*.js",
258
+ "#models": "./models/index.js",
259
+ "#models/*.js": "./models/*.js",
260
+ "#routes/*.js": "./routes/*.js",
261
+ "#commons/*.js": "./commons/*.js",
262
+ "#middleware/*.js": "./middleware/*.js",
263
+ };
257
264
  pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
258
265
  pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
259
266
  pkg.devDependencies = Object.assign(
package/src/cli/main.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  const { parseFlags, OutputContext } = require("./flags");
@@ -9,6 +9,7 @@ const generateCmd = require("./commands/generate");
9
9
  const doctorCmd = require("./commands/doctor");
10
10
  const diffCmd = require("./commands/diff");
11
11
  const helpCmd = require("./commands/help");
12
+ const dbManagerCmd = require("./commands/db-manager");
12
13
 
13
14
  /**
14
15
  * Map of subcommand names to their handler functions.
@@ -19,6 +20,7 @@ const COMMANDS = {
19
20
  generate: generateCmd,
20
21
  doctor: doctorCmd,
21
22
  diff: diffCmd,
23
+ "db-manager": dbManagerCmd,
22
24
  help: helpCmd,
23
25
  };
24
26
 
@@ -31,6 +33,7 @@ const COMMAND_DESCRIPTIONS = {
31
33
  generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
32
34
  doctor: "Validate schema, check dependencies, and verify file sync",
33
35
  diff: "Preview changes between current files and what the schema would produce",
36
+ "db-manager": "Start a live database management UI",
34
37
  help: "Show help for a command",
35
38
  };
36
39
 
@@ -68,6 +71,10 @@ const COMMAND_FLAGS = {
68
71
  ],
69
72
  doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
70
73
  diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
74
+ "db-manager": [
75
+ ["--env <path>", "Path to .env file (default: .env)"],
76
+ ["--port <number>", "Server port (default: 4000)"],
77
+ ],
71
78
  };
72
79
 
73
80
  /**
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SaaS middleware generators.
5
+ *
6
+ * Each function returns a { relPath, content } object for a middleware file.
7
+ * Generated code uses ES6 module syntax (import/export).
8
+ */
9
+
10
+ /**
11
+ * Generate the authenticate middleware file.
12
+ *
13
+ * @returns {{ relPath: string, content: string }}
14
+ */
15
+ function generateAuthenticateMiddleware() {
16
+ const relPath = "middleware/authenticate.js";
17
+ const content = `/**
18
+ * Authentication middleware.
19
+ *
20
+ * Validates that the request has an active session with a user object.
21
+ * Responds with 401 Unauthorized if no valid session exists.
22
+ */
23
+ function authenticate(req, res, next) {
24
+ if (!req.session || !req.session.user) {
25
+ return res.status(401).json({ message: "Unauthorized" });
26
+ }
27
+ next();
28
+ }
29
+
30
+ export default authenticate;
31
+ `;
32
+ return { relPath, content };
33
+ }
34
+
35
+ /**
36
+ * Generate the tenant isolation middleware file.
37
+ *
38
+ * @returns {{ relPath: string, content: string }}
39
+ */
40
+ function generateTenantIsolationMiddleware() {
41
+ const relPath = "middleware/tenantIsolation.js";
42
+ const content = `/**
43
+ * Tenant isolation middleware.
44
+ *
45
+ * Restricts data access to the user's own tenant unless the user
46
+ * has a global-scoped permission. Injects tenant_id into query
47
+ * and body parameters for non-global users.
48
+ */
49
+ function tenantIsolation(req, res, next) {
50
+ const hasGlobal = req.session.permission.some((p) => p.scope === "global");
51
+ if (!hasGlobal) {
52
+ if (!req.query) req.query = {};
53
+ if (!req.body) req.body = {};
54
+ req.query.tenant_id = req.session.user.tenant_id;
55
+ req.body.tenant_id = req.session.user.tenant_id;
56
+ }
57
+ next();
58
+ }
59
+
60
+ export default tenantIsolation;
61
+ `;
62
+ return { relPath, content };
63
+ }
64
+
65
+ /**
66
+ * Generate the hasPermission middleware file.
67
+ *
68
+ * @returns {{ relPath: string, content: string }}
69
+ */
70
+ function generateHasPermissionMiddleware() {
71
+ const relPath = "middleware/hasPermission.js";
72
+ const content = `import { isValidModule } from "#commons/modules.js";
73
+
74
+ /**
75
+ * Permission validation middleware factory.
76
+ *
77
+ * Returns a middleware function that checks whether the authenticated user
78
+ * has the required permission for the specified module and action.
79
+ * A permission entry with action "global" grants access to any action
80
+ * on that module.
81
+ *
82
+ * @param {string} module - The module name to check permission for
83
+ * @param {string} action - The required action
84
+ * @returns {function} Express middleware function
85
+ */
86
+ function hasPermission(module, action) {
87
+ return (req, res, next) => {
88
+ if (!isValidModule(module)) {
89
+ return res.status(403).json({ message: "Invalid module" });
90
+ }
91
+ const match = req.session.permission.find(
92
+ (p) => p.module === module && (p.action === action || p.action === "global")
93
+ );
94
+ if (!match) {
95
+ return res.status(403).json({ message: "Forbidden" });
96
+ }
97
+ next();
98
+ };
99
+ }
100
+
101
+ export default hasPermission;
102
+ `;
103
+ return { relPath, content };
104
+ }
105
+
106
+ module.exports = {
107
+ generateAuthenticateMiddleware,
108
+ generateTenantIsolationMiddleware,
109
+ generateHasPermissionMiddleware,
110
+ };