db-model-router 1.0.5 → 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 -14
- 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 +23 -45
- package/demo/.env.example +1 -0
- package/demo/app.js +3 -1
- package/demo/commons/db.js +11 -0
- package/demo/commons/migrate.js +3 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +22 -46
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
- package/demo/models/addresses.js +5 -3
- package/demo/models/cart_items.js +5 -3
- package/demo/models/carts.js +5 -3
- package/demo/models/categories.js +5 -3
- package/demo/models/coupons.js +5 -3
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +4 -2
- package/demo/models/orders.js +5 -3
- package/demo/models/payments.js +5 -3
- package/demo/models/product_images.js +4 -2
- package/demo/models/product_reviews.js +5 -3
- package/demo/models/product_variants.js +5 -3
- package/demo/models/products.js +5 -3
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +5 -3
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +12 -8
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +4 -2
- package/demo/openapi.json +1744 -616
- package/demo/package-lock.json +24 -24
- package/demo/package.json +9 -0
- package/demo/routes/{addresses.js → addresses/index.js} +1 -1
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
- package/demo/routes/{carts.js → carts/index.js} +1 -1
- package/demo/routes/{categories.js → categories/index.js} +1 -1
- package/demo/routes/{coupons.js → coupons/index.js} +1 -1
- package/demo/routes/index.js +39 -24
- package/demo/routes/{orders.js → orders/index.js} +1 -1
- package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
- package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
- package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
- package/demo/routes/{products.js → products/index.js} +1 -1
- package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
- package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
- package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
- 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.js → wishlists/index.js} +1 -1
- 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 +112 -43
- package/src/cli/commands/help.js +0 -1
- package/src/cli/diff-engine.js +2 -1
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +61 -22
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +42 -30
- 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/sqlite3/db.js +11 -0
- package/demo/docs/llm.md +0 -197
- package/demo/llms.txt +0 -70
- package/demo/routes/users.js +0 -6
- package/src/cli/commands/generate-llm-docs.js +0 -418
- /package/demo/migrations/{20260430155808_create_migrations_table.sql → 20260509170349_create_migrations_table.sql} +0 -0
|
@@ -5,6 +5,13 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
5
5
|
const basePath = options.basePath || "/api";
|
|
6
6
|
const title = options.title || "REST Router API";
|
|
7
7
|
const version = options.version || "1.0.0";
|
|
8
|
+
const relationships = options.relationships || [];
|
|
9
|
+
|
|
10
|
+
// Build a lookup: child table -> { parent, foreignKey }
|
|
11
|
+
const childMap = {};
|
|
12
|
+
for (const rel of relationships) {
|
|
13
|
+
childMap[rel.child] = { parent: rel.parent, foreignKey: rel.foreignKey };
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
const spec = {
|
|
10
17
|
openapi: "3.0.3",
|
|
@@ -39,14 +46,34 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
const ref = { $ref: `#/components/schemas/${schemaName}` };
|
|
42
|
-
|
|
49
|
+
|
|
50
|
+
// Determine path prefix: nested under parent if this is a child table
|
|
51
|
+
let prefix;
|
|
52
|
+
const isChild = !!childMap[m.table];
|
|
53
|
+
let fkParam = null;
|
|
54
|
+
if (isChild) {
|
|
55
|
+
const { parent, foreignKey } = childMap[m.table];
|
|
56
|
+
prefix = `${basePath}/${parent}/{${foreignKey}}/${m.table}`;
|
|
57
|
+
fkParam = {
|
|
58
|
+
name: foreignKey,
|
|
59
|
+
in: "path",
|
|
60
|
+
required: true,
|
|
61
|
+
schema: { type: "string" },
|
|
62
|
+
description: `${capitalize(parent)} foreign key`,
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
prefix = `${basePath}/${m.table}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper: prepend FK path param for child routes
|
|
69
|
+
const withFk = (params) => (fkParam ? [fkParam, ...params] : params);
|
|
43
70
|
|
|
44
71
|
// GET / — list
|
|
45
72
|
spec.paths[`${prefix}/`] = {
|
|
46
73
|
get: {
|
|
47
74
|
tags: [tag],
|
|
48
75
|
summary: `List ${m.table}`,
|
|
49
|
-
parameters: [
|
|
76
|
+
parameters: withFk([
|
|
50
77
|
{
|
|
51
78
|
name: "page",
|
|
52
79
|
in: "query",
|
|
@@ -69,7 +96,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
69
96
|
in: "query",
|
|
70
97
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
71
98
|
},
|
|
72
|
-
],
|
|
99
|
+
]),
|
|
73
100
|
responses: {
|
|
74
101
|
200: {
|
|
75
102
|
description: "Success",
|
|
@@ -141,7 +168,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
141
168
|
get: {
|
|
142
169
|
tags: [tag],
|
|
143
170
|
summary: `Get ${m.table} by ${pk}`,
|
|
144
|
-
parameters: [
|
|
171
|
+
parameters: withFk([
|
|
145
172
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
146
173
|
{ name: "select_columns", in: "query", schema: { type: "string" } },
|
|
147
174
|
{
|
|
@@ -149,7 +176,7 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
149
176
|
in: "query",
|
|
150
177
|
schema: { type: "string", enum: ["json", "csv", "xml"] },
|
|
151
178
|
},
|
|
152
|
-
],
|
|
179
|
+
]),
|
|
153
180
|
responses: {
|
|
154
181
|
200: {
|
|
155
182
|
description: "Success",
|
|
@@ -161,9 +188,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
161
188
|
post: {
|
|
162
189
|
tags: [tag],
|
|
163
190
|
summary: `Insert a ${m.table}`,
|
|
164
|
-
parameters: [
|
|
191
|
+
parameters: withFk([
|
|
165
192
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
166
|
-
],
|
|
193
|
+
]),
|
|
167
194
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
168
195
|
responses: {
|
|
169
196
|
200: {
|
|
@@ -175,9 +202,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
175
202
|
put: {
|
|
176
203
|
tags: [tag],
|
|
177
204
|
summary: `Update a ${m.table}`,
|
|
178
|
-
parameters: [
|
|
205
|
+
parameters: withFk([
|
|
179
206
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
180
|
-
],
|
|
207
|
+
]),
|
|
181
208
|
requestBody: { content: { "application/json": { schema: ref } } },
|
|
182
209
|
responses: {
|
|
183
210
|
200: {
|
|
@@ -190,9 +217,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
190
217
|
patch: {
|
|
191
218
|
tags: [tag],
|
|
192
219
|
summary: `Partial update a ${m.table}`,
|
|
193
|
-
parameters: [
|
|
220
|
+
parameters: withFk([
|
|
194
221
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
195
|
-
],
|
|
222
|
+
]),
|
|
196
223
|
requestBody: {
|
|
197
224
|
content: { "application/json": { schema: { type: "object" } } },
|
|
198
225
|
},
|
|
@@ -207,9 +234,9 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
207
234
|
delete: {
|
|
208
235
|
tags: [tag],
|
|
209
236
|
summary: `Delete a ${m.table}`,
|
|
210
|
-
parameters: [
|
|
237
|
+
parameters: withFk([
|
|
211
238
|
{ name: pk, in: "path", required: true, schema: { type: "string" } },
|
|
212
|
-
],
|
|
239
|
+
]),
|
|
213
240
|
responses: {
|
|
214
241
|
200: { description: "Deleted" },
|
|
215
242
|
404: { description: "Not Found" },
|
|
@@ -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";
|
|
30
31
|
|
|
32
|
+
const router = express.Router({ mergeParams: true });
|
|
31
33
|
const { route } = dbModelRouter;
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
router.use("/", route(${varName}));
|
|
36
|
+
|
|
37
|
+
export default router;
|
|
34
38
|
`;
|
|
35
39
|
}
|
|
36
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
|
+
}
|
|
61
|
+
|
|
62
|
+
code += `
|
|
63
|
+
const router = express.Router({ mergeParams: true });
|
|
64
|
+
const { route } = dbModelRouter;
|
|
65
|
+
|
|
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;
|
|
81
|
+
}
|
|
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,19 +144,13 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
100
144
|
imports += `router.use("/docs", docsRoute);\n`;
|
|
101
145
|
}
|
|
102
146
|
|
|
103
|
-
// Mount top-level routes
|
|
147
|
+
// Mount top-level routes (children are already mounted inside their parent's index.js)
|
|
104
148
|
for (const table of tableNames) {
|
|
105
149
|
if (nestedChildren.has(table)) continue;
|
|
106
150
|
const varName = safeVarName(table);
|
|
107
151
|
imports += `router.use("/${table}", ${varName}Route);\n`;
|
|
108
152
|
}
|
|
109
153
|
|
|
110
|
-
// Mount child routes under parent path
|
|
111
|
-
for (const rel of relationships) {
|
|
112
|
-
const childVar = safeVarName(rel.child);
|
|
113
|
-
imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
154
|
imports += "\nexport default router;\n";
|
|
117
155
|
return imports;
|
|
118
156
|
}
|
|
@@ -472,7 +510,7 @@ async function main() {
|
|
|
472
510
|
}
|
|
473
511
|
}
|
|
474
512
|
if (modelMeta.length > 0) {
|
|
475
|
-
const spec = generateOpenAPISpec(modelMeta);
|
|
513
|
+
const spec = generateOpenAPISpec(modelMeta, { relationships });
|
|
476
514
|
const specPath = path.join(routesDir, "openapi.json");
|
|
477
515
|
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
478
516
|
console.log(` Created ${specPath}`);
|
|
@@ -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,122 @@
|
|
|
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
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read the existing .gitignore file and append `credentials.md` if not already present.
|
|
27
|
+
* Returns the full .gitignore content to be written.
|
|
28
|
+
*
|
|
29
|
+
* @returns {string} Updated .gitignore content
|
|
30
|
+
*/
|
|
31
|
+
function getGitignoreContent() {
|
|
32
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
33
|
+
let content = "";
|
|
34
|
+
if (fs.existsSync(gitignorePath)) {
|
|
35
|
+
content = fs.readFileSync(gitignorePath, "utf8");
|
|
36
|
+
}
|
|
37
|
+
if (!content.includes("credentials.md")) {
|
|
38
|
+
content = content.trimEnd() + "\ncredentials.md\n";
|
|
39
|
+
}
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Main SaaS structure generator orchestrator.
|
|
45
|
+
*
|
|
46
|
+
* Calls all sub-generators and aggregates their output into a single
|
|
47
|
+
* planned[] array of { relPath, content } objects compatible with the
|
|
48
|
+
* existing generate command's file write loop.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} adapter - Database adapter name (e.g. "postgres", "mysql", "sqlite3")
|
|
51
|
+
* @param {object} [options] - Generation options
|
|
52
|
+
* @param {boolean} [options.dryRun] - If true, files will not be written (handled by caller)
|
|
53
|
+
* @param {boolean} [options.json] - If true, output JSON format (handled by caller)
|
|
54
|
+
* @param {Date|number} [options.timestamp] - Base timestamp for migration files
|
|
55
|
+
* @param {string[]} [options.tableNames] - Schema-generated table names for routes index
|
|
56
|
+
* @param {Array<{parent, child, foreignKey}>} [options.relationships] - Schema relationships for routes index
|
|
57
|
+
* @param {{ includeDocs?: boolean }} [options.routeOptions] - Options for routes index generation
|
|
58
|
+
* @returns {Array<{ relPath: string, content: string }>} Combined planned file array
|
|
59
|
+
*/
|
|
60
|
+
function generateSaasStructure(adapter, options) {
|
|
61
|
+
const opts = options || {};
|
|
62
|
+
const planned = [];
|
|
63
|
+
|
|
64
|
+
// 1. Migrations
|
|
65
|
+
const timestamp = opts.timestamp || new Date();
|
|
66
|
+
const migrations = generateSaasMigrations(adapter, timestamp);
|
|
67
|
+
for (const entry of migrations) {
|
|
68
|
+
planned.push(entry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Models (includes models/index.js barrel with both SaaS + dbmr tables)
|
|
72
|
+
const models = generateSaasModels(adapter, opts.tableNames || []);
|
|
73
|
+
for (const entry of models) {
|
|
74
|
+
planned.push(entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Middleware
|
|
78
|
+
planned.push(generateAuthenticateMiddleware());
|
|
79
|
+
planned.push(generateTenantIsolationMiddleware());
|
|
80
|
+
planned.push(generateHasPermissionMiddleware());
|
|
81
|
+
|
|
82
|
+
// 4. Routes (CRUD + auth + combined index with dbmr routes)
|
|
83
|
+
const crudRoutes = generateCrudRoutes();
|
|
84
|
+
for (const entry of crudRoutes) {
|
|
85
|
+
planned.push(entry);
|
|
86
|
+
}
|
|
87
|
+
planned.push(generateAuthRoutes());
|
|
88
|
+
planned.push(
|
|
89
|
+
generateRoutesIndex(
|
|
90
|
+
opts.tableNames || [],
|
|
91
|
+
opts.relationships || [],
|
|
92
|
+
opts.routeOptions || {},
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 5. Seeds
|
|
97
|
+
const seeds = generateSaasSeeds(adapter);
|
|
98
|
+
for (const entry of seeds) {
|
|
99
|
+
planned.push(entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 6. Utilities
|
|
103
|
+
planned.push({
|
|
104
|
+
relPath: "commons/password.js",
|
|
105
|
+
content: generatePasswordUtil(),
|
|
106
|
+
});
|
|
107
|
+
planned.push({
|
|
108
|
+
relPath: "commons/modules.js",
|
|
109
|
+
content: generateModulesUtil(),
|
|
110
|
+
});
|
|
111
|
+
planned.push({
|
|
112
|
+
relPath: "commons/webhook.js",
|
|
113
|
+
content: generateWebhookUtil(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 7. .gitignore update (add credentials.md)
|
|
117
|
+
planned.push({ relPath: ".gitignore", content: getGitignoreContent() });
|
|
118
|
+
|
|
119
|
+
return planned;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { generateSaasStructure, getGitignoreContent };
|
|
@@ -169,6 +169,7 @@ 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");
|
|
174
175
|
|
|
@@ -367,21 +368,21 @@ function generateAppJs(answers) {
|
|
|
367
368
|
answers.framework === "ultimate-express" ? "ultimate-express" : "express";
|
|
368
369
|
|
|
369
370
|
// Imports
|
|
370
|
-
let imports = `
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
let imports = `import express from "${frameworkPkg}";
|
|
372
|
+
import { init, db } from "db-model-router";
|
|
373
|
+
import session from "express-session";`;
|
|
373
374
|
|
|
374
375
|
if (answers.session === "redis") {
|
|
375
|
-
imports += `\
|
|
376
|
-
|
|
376
|
+
imports += `\nimport RedisStore from "connect-redis";
|
|
377
|
+
import { Redis } from "ioredis";`;
|
|
377
378
|
}
|
|
378
379
|
if (answers.rateLimiting) {
|
|
379
|
-
imports += `\
|
|
380
|
+
imports += `\nimport rateLimit from "express-rate-limit";`;
|
|
380
381
|
}
|
|
381
382
|
if (answers.helmet) {
|
|
382
|
-
imports += `\
|
|
383
|
+
imports += `\nimport helmet from "helmet";`;
|
|
383
384
|
}
|
|
384
|
-
imports += `\
|
|
385
|
+
imports += `\nimport logger from "./middleware/logger.js";`;
|
|
385
386
|
|
|
386
387
|
// Rate limiting block
|
|
387
388
|
const rateLimitBlock = answers.rateLimiting
|
|
@@ -396,9 +397,7 @@ const { Redis } = require("ioredis");`;
|
|
|
396
397
|
const helmetBlock = answers.helmet ? `app.use(helmet());` : "";
|
|
397
398
|
|
|
398
399
|
return `${imports}
|
|
399
|
-
|
|
400
|
-
// Load environment variables
|
|
401
|
-
require("dotenv").config();
|
|
400
|
+
import "dotenv/config";
|
|
402
401
|
|
|
403
402
|
// Initialize database adapter
|
|
404
403
|
init("${answers.database}");
|
|
@@ -413,6 +412,10 @@ app.use(express.urlencoded({ extended: true }));
|
|
|
413
412
|
${helmetBlock ? helmetBlock + "\n" : ""}${rateLimitBlock ? rateLimitBlock + "\n" : ""}${sessionBlock(answers)}
|
|
414
413
|
app.use(logger);
|
|
415
414
|
|
|
415
|
+
// Routes
|
|
416
|
+
import routes from "#routes/index.js";
|
|
417
|
+
app.use(process.env.API_BASE_PATH || "/api", routes);
|
|
418
|
+
|
|
416
419
|
// Health check
|
|
417
420
|
app.get("/health", (req, res) => {
|
|
418
421
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
@@ -428,7 +431,7 @@ app.listen(PORT, () => {
|
|
|
428
431
|
console.log(\`Server running on port \${PORT}\`);
|
|
429
432
|
});
|
|
430
433
|
|
|
431
|
-
|
|
434
|
+
export default app;
|
|
432
435
|
`;
|
|
433
436
|
}
|
|
434
437
|
|
|
@@ -551,14 +554,15 @@ function generateMigrateScript(answers) {
|
|
|
551
554
|
|
|
552
555
|
if (isNoSql) {
|
|
553
556
|
return `#!/usr/bin/env node
|
|
554
|
-
|
|
557
|
+
import fs from "fs";
|
|
558
|
+
import path from "path";
|
|
559
|
+
import crypto from "crypto";
|
|
560
|
+
import { fileURLToPath } from "url";
|
|
561
|
+
import "dotenv/config";
|
|
555
562
|
|
|
556
|
-
|
|
557
|
-
const path = require("path");
|
|
558
|
-
const crypto = require("crypto");
|
|
559
|
-
require("dotenv").config();
|
|
563
|
+
import { init, db } from "db-model-router";
|
|
560
564
|
|
|
561
|
-
const
|
|
565
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
562
566
|
|
|
563
567
|
init("${answers.database}");
|
|
564
568
|
|
|
@@ -598,7 +602,7 @@ async function migrate() {
|
|
|
598
602
|
const content = fs.readFileSync(filePath, "utf8");
|
|
599
603
|
const checksum = crypto.createHash("md5").update(content).digest("hex");
|
|
600
604
|
|
|
601
|
-
const migration =
|
|
605
|
+
const migration = await import(filePath);
|
|
602
606
|
console.log(\` Running migration: \${file}\`);
|
|
603
607
|
await migration.up(db);
|
|
604
608
|
await recordMigration(file, checksum);
|
|
@@ -622,14 +626,15 @@ migrate().catch(err => {
|
|
|
622
626
|
}
|
|
623
627
|
|
|
624
628
|
return `#!/usr/bin/env node
|
|
625
|
-
|
|
629
|
+
import fs from "fs";
|
|
630
|
+
import path from "path";
|
|
631
|
+
import crypto from "crypto";
|
|
632
|
+
import { fileURLToPath } from "url";
|
|
633
|
+
import "dotenv/config";
|
|
626
634
|
|
|
627
|
-
|
|
628
|
-
const path = require("path");
|
|
629
|
-
const crypto = require("crypto");
|
|
630
|
-
require("dotenv").config();
|
|
635
|
+
import { init, db } from "db-model-router";
|
|
631
636
|
|
|
632
|
-
const
|
|
637
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
633
638
|
|
|
634
639
|
init("${answers.database}");
|
|
635
640
|
|
|
@@ -700,14 +705,15 @@ function generateAddMigrationScript(answers) {
|
|
|
700
705
|
const isNoSql = NOSQL_DATABASES.includes(answers.database);
|
|
701
706
|
const ext = isNoSql ? "js" : "sql";
|
|
702
707
|
const template = isNoSql
|
|
703
|
-
? `
|
|
708
|
+
? `export async function up(db) {\\n // Write your migration here\\n}\\n\\nexport async function down(db) {\\n // Write your rollback here\\n}\\n`
|
|
704
709
|
: `-- Write your migration SQL here\\n`;
|
|
705
710
|
|
|
706
711
|
return `#!/usr/bin/env node
|
|
707
|
-
|
|
712
|
+
import fs from "fs";
|
|
713
|
+
import path from "path";
|
|
714
|
+
import { fileURLToPath } from "url";
|
|
708
715
|
|
|
709
|
-
const
|
|
710
|
-
const path = require("path");
|
|
716
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
711
717
|
|
|
712
718
|
const migrationsDir = path.join(__dirname, "migrations");
|
|
713
719
|
|
|
@@ -1422,6 +1428,8 @@ if (isMain) {
|
|
|
1422
1428
|
const pkg = await import("db-model-router");
|
|
1423
1429
|
const mod = pkg.default || pkg;
|
|
1424
1430
|
mod.init("${answers.database}");
|
|
1431
|
+
mod.db.connect({
|
|
1432
|
+
${dbConnectArgs(answers.database)}});
|
|
1425
1433
|
const migrationsDir = path.join(__dirname, "${migrationsRel}");
|
|
1426
1434
|
runMigrations(mod.db, migrationsDir)
|
|
1427
1435
|
.then(() => process.exit(0))
|
|
@@ -1490,6 +1498,8 @@ if (isMain) {
|
|
|
1490
1498
|
const pkg = await import("db-model-router");
|
|
1491
1499
|
const mod = pkg.default || pkg;
|
|
1492
1500
|
mod.init("${answers.database}");
|
|
1501
|
+
mod.db.connect({
|
|
1502
|
+
${dbConnectArgs(answers.database)}});
|
|
1493
1503
|
const migrationsDir = path.join(__dirname, "${migrationsRel}");
|
|
1494
1504
|
runMigrations(mod.db, migrationsDir)
|
|
1495
1505
|
.then(() => process.exit(0))
|
|
@@ -1726,7 +1736,9 @@ import configureSession from "${commonsPrefix}/session.js";
|
|
|
1726
1736
|
import applySecurity from "${commonsPrefix}/security.js";
|
|
1727
1737
|
import logger from "${middlewarePrefix}/logger.js";
|
|
1728
1738
|
import route from "${routePrefix}/index.js";
|
|
1729
|
-
|
|
1739
|
+
import { fileURLToPath } from 'node:url';
|
|
1740
|
+
import path from "path";
|
|
1741
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1730
1742
|
const app = express();
|
|
1731
1743
|
const PORT = process.env.PORT || 3000;
|
|
1732
1744
|
|
package/src/cli/init.js
CHANGED
|
@@ -254,6 +254,14 @@ function updatePackageJson(answers, outputDir) {
|
|
|
254
254
|
const scripts = getScripts(outputDir);
|
|
255
255
|
|
|
256
256
|
pkg.type = "module";
|
|
257
|
+
pkg.imports = {
|
|
258
|
+
"#root/*.js": "./*.js",
|
|
259
|
+
"#models": "./models/index.js",
|
|
260
|
+
"#models/*.js": "./models/*.js",
|
|
261
|
+
"#routes/*.js": "./routes/*.js",
|
|
262
|
+
"#commons/*.js": "./commons/*.js",
|
|
263
|
+
"#middleware/*.js": "./middleware/*.js",
|
|
264
|
+
};
|
|
257
265
|
pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
|
|
258
266
|
pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
|
|
259
267
|
pkg.devDependencies = Object.assign(
|
package/src/cli/main.js
CHANGED
|
@@ -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
|
|
|
@@ -64,10 +67,14 @@ const COMMAND_FLAGS = {
|
|
|
64
67
|
["--routes", "Generate only route files"],
|
|
65
68
|
["--openapi", "Generate only OpenAPI spec"],
|
|
66
69
|
["--tests", "Generate only test files"],
|
|
67
|
-
["--
|
|
70
|
+
["--db-manager", "Generate DB Manager UI (SQL adapters only)"],
|
|
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
|
/**
|