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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS migration generator.
|
|
5
|
+
*
|
|
6
|
+
* Generates CREATE TABLE migration files for all SaaS tables with proper
|
|
7
|
+
* column types, foreign key constraints, and unique constraints per adapter.
|
|
8
|
+
* Supports SQL adapters (postgres, mysql, sqlite3, mssql, oracle, cockroachdb)
|
|
9
|
+
* and NoSQL adapters (mongodb, dynamodb, redis).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { mapColumnType } = require("../generate-migration");
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const SQL_ADAPTERS = [
|
|
19
|
+
"mysql",
|
|
20
|
+
"postgres",
|
|
21
|
+
"sqlite3",
|
|
22
|
+
"mssql",
|
|
23
|
+
"cockroachdb",
|
|
24
|
+
"oracle",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Map a TEXT column type per adapter.
|
|
29
|
+
* Used for columns that need unbounded text storage (e.g. response_body).
|
|
30
|
+
* @param {string} adapter
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function textType(adapter) {
|
|
34
|
+
if (adapter === "oracle") return "CLOB";
|
|
35
|
+
if (adapter === "mssql") return "NVARCHAR(MAX)";
|
|
36
|
+
return "TEXT";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Table definitions in dependency order.
|
|
41
|
+
* Each entry defines columns, primary key, foreign keys, and unique constraints.
|
|
42
|
+
*/
|
|
43
|
+
const TABLE_DEFINITIONS = [
|
|
44
|
+
{
|
|
45
|
+
name: "tenants",
|
|
46
|
+
columns: [
|
|
47
|
+
{ name: "tenant_id", rule: "auto_increment", pk: true },
|
|
48
|
+
{ name: "name", rule: "required|string" },
|
|
49
|
+
{ name: "slug", rule: "required|string" },
|
|
50
|
+
{ name: "attributes", rule: "object" },
|
|
51
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
52
|
+
{ name: "modified_at", rule: "datetime", timestamp: true },
|
|
53
|
+
],
|
|
54
|
+
foreignKeys: [],
|
|
55
|
+
unique: [["slug"]],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "roles",
|
|
59
|
+
columns: [
|
|
60
|
+
{ name: "role_id", rule: "auto_increment", pk: true },
|
|
61
|
+
{ name: "tenant_id", rule: "integer" },
|
|
62
|
+
{ name: "name", rule: "required|string" },
|
|
63
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
64
|
+
{ name: "modified_at", rule: "datetime", timestamp: true },
|
|
65
|
+
],
|
|
66
|
+
foreignKeys: [
|
|
67
|
+
{ column: "tenant_id", references: "tenants", refColumn: "tenant_id" },
|
|
68
|
+
],
|
|
69
|
+
unique: [["tenant_id", "name"]],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "users",
|
|
73
|
+
columns: [
|
|
74
|
+
{ name: "user_id", rule: "auto_increment", pk: true },
|
|
75
|
+
{ name: "email", rule: "required|string" },
|
|
76
|
+
{ name: "phone", rule: "string" },
|
|
77
|
+
{ name: "password_hash", rule: "required|string" },
|
|
78
|
+
{ name: "name", rule: "required|string" },
|
|
79
|
+
{ name: "unique_attribute", rule: "required|string" },
|
|
80
|
+
{ name: "tenant_id", rule: "integer" },
|
|
81
|
+
{ name: "role_id", rule: "required|integer" },
|
|
82
|
+
{ name: "attributes", rule: "object" },
|
|
83
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
84
|
+
{ name: "modified_at", rule: "datetime", timestamp: true },
|
|
85
|
+
],
|
|
86
|
+
foreignKeys: [
|
|
87
|
+
{ column: "tenant_id", references: "tenants", refColumn: "tenant_id" },
|
|
88
|
+
{ column: "role_id", references: "roles", refColumn: "role_id" },
|
|
89
|
+
],
|
|
90
|
+
unique: [["tenant_id", "unique_attribute"]],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "role_permissions",
|
|
94
|
+
columns: [
|
|
95
|
+
{ name: "role_permission_id", rule: "auto_increment", pk: true },
|
|
96
|
+
{ name: "role_id", rule: "required|integer" },
|
|
97
|
+
{ name: "permission", rule: "required|object" },
|
|
98
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
99
|
+
{ name: "modified_at", rule: "datetime", timestamp: true },
|
|
100
|
+
],
|
|
101
|
+
foreignKeys: [
|
|
102
|
+
{ column: "role_id", references: "roles", refColumn: "role_id" },
|
|
103
|
+
],
|
|
104
|
+
unique: [],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "webhooks",
|
|
108
|
+
columns: [
|
|
109
|
+
{ name: "webhook_id", rule: "auto_increment", pk: true },
|
|
110
|
+
{ name: "tenant_id", rule: "required|integer" },
|
|
111
|
+
{ name: "url", rule: "required|string" },
|
|
112
|
+
{ name: "key", rule: "required|string" },
|
|
113
|
+
{ name: "secret", rule: "required|string" },
|
|
114
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
115
|
+
{ name: "modified_at", rule: "datetime", timestamp: true },
|
|
116
|
+
],
|
|
117
|
+
foreignKeys: [
|
|
118
|
+
{ column: "tenant_id", references: "tenants", refColumn: "tenant_id" },
|
|
119
|
+
],
|
|
120
|
+
unique: [],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "webhook_logs",
|
|
124
|
+
columns: [
|
|
125
|
+
{ name: "webhook_log_id", rule: "auto_increment", pk: true },
|
|
126
|
+
{ name: "webhook_id", rule: "required|integer" },
|
|
127
|
+
{ name: "tenant_id", rule: "required|integer" },
|
|
128
|
+
{ name: "event_type", rule: "required|string" },
|
|
129
|
+
{ name: "payload", rule: "required|object" },
|
|
130
|
+
{ name: "status", rule: "required|string" },
|
|
131
|
+
{ name: "response_body", rule: "text" },
|
|
132
|
+
{ name: "response_status_code", rule: "integer" },
|
|
133
|
+
{ name: "created_at", rule: "datetime", timestamp: true },
|
|
134
|
+
],
|
|
135
|
+
foreignKeys: [
|
|
136
|
+
{ column: "webhook_id", references: "webhooks", refColumn: "webhook_id" },
|
|
137
|
+
],
|
|
138
|
+
unique: [],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Helpers
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Quote an identifier based on adapter.
|
|
148
|
+
* @param {string} name
|
|
149
|
+
* @param {string} adapter
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
function quoteIdent(name, adapter) {
|
|
153
|
+
if (adapter === "mssql") return `[${name}]`;
|
|
154
|
+
if (adapter === "oracle") return `"${name.toUpperCase()}"`;
|
|
155
|
+
return name;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format a Date as YYYYMMDDHHMMSS (14-digit string).
|
|
160
|
+
* @param {Date} date
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
function migrationTimestamp(date) {
|
|
164
|
+
const y = String(date.getFullYear()).padStart(4, "0");
|
|
165
|
+
const mo = String(date.getMonth() + 1).padStart(2, "0");
|
|
166
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
167
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
168
|
+
const mi = String(date.getMinutes()).padStart(2, "0");
|
|
169
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
170
|
+
return `${y}${mo}${d}${h}${mi}${s}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Default timestamp expression per adapter.
|
|
175
|
+
* @param {string} adapter
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
function defaultTimestamp(adapter) {
|
|
179
|
+
switch (adapter) {
|
|
180
|
+
case "mssql":
|
|
181
|
+
return " DEFAULT GETDATE()";
|
|
182
|
+
default:
|
|
183
|
+
return " DEFAULT CURRENT_TIMESTAMP";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// SQL Migration Generation
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate a CREATE TABLE SQL statement for a SaaS table definition.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} tableDef - Table definition from TABLE_DEFINITIONS
|
|
195
|
+
* @param {string} adapter - Database adapter name
|
|
196
|
+
* @returns {string} SQL CREATE TABLE statement
|
|
197
|
+
*/
|
|
198
|
+
function generateCreateTableSQL(tableDef, adapter) {
|
|
199
|
+
const lines = [];
|
|
200
|
+
|
|
201
|
+
for (const col of tableDef.columns) {
|
|
202
|
+
let line;
|
|
203
|
+
|
|
204
|
+
// Handle "text" type specially (not supported by mapColumnType)
|
|
205
|
+
if (col.rule === "text") {
|
|
206
|
+
const sqlType = textType(adapter);
|
|
207
|
+
line = ` ${quoteIdent(col.name, adapter)} ${sqlType}`;
|
|
208
|
+
// text columns are nullable by default
|
|
209
|
+
} else {
|
|
210
|
+
const { sqlType, nullable, isAutoIncrement } = mapColumnType(
|
|
211
|
+
col.rule,
|
|
212
|
+
adapter,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (col.pk) {
|
|
216
|
+
if (isAutoIncrement && adapter === "sqlite3") {
|
|
217
|
+
line = ` ${quoteIdent(col.name, adapter)} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
218
|
+
} else {
|
|
219
|
+
line = ` ${quoteIdent(col.name, adapter)} ${sqlType} PRIMARY KEY`;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
line = ` ${quoteIdent(col.name, adapter)} ${sqlType}`;
|
|
223
|
+
if (!nullable) {
|
|
224
|
+
line += " NOT NULL";
|
|
225
|
+
}
|
|
226
|
+
if (col.timestamp) {
|
|
227
|
+
line += defaultTimestamp(adapter);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push(line);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Foreign key constraints
|
|
236
|
+
for (const fk of tableDef.foreignKeys) {
|
|
237
|
+
lines.push(
|
|
238
|
+
` FOREIGN KEY (${quoteIdent(fk.column, adapter)}) REFERENCES ${quoteIdent(fk.references, adapter)}(${quoteIdent(fk.refColumn, adapter)})`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Unique constraints
|
|
243
|
+
for (const cols of tableDef.unique) {
|
|
244
|
+
const quotedCols = cols.map((c) => quoteIdent(c, adapter)).join(", ");
|
|
245
|
+
lines.push(` UNIQUE (${quotedCols})`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const createPrefix =
|
|
249
|
+
adapter === "oracle" || adapter === "mssql"
|
|
250
|
+
? `CREATE TABLE ${quoteIdent(tableDef.name, adapter)}`
|
|
251
|
+
: `CREATE TABLE IF NOT EXISTS ${quoteIdent(tableDef.name, adapter)}`;
|
|
252
|
+
|
|
253
|
+
return `${createPrefix} (\n${lines.join(",\n")}\n);\n`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// NoSQL Migration Generation
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Generate a MongoDB migration JS file for a SaaS table (collection).
|
|
262
|
+
* @param {object} tableDef - Table definition
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
function generateMongoDBMigration(tableDef) {
|
|
266
|
+
const indexLines = [];
|
|
267
|
+
|
|
268
|
+
// Unique indexes
|
|
269
|
+
for (const cols of tableDef.unique) {
|
|
270
|
+
const indexObj = cols.map((c) => `${c}: 1`).join(", ");
|
|
271
|
+
indexLines.push(
|
|
272
|
+
` await db.collection("${tableDef.name}").createIndex({ ${indexObj} }, { unique: true });`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return `"use strict";
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
async up(db) {
|
|
280
|
+
await db.createCollection("${tableDef.name}");
|
|
281
|
+
${indexLines.length > 0 ? indexLines.join("\n") + "\n" : ""} },
|
|
282
|
+
|
|
283
|
+
async down(db) {
|
|
284
|
+
await db.collection("${tableDef.name}").drop();
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate a DynamoDB migration JS file for a SaaS table.
|
|
292
|
+
* @param {object} tableDef - Table definition
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
function generateDynamoDBMigration(tableDef) {
|
|
296
|
+
const pk = tableDef.columns.find((c) => c.pk);
|
|
297
|
+
const pkName = pk ? pk.name : "id";
|
|
298
|
+
|
|
299
|
+
return `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
|
|
300
|
+
|
|
301
|
+
export async function up(db) {
|
|
302
|
+
await db.send(new CreateTableCommand({
|
|
303
|
+
TableName: "${tableDef.name}",
|
|
304
|
+
KeySchema: [{ AttributeName: "${pkName}", KeyType: "HASH" }],
|
|
305
|
+
AttributeDefinitions: [{ AttributeName: "${pkName}", AttributeType: "N" }],
|
|
306
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function down(db) {
|
|
311
|
+
await db.send(new DeleteTableCommand({ TableName: "${tableDef.name}" }));
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate a Redis migration JS file for a SaaS table.
|
|
318
|
+
* @param {object} tableDef - Table definition
|
|
319
|
+
* @returns {string}
|
|
320
|
+
*/
|
|
321
|
+
function generateRedisMigration(tableDef) {
|
|
322
|
+
return `"use strict";
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
async up(db) {
|
|
326
|
+
// Redis is schema-less. This migration is a placeholder.
|
|
327
|
+
// Data for "${tableDef.name}" will be stored as hash keys: ${tableDef.name}:<id>
|
|
328
|
+
console.log("Redis: ${tableDef.name} collection ready (schema-less).");
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async down(db) {
|
|
332
|
+
// Warning: this deletes ALL keys matching the pattern
|
|
333
|
+
// In production, use SCAN instead of KEYS
|
|
334
|
+
const keys = await db.keys("${tableDef.name}:*");
|
|
335
|
+
if (keys.length > 0) {
|
|
336
|
+
await db.del(...keys);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// Public API
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate migration files for all SaaS tables.
|
|
349
|
+
*
|
|
350
|
+
* For SQL adapters: produces a single .sql file with all CREATE TABLE statements.
|
|
351
|
+
* For NoSQL adapters: produces a single .js file with all collection setups.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} adapter - Database adapter name
|
|
354
|
+
* @param {Date|number} [timestamp] - Base timestamp (Date object or ms since epoch). Defaults to new Date().
|
|
355
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
356
|
+
*/
|
|
357
|
+
function generateSaasMigrations(adapter, timestamp) {
|
|
358
|
+
const baseDate =
|
|
359
|
+
timestamp instanceof Date
|
|
360
|
+
? timestamp
|
|
361
|
+
: typeof timestamp === "number"
|
|
362
|
+
? new Date(timestamp)
|
|
363
|
+
: new Date();
|
|
364
|
+
|
|
365
|
+
const isSql = SQL_ADAPTERS.includes(adapter);
|
|
366
|
+
const tsStr = migrationTimestamp(baseDate);
|
|
367
|
+
|
|
368
|
+
if (isSql) {
|
|
369
|
+
// Single SQL file with all CREATE TABLE statements
|
|
370
|
+
const statements = [];
|
|
371
|
+
for (const tableDef of TABLE_DEFINITIONS) {
|
|
372
|
+
statements.push(generateCreateTableSQL(tableDef, adapter));
|
|
373
|
+
}
|
|
374
|
+
const content = statements.join("\n");
|
|
375
|
+
return [
|
|
376
|
+
{
|
|
377
|
+
relPath: `migrations/${tsStr}_create_saas_tables.sql`,
|
|
378
|
+
content,
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
} else if (adapter === "mongodb") {
|
|
382
|
+
// Single JS file with all MongoDB collection setups
|
|
383
|
+
const parts = TABLE_DEFINITIONS.map((td) => generateMongoDBMigration(td));
|
|
384
|
+
const content = `"use strict";
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
async up(db) {
|
|
388
|
+
${TABLE_DEFINITIONS.map((td) => {
|
|
389
|
+
const indexLines = [];
|
|
390
|
+
for (const cols of td.unique) {
|
|
391
|
+
const indexObj = cols.map((c) => `${c}: 1`).join(", ");
|
|
392
|
+
indexLines.push(
|
|
393
|
+
` await db.collection("${td.name}").createIndex({ ${indexObj} }, { unique: true });`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return ` await db.createCollection("${td.name}");\n${indexLines.join("\n")}`;
|
|
397
|
+
}).join("\n")}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
async down(db) {
|
|
401
|
+
${TABLE_DEFINITIONS.map((td) => ` await db.collection("${td.name}").drop();`).join("\n")}
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
`;
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
relPath: `migrations/${tsStr}_create_saas_tables.js`,
|
|
408
|
+
content,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
} else if (adapter === "dynamodb") {
|
|
412
|
+
const pkNames = TABLE_DEFINITIONS.map((td) => {
|
|
413
|
+
const pk = td.columns.find((c) => c.pk);
|
|
414
|
+
return { table: td.name, pk: pk ? pk.name : "id" };
|
|
415
|
+
});
|
|
416
|
+
const content = `import { CreateTableCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb";
|
|
417
|
+
|
|
418
|
+
export async function up(db) {
|
|
419
|
+
${pkNames
|
|
420
|
+
.map(
|
|
421
|
+
(t) => ` await db.send(new CreateTableCommand({
|
|
422
|
+
TableName: "${t.table}",
|
|
423
|
+
KeySchema: [{ AttributeName: "${t.pk}", KeyType: "HASH" }],
|
|
424
|
+
AttributeDefinitions: [{ AttributeName: "${t.pk}", AttributeType: "N" }],
|
|
425
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
426
|
+
}));`,
|
|
427
|
+
)
|
|
428
|
+
.join("\n")}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function down(db) {
|
|
432
|
+
${pkNames.map((t) => ` await db.send(new DeleteTableCommand({ TableName: "${t.table}" }));`).join("\n")}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
return [
|
|
436
|
+
{
|
|
437
|
+
relPath: `migrations/${tsStr}_create_saas_tables.js`,
|
|
438
|
+
content,
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
} else if (adapter === "redis") {
|
|
442
|
+
const content = `"use strict";
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
async up(db) {
|
|
446
|
+
// Redis is schema-less. This migration is a placeholder.
|
|
447
|
+
${TABLE_DEFINITIONS.map((td) => ` console.log("Redis: ${td.name} collection ready (schema-less).");`).join("\n")}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
async down(db) {
|
|
451
|
+
${TABLE_DEFINITIONS.map(
|
|
452
|
+
(td) => ` const ${td.name}Keys = await db.keys("${td.name}:*");
|
|
453
|
+
if (${td.name}Keys.length > 0) await db.del(...${td.name}Keys);`,
|
|
454
|
+
).join("\n")}
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
`;
|
|
458
|
+
return [
|
|
459
|
+
{
|
|
460
|
+
relPath: `migrations/${tsStr}_create_saas_tables.js`,
|
|
461
|
+
content,
|
|
462
|
+
},
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fallback
|
|
467
|
+
return [
|
|
468
|
+
{
|
|
469
|
+
relPath: `migrations/${tsStr}_create_saas_tables.js`,
|
|
470
|
+
content: `// Migration for SaaS tables\n`,
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
module.exports = {
|
|
476
|
+
generateSaasMigrations,
|
|
477
|
+
generateCreateTableSQL,
|
|
478
|
+
TABLE_DEFINITIONS,
|
|
479
|
+
SQL_ADAPTERS,
|
|
480
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS model generator.
|
|
5
|
+
*
|
|
6
|
+
* Generates model files for all SaaS tables following the existing
|
|
7
|
+
* `generateModelFile` pattern with `model(db, table, structure, pk, unique, option)`.
|
|
8
|
+
* Models are adapter-agnostic (validation rules, not SQL types).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Model Definitions
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SaaS model definitions in the format expected by the model system.
|
|
17
|
+
* Each entry defines: table name, structure (validation rules), primary key,
|
|
18
|
+
* unique constraints, and option (timestamp columns).
|
|
19
|
+
*/
|
|
20
|
+
const MODEL_DEFINITIONS = [
|
|
21
|
+
{
|
|
22
|
+
table: "tenants",
|
|
23
|
+
structure: {
|
|
24
|
+
name: "required|string",
|
|
25
|
+
slug: "required|string",
|
|
26
|
+
attributes: "object",
|
|
27
|
+
},
|
|
28
|
+
primary_key: "tenant_id",
|
|
29
|
+
unique: ["slug"],
|
|
30
|
+
option: { created_at: "created_at", modified_at: "modified_at" },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
table: "roles",
|
|
34
|
+
structure: {
|
|
35
|
+
tenant_id: "integer",
|
|
36
|
+
name: "required|string",
|
|
37
|
+
},
|
|
38
|
+
primary_key: "role_id",
|
|
39
|
+
unique: ["tenant_id", "name"],
|
|
40
|
+
option: { created_at: "created_at", modified_at: "modified_at" },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
table: "users",
|
|
44
|
+
structure: {
|
|
45
|
+
email: "required|string",
|
|
46
|
+
phone: "string",
|
|
47
|
+
password_hash: "required|string",
|
|
48
|
+
name: "required|string",
|
|
49
|
+
unique_attribute: "required|string",
|
|
50
|
+
tenant_id: "integer",
|
|
51
|
+
role_id: "required|integer",
|
|
52
|
+
attributes: "object",
|
|
53
|
+
},
|
|
54
|
+
primary_key: "user_id",
|
|
55
|
+
unique: ["tenant_id", "unique_attribute"],
|
|
56
|
+
option: { created_at: "created_at", modified_at: "modified_at" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
table: "role_permissions",
|
|
60
|
+
structure: {
|
|
61
|
+
role_id: "required|integer",
|
|
62
|
+
permission: "required|object",
|
|
63
|
+
},
|
|
64
|
+
primary_key: "role_permission_id",
|
|
65
|
+
unique: ["role_permission_id"],
|
|
66
|
+
option: { created_at: "created_at", modified_at: "modified_at" },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
table: "webhooks",
|
|
70
|
+
structure: {
|
|
71
|
+
tenant_id: "required|integer",
|
|
72
|
+
url: "required|string",
|
|
73
|
+
key: "required|string",
|
|
74
|
+
secret: "required|string",
|
|
75
|
+
},
|
|
76
|
+
primary_key: "webhook_id",
|
|
77
|
+
unique: ["webhook_id"],
|
|
78
|
+
option: { created_at: "created_at", modified_at: "modified_at" },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
table: "webhook_logs",
|
|
82
|
+
structure: {
|
|
83
|
+
webhook_id: "required|integer",
|
|
84
|
+
tenant_id: "required|integer",
|
|
85
|
+
event_type: "required|string",
|
|
86
|
+
payload: "required|object",
|
|
87
|
+
status: "required|string",
|
|
88
|
+
response_body: "string",
|
|
89
|
+
response_status_code: "integer",
|
|
90
|
+
},
|
|
91
|
+
primary_key: "webhook_log_id",
|
|
92
|
+
unique: ["webhook_log_id"],
|
|
93
|
+
option: { created_at: "created_at" },
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Code Generation
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate the content of a single model file following the existing pattern.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} def - Model definition object
|
|
105
|
+
* @returns {string} Generated model file content
|
|
106
|
+
*/
|
|
107
|
+
function generateModelContent(def) {
|
|
108
|
+
const varName = def.table;
|
|
109
|
+
const structStr = JSON.stringify(def.structure, null, 4);
|
|
110
|
+
const uniqueStr = JSON.stringify(def.unique);
|
|
111
|
+
|
|
112
|
+
const optionParts = [];
|
|
113
|
+
if (def.option.created_at)
|
|
114
|
+
optionParts.push(`created_at: "${def.option.created_at}"`);
|
|
115
|
+
if (def.option.modified_at)
|
|
116
|
+
optionParts.push(`modified_at: "${def.option.modified_at}"`);
|
|
117
|
+
const optionStr = `{ ${optionParts.join(", ")} }`;
|
|
118
|
+
|
|
119
|
+
return `import dbModelRouter from "db-model-router";
|
|
120
|
+
|
|
121
|
+
const { db, model } = dbModelRouter;
|
|
122
|
+
|
|
123
|
+
const ${varName} = model(
|
|
124
|
+
db,
|
|
125
|
+
"${def.table}",
|
|
126
|
+
${structStr},
|
|
127
|
+
"${def.primary_key}",
|
|
128
|
+
${uniqueStr},
|
|
129
|
+
${optionStr},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
export default ${varName};
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Public API
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate model files for all SaaS tables.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} adapter - Database adapter name (accepted for API consistency, not used for model content)
|
|
144
|
+
* @param {string[]} [additionalTables] - Additional table names from dbmr schema to include in models/index.js
|
|
145
|
+
* @returns {Array<{ relPath: string, content: string }>}
|
|
146
|
+
*/
|
|
147
|
+
function generateSaasModels(adapter, additionalTables) {
|
|
148
|
+
const results = [];
|
|
149
|
+
|
|
150
|
+
for (const def of MODEL_DEFINITIONS) {
|
|
151
|
+
results.push({
|
|
152
|
+
relPath: `models/${def.table}.js`,
|
|
153
|
+
content: generateModelContent(def),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Generate models/index.js that re-exports all models (SaaS + dbmr) as named exports
|
|
158
|
+
results.push({
|
|
159
|
+
relPath: "models/index.js",
|
|
160
|
+
content: generateModelsIndex(additionalTables || []),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate the models/index.js barrel file.
|
|
168
|
+
* Imports all SaaS models and any additional dbmr schema models,
|
|
169
|
+
* then re-exports them all as named exports.
|
|
170
|
+
*
|
|
171
|
+
* @param {string[]} additionalTables - Additional table names from dbmr schema
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function generateModelsIndex(additionalTables) {
|
|
175
|
+
const saasTableNames = MODEL_DEFINITIONS.map((def) => def.table);
|
|
176
|
+
// Filter out dbmr tables that overlap with SaaS tables
|
|
177
|
+
const dbmrTables = (additionalTables || []).filter(
|
|
178
|
+
(t) => !saasTableNames.includes(t),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
let code = "";
|
|
182
|
+
// SaaS model imports
|
|
183
|
+
for (const def of MODEL_DEFINITIONS) {
|
|
184
|
+
code += `import ${def.table} from "./${def.table}.js";\n`;
|
|
185
|
+
}
|
|
186
|
+
// dbmr schema model imports
|
|
187
|
+
for (const table of dbmrTables) {
|
|
188
|
+
code += `import ${safeVarName(table)} from "./${table}.js";\n`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
code += "\nexport {\n";
|
|
192
|
+
for (const def of MODEL_DEFINITIONS) {
|
|
193
|
+
code += ` ${def.table},\n`;
|
|
194
|
+
}
|
|
195
|
+
for (const table of dbmrTables) {
|
|
196
|
+
code += ` ${safeVarName(table)},\n`;
|
|
197
|
+
}
|
|
198
|
+
code += "};\n";
|
|
199
|
+
return code;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function safeVarName(name) {
|
|
203
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return name;
|
|
204
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
generateSaasModels,
|
|
209
|
+
generateModelContent,
|
|
210
|
+
MODEL_DEFINITIONS,
|
|
211
|
+
};
|