db-model-router 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +14 -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 +28 -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 +17 -0
- package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/20260509170349_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 +2810 -0
- package/demo/package.json +43 -0
- package/demo/routes/addresses/index.js +6 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +7 -0
- package/demo/routes/carts/index.js +6 -0
- package/demo/routes/categories/index.js +6 -0
- package/demo/routes/coupons/index.js +6 -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 +6 -0
- package/demo/routes/orders/order_items/index.js +7 -0
- package/demo/routes/orders/payments/index.js +7 -0
- package/demo/routes/orders/shipments/index.js +7 -0
- package/demo/routes/products/index.js +6 -0
- package/demo/routes/products/product_images/index.js +7 -0
- package/demo/routes/products/product_reviews/index.js +7 -0
- package/demo/routes/products/product_variants/index.js +7 -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 +6 -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 +60 -21
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +6 -0
- package/src/cli/init.js +8 -0
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +108 -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-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- 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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS route generators.
|
|
5
|
+
*
|
|
6
|
+
* Generates CRUD routes, auth routes, and the routes index file
|
|
7
|
+
* for the SaaS structure generator. All generated code uses ES6 module syntax.
|
|
8
|
+
* Routes use folder-based structure: routes/<module>/index.js
|
|
9
|
+
* Imports use package.json #imports aliases: #models, #middleware, #commons
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate CRUD route files for users, tenants, roles, and role_permissions.
|
|
14
|
+
*
|
|
15
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
16
|
+
*/
|
|
17
|
+
function generateCrudRoutes() {
|
|
18
|
+
const files = [];
|
|
19
|
+
|
|
20
|
+
files.push({
|
|
21
|
+
relPath: "routes/users/index.js",
|
|
22
|
+
content: generateUsersRoute(),
|
|
23
|
+
});
|
|
24
|
+
files.push({
|
|
25
|
+
relPath: "routes/tenants/index.js",
|
|
26
|
+
content: generateTenantsRoute(),
|
|
27
|
+
});
|
|
28
|
+
files.push({
|
|
29
|
+
relPath: "routes/roles/index.js",
|
|
30
|
+
content: generateRolesRoute(),
|
|
31
|
+
});
|
|
32
|
+
files.push({
|
|
33
|
+
relPath: "routes/roles/permissions/index.js",
|
|
34
|
+
content: generatePermissionsRoute(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateUsersRoute() {
|
|
41
|
+
return `import express from "express";
|
|
42
|
+
import authenticate from "#middleware/authenticate.js";
|
|
43
|
+
import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
44
|
+
import hasPermission from "#middleware/hasPermission.js";
|
|
45
|
+
import { users } from "#models";
|
|
46
|
+
|
|
47
|
+
const router = express.Router({ mergeParams: true });
|
|
48
|
+
|
|
49
|
+
router.get("/", authenticate, tenantIsolation, hasPermission("users", "read"), async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const results = await users.findAll(req.query);
|
|
52
|
+
res.json(results);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
res.status(500).json({ message: err.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
router.post("/", authenticate, tenantIsolation, hasPermission("users", "write"), async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const result = await users.create(req.body);
|
|
61
|
+
res.status(201).json(result);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
res.status(500).json({ message: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
router.put("/:id", authenticate, tenantIsolation, hasPermission("users", "update"), async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await users.update(req.params.id, req.body);
|
|
70
|
+
res.json(result);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
res.status(500).json({ message: err.message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.delete("/:id", authenticate, tenantIsolation, hasPermission("users", "delete"), async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const result = await users.delete(req.params.id);
|
|
79
|
+
res.json(result);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
res.status(500).json({ message: err.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export default router;
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function generateTenantsRoute() {
|
|
90
|
+
return `import express from "express";
|
|
91
|
+
import authenticate from "#middleware/authenticate.js";
|
|
92
|
+
import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
93
|
+
import hasPermission from "#middleware/hasPermission.js";
|
|
94
|
+
import { tenants } from "#models";
|
|
95
|
+
|
|
96
|
+
const router = express.Router({ mergeParams: true });
|
|
97
|
+
|
|
98
|
+
router.get("/", authenticate, tenantIsolation, hasPermission("tenants", "read"), async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const results = await tenants.findAll(req.query);
|
|
101
|
+
res.json(results);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
res.status(500).json({ message: err.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.post("/", authenticate, tenantIsolation, hasPermission("tenants", "write"), async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const result = await tenants.create(req.body);
|
|
110
|
+
res.status(201).json(result);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
res.status(500).json({ message: err.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
router.put("/:id", authenticate, tenantIsolation, hasPermission("tenants", "update"), async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await tenants.update(req.params.id, req.body);
|
|
119
|
+
res.json(result);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
res.status(500).json({ message: err.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
router.delete("/:id", authenticate, tenantIsolation, hasPermission("tenants", "delete"), async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const result = await tenants.delete(req.params.id);
|
|
128
|
+
res.json(result);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
res.status(500).json({ message: err.message });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export default router;
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function generateRolesRoute() {
|
|
139
|
+
return `import express from "express";
|
|
140
|
+
import authenticate from "#middleware/authenticate.js";
|
|
141
|
+
import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
142
|
+
import hasPermission from "#middleware/hasPermission.js";
|
|
143
|
+
import { roles } from "#models";
|
|
144
|
+
|
|
145
|
+
const router = express.Router({ mergeParams: true });
|
|
146
|
+
|
|
147
|
+
function userHasGlobalPermission(req) {
|
|
148
|
+
return req.session.permission.some((p) => p.scope === "global");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function guardSystemRole(req, res, role) {
|
|
152
|
+
if (role.tenant_id === null && !userHasGlobalPermission(req)) {
|
|
153
|
+
res.status(403).json({ message: "Cannot modify system roles" });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function guardGlobalPermissionEscalation(req, res) {
|
|
160
|
+
const permissions = req.body.permissions || [];
|
|
161
|
+
const hasGlobalEntry = permissions.some((p) => p.scope === "global");
|
|
162
|
+
if (hasGlobalEntry && !userHasGlobalPermission(req)) {
|
|
163
|
+
res.status(403).json({ message: "Cannot assign global permissions" });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
router.get("/", authenticate, tenantIsolation, hasPermission("roles", "read"), async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const results = await roles.findAll(req.query);
|
|
172
|
+
res.json(results);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
res.status(500).json({ message: err.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
router.post("/", authenticate, tenantIsolation, hasPermission("roles", "write"), async (req, res) => {
|
|
179
|
+
try {
|
|
180
|
+
if (guardGlobalPermissionEscalation(req, res)) return;
|
|
181
|
+
const result = await roles.create(req.body);
|
|
182
|
+
res.status(201).json(result);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
res.status(500).json({ message: err.message });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
router.put("/:id", authenticate, tenantIsolation, hasPermission("roles", "update"), async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const role = await roles.findById(req.params.id);
|
|
191
|
+
if (!role) return res.status(404).json({ message: "Role not found" });
|
|
192
|
+
if (guardSystemRole(req, res, role)) return;
|
|
193
|
+
if (guardGlobalPermissionEscalation(req, res)) return;
|
|
194
|
+
const result = await roles.update(req.params.id, req.body);
|
|
195
|
+
res.json(result);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
res.status(500).json({ message: err.message });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
router.delete("/:id", authenticate, tenantIsolation, hasPermission("roles", "delete"), async (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const role = await roles.findById(req.params.id);
|
|
204
|
+
if (!role) return res.status(404).json({ message: "Role not found" });
|
|
205
|
+
if (guardSystemRole(req, res, role)) return;
|
|
206
|
+
const result = await roles.delete(req.params.id);
|
|
207
|
+
res.json(result);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
res.status(500).json({ message: err.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export default router;
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function generatePermissionsRoute() {
|
|
218
|
+
return `import express from "express";
|
|
219
|
+
import authenticate from "#middleware/authenticate.js";
|
|
220
|
+
import tenantIsolation from "#middleware/tenantIsolation.js";
|
|
221
|
+
import hasPermission from "#middleware/hasPermission.js";
|
|
222
|
+
import { role_permissions } from "#models";
|
|
223
|
+
|
|
224
|
+
const router = express.Router({ mergeParams: true });
|
|
225
|
+
|
|
226
|
+
router.get("/", authenticate, tenantIsolation, hasPermission("permissions", "read"), async (req, res) => {
|
|
227
|
+
try {
|
|
228
|
+
const query = { ...req.query, role_id: req.params.role_id };
|
|
229
|
+
const results = await role_permissions.findAll(query);
|
|
230
|
+
res.json(results);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
res.status(500).json({ message: err.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
router.post("/", authenticate, tenantIsolation, hasPermission("permissions", "write"), async (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const data = { ...req.body, role_id: req.params.role_id };
|
|
239
|
+
const result = await role_permissions.create(data);
|
|
240
|
+
res.status(201).json(result);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
res.status(500).json({ message: err.message });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
router.put("/:permission_id", authenticate, tenantIsolation, hasPermission("permissions", "update"), async (req, res) => {
|
|
247
|
+
try {
|
|
248
|
+
const result = await role_permissions.update(req.params.permission_id, req.body);
|
|
249
|
+
res.json(result);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
res.status(500).json({ message: err.message });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
router.delete("/:permission_id", authenticate, tenantIsolation, hasPermission("permissions", "delete"), async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await role_permissions.delete(req.params.permission_id);
|
|
258
|
+
res.json(result);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
res.status(500).json({ message: err.message });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export default router;
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate the auth routes file (login and logout) in ES6.
|
|
270
|
+
*
|
|
271
|
+
* @returns {{ relPath: string, content: string }}
|
|
272
|
+
*/
|
|
273
|
+
function generateAuthRoutes() {
|
|
274
|
+
const relPath = "routes/auth/index.js";
|
|
275
|
+
const content = `import express from "express";
|
|
276
|
+
import authenticate from "#middleware/authenticate.js";
|
|
277
|
+
import { verifyPassword } from "#commons/password.js";
|
|
278
|
+
import { users, roles, role_permissions } from "#models";
|
|
279
|
+
|
|
280
|
+
const router = express.Router({ mergeParams: true });
|
|
281
|
+
|
|
282
|
+
// POST /api/auth/login - Authenticate user and create session
|
|
283
|
+
router.post("/login", async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const { email, password } = req.body;
|
|
286
|
+
|
|
287
|
+
if (!email || !password) {
|
|
288
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const userResults = await users.findAll({ email });
|
|
292
|
+
const user = Array.isArray(userResults) ? userResults[0] : (userResults?.data?.[0] ?? null);
|
|
293
|
+
|
|
294
|
+
if (!user) {
|
|
295
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const isValid = await verifyPassword(password, user.password_hash);
|
|
299
|
+
if (!isValid) {
|
|
300
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const role = await roles.findById(user.role_id);
|
|
304
|
+
const permsResult = await role_permissions.findAll({ role_id: user.role_id });
|
|
305
|
+
const permissionList = Array.isArray(permsResult) ? permsResult : (permsResult?.data ?? []);
|
|
306
|
+
|
|
307
|
+
req.session.user = user;
|
|
308
|
+
req.session.role = role;
|
|
309
|
+
req.session.permission = permissionList.map((p) =>
|
|
310
|
+
typeof p.permission === "string" ? JSON.parse(p.permission) : p.permission
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
res.json({ message: "Login successful", user: { id: user.user_id, email: user.email, name: user.name } });
|
|
314
|
+
} catch (err) {
|
|
315
|
+
res.status(500).json({ message: err.message });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// POST /api/auth/logout - Destroy session
|
|
320
|
+
router.post("/logout", authenticate, (req, res) => {
|
|
321
|
+
req.session.destroy((err) => {
|
|
322
|
+
if (err) {
|
|
323
|
+
return res.status(500).json({ message: "Failed to destroy session" });
|
|
324
|
+
}
|
|
325
|
+
res.json({ message: "Logout successful" });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
export default router;
|
|
330
|
+
`;
|
|
331
|
+
return { relPath, content };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Generate the routes index file that wires both SaaS routes and dbmr-generated routes.
|
|
336
|
+
* Uses folder-based imports with #imports aliases.
|
|
337
|
+
*
|
|
338
|
+
* @param {string[]} [tableNames] - Schema-generated table names (from dbmr)
|
|
339
|
+
* @param {Array<{parent, child, foreignKey}>} [relationships] - Schema relationships
|
|
340
|
+
* @param {{ includeDocs?: boolean }} [options]
|
|
341
|
+
* @returns {{ relPath: string, content: string }}
|
|
342
|
+
*/
|
|
343
|
+
function generateRoutesIndex(tableNames, relationships, options) {
|
|
344
|
+
tableNames = tableNames || [];
|
|
345
|
+
relationships = relationships || [];
|
|
346
|
+
options = options || {};
|
|
347
|
+
|
|
348
|
+
// SaaS tables that have their own dedicated route folders
|
|
349
|
+
const saasRouteModules = new Set([
|
|
350
|
+
"users",
|
|
351
|
+
"tenants",
|
|
352
|
+
"roles",
|
|
353
|
+
"role_permissions",
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
// Collect child tables from relationships
|
|
357
|
+
const nestedChildren = new Set();
|
|
358
|
+
for (const rel of relationships) {
|
|
359
|
+
nestedChildren.add(rel.child);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let code = `import express from "express";\n\nconst router = express.Router({ mergeParams: true });\n\n`;
|
|
363
|
+
|
|
364
|
+
// --- SaaS route imports (folder-based with #routes alias) ---
|
|
365
|
+
code += `// SaaS auth & CRUD routes\n`;
|
|
366
|
+
code += `import authRoute from "#routes/auth/index.js";\n`;
|
|
367
|
+
code += `import saasUsersRoute from "#routes/users/index.js";\n`;
|
|
368
|
+
code += `import saasTenantsRoute from "#routes/tenants/index.js";\n`;
|
|
369
|
+
code += `import saasRolesRoute from "#routes/roles/index.js";\n`;
|
|
370
|
+
code += `import saasPermissionsRoute from "#routes/roles/permissions/index.js";\n\n`;
|
|
371
|
+
|
|
372
|
+
// --- dbmr schema-generated route imports (folder-based, skip SaaS-owned tables) ---
|
|
373
|
+
const dbmrTables = tableNames.filter(
|
|
374
|
+
(t) => !saasRouteModules.has(t) && !nestedChildren.has(t),
|
|
375
|
+
);
|
|
376
|
+
if (dbmrTables.length > 0 || relationships.length > 0) {
|
|
377
|
+
code += `// Schema-generated routes\n`;
|
|
378
|
+
}
|
|
379
|
+
for (const table of dbmrTables) {
|
|
380
|
+
code += `import ${safeVarName(table)}Route from "#routes/${table}/index.js";\n`;
|
|
381
|
+
}
|
|
382
|
+
for (const rel of relationships) {
|
|
383
|
+
if (saasRouteModules.has(rel.child)) continue;
|
|
384
|
+
code += `import ${safeVarName(rel.child)}ChildRoute from "#routes/${rel.parent}/${rel.child}/index.js";\n`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (options.includeDocs) {
|
|
388
|
+
code += `import docsRoute from "#routes/docs.js";\n`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
code += `\n`;
|
|
392
|
+
|
|
393
|
+
// --- Mount SaaS routes ---
|
|
394
|
+
code += `// SaaS routes\n`;
|
|
395
|
+
code += `router.use("/auth", authRoute);\n`;
|
|
396
|
+
code += `router.use("/users", saasUsersRoute);\n`;
|
|
397
|
+
code += `router.use("/tenants", saasTenantsRoute);\n`;
|
|
398
|
+
code += `router.use("/roles", saasRolesRoute);\n`;
|
|
399
|
+
code += `router.use("/roles/:role_id/permissions", saasPermissionsRoute);\n\n`;
|
|
400
|
+
|
|
401
|
+
// --- Mount docs route ---
|
|
402
|
+
if (options.includeDocs) {
|
|
403
|
+
code += `router.use("/docs", docsRoute);\n`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- Mount dbmr child routes before parent routes ---
|
|
407
|
+
for (const rel of relationships) {
|
|
408
|
+
if (saasRouteModules.has(rel.child)) continue;
|
|
409
|
+
const childVar = safeVarName(rel.child);
|
|
410
|
+
code += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Mount dbmr top-level routes ---
|
|
414
|
+
if (dbmrTables.length > 0) {
|
|
415
|
+
code += `\n// Schema-generated routes\n`;
|
|
416
|
+
}
|
|
417
|
+
for (const table of dbmrTables) {
|
|
418
|
+
code += `router.use("/${table}", ${safeVarName(table)}Route);\n`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
code += `\nexport default router;\n`;
|
|
422
|
+
|
|
423
|
+
return { relPath: "routes/index.js", content: code };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function safeVarName(name) {
|
|
427
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return name;
|
|
428
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
generateCrudRoutes,
|
|
433
|
+
generateAuthRoutes,
|
|
434
|
+
generateRoutesIndex,
|
|
435
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SaaS seed generator.
|
|
7
|
+
*
|
|
8
|
+
* Generates seed files for the Super Admin user and Tenant Admin Role,
|
|
9
|
+
* plus a credentials.md file with the generated password.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* All valid modules in the SaaS system.
|
|
18
|
+
* @type {string[]}
|
|
19
|
+
*/
|
|
20
|
+
const MODULES = ["users", "tenants", "roles", "permissions", "webhooks"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* All valid actions in the permission system.
|
|
24
|
+
* @type {string[]}
|
|
25
|
+
*/
|
|
26
|
+
const ACTIONS = [
|
|
27
|
+
"read",
|
|
28
|
+
"write",
|
|
29
|
+
"update",
|
|
30
|
+
"delete",
|
|
31
|
+
"export",
|
|
32
|
+
"approve",
|
|
33
|
+
"global",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Super Admin permissions: all actions for all modules with global scope.
|
|
38
|
+
* @type {Array<{ module: string, action: string, scope: string }>}
|
|
39
|
+
*/
|
|
40
|
+
const SUPER_ADMIN_PERMISSIONS = [];
|
|
41
|
+
for (const mod of MODULES) {
|
|
42
|
+
for (const action of ACTIONS) {
|
|
43
|
+
SUPER_ADMIN_PERMISSIONS.push({ module: mod, action, scope: "global" });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Tenant Admin permissions: CRUD actions for users and roles with tenant scope.
|
|
49
|
+
* No global permissions allowed.
|
|
50
|
+
* @type {Array<{ module: string, action: string, scope: string }>}
|
|
51
|
+
*/
|
|
52
|
+
const TENANT_ADMIN_PERMISSIONS = [];
|
|
53
|
+
for (const mod of ["users", "roles"]) {
|
|
54
|
+
for (const action of ["read", "write", "update", "delete"]) {
|
|
55
|
+
TENANT_ADMIN_PERMISSIONS.push({ module: mod, action, scope: "tenant" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Seed File Content Generation
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate the content of the `seeds/saas-seed.js` file.
|
|
65
|
+
*
|
|
66
|
+
* The seed file, when executed:
|
|
67
|
+
* - Hashes the embedded password using the generated password utility
|
|
68
|
+
* - Inserts the Super Admin user with NULL tenant_id and all permissions
|
|
69
|
+
* - Inserts the Tenant Admin Role with NULL tenant_id and tenant-scoped permissions
|
|
70
|
+
* - Writes credentials.md with the super admin email and password
|
|
71
|
+
*
|
|
72
|
+
* @param {string} password - The generated random password to embed
|
|
73
|
+
* @returns {string} File content for seeds/saas-seed.js
|
|
74
|
+
*/
|
|
75
|
+
function generateSeedContent(password) {
|
|
76
|
+
const superAdminPermsStr = JSON.stringify(SUPER_ADMIN_PERMISSIONS, null, 2);
|
|
77
|
+
const tenantAdminPermsStr = JSON.stringify(TENANT_ADMIN_PERMISSIONS, null, 2);
|
|
78
|
+
|
|
79
|
+
return `"use strict";
|
|
80
|
+
|
|
81
|
+
const path = require("path");
|
|
82
|
+
const fs = require("fs");
|
|
83
|
+
const { hashPassword } = require("../commons/password");
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Super Admin email address.
|
|
87
|
+
*/
|
|
88
|
+
const SUPER_ADMIN_EMAIL = "admin@system.local";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generated password for the Super Admin.
|
|
92
|
+
* This is cryptographically random and unique per generation.
|
|
93
|
+
*/
|
|
94
|
+
const SUPER_ADMIN_PASSWORD = "${password}";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Super Admin permissions: all actions for all modules with global scope.
|
|
98
|
+
*/
|
|
99
|
+
const SUPER_ADMIN_PERMISSIONS = ${superAdminPermsStr};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Tenant Admin permissions: CRUD for users and roles with tenant scope.
|
|
103
|
+
*/
|
|
104
|
+
const TENANT_ADMIN_PERMISSIONS = ${tenantAdminPermsStr};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Write the credentials.md file with the super admin login details.
|
|
108
|
+
*/
|
|
109
|
+
function writeCredentials() {
|
|
110
|
+
const content = \`# Super Admin Credentials
|
|
111
|
+
|
|
112
|
+
**Email:** \${SUPER_ADMIN_EMAIL}
|
|
113
|
+
**Password:** \${SUPER_ADMIN_PASSWORD}
|
|
114
|
+
|
|
115
|
+
> ⚠️ **WARNING:** Change this password after first login. This file should not be committed to version control.
|
|
116
|
+
\`;
|
|
117
|
+
fs.writeFileSync(path.join(process.cwd(), "credentials.md"), content, "utf8");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Seed the database with Super Admin user and Tenant Admin Role.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} db - Database connection/query interface
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async function seed(db) {
|
|
127
|
+
const passwordHash = await hashPassword(SUPER_ADMIN_PASSWORD);
|
|
128
|
+
|
|
129
|
+
// Insert Super Admin role with all permissions
|
|
130
|
+
const superAdminRoleResult = await db("roles").insert({
|
|
131
|
+
tenant_id: null,
|
|
132
|
+
name: "Super Admin",
|
|
133
|
+
created_at: new Date(),
|
|
134
|
+
modified_at: new Date(),
|
|
135
|
+
});
|
|
136
|
+
const superAdminRoleId = Array.isArray(superAdminRoleResult)
|
|
137
|
+
? superAdminRoleResult[0]
|
|
138
|
+
: superAdminRoleResult;
|
|
139
|
+
|
|
140
|
+
// Insert Super Admin permissions
|
|
141
|
+
for (const perm of SUPER_ADMIN_PERMISSIONS) {
|
|
142
|
+
await db("role_permissions").insert({
|
|
143
|
+
role_id: superAdminRoleId,
|
|
144
|
+
permission: JSON.stringify(perm),
|
|
145
|
+
created_at: new Date(),
|
|
146
|
+
modified_at: new Date(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Insert Super Admin user
|
|
151
|
+
await db("users").insert({
|
|
152
|
+
email: SUPER_ADMIN_EMAIL,
|
|
153
|
+
password_hash: passwordHash,
|
|
154
|
+
name: "Super Admin",
|
|
155
|
+
tenant_id: null,
|
|
156
|
+
role_id: superAdminRoleId,
|
|
157
|
+
created_at: new Date(),
|
|
158
|
+
modified_at: new Date(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Insert Tenant Admin Role
|
|
162
|
+
const tenantAdminRoleResult = await db("roles").insert({
|
|
163
|
+
tenant_id: null,
|
|
164
|
+
name: "Tenant Admin",
|
|
165
|
+
created_at: new Date(),
|
|
166
|
+
modified_at: new Date(),
|
|
167
|
+
});
|
|
168
|
+
const tenantAdminRoleId = Array.isArray(tenantAdminRoleResult)
|
|
169
|
+
? tenantAdminRoleResult[0]
|
|
170
|
+
: tenantAdminRoleResult;
|
|
171
|
+
|
|
172
|
+
// Insert Tenant Admin permissions
|
|
173
|
+
for (const perm of TENANT_ADMIN_PERMISSIONS) {
|
|
174
|
+
await db("role_permissions").insert({
|
|
175
|
+
role_id: tenantAdminRoleId,
|
|
176
|
+
permission: JSON.stringify(perm),
|
|
177
|
+
created_at: new Date(),
|
|
178
|
+
modified_at: new Date(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Write credentials file
|
|
183
|
+
writeCredentials();
|
|
184
|
+
|
|
185
|
+
console.log("SaaS seed completed successfully.");
|
|
186
|
+
console.log("Super Admin:", SUPER_ADMIN_EMAIL);
|
|
187
|
+
console.log("Credentials written to credentials.md");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { seed, SUPER_ADMIN_PERMISSIONS, TENANT_ADMIN_PERMISSIONS, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD };
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate the content of the `credentials.md` file.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} password - The generated random password
|
|
198
|
+
* @returns {string} File content for credentials.md
|
|
199
|
+
*/
|
|
200
|
+
function generateCredentialsContent(password) {
|
|
201
|
+
return `# Super Admin Credentials
|
|
202
|
+
|
|
203
|
+
**Email:** admin@system.local
|
|
204
|
+
**Password:** ${password}
|
|
205
|
+
|
|
206
|
+
> ⚠️ **WARNING:** Change this password after first login. This file should not be committed to version control.
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Public API
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Generate seed files for the SaaS structure.
|
|
216
|
+
*
|
|
217
|
+
* Produces:
|
|
218
|
+
* - `seeds/saas-seed.js`: Seed script that inserts Super Admin and Tenant Admin Role
|
|
219
|
+
* - `credentials.md`: File with super admin email and generated password
|
|
220
|
+
*
|
|
221
|
+
* @param {string} adapter - Database adapter name (accepted for API consistency)
|
|
222
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
223
|
+
*/
|
|
224
|
+
function generateSaasSeeds(adapter) {
|
|
225
|
+
const password = crypto.randomBytes(16).toString("hex");
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
relPath: "seeds/saas-seed.js",
|
|
230
|
+
content: generateSeedContent(password),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
relPath: "credentials.md",
|
|
234
|
+
content: generateCredentialsContent(password),
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
generateSaasSeeds,
|
|
241
|
+
SUPER_ADMIN_PERMISSIONS,
|
|
242
|
+
TENANT_ADMIN_PERMISSIONS,
|
|
243
|
+
};
|