db-model-router 1.0.7 → 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 (38) hide show
  1. package/demo/.env.example +1 -0
  2. package/demo/app.js +2 -2
  3. package/demo/commons/db.js +0 -11
  4. package/demo/middleware/tenantIsolation.js +2 -0
  5. package/demo/package-lock.json +20 -3
  6. package/demo/package.json +1 -2
  7. package/demo/routes/addresses/index.js +5 -1
  8. package/demo/routes/auth/index.js +1 -1
  9. package/demo/routes/carts/cart_items/index.js +5 -1
  10. package/demo/routes/carts/index.js +9 -1
  11. package/demo/routes/categories/index.js +5 -1
  12. package/demo/routes/coupons/index.js +5 -1
  13. package/demo/routes/index.js +1 -1
  14. package/demo/routes/orders/index.js +13 -1
  15. package/demo/routes/orders/order_items/index.js +5 -1
  16. package/demo/routes/orders/payments/index.js +5 -1
  17. package/demo/routes/orders/shipments/index.js +5 -1
  18. package/demo/routes/products/index.js +13 -1
  19. package/demo/routes/products/product_images/index.js +5 -1
  20. package/demo/routes/products/product_reviews/index.js +5 -1
  21. package/demo/routes/products/product_variants/index.js +5 -1
  22. package/demo/routes/roles/index.js +1 -1
  23. package/demo/routes/tenants/index.js +1 -1
  24. package/demo/routes/users/index.js +1 -1
  25. package/demo/routes/wishlists/index.js +5 -1
  26. package/demo/seeds/saas-seed.js +1 -1
  27. package/package.json +1 -1
  28. package/src/cli/generate-route.js +6 -6
  29. package/src/cli/generate-saas-structure.js +8 -1
  30. package/src/cli/init/dependencies.js +1 -1
  31. package/src/cli/init/generators.js +4 -81
  32. package/src/cli/init.js +1 -2
  33. package/src/cli/saas/generate-saas-middleware.js +2 -0
  34. package/src/cli/saas/generate-saas-tests.js +473 -0
  35. package/src/commons/route.js +6 -6
  36. /package/demo/migrations/{20260509170349_create_migrations_table.sql → 20260510092158_create_migrations_table.sql} +0 -0
  37. /package/demo/migrations/{20260509170349_create_saas_tables.sql → 20260510092159_create_saas_tables.sql} +0 -0
  38. /package/demo/migrations/{20260509170349_create_tables.sql → 20260510092159_create_tables.sql} +0 -0
package/demo/.env.example CHANGED
@@ -3,6 +3,7 @@ PORT=3000
3
3
  API_BASE_PATH=/api
4
4
 
5
5
  # Database
6
+ DB_TYPE=sqlite3
6
7
  DB_NAME=./data/data.db
7
8
 
8
9
  # Session
package/demo/app.js CHANGED
@@ -3,7 +3,7 @@ import "./commons/db.js";
3
3
  import configureSession from "./commons/session.js";
4
4
  import applySecurity from "./commons/security.js";
5
5
  import logger from "./middleware/logger.js";
6
- import route from "./routes/index.js";
6
+ import routes from "./routes/index.js";
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import path from "path";
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -24,7 +24,7 @@ app.use(configureSession());
24
24
  app.use(logger);
25
25
 
26
26
  // Routes
27
- app.use(route);
27
+ app.use(process.env.API_BASE_PATH || "/api", routes);
28
28
 
29
29
  // Error handler
