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
package/skill/SKILL.md CHANGED
@@ -33,7 +33,10 @@ MySQL/MariaDB use `mysql2` — no separate reference file needed (see Connection
33
33
  3. **Migrations**: Write SQL/JS files into `migrations/`, then `npm run migrate`
34
34
  4. **Generate models**: `db-model-router generate --from dbmr.schema.json --models`
35
35
  5. **Generate routes + tests**: `db-model-router generate --from dbmr.schema.json --routes --tests`
36
- 6. **Run**: `npm run dev`
36
+ 6. **Generate SaaS structure** (if multi-tenant): `db-model-router generate --from dbmr.schema.json --saas-structure`
37
+ 7. **Run**: `npm run dev`
38
+
39
+ > When using `--saas-structure`, skip defining `users`, `tenants`, `roles`, `role_permissions` in your schema — they're auto-generated with auth middleware, permission system, and tenant isolation.
37
40
 
38
41
  For existing databases, use `inspect` first:
39
42
 
@@ -267,10 +270,25 @@ db-model-router inspect --type postgres --env .env [--out schema.json] [--tables
267
270
  ### `generate` — Generate code from schema
268
271
 
269
272
  ```bash
270
- db-model-router generate --from dbmr.schema.json [--models] [--routes] [--openapi] [--tests] [--llm-docs]
273
+ db-model-router generate --from dbmr.schema.json [--models=false] [--routes=false] [--openapi=false] [--tests=false] [--migrations=false] [--saas-structure=false]
271
274
  ```
272
275
 
273
- No flags = generate all.
276
+ All artifact types are **enabled by default**. Use `--flag=false` to disable specific ones.
277
+
278
+ #### `--saas-structure` (default: enabled)
279
+
280
+ SaaS structure is always generated unless explicitly disabled with `--saas-structure=false`. It produces a complete multi-tenant SaaS backend on top of schema-generated code:
281
+
282
+ - **Tables**: `tenants`, `users`, `roles`, `role_permissions`, `webhooks`, `webhook_logs`
283
+ - **Middleware**: `authenticate.js`, `tenantIsolation.js`, `hasPermission.js`
284
+ - **Routes**: CRUD for users/tenants/roles/permissions + auth (login/logout)
285
+ - **Utilities**: `commons/password.js` (scrypt), `commons/modules.js`, `commons/webhook.js`
286
+ - **Seeds**: Super Admin (all permissions, global scope) + Tenant Admin role template
287
+ - **Migrations**: Single consolidated `.sql` file with all SaaS tables
288
+
289
+ > **Critical**: Since `--saas-structure` is on by default, `users`, `tenants`, `roles`, and `role_permissions` are already generated. **Do NOT add these tables to `dbmr.schema.json`** — only define your product/domain tables (e.g., `products`, `orders`, `invoices`).
290
+
291
+ The generated `routes/index.js` combines SaaS routes (`/api/auth`, `/api/users`, `/api/tenants`, `/api/roles`, `/api/roles/:role_id/permissions`) with schema-generated product routes. Swagger docs include all SaaS endpoints first, then product endpoints.
274
292
 
275
293
  ### `doctor` — Validate schema + check file sync
276
294
 
@@ -286,6 +304,23 @@ db-model-router diff [--from dbmr.schema.json] [--json]
286
304
 
287
305
  Universal flags (all commands): `--yes`, `--json`, `--dry-run`, `--no-install`, `--help`
288
306
 
307
+ ### `db-manager` — Launch database management UI
308
+
309
+ ```bash
310
+ db-model-router db-manager [--env .env] [--port 4000]
311
+ ```
312
+
313
+ Starts a built-in web dashboard for browsing and managing your database. Features:
314
+
315
+ - Table browser with filtering, sorting, pagination, inline editing
316
+ - Raw SQL query editor with CSV export
317
+ - Query history tracking
318
+ - Dashboard overview (table stats: columns, indexes, rows, size)
319
+ - Light / Dark / System theme modes (persisted via localStorage)
320
+ - Typography: Fira Sans (UI) + Fira Code (data/code)
321
+
322
+ Requires a `.env` file with `DB_TYPE` and connection variables.
323
+
289
324
  ---
290
325
 
291
326
  ## Schema-Driven Workflow (dbmr.schema.json)
@@ -446,6 +481,85 @@ When `logger=true`: adds `APP_NAME LOG_LEVEL LOKI_HOST`.
446
481
 
447
482
  ---
448
483
 
484
+ ## Kafka Event Production
485
+
486
+ Built-in Kafka support. When `KAFKA_BROKER` env var is set, every write operation automatically produces a Kafka event per affected row.
487
+
488
+ ### Install & Configure
489
+
490
+ ```bash
491
+ npm install kafkajs
492
+ ```
493
+
494
+ ```env
495
+ KAFKA_BROKER=localhost:9092
496
+ KAFKA_CLIENT_ID=my-app
497
+ KAFKA_TOPIC_PREFIX=dbmr
498
+ ```
499
+
500
+ ### Initialize
501
+
502
+ ```js
503
+ import dbModelRouter from "db-model-router";
504
+ const { init, db, kafka } = dbModelRouter;
505
+
506
+ init("postgres");
507
+ db.connect({ host: "localhost", database: "my_app" });
508
+
509
+ // Connect Kafka producer
510
+ await kafka.init();
511
+ // Or with explicit options:
512
+ await kafka.init({
513
+ broker: "localhost:9092",
514
+ clientId: "my-app",
515
+ topicPrefix: "dbmr",
516
+ });
517
+ ```
518
+
519
+ ### kafka API
520
+
521
+ | Method | Description |
522
+ | --------------------------------------- | -------------------------------------------- |
523
+ | `kafka.init(opts?)` | Connect producer. Returns `true` on success. |
524
+ | `kafka.disconnect()` | Graceful shutdown. |
525
+ | `kafka.produce(table, operation, data)` | Manually produce event(s). |
526
+ | `kafka.status()` | Returns `true` if connected. |
527
+
528
+ ### Event Format
529
+
530
+ Topic: `{KAFKA_TOPIC_PREFIX}.{table_name}` (e.g. `dbmr.users`)
531
+
532
+ ```json
533
+ {
534
+ "table_name": "users",
535
+ "operation_type": "insert",
536
+ "data": { "id": 1, "name": "Alice", "email": "alice@example.com" },
537
+ "timestamp": "2026-05-09T12:00:00.000Z"
538
+ }
539
+ ```
540
+
541
+ - `operation_type`: `"insert"` | `"update"` | `"upsert"` | `"delete"`
542
+ - `data`: Single object (one event per affected row)
543
+ - Bulk ops (e.g. 1000 inserts) → 1000 events, batched in chunks of 500
544
+
545
+ ### Rules
546
+
547
+ - No `KAFKA_BROKER` → Kafka fully disabled, zero overhead
548
+ - Events fire after successful DB write only
549
+ - Read ops (`find`, `list`, `byId`, `findOne`) never produce events
550
+ - Failed produce logs warning, does not throw or affect API response
551
+ - `kafkajs` is an optional peer dependency — only required when `KAFKA_BROKER` is set
552
+
553
+ ### Environment Variables
554
+
555
+ | Variable | Default | Description |
556
+ | -------------------- | -------------------- | --------------------------- |
557
+ | `KAFKA_BROKER` | _(unset = disabled)_ | Comma-separated broker URLs |
558
+ | `KAFKA_CLIENT_ID` | `db-model-router` | Kafka client identifier |
559
+ | `KAFKA_TOPIC_PREFIX` | `dbmr` | Topic prefix |
560
+
561
+ ---
562
+
449
563
  ## Rules
450
564
 
451
565
  1. `init()` before `db.connect()`. Don't destructure `db` before `init()` — it's a getter.
@@ -462,3 +576,5 @@ When `logger=true`: adds `APP_NAME LOG_LEVEL LOKI_HOST`.
462
576
  12. Docker passwords are randomly generated and shared between `.env` and `docker-compose.yml`.
463
577
  13. PK convention: `<table>_id` (e.g. `user_id`, `post_id`). Include ALL columns in schema.
464
578
  14. Use `parent` only for domain hierarchies (e.g. `posts → comments`), not system tables.
579
+ 15. When `--saas-structure` is active, do NOT define `users`, `tenants`, `roles`, or `role_permissions` in `dbmr.schema.json` — they are already generated with models, routes, middleware, and migrations. Only add your product-specific tables to the schema.
580
+ 16. Kafka is opt-in via `KAFKA_BROKER` env var. Call `kafka.init()` after `db.connect()`. Each write op produces one event per row to `{prefix}.{table}` topic.
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ /**
7
+ * DB Manager command handler for the unified CLI.
8
+ *
9
+ * Starts a live Express-based database management UI that connects
10
+ * to any supported database through the library's adapter layer.
11
+ *
12
+ * Supported flags:
13
+ * --env Path to .env file (default: ".env" in cwd)
14
+ * --port Server port (default: 4000)
15
+ *
16
+ * @param {object} args - Parsed key-value args
17
+ * @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
18
+ * @param {import('../flags').OutputContext} ctx - Output context
19
+ */
20
+ async function dbManager(args, flags, ctx) {
21
+ // Parse flags with defaults
22
+ const envPath = path.resolve(args.env || ".env");
23
+ const port = parseInt(args.port, 10) || 4000;
24
+
25
+ // Validate env file exists
26
+ if (!fs.existsSync(envPath)) {
27
+ ctx.log(`Error: Environment file not found: ${envPath}`);
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+
32
+ // Load env vars from the specified file
33
+ require("dotenv").config({ path: envPath });
34
+
35
+ // Validate DB_TYPE is present
36
+ const dbType = process.env.DB_TYPE;
37
+ if (!dbType) {
38
+ ctx.log(`Error: DB_TYPE not specified in ${envPath}`);
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+
43
+ // Initialize the library adapter
44
+ const restRouter = require("../../index.js");
45
+ try {
46
+ restRouter.init(dbType);
47
+ } catch (err) {
48
+ ctx.log(`Error: Failed to connect to ${dbType} database: ${err.message}`);
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ const db = restRouter.db;
54
+
55
+ // Build connection config based on database type
56
+ const config = { dateStrings: true };
57
+
58
+ switch (dbType) {
59
+ case "sqlite3":
60
+ config.database = process.env.DB_NAME;
61
+ config.filename = process.env.DB_NAME;
62
+ break;
63
+ case "mssql":
64
+ config.server = process.env.DB_HOST || "localhost";
65
+ config.port = process.env.DB_PORT;
66
+ config.database = process.env.DB_NAME;
67
+ config.user = process.env.DB_USER;
68
+ config.password = process.env.DB_PASS;
69
+ config.options = { encrypt: false, trustServerCertificate: true };
70
+ break;
71
+ default:
72
+ // mysql, postgres, cockroachdb, etc.
73
+ config.host = process.env.DB_HOST || "localhost";
74
+ config.port = process.env.DB_PORT;
75
+ config.database = process.env.DB_NAME;
76
+ config.user = process.env.DB_USER;
77
+ config.password = process.env.DB_PASS;
78
+ break;
79
+ }
80
+
81
+ // Connect to the target database
82
+ try {
83
+ db.connect(config);
84
+ } catch (err) {
85
+ ctx.log(`Error: Failed to connect to ${dbType} database: ${err.message}`);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ // Initialize metadata DB
91
+ const createMetadataDb = require("../../../db-manager/metadata-db.js");
92
+ const metaDbPath = path.join(
93
+ __dirname,
94
+ "../../../db-manager/.dbmanager.sqlite",
95
+ );
96
+ const metaDb = createMetadataDb(metaDbPath);
97
+ metaDb.init();
98
+
99
+ // Record connection in history
100
+ metaDb.recordConnection(
101
+ dbType,
102
+ process.env.DB_HOST || null,
103
+ process.env.DB_NAME || dbType,
104
+ );
105
+
106
+ // Create Express app
107
+ const createApp = require("../../../db-manager/server.js");
108
+ const app = createApp(db, metaDb, dbType);
109
+
110
+ // Start server
111
+ const server = app.listen(port, () => {
112
+ ctx.log(`DB Manager running at http://localhost:${port}`);
113
+ });
114
+
115
+ // Graceful shutdown handler
116
+ const shutdown = () => {
117
+ server.close(() => {
118
+ try {
119
+ if (db.disconnect) {
120
+ db.disconnect();
121
+ }
122
+ } catch (_) {
123
+ // ignore disconnect errors
124
+ }
125
+ metaDb.close();
126
+ process.exit(0);
127
+ });
128
+ };
129
+
130
+ process.on("SIGTERM", shutdown);
131
+ process.on("SIGINT", shutdown);
132
+ }
133
+
134
+ module.exports = dbManager;
@@ -7,6 +7,7 @@ const { schemaToModelMeta } = require("../../schema/schema-to-meta");
7
7
  const { generateModelFile } = require("../generate-model");
8
8
  const {
9
9
  generateRouteFile,
10
+ generateParentRouteFile,
10
11
  generateChildRouteFile,
11
12
  generateRoutesIndexFile,
12
13
  generateTestFile,
@@ -15,8 +16,25 @@ const {
15
16
  const { generateOpenAPISpec } = require("../generate-openapi");
16
17
  const { generateMigrationFiles } = require("../generate-migration");
17
18
  const { generateDocsRoute } = require("../generate-docs-route");
18
- const { generateDbManager } = require("../generate-db-manager");
19
19
  const { migrationTimestamp } = require("../init/generators");
20
+ const { generateSaasStructure } = require("../generate-saas-structure");
21
+ const { generateSaasOpenAPIPaths } = require("../saas/generate-saas-openapi");
22
+
23
+ /**
24
+ * Supported database adapters for the SaaS structure generator.
25
+ * @type {string[]}
26
+ */
27
+ const SUPPORTED_ADAPTERS = [
28
+ "postgres",
29
+ "mysql",
30
+ "sqlite3",
31
+ "mssql",
32
+ "oracle",
33
+ "cockroachdb",
34
+ "mongodb",
35
+ "dynamodb",
36
+ "redis",
37
+ ];
20
38
 
21
39
  /**
22
40
  * Generate command handler for the unified CLI.
@@ -31,7 +49,6 @@ const { migrationTimestamp } = require("../init/generators");
31
49
  * --openapi Generate only OpenAPI spec + docs route
32
50
  * --tests Generate only test files
33
51
  * --migrations Generate only migration files
34
- * --db-manager Generate DB Manager UI (SQL adapters only)
35
52
  * --dry-run Report planned files without writing
36
53
  * --json Output JSON result via ctx
37
54
  *
@@ -42,6 +59,7 @@ const { migrationTimestamp } = require("../init/generators");
42
59
  * @param {import('../flags').OutputContext} ctx - Output context
43
60
  */
44
61
  async function generate(args, flags, ctx) {
62
+ // --- Standard schema-based generation ---
45
63
  const schemaPath = path.resolve(args.from || "dbmr.schema.json");
46
64
 
47
65
  if (!fs.existsSync(schemaPath)) {
@@ -80,23 +98,14 @@ async function generate(args, flags, ctx) {
80
98
  const tableNames = meta.map((m) => m.table).sort();
81
99
 
82
100
  // Determine which artifact types to generate
83
- const hasArtifactFlag =
84
- args.models === true ||
85
- args.routes === true ||
86
- args.openapi === true ||
87
- args.tests === true ||
88
- args.migrations === true ||
89
- args["db-manager"] === true;
90
-
91
- const genModels = !hasArtifactFlag || args.models === true;
92
- const genRoutes = !hasArtifactFlag || args.routes === true;
93
- const genOpenapi = !hasArtifactFlag || args.openapi === true;
94
- const genTests = !hasArtifactFlag || args.tests === true;
95
- const genMigrations = !hasArtifactFlag || args.migrations === true;
96
- //const genDbManager = !hasArtifactFlag || args["db-manager"] === true;
97
- const genDbManager = false;
98
-
99
- const modelsRelPath = "../models";
101
+ // All flags default to true — use --flag=false to disable
102
+ const genModels = args.models !== false;
103
+ const genRoutes = args.routes !== false;
104
+ const genOpenapi = args.openapi !== false;
105
+ const genTests = args.tests !== false;
106
+ const genMigrations = args.migrations !== false;
107
+ const genSaas = args["saas-structure"] !== false;
108
+
100
109
  const baseDir = process.cwd();
101
110
 
102
111
  // Collect all planned files: { relPath, content }
@@ -114,31 +123,40 @@ async function generate(args, flags, ctx) {
114
123
 
115
124
  // --- Route files ---
116
125
  if (genRoutes) {
117
- // Collect child tables to skip generating top-level route files for them
126
+ // Collect child tables and group by parent
118
127
  const nestedChildren = new Set();
128
+ const childrenByParent = {};
119
129
  for (const rel of relationships) {
120
130
  nestedChildren.add(rel.child);
131
+ if (!childrenByParent[rel.parent]) childrenByParent[rel.parent] = [];
132
+ childrenByParent[rel.parent].push(rel);
121
133
  }
122
134
 
123
- // One route per top-level table (skip children)
135
+ // Generate route files for each table
124
136
  for (const m of meta) {
125
137
  if (nestedChildren.has(m.table)) continue;
126
- planned.push({
127
- relPath: `routes/${m.table}.js`,
128
- content: generateRouteFile(m.table, modelsRelPath),
129
- });
138
+
139
+ const children = childrenByParent[m.table] || [];
140
+ if (children.length > 0) {
141
+ // Parent with children: generates index.js that mounts child routes
142
+ planned.push({
143
+ relPath: `routes/${m.table}/index.js`,
144
+ content: generateParentRouteFile(m.table, children),
145
+ });
146
+ } else {
147
+ // Simple table: just CRUD
148
+ planned.push({
149
+ relPath: `routes/${m.table}/index.js`,
150
+ content: generateRouteFile(m.table),
151
+ });
152
+ }
130
153
  }
131
154
 
132
- // Child route files in subfolders: routes/<parent>/<child>.js
155
+ // Child route files inside parent folders: routes/<parent>/<child>/index.js
133
156
  for (const rel of relationships) {
134
157
  planned.push({
135
- relPath: `routes/${rel.parent}/${rel.child}.js`,
136
- content: generateChildRouteFile(
137
- rel.child,
138
- rel.parent,
139
- rel.foreignKey,
140
- `../../models`,
141
- ),
158
+ relPath: `routes/${rel.parent}/${rel.child}/index.js`,
159
+ content: generateChildRouteFile(rel.child, rel.parent, rel.foreignKey),
142
160
  });
143
161
  }
144
162
 
@@ -153,11 +171,30 @@ async function generate(args, flags, ctx) {
153
171
 
154
172
  // --- OpenAPI spec + docs route ---
155
173
  if (genOpenapi) {
174
+ const spec = generateOpenAPISpec(meta, { relationships });
175
+
176
+ // Merge SaaS routes into the OpenAPI spec when saas-structure is active
177
+ // SaaS routes appear BEFORE product routes in the docs
178
+ if (genSaas) {
179
+ const saasApi = generateSaasOpenAPIPaths();
180
+
181
+ // Prepend SaaS paths before product paths
182
+ const productPaths = spec.paths;
183
+ spec.paths = { ...saasApi.paths, ...productPaths };
184
+
185
+ // Prepend SaaS schemas before product schemas
186
+ const productSchemas = spec.components.schemas;
187
+ spec.components.schemas = { ...saasApi.schemas, ...productSchemas };
188
+
189
+ if (!spec.components.securitySchemes) {
190
+ spec.components.securitySchemes = {};
191
+ }
192
+ Object.assign(spec.components.securitySchemes, saasApi.securitySchemes);
193
+ }
194
+
156
195
  planned.push({
157
196
  relPath: "openapi.json",
158
- content:
159
- JSON.stringify(generateOpenAPISpec(meta, { relationships }), null, 2) +
160
- "\n",
197
+ content: JSON.stringify(spec, null, 2) + "\n",
161
198
  });
162
199
 
163
200
  // Generate Swagger UI docs route
@@ -211,35 +248,45 @@ async function generate(args, flags, ctx) {
211
248
  }
212
249
  }
213
250
 
214
- // --- DB Manager ---
215
- if (genDbManager) {
216
- const dbmOptions = {};
217
- const envPath = path.join(baseDir, ".env");
218
- const envExamplePath = path.join(baseDir, ".env.example");
219
- const appJsPath = path.join(baseDir, "app.js");
220
- const pkgJsonPath = path.join(baseDir, "package.json");
251
+ // --- SaaS structure files (additive, on top of schema-based generation) ---
252
+ if (genSaas) {
253
+ // Determine adapter: from --adapter flag, or from the schema's adapter field
254
+ const adapter = args.adapter || schema.adapter;
221
255
 
222
- if (fs.existsSync(envPath)) {
223
- dbmOptions.envContent = fs.readFileSync(envPath, "utf8");
224
- }
225
- if (fs.existsSync(envExamplePath)) {
226
- dbmOptions.envExampleContent = fs.readFileSync(envExamplePath, "utf8");
227
- }
228
- if (fs.existsSync(appJsPath)) {
229
- dbmOptions.appJsContent = fs.readFileSync(appJsPath, "utf8");
230
- }
231
- if (fs.existsSync(pkgJsonPath)) {
232
- dbmOptions.packageJsonContent = fs.readFileSync(pkgJsonPath, "utf8");
256
+ if (!adapter || !SUPPORTED_ADAPTERS.includes(adapter)) {
257
+ const msg = adapter
258
+ ? `Invalid adapter: ${adapter}. Supported: ${SUPPORTED_ADAPTERS.join(", ")}`
259
+ : `Adapter is required for saas-structure generation. Provide --adapter or set adapter in schema.`;
260
+ if (flags.json) {
261
+ ctx.result({ error: true, code: "INVALID_ADAPTER", message: msg });
262
+ } else {
263
+ ctx.log(`Error: ${msg}`);
264
+ }
265
+ process.exitCode = 1;
266
+ return;
233
267
  }
234
268
 
235
- const dbmResult = generateDbManager(schema, dbmOptions);
269
+ const saasFiles = generateSaasStructure(adapter, {
270
+ dryRun: flags.dryRun,
271
+ json: flags.json,
272
+ timestamp: new Date(),
273
+ tableNames,
274
+ relationships,
275
+ routeOptions: { includeDocs: genOpenapi },
276
+ });
236
277
 
237
- for (const f of dbmResult.files) {
238
- planned.push(f);
278
+ // The SaaS generator produces a combined routes/index.js that includes
279
+ // both SaaS routes and dbmr schema-generated routes. Remove any
280
+ // previously-planned routes/index.js from the schema generator.
281
+ const existingIndexIdx = planned.findIndex(
282
+ (p) => p.relPath === "routes/index.js",
283
+ );
284
+ if (existingIndexIdx !== -1) {
285
+ planned.splice(existingIndexIdx, 1);
239
286
  }
240
287
 
241
- for (const w of dbmResult.warnings) {
242
- ctx.log(` warning: ${w}`);
288
+ for (const entry of saasFiles) {
289
+ planned.push(entry);
243
290
  }
244
291
  }
245
292
 
@@ -275,7 +322,6 @@ async function generate(args, flags, ctx) {
275
322
  ctx.log(` created ${relPath}`);
276
323
  }
277
324
  }
278
-
279
325
  // --- Output ---
280
326
  if (flags.dryRun) {
281
327
  if (flags.json) {
@@ -82,7 +82,6 @@ Options:
82
82
  --openapi Generate only OpenAPI spec + Swagger UI docs route
83
83
  --tests Generate only test files
84
84
  --migrations Generate only database migration files
85
- --db-manager Generate DB Manager UI (SQL adapters only)
86
85
  --yes Accept all defaults without prompting
87
86
  --json Output machine-readable JSON
88
87
  --dry-run Report planned files without writing
@@ -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 ${varName} from "${modelsRelPath}/${tableName}.js";
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
- export default route(${varName});
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., posts/:post_id/comments — filters comments where post_id = :post_id
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 ${varName} from "${modelsRelPath}/${childTable}.js";
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
- export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
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 (not children)
128
+ // Import top-level routes only (children are mounted inside parent folders)
79
129
  for (const table of tableNames) {
80
130
  if (nestedChildren.has(table)) continue;
81
131
  const varName = safeVarName(table);
82
- imports += `import ${varName}Route from "./${table}.js";\n`;
83
- }
84
-
85
- // Import child routes from subfolders
86
- for (const rel of relationships) {
87
- const varName = safeVarName(rel.child);
88
- imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
132
+ imports += `import ${varName}Route from "./${table}/index.js";\n`;
89
133
  }
90
134
 
91
135
  // Import docs route if openapi is generated
@@ -100,13 +144,7 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
100
144
  imports += `router.use("/docs", docsRoute);\n`;
101
145
  }
102
146
 
103
- // Mount child routes BEFORE parent routes to prevent path clashing
104
- for (const rel of relationships) {
105
- const childVar = safeVarName(rel.child);
106
- imports += `router.use("/${rel.parent}/:${rel.foreignKey}/${rel.child}", ${childVar}ChildRoute);\n`;
107
- }
108
-
109
- // Mount top-level routes
147
+ // Mount top-level routes (children are already mounted inside their parent's index.js)
110
148
  for (const table of tableNames) {
111
149
  if (nestedChildren.has(table)) continue;
112
150
  const varName = safeVarName(table);
@@ -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,