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.
Files changed (137) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +14 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +28 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +17 -0
  62. package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260509170349_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2810 -0
  88. package/demo/package.json +43 -0
  89. package/demo/routes/addresses/index.js +6 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +7 -0
  92. package/demo/routes/carts/index.js +6 -0
  93. package/demo/routes/categories/index.js +6 -0
  94. package/demo/routes/coupons/index.js +6 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +6 -0
  99. package/demo/routes/orders/order_items/index.js +7 -0
  100. package/demo/routes/orders/payments/index.js +7 -0
  101. package/demo/routes/orders/shipments/index.js +7 -0
  102. package/demo/routes/products/index.js +6 -0
  103. package/demo/routes/products/product_images/index.js +7 -0
  104. package/demo/routes/products/product_reviews/index.js +7 -0
  105. package/demo/routes/products/product_variants/index.js +7 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +6 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +60 -21
  120. package/src/cli/generate-saas-structure.js +122 -0
  121. package/src/cli/init/generators.js +6 -0
  122. package/src/cli/init.js +8 -0
  123. package/src/cli/main.js +8 -1
  124. package/src/cli/saas/generate-saas-middleware.js +108 -0
  125. package/src/cli/saas/generate-saas-migrations.js +480 -0
  126. package/src/cli/saas/generate-saas-models.js +211 -0
  127. package/src/cli/saas/generate-saas-openapi.js +419 -0
  128. package/src/cli/saas/generate-saas-routes.js +435 -0
  129. package/src/cli/saas/generate-saas-seeds.js +243 -0
  130. package/src/cli/saas/generate-saas-utils.js +176 -0
  131. package/src/commons/kafka.js +139 -0
  132. package/src/commons/model.js +29 -9
  133. package/src/index.js +2 -0
  134. package/src/mssql/db.js +41 -3
  135. package/src/mysql/db.js +3 -0
  136. package/src/postgres/db.js +6 -0
  137. 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
+ };