30
30
  app.use((err, req, res, next) => {
@@ -9,17 +9,6 @@ dbModelRouter.db.connect({
9
9
  database: process.env.DB_NAME || "./data/data.db",
10
10
  });
11
11
 
12
- // Initialize Kafka if KAFKA_BROKER is configured
13
- if (process.env.KAFKA_BROKER) {
14
- dbModelRouter.kafka.init().then((connected) => {
15
- if (connected) {
16
- console.log("[kafka] Producer connected to", process.env.KAFKA_BROKER);
17
- } else {
18
- console.warn("[kafka] Failed to connect to Kafka broker");
19
- }
20
- });
21
- }
22
-
23
12
  // Make db available globally across the application
24
13
  const db = dbModelRouter.db;
25
14
  global.db = db;
@@ -8,6 +8,8 @@
8
8
  function tenantIsolation(req, res, next) {
9
9
  const hasGlobal = req.session.permission.some((p) => p.scope === "global");
10
10
  if (!hasGlobal) {
11
+ if (!req.query) req.query = {};
12
+ if (!req.body) req.body = {};
11
13
  req.query.tenant_id = req.session.user.tenant_id;
12
14
  req.body.tenant_id = req.session.user.tenant_id;
13
15
  }
@@ -575,12 +575,13 @@
575
575
  }
576
576
  },
577
577
  "node_modules/db-model-router": {
578
- "version": "1.0.6",
579
- "resolved": "https://registry.npmjs.org/db-model-router/-/db-model-router-1.0.6.tgz",
580
- "integrity": "sha512-8GUCYQbWqhPpMOuweHJ3eLIzovJQDIiFRYJ2kM52KR/ryt7EM1Pn0tC+rk+kr92hdDnpVy5wXhEwKE8nxOuM1Q==",
578
+ "version": "1.0.7",
579
+ "resolved": "https://registry.npmjs.org/db-model-router/-/db-model-router-1.0.7.tgz",
580
+ "integrity": "sha512-YcvqApBdj0d1AB+lqARKp5gSWBdnPMN3X750dk/7Z7GyL2x1d7WRyijDAxGGeQPxWjiZVMMqmVv0lnk5+vB/hQ==",
581
581
  "license": "Apache-2.0",
582
582
  "dependencies": {
583
583
  "dotenv": "^10.0.0",
584
+ "ejs": "^5.0.2",
584
585
  "inquirer": "^8.2.6",
585
586
  "lodash": "^4.17.21",
586
587
  "node-input-validator": "^4.5.0"
@@ -594,6 +595,7 @@
594
595
  "better-sqlite3": "^12.9.0",
595
596
  "express": "^4.17.2 || ^5.0.0",
596
597
  "ioredis": "^5.10.1",
598
+ "kafkajs": "^2.2.4",
597
599
  "mongodb": "^7.2.0",
598
600
  "mssql": "^12.5.0",
599
601
  "mysql2": "^3.22.3",
@@ -617,6 +619,9 @@
617
619
  "ioredis": {
618
620
  "optional": true
619
621
  },
622
+ "kafkajs": {
623
+ "optional": true
624
+ },
620
625
  "mongodb": {
621
626
  "optional": true
622
627
  },
@@ -749,6 +754,18 @@
749
754
  "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
750
755
  "license": "MIT"
751
756
  },
757
+ "node_modules/ejs": {
758
+ "version": "5.0.2",
759
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz",
760
+ "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==",
761
+ "license": "Apache-2.0",
762
+ "bin": {
763
+ "ejs": "bin/cli.js"
764
+ },
765
+ "engines": {
766
+ "node": ">=0.12.18"
767
+ }
768
+ },
752
769
  "node_modules/emoji-regex": {
753
770
  "version": "8.0.0",
754
771
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
package/demo/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
7
+ "test": "dotenv -- mocha --exit",
8
8
  "start": "node app.js",
9
9
  "dev": "nodemon app.js",
10
10
  "migrate": "node commons/migrate.js",
@@ -33,7 +33,6 @@
33
33
  "better-sqlite3": "latest",
34
34
  "express-rate-limit": "latest",
35
35
  "helmet": "latest",
36
- "kafkajs": "latest",
37
36
  "winston": "latest",
38
37
  "swagger-ui-express": "latest"
39
38
  },
@@ -1,6 +1,10 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { addresses } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
- export default route(addresses);
8
+ router.use("/", route(addresses));
9
+
10
+ export default router;
@@ -3,7 +3,7 @@ import authenticate from "#middleware/authenticate.js";
3
3
  import { verifyPassword } from "#commons/password.js";
4
4
  import { users, roles, role_permissions } from "#models";
5
5
 
6
- const router = express.Router();
6
+ const router = express.Router({ mergeParams: true });
7
7
 
8
8
  // POST /api/auth/login - Authenticate user and create session
9
9
  router.post("/login", async (req, res) => {
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { cart_items } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent carts via cart_id
7
- export default route(cart_items, { cart_id: "params.cart_id" });
9
+ router.use("/", route(cart_items, { cart_id: "params.cart_id" }));
10
+
11
+ export default router;
@@ -1,6 +1,14 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { carts } from "#models";
4
+ import cart_itemsRoute from "./cart_items/index.js";
3
5
 
6
+ const router = express.Router({ mergeParams: true });
4
7
  const { route } = dbModelRouter;
5
8
 
6
- export default route(carts);
9
+ router.use("/:cart_id/cart_items", cart_itemsRoute);
10
+
11
+ // CRUD routes for carts
12
+ router.use("/", route(carts));
13
+
14
+ export default router;
@@ -1,6 +1,10 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { categories } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
- export default route(categories);
8
+ router.use("/", route(categories));
9
+
10
+ export default router;
@@ -1,6 +1,10 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { coupons } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
- export default route(coupons);
8
+ router.use("/", route(coupons));
9
+
10
+ export default router;
@@ -1,6 +1,6 @@
1
1
  import express from "express";
2
2
 
3
- const router = express.Router();
3
+ const router = express.Router({ mergeParams: true });
4
4
 
5
5
  // SaaS auth & CRUD routes
6
6
  import authRoute from "#routes/auth/index.js";
@@ -1,6 +1,18 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { orders } from "#models";
4
+ import order_itemsRoute from "./order_items/index.js";
5
+ import paymentsRoute from "./payments/index.js";
6
+ import shipmentsRoute from "./shipments/index.js";
3
7
 
8
+ const router = express.Router({ mergeParams: true });
4
9
  const { route } = dbModelRouter;
5
10
 
6
- export default route(orders);
11
+ router.use("/:order_id/order_items", order_itemsRoute);
12
+ router.use("/:order_id/payments", paymentsRoute);
13
+ router.use("/:order_id/shipments", shipmentsRoute);
14
+
15
+ // CRUD routes for orders
16
+ router.use("/", route(orders));
17
+
18
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { order_items } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent orders via order_id
7
- export default route(order_items, { order_id: "params.order_id" });
9
+ router.use("/", route(order_items, { order_id: "params.order_id" }));
10
+
11
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { payments } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent orders via order_id
7
- export default route(payments, { order_id: "params.order_id" });
9
+ router.use("/", route(payments, { order_id: "params.order_id" }));
10
+
11
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { shipments } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent orders via order_id
7
- export default route(shipments, { order_id: "params.order_id" });
9
+ router.use("/", route(shipments, { order_id: "params.order_id" }));
10
+
11
+ export default router;
@@ -1,6 +1,18 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { products } from "#models";
4
+ import product_imagesRoute from "./product_images/index.js";
5
+ import product_variantsRoute from "./product_variants/index.js";
6
+ import product_reviewsRoute from "./product_reviews/index.js";
3
7
 
8
+ const router = express.Router({ mergeParams: true });
4
9
  const { route } = dbModelRouter;
5
10
 
6
- export default route(products);
11
+ router.use("/:product_id/product_images", product_imagesRoute);
12
+ router.use("/:product_id/product_variants", product_variantsRoute);
13
+ router.use("/:product_id/product_reviews", product_reviewsRoute);
14
+
15
+ // CRUD routes for products
16
+ router.use("/", route(products));
17
+
18
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { product_images } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent products via product_id
7
- export default route(product_images, { product_id: "params.product_id" });
9
+ router.use("/", route(product_images, { product_id: "params.product_id" }));
10
+
11
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { product_reviews } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent products via product_id
7
- export default route(product_reviews, { product_id: "params.product_id" });
9
+ router.use("/", route(product_reviews, { product_id: "params.product_id" }));
10
+
11
+ export default router;
@@ -1,7 +1,11 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { product_variants } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
8
  // Child route: scoped by parent products via product_id
7
- export default route(product_variants, { product_id: "params.product_id" });
9
+ router.use("/", route(product_variants, { product_id: "params.product_id" }));
10
+
11
+ export default router;
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
4
4
  import hasPermission from "#middleware/hasPermission.js";
5
5
  import { roles } from "#models";
6
6
 
7
- const router = express.Router();
7
+ const router = express.Router({ mergeParams: true });
8
8
 
9
9
  function userHasGlobalPermission(req) {
10
10
  return req.session.permission.some((p) => p.scope === "global");
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
4
4
  import hasPermission from "#middleware/hasPermission.js";
5
5
  import { tenants } from "#models";
6
6
 
7
- const router = express.Router();
7
+ const router = express.Router({ mergeParams: true });
8
8
 
9
9
  router.get("/", authenticate, tenantIsolation, hasPermission("tenants", "read"), async (req, res) => {
10
10
  try {
@@ -4,7 +4,7 @@ import tenantIsolation from "#middleware/tenantIsolation.js";
4
4
  import hasPermission from "#middleware/hasPermission.js";
5
5
  import { users } from "#models";
6
6
 
7
- const router = express.Router();
7
+ const router = express.Router({ mergeParams: true });
8
8
 
9
9
  router.get("/", authenticate, tenantIsolation, hasPermission("users", "read"), async (req, res) => {
10
10
  try {
@@ -1,6 +1,10 @@
1
1
  import dbModelRouter from "db-model-router";
2
+ import express from "express";
2
3
  import { wishlists } from "#models";
3
4
 
5
+ const router = express.Router({ mergeParams: true });
4
6
  const { route } = dbModelRouter;
5
7
 
6
- export default route(wishlists);
8
+ router.use("/", route(wishlists));
9
+
10
+ export default router;
@@ -13,7 +13,7 @@ const SUPER_ADMIN_EMAIL = "admin@system.local";
13
13
  * Generated password for the Super Admin.
14
14
  * This is cryptographically random and unique per generation.
15
15
  */
16
- const SUPER_ADMIN_PASSWORD = "64c2b0d97c199df8b674e4854baa4d28";
16
+ const SUPER_ADMIN_PASSWORD = "ba7e10b824a6c38c0cfb05bad715bc36";
17
17
 
18
18
  /**
19
19
  * Super Admin permissions: all actions for all modules with global scope.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-model-router",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Generative API Creation using mysql2 and express libraries in node js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -171,13 +171,13 @@ function generateTestFile(tableName, pk) {
171
171
  return `import assert from "assert";
172
172
  import express from "express";
173
173
  import request from "supertest";
174
+ import "dotenv/config";
175
+ import "#commons/db.js";
174
176
  import dbModelRouter from "db-model-router";
177
+ import { ${varName} } from "#models";
175
178
 
176
179
  const { route } = dbModelRouter;
177
180
 
178
- // Adjust the path to your model file as needed
179
- import ${varName} from "../models/${tableName}.js";
180
-
181
181
  function createApp() {
182
182
  const app = express();
183
183
  app.use(express.json());
@@ -205,7 +205,7 @@ describe("${tableName} routes", function () {
205
205
  const res = await request(app)
206
206
  .post("/${tableName}/add")
207
207
  .send({});
208
- assert.ok([200, 201, 400].includes(res.status));
208
+ assert.ok([200, 201, 400, 422, 500].includes(res.status));
209
209
  });
210
210
  });
211
211
 
@@ -214,7 +214,7 @@ describe("${tableName} routes", function () {
214
214
  const res = await request(app)
215
215
  .post("/${tableName}/")
216
216
  .send({ data: [] });
217
- assert.ok([200, 201, 400].includes(res.status));
217
+ assert.ok([200, 201, 400, 422, 500].includes(res.status));
218
218
  });
219
219
  });
220
220
 
@@ -255,7 +255,7 @@ describe("${tableName} routes", function () {
255
255
  const res = await request(app)
256
256
  .put("/${tableName}/")
257
257
  .send({ data: [] });
258
- assert.ok([200, 400].includes(res.status));
258
+ assert.ok([200, 400, 422, 500].includes(res.status));
259
259
  });
260
260
  });
261
261
 
@@ -21,6 +21,7 @@ const {
21
21
  generateModulesUtil,
22
22
  generateWebhookUtil,
23
23
  } = require("./saas/generate-saas-utils");
24
+ const { generateSaasTests } = require("./saas/generate-saas-tests");
24
25
 
25
26
  /**
26
27
  * Read the existing .gitignore file and append `credentials.md` if not already present.
@@ -113,7 +114,13 @@ function generateSaasStructure(adapter, options) {
113
114
  content: generateWebhookUtil(),
114
115
  });
115
116
 
116
- // 7. .gitignore update (add credentials.md)
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)
117
124
  planned.push({ relPath: ".gitignore", content: getGitignoreContent() });
118
125
 
119
126
  return planned;
@@ -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 .",
@@ -172,6 +172,7 @@ function buildEnvContent(answers, mode, secrets) {
172
172
  lines.push("API_BASE_PATH=/api");
173
173
  lines.push("");
174
174
  lines.push("# Database");
175
+ lines.push(`DB_TYPE=${answers.database}`);
175
176
 
176
177
  const vars = DB_ENV_MAP[answers.database] || [];
177
178
  for (const v of vars) {
@@ -358,83 +359,6 @@ app.use(session({
358
359
  }));`;
359
360
  }
360
361
 
361
- /**
362
- * Generate the app.js file content.
363
- * @param {import('./types').InitAnswers} answers
364
- * @returns {string}
365
- */
366
- function generateAppJs(answers) {
367
- const frameworkPkg =
368
- answers.framework === "ultimate-express" ? "ultimate-express" : "express";
369
-
370
- // Imports
371
- let imports = `import express from "${frameworkPkg}";
372
- import { init, db } from "db-model-router";
373
- import session from "express-session";`;
374
-
375
- if (answers.session === "redis") {
376
- imports += `\nimport RedisStore from "connect-redis";
377
- import { Redis } from "ioredis";`;
378
- }
379
- if (answers.rateLimiting) {
380
- imports += `\nimport rateLimit from "express-rate-limit";`;
381
- }
382
- if (answers.helmet) {
383
- imports += `\nimport helmet from "helmet";`;
384
- }
385
- imports += `\nimport logger from "./middleware/logger.js";`;
386
-
387
- // Rate limiting block
388
- const rateLimitBlock = answers.rateLimiting
389
- ? `app.use(rateLimit({
390
- windowMs: 15 * 60 * 1000,
391
- max: 100,
392
- standardHeaders: true,
393
- legacyHeaders: false,
394
- }));`
395
- : "";
396
-
397
- const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
398
-
399
- return `${imports}
400
- import "dotenv/config";
401
-
402
- // Initialize database adapter
403
- init("${answers.database}");
404
- ${dbConnectBlock(answers.database)}
405
-
406
- const app = express();
407
- const PORT = process.env.PORT || 3000;
408
-
409
- // Middleware
410
- app.use(express.json());
411
- app.use(express.urlencoded({ extended: true }));
412
- ${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
413
- app.use(logger);
414
-
415
- // Routes
416
- import routes from "#routes/index.js";
417
- app.use(process.env.API_BASE_PATH || "/api", routes);
418
-
419
- // Health check
420
- app.get("/health", (req, res) => {
421
- res.json({ status: "ok", timestamp: new Date().toISOString() });
422
- });
423
-
424
- // Error handler
425
- app.use((err, req, res, next) => {
426
- console.error(err.stack);
427
- res.status(500).json({ type: "danger", message: "Internal Server Error" });
428
- });
429
-
430
- app.listen(PORT, () => {
431
- console.log(\`Server running on port \${PORT}\`);
432
- });
433
-
434
- export default app;
435
- `;
436
- }
437
-
438
362
  // ---------------------------------------------------------------------------
439
363
  // Logger middleware generator
440
364
  // ---------------------------------------------------------------------------
@@ -1720,7 +1644,7 @@ export default db;
1720
1644
  * @param {string} [outputDir] - relative output directory for source files (e.g. "backend")
1721
1645
  * @returns {string}
1722
1646
  */
1723
- function generateAppJsV2(answers, outputDir) {
1647
+ function generateAppJs(answers, outputDir) {
1724
1648
  const frameworkPkg =
1725
1649
  answers.framework === "ultimate-express" ? "ultimate-express" : "express";
1726
1650
 
@@ -1735,7 +1659,7 @@ import "${commonsPrefix}/db.js";
1735
1659
  import configureSession from "${commonsPrefix}/session.js";
1736
1660
  import applySecurity from "${commonsPrefix}/security.js";
1737
1661
  import logger from "${middlewarePrefix}/logger.js";
1738
- import route from "${routePrefix}/index.js";
1662
+ import routes from "${routePrefix}/index.js";
1739
1663
  import { fileURLToPath } from 'node:url';
1740
1664
  import path from "path";
1741
1665
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -1756,7 +1680,7 @@ app.use(configureSession());
1756
1680
  app.use(logger);
1757
1681
 
1758
1682
  // Routes
1759
- app.use(route);
1683
+ app.use(process.env.API_BASE_PATH || "/api", routes);
1760
1684
 
1761
1685
  // Error handler
1762
1686
  app.use((err, req, res, next) => {
@@ -1777,7 +1701,6 @@ module.exports = {
1777
1701
  isSql,
1778
1702
  randomPassword,
1779
1703
  generateAppJs,
1780
- generateAppJsV2,
1781
1704
  generateEnvFile,
1782
1705
  generateEnvExample,
1783
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");
@@ -49,6 +49,8 @@ function generateTenantIsolationMiddleware() {
49
49
  function tenantIsolation(req, res, next) {
50
50
  const hasGlobal = req.session.permission.some((p) => p.scope === "global");
51
51
  if (!hasGlobal) {
52
+ if (!req.query) req.query = {};
53
+ if (!req.body) req.body = {};
52
54
  req.query.tenant_id = req.session.user.tenant_id;
53
55
  req.body.tenant_id = req.session.user.tenant_id;
54
56
  }
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SaaS test generators.
5
+ *
6
+ * Generates test files for SaaS routes: auth (login/logout), users, tenants, roles, permissions.
7
+ * Tests use supertest with a mock session to bypass authentication middleware.
8
+ * Generated code uses ES6 module syntax.
9
+ */
10
+
11
+ /**
12
+ * Generate all SaaS test files.
13
+ *
14
+ * @returns {Array<{ relPath: string, content: string }>}
15
+ */
16
+ function generateSaasTests() {
17
+ return [
18
+ { relPath: "test/auth.test.js", content: generateAuthTest() },
19
+ { relPath: "test/users.test.js", content: generateUsersTest() },
20
+ { relPath: "test/tenants.test.js", content: generateTenantsTest() },
21
+ { relPath: "test/roles.test.js", content: generateRolesTest() },
22
+ { relPath: "test/permissions.test.js", content: generatePermissionsTest() },
23
+ ];
24
+ }
25
+
26
+ function generateAuthTest() {
27
+ return `import assert from "assert";
28
+ import express from "express";
29
+ import request from "supertest";
30
+
31
+ // Import the auth route
32
+ import authRoute from "#routes/auth/index.js";
33
+
34
+ function createApp() {
35
+ const app = express();
36
+ app.use(express.json());
37
+ // Mock session object on each request
38
+ app.use((req, res, next) => {
39
+ req.session = {};
40
+ next();
41
+ });
42
+ app.use("/auth", authRoute);
43
+ return app;
44
+ }
45
+
46
+ describe("Auth Routes", function () {
47
+ let app;
48
+
49
+ before(function () {
50
+ app = createApp();
51
+ });
52
+
53
+ describe("POST /auth/login", function () {
54
+ it("should return 401 when email is missing", async function () {
55
+ const res = await request(app)
56
+ .post("/auth/login")
57
+ .send({ password: "test123" });
58
+ assert.strictEqual(res.status, 401);
59
+ assert.ok(res.body.message);
60
+ });
61
+
62
+ it("should return 401 when password is missing", async function () {
63
+ const res = await request(app)
64
+ .post("/auth/login")
65
+ .send({ email: "admin@system.local" });
66
+ assert.strictEqual(res.status, 401);
67
+ assert.ok(res.body.message);
68
+ });
69
+
70
+ it("should return 401 with empty body", async function () {
71
+ const res = await request(app)
72
+ .post("/auth/login")
73
+ .send({});
74
+ assert.strictEqual(res.status, 401);
75
+ });
76
+
77
+ it("should return 401 for non-existent user", async function () {
78
+ const res = await request(app)
79
+ .post("/auth/login")
80
+ .send({ email: "nobody@example.com", password: "wrong" });
81
+ assert.ok([401, 500].includes(res.status));
82
+ });
83
+ });
84
+
85
+ describe("POST /auth/logout", function () {
86
+ it("should return 401 when not authenticated", async function () {
87
+ const res = await request(app)
88
+ .post("/auth/logout");
89
+ assert.strictEqual(res.status, 401);
90
+ });
91
+ });
92
+ });
93
+ `;
94
+ }
95
+
96
+ function generateUsersTest() {
97
+ return `import assert from "assert";
98
+ import express from "express";
99
+ import request from "supertest";
100
+
101
+ import usersRoute from "#routes/users/index.js";
102
+
103
+ /**
104
+ * Create a test app with a pre-populated session (bypasses real auth).
105
+ * Injects session data via middleware before the route.
106
+ */
107
+ function createApp(sessionData) {
108
+ const app = express();
109
+ app.use(express.json());
110
+ // Mock session injection (no real session store needed for testing)
111
+ app.use((req, res, next) => {
112
+ req.session = {
113
+ user: sessionData.user,
114
+ role: sessionData.role,
115
+ permission: sessionData.permission,
116
+ };
117
+ next();
118
+ });
119
+ app.use("/users", usersRoute);
120
+ return app;
121
+ }
122
+
123
+ const mockSession = {
124
+ user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
125
+ role: { role_id: 1, name: "Super Admin", tenant_id: null },
126
+ permission: [
127
+ { module: "users", action: "global", scope: "global" },
128
+ ],
129
+ };
130
+
131
+ describe("Users Routes (SaaS)", function () {
132
+ let app;
133
+
134
+ before(function () {
135
+ app = createApp(mockSession);
136
+ });
137
+
138
+ describe("GET /users/", function () {
139
+ it("should list users", async function () {
140
+ const res = await request(app).get("/users/");
141
+ assert.ok([200, 500].includes(res.status));
142
+ });
143
+ });
144
+
145
+ describe("POST /users/", function () {
146
+ it("should create a user", async function () {
147
+ const res = await request(app)
148
+ .post("/users/")
149
+ .send({
150
+ email: "test@example.com",
151
+ name: "Test User",
152
+ password_hash: "hashed",
153
+ unique_attribute: "test-unique",
154
+ role_id: 1,
155
+ });
156
+ assert.ok([200, 201, 400, 500].includes(res.status));
157
+ });
158
+ });
159
+
160
+ describe("PUT /users/:id", function () {
161
+ it("should update a user", async function () {
162
+ const res = await request(app)
163
+ .put("/users/1")
164
+ .send({ name: "Updated" });
165
+ assert.ok([200, 400, 404, 500].includes(res.status));
166
+ });
167
+ });
168
+
169
+ describe("DELETE /users/:id", function () {
170
+ it("should delete a user", async function () {
171
+ const res = await request(app).delete("/users/1");
172
+ assert.ok([200, 204, 404, 500].includes(res.status));
173
+ });
174
+ });
175
+
176
+ describe("Permission enforcement", function () {
177
+ it("should return 403 without proper permissions", async function () {
178
+ const noPermApp = createApp({
179
+ user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
180
+ role: { role_id: 2, name: "Viewer", tenant_id: 1 },
181
+ permission: [{ module: "tenants", action: "read", scope: "tenant" }],
182
+ });
183
+ const res = await request(noPermApp).get("/users/");
184
+ assert.strictEqual(res.status, 403);
185
+ });
186
+ });
187
+ });
188
+ `;
189
+ }
190
+
191
+ function generateTenantsTest() {
192
+ return `import assert from "assert";
193
+ import express from "express";
194
+ import request from "supertest";
195
+
196
+ import tenantsRoute from "#routes/tenants/index.js";
197
+
198
+ function createApp(sessionData) {
199
+ const app = express();
200
+ app.use(express.json());
201
+ app.use((req, res, next) => {
202
+ req.session = {
203
+ user: sessionData.user,
204
+ role: sessionData.role,
205
+ permission: sessionData.permission,
206
+ };
207
+ next();
208
+ });
209
+ app.use("/tenants", tenantsRoute);
210
+ return app;
211
+ }
212
+
213
+ const mockSession = {
214
+ user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
215
+ role: { role_id: 1, name: "Super Admin", tenant_id: null },
216
+ permission: [
217
+ { module: "tenants", action: "global", scope: "global" },
218
+ ],
219
+ };
220
+
221
+ describe("Tenants Routes (SaaS)", function () {
222
+ let app;
223
+
224
+ before(function () {
225
+ app = createApp(mockSession);
226
+ });
227
+
228
+ describe("GET /tenants/", function () {
229
+ it("should list tenants", async function () {
230
+ const res = await request(app).get("/tenants/");
231
+ assert.ok([200, 500].includes(res.status));
232
+ });
233
+ });
234
+
235
+ describe("POST /tenants/", function () {
236
+ it("should create a tenant", async function () {
237
+ const res = await request(app)
238
+ .post("/tenants/")
239
+ .send({ name: "Acme Corp", slug: "acme-corp" });
240
+ assert.ok([200, 201, 400, 500].includes(res.status));
241
+ });
242
+ });
243
+
244
+ describe("PUT /tenants/:id", function () {
245
+ it("should update a tenant", async function () {
246
+ const res = await request(app)
247
+ .put("/tenants/1")
248
+ .send({ name: "Acme Updated" });
249
+ assert.ok([200, 400, 404, 500].includes(res.status));
250
+ });
251
+ });
252
+
253
+ describe("DELETE /tenants/:id", function () {
254
+ it("should delete a tenant", async function () {
255
+ const res = await request(app).delete("/tenants/1");
256
+ assert.ok([200, 204, 404, 500].includes(res.status));
257
+ });
258
+ });
259
+
260
+ describe("Permission enforcement", function () {
261
+ it("should return 403 without tenants permission", async function () {
262
+ const noPermApp = createApp({
263
+ user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
264
+ role: { role_id: 2, name: "Viewer", tenant_id: 1 },
265
+ permission: [{ module: "users", action: "read", scope: "tenant" }],
266
+ });
267
+ const res = await request(noPermApp).get("/tenants/");
268
+ assert.strictEqual(res.status, 403);
269
+ });
270
+ });
271
+ });
272
+ `;
273
+ }
274
+
275
+ function generateRolesTest() {
276
+ return `import assert from "assert";
277
+ import express from "express";
278
+ import request from "supertest";
279
+
280
+ import rolesRoute from "#routes/roles/index.js";
281
+
282
+ function createApp(sessionData) {
283
+ const app = express();
284
+ app.use(express.json());
285
+ app.use((req, res, next) => {
286
+ req.session = {
287
+ user: sessionData.user,
288
+ role: sessionData.role,
289
+ permission: sessionData.permission,
290
+ };
291
+ next();
292
+ });
293
+ app.use("/roles", rolesRoute);
294
+ return app;
295
+ }
296
+
297
+ const globalSession = {
298
+ user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
299
+ role: { role_id: 1, name: "Super Admin", tenant_id: null },
300
+ permission: [
301
+ { module: "roles", action: "global", scope: "global" },
302
+ ],
303
+ };
304
+
305
+ const tenantSession = {
306
+ user: { user_id: 2, email: "tenant@test.com", name: "Tenant Admin", tenant_id: 1 },
307
+ role: { role_id: 2, name: "Tenant Admin", tenant_id: 1 },
308
+ permission: [
309
+ { module: "roles", action: "read", scope: "tenant" },
310
+ { module: "roles", action: "write", scope: "tenant" },
311
+ { module: "roles", action: "update", scope: "tenant" },
312
+ { module: "roles", action: "delete", scope: "tenant" },
313
+ ],
314
+ };
315
+
316
+ describe("Roles Routes (SaaS)", function () {
317
+ describe("with global permissions", function () {
318
+ let app;
319
+
320
+ before(function () {
321
+ app = createApp(globalSession);
322
+ });
323
+
324
+ describe("GET /roles/", function () {
325
+ it("should list roles", async function () {
326
+ const res = await request(app).get("/roles/");
327
+ assert.ok([200, 500].includes(res.status));
328
+ });
329
+ });
330
+
331
+ describe("POST /roles/", function () {
332
+ it("should create a role", async function () {
333
+ const res = await request(app)
334
+ .post("/roles/")
335
+ .send({ name: "Editor", tenant_id: 1 });
336
+ assert.ok([200, 201, 400, 500].includes(res.status));
337
+ });
338
+
339
+ it("should allow creating role with global permissions for global user", async function () {
340
+ const res = await request(app)
341
+ .post("/roles/")
342
+ .send({
343
+ name: "Global Role",
344
+ tenant_id: null,
345
+ permissions: [{ module: "users", action: "read", scope: "global" }],
346
+ });
347
+ assert.ok([200, 201, 400, 500].includes(res.status));
348
+ // Should NOT be 403 for global user
349
+ assert.notStrictEqual(res.status, 403);
350
+ });
351
+ });
352
+ });
353
+
354
+ describe("with tenant permissions", function () {
355
+ let app;
356
+
357
+ before(function () {
358
+ app = createApp(tenantSession);
359
+ });
360
+
361
+ describe("POST /roles/", function () {
362
+ it("should return 403 when creating role with global permissions", async function () {
363
+ const res = await request(app)
364
+ .post("/roles/")
365
+ .send({
366
+ name: "Escalated Role",
367
+ permissions: [{ module: "users", action: "read", scope: "global" }],
368
+ });
369
+ assert.strictEqual(res.status, 403);
370
+ });
371
+ });
372
+ });
373
+
374
+ describe("Permission enforcement", function () {
375
+ it("should return 403 without roles permission", async function () {
376
+ const noPermApp = createApp({
377
+ user: { user_id: 3, email: "noperm@test.com", name: "No Perm", tenant_id: 1 },
378
+ role: { role_id: 3, name: "None", tenant_id: 1 },
379
+ permission: [{ module: "tenants", action: "read", scope: "tenant" }],
380
+ });
381
+ const res = await request(noPermApp).get("/roles/");
382
+ assert.strictEqual(res.status, 403);
383
+ });
384
+ });
385
+ });
386
+ `;
387
+ }
388
+
389
+ function generatePermissionsTest() {
390
+ return `import assert from "assert";
391
+ import express from "express";
392
+ import request from "supertest";
393
+
394
+ import permissionsRoute from "#routes/roles/permissions/index.js";
395
+
396
+ function createApp(sessionData) {
397
+ const app = express();
398
+ app.use(express.json());
399
+ app.use((req, res, next) => {
400
+ req.session = {
401
+ user: sessionData.user,
402
+ role: sessionData.role,
403
+ permission: sessionData.permission,
404
+ };
405
+ next();
406
+ });
407
+ app.use("/roles/:role_id/permissions", permissionsRoute);
408
+ return app;
409
+ }
410
+
411
+ const mockSession = {
412
+ user: { user_id: 1, email: "admin@system.local", name: "Admin", tenant_id: null },
413
+ role: { role_id: 1, name: "Super Admin", tenant_id: null },
414
+ permission: [
415
+ { module: "permissions", action: "global", scope: "global" },
416
+ ],
417
+ };
418
+
419
+ describe("Permissions Routes (SaaS)", function () {
420
+ let app;
421
+
422
+ before(function () {
423
+ app = createApp(mockSession);
424
+ });
425
+
426
+ describe("GET /roles/:role_id/permissions/", function () {
427
+ it("should list permissions for a role", async function () {
428
+ const res = await request(app).get("/roles/1/permissions/");
429
+ assert.ok([200, 500].includes(res.status));
430
+ });
431
+ });
432
+
433
+ describe("POST /roles/:role_id/permissions/", function () {
434
+ it("should create a permission entry", async function () {
435
+ const res = await request(app)
436
+ .post("/roles/1/permissions/")
437
+ .send({ permission: { module: "users", action: "read", scope: "tenant" } });
438
+ assert.ok([200, 201, 400, 500].includes(res.status));
439
+ });
440
+ });
441
+
442
+ describe("PUT /roles/:role_id/permissions/:permission_id", function () {
443
+ it("should update a permission entry", async function () {
444
+ const res = await request(app)
445
+ .put("/roles/1/permissions/1")
446
+ .send({ permission: { module: "users", action: "write", scope: "tenant" } });
447
+ assert.ok([200, 400, 404, 500].includes(res.status));
448
+ });
449
+ });
450
+
451
+ describe("DELETE /roles/:role_id/permissions/:permission_id", function () {
452
+ it("should delete a permission entry", async function () {
453
+ const res = await request(app).delete("/roles/1/permissions/1");
454
+ assert.ok([200, 204, 404, 500].includes(res.status));
455
+ });
456
+ });
457
+
458
+ describe("Permission enforcement", function () {
459
+ it("should return 403 without permissions module access", async function () {
460
+ const noPermApp = createApp({
461
+ user: { user_id: 2, email: "user@test.com", name: "User", tenant_id: 1 },
462
+ role: { role_id: 2, name: "Viewer", tenant_id: 1 },
463
+ permission: [{ module: "users", action: "read", scope: "tenant" }],
464
+ });
465
+ const res = await request(noPermApp).get("/roles/1/permissions/");
466
+ assert.strictEqual(res.status, 403);
467
+ });
468
+ });
469
+ });
470
+ `;
471
+ }
472
+
473
+ module.exports = { generateSaasTests };
@@ -31,7 +31,7 @@ module.exports = function route(model, override = {}) {
31
31
  .Router({ mergeParams: true })
32
32
  .get("/:" + model.pk, (req, res) => {
33
33
  let payload = payloadOverride(
34
- { ...req.query, ...req.params },
34
+ { ...(req.query || {}), ...(req.params || {}) },
35
35
  req,
36
36
  override,
37
37
  );
@@ -52,7 +52,7 @@ module.exports = function route(model, override = {}) {
52
52
  });
53
53
  })
54
54
  .post("/:id", (req, res) => {
55
- let payload = payloadOverride(req.body, req, override);
55
+ let payload = payloadOverride(req.body || {}, req, override);
56
56
  delete payload[model.pk];
57
57
  model
58
58
  .insert(payload)
@@ -64,7 +64,7 @@ module.exports = function route(model, override = {}) {
64
64
  });
65
65
  })
66
66
  .put("/:id", (req, res) => {
67
- let payload = payloadOverride(req.body, req, override);
67
+ let payload = payloadOverride(req.body || {}, req, override);
68
68
  payload[model.pk] = req.params.id;
69
69
  let validateAccessPayload = payloadOverride({}, req, override);
70
70
  validateAccessPayload[model.pk] = req.params.id;
@@ -89,7 +89,7 @@ module.exports = function route(model, override = {}) {
89
89
  });
90
90
  })
91
91
  .patch("/:id", (req, res) => {
92
- let payload = payloadOverride(req.body, req, override);
92
+ let payload = payloadOverride(req.body || {}, req, override);
93
93
  payload[model.pk] = req.params.id;
94
94
  let validateAccessPayload = payloadOverride({}, req, override);
95
95
  validateAccessPayload[model.pk] = req.params.id;
@@ -114,7 +114,7 @@ module.exports = function route(model, override = {}) {
114
114
  });
115
115
  })
116
116
  .delete("/:id", (req, res) => {
117
- let payload = payloadOverride(req.body, req, override);
117
+ let payload = payloadOverride(req.body || {}, req, override);
118
118
  payload[model.pk] = req.params.id;
119
119
  let validateAccessPayload = payloadOverride({}, req, override);
120
120
  validateAccessPayload[model.pk] = req.params.id;
@@ -140,7 +140,7 @@ module.exports = function route(model, override = {}) {
140
140
  })
141
141
  .get("/", (req, res) => {
142
142
  let payload = payloadOverride(
143
- { ...req.query, ...req.params },
143
+ { ...(req.query || {}), ...(req.params || {}) },
144
144
  req,
145
145
  override,
146
146
  );