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.
- package/README.md +150 -11
- package/TODO.md +0 -15
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/README.md +223 -0
- package/db-manager/adapter-proxy.js +361 -0
- package/db-manager/demo/cockroachdb.env +6 -0
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +7 -0
- package/db-manager/demo/mongodb.env +4 -0
- package/db-manager/demo/mssql.env +6 -0
- package/db-manager/demo/mysql.env +6 -0
- package/db-manager/demo/oracle.env +6 -0
- package/db-manager/demo/postgres.env +6 -0
- package/db-manager/demo/redis.env +4 -0
- package/db-manager/demo/seeds/cockroachdb.sql +32 -0
- package/db-manager/demo/seeds/mssql.sql +32 -0
- package/db-manager/demo/seeds/mysql.sql +32 -0
- package/db-manager/demo/seeds/oracle.sql +43 -0
- package/db-manager/demo/seeds/postgres.sql +32 -0
- package/db-manager/demo/seeds/sqlite3.sql +32 -0
- package/db-manager/demo/sqlite3.env +2 -0
- package/db-manager/metadata-db.js +170 -0
- package/db-manager/public/.gitkeep +1 -0
- package/db-manager/public/css/style.css +1413 -0
- package/db-manager/public/js/app.js +1370 -0
- package/db-manager/routes/api.js +388 -0
- package/db-manager/routes/views.js +61 -0
- package/db-manager/server.js +39 -0
- package/db-manager/utils/build-filter-config.js +18 -0
- package/db-manager/utils/csv-export.js +59 -0
- package/db-manager/utils/export-filename.js +39 -0
- package/db-manager/utils/filter-tables.js +20 -0
- package/db-manager/utils/parse-filters.js +93 -0
- package/db-manager/utils/sort-state.js +35 -0
- package/db-manager/views/.gitkeep +1 -0
- package/db-manager/views/dashboard.ejs +53 -0
- package/db-manager/views/history.ejs +52 -0
- package/db-manager/views/index.ejs +35 -0
- package/db-manager/views/layout.ejs +31 -0
- package/db-manager/views/partials/data-panel.ejs +74 -0
- package/db-manager/views/partials/header.ejs +36 -0
- package/db-manager/views/partials/sidebar.ejs +30 -0
- package/db-manager/views/query.ejs +58 -0
- package/dbmr.schema.json +22 -44
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +15 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +39 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +68 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +338 -0
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/middleware/tenantIsolation.js +19 -0
- package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
- package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
- package/demo/migrations/20260510092159_create_tables.sql +193 -0
- package/demo/models/addresses.js +24 -0
- package/demo/models/cart_items.js +20 -0
- package/demo/models/carts.js +18 -0
- package/demo/models/categories.js +22 -0
- package/demo/models/coupons.js +25 -0
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +23 -0
- package/demo/models/orders.js +27 -0
- package/demo/models/payments.js +23 -0
- package/demo/models/product_images.js +20 -0
- package/demo/models/product_reviews.js +22 -0
- package/demo/models/product_variants.js +22 -0
- package/demo/models/products.js +32 -0
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +21 -0
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +23 -0
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +17 -0
- package/demo/openapi.json +7000 -0
- package/demo/package-lock.json +2827 -0
- package/demo/package.json +42 -0
- package/demo/routes/addresses/index.js +10 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +11 -0
- package/demo/routes/carts/index.js +14 -0
- package/demo/routes/categories/index.js +10 -0
- package/demo/routes/coupons/index.js +10 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +54 -0
- package/demo/routes/orders/index.js +18 -0
- package/demo/routes/orders/order_items/index.js +11 -0
- package/demo/routes/orders/payments/index.js +11 -0
- package/demo/routes/orders/shipments/index.js +11 -0
- package/demo/routes/products/index.js +18 -0
- package/demo/routes/products/product_images/index.js +11 -0
- package/demo/routes/products/product_reviews/index.js +11 -0
- package/demo/routes/products/product_variants/index.js +11 -0
- package/demo/routes/roles/index.js +75 -0
- package/demo/routes/roles/permissions/index.js +47 -0
- package/demo/routes/tenants/index.js +45 -0
- package/demo/routes/users/index.js +45 -0
- package/demo/routes/wishlists/index.js +10 -0
- package/demo/seeds/saas-seed.js +329 -0
- package/docker-compose.yml +61 -0
- package/package.json +120 -113
- package/scripts/demo-create.js +1 -1
- package/skill/SKILL.md +119 -3
- package/src/cli/commands/db-manager.js +134 -0
- package/src/cli/commands/generate.js +106 -60
- package/src/cli/commands/help.js +0 -1
- package/src/cli/generate-route.js +66 -27
- package/src/cli/generate-saas-structure.js +129 -0
- package/src/cli/init/dependencies.js +1 -1
- package/src/cli/init/generators.js +6 -77
- package/src/cli/init.js +9 -2
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +110 -0
- package/src/cli/saas/generate-saas-migrations.js +480 -0
- package/src/cli/saas/generate-saas-models.js +211 -0
- package/src/cli/saas/generate-saas-openapi.js +419 -0
- package/src/cli/saas/generate-saas-routes.js +435 -0
- package/src/cli/saas/generate-saas-seeds.js +243 -0
- package/src/cli/saas/generate-saas-tests.js +473 -0
- package/src/cli/saas/generate-saas-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- package/src/commons/route.js +6 -6
- package/src/index.js +2 -0
- package/src/mssql/db.js +41 -3
- package/src/mysql/db.js +3 -0
- package/src/postgres/db.js +6 -0
- 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
|
|
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.,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
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",
|
|
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
|
+
};
|