db-model-router 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -14
  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 +23 -45
  45. package/demo/.env.example +1 -0
  46. package/demo/app.js +3 -1
  47. package/demo/commons/db.js +11 -0
  48. package/demo/commons/migrate.js +3 -0
  49. package/demo/commons/modules.js +18 -0
  50. package/demo/commons/password.js +36 -0
  51. package/demo/commons/webhook.js +81 -0
  52. package/demo/dbmr.schema.json +22 -46
  53. package/demo/middleware/authenticate.js +14 -0
  54. package/demo/middleware/hasPermission.js +30 -0
  55. package/demo/middleware/tenantIsolation.js +17 -0
  56. package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
  57. package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
  58. package/demo/models/addresses.js +5 -3
  59. package/demo/models/cart_items.js +5 -3
  60. package/demo/models/carts.js +5 -3
  61. package/demo/models/categories.js +5 -3
  62. package/demo/models/coupons.js +5 -3
  63. package/demo/models/index.js +43 -0
  64. package/demo/models/order_items.js +4 -2
  65. package/demo/models/orders.js +5 -3
  66. package/demo/models/payments.js +5 -3
  67. package/demo/models/product_images.js +4 -2
  68. package/demo/models/product_reviews.js +5 -3
  69. package/demo/models/product_variants.js +5 -3
  70. package/demo/models/products.js +5 -3
  71. package/demo/models/role_permissions.js +17 -0
  72. package/demo/models/roles.js +17 -0
  73. package/demo/models/shipments.js +5 -3
  74. package/demo/models/tenants.js +18 -0
  75. package/demo/models/users.js +12 -8
  76. package/demo/models/webhook_logs.js +22 -0
  77. package/demo/models/webhooks.js +19 -0
  78. package/demo/models/wishlists.js +4 -2
  79. package/demo/openapi.json +1744 -616
  80. package/demo/package-lock.json +24 -24
  81. package/demo/package.json +9 -0
  82. package/demo/routes/{addresses.js → addresses/index.js} +1 -1
  83. package/demo/routes/auth/index.js +55 -0
  84. package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
  85. package/demo/routes/{carts.js → carts/index.js} +1 -1
  86. package/demo/routes/{categories.js → categories/index.js} +1 -1
  87. package/demo/routes/{coupons.js → coupons/index.js} +1 -1
  88. package/demo/routes/index.js +39 -24
  89. package/demo/routes/{orders.js → orders/index.js} +1 -1
  90. package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
  91. package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
  92. package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
  93. package/demo/routes/{products.js → products/index.js} +1 -1
  94. package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
  95. package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
  96. package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
  97. package/demo/routes/roles/index.js +75 -0
  98. package/demo/routes/roles/permissions/index.js +47 -0
  99. package/demo/routes/tenants/index.js +45 -0
  100. package/demo/routes/users/index.js +45 -0
  101. package/demo/routes/{wishlists.js → wishlists/index.js} +1 -1
  102. package/demo/seeds/saas-seed.js +329 -0
  103. package/docker-compose.yml +61 -0
  104. package/package.json +120 -113
  105. package/scripts/demo-create.js +1 -1
  106. package/skill/SKILL.md +119 -3
  107. package/src/cli/commands/db-manager.js +134 -0
  108. package/src/cli/commands/generate.js +112 -43
  109. package/src/cli/commands/help.js +0 -1
  110. package/src/cli/diff-engine.js +2 -1
  111. package/src/cli/generate-model.js +9 -4
  112. package/src/cli/generate-openapi.js +40 -13
  113. package/src/cli/generate-route.js +61 -22
  114. package/src/cli/generate-saas-structure.js +122 -0
  115. package/src/cli/init/generators.js +42 -30
  116. package/src/cli/init.js +8 -0
  117. package/src/cli/main.js +8 -1
  118. package/src/cli/saas/generate-saas-middleware.js +108 -0
  119. package/src/cli/saas/generate-saas-migrations.js +480 -0
  120. package/src/cli/saas/generate-saas-models.js +211 -0
  121. package/src/cli/saas/generate-saas-openapi.js +419 -0
  122. package/src/cli/saas/generate-saas-routes.js +435 -0
  123. package/src/cli/saas/generate-saas-seeds.js +243 -0
  124. package/src/cli/saas/generate-saas-utils.js +176 -0
  125. package/src/commons/kafka.js +139 -0
  126. package/src/commons/model.js +29 -9
  127. package/src/index.js +2 -0
  128. package/src/mssql/db.js +41 -3
  129. package/src/mysql/db.js +3 -0
  130. package/src/postgres/db.js +6 -0
  131. package/src/sqlite3/db.js +11 -0
  132. package/demo/docs/llm.md +0 -197
  133. package/demo/llms.txt +0 -70
  134. package/demo/routes/users.js +0 -6
  135. package/src/cli/commands/generate-llm-docs.js +0 -418
  136. /package/demo/migrations/{20260430155808_create_migrations_table.sql → 20260509170349_create_migrations_table.sql} +0 -0
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,16 +7,34 @@ 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,
13
14
  generateChildTestFile,
14
15
  } = require("../generate-route");
15
16
  const { generateOpenAPISpec } = require("../generate-openapi");
16
- const { generateLlmsTxt, generateLlmMd } = require("./generate-llm-docs");
17
17
  const { generateMigrationFiles } = require("../generate-migration");
18
18
  const { generateDocsRoute } = require("../generate-docs-route");
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
- * --llm-docs Generate only LLM documentation
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,22 +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["llm-docs"] === 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 genLlmDocs = !hasArtifactFlag || args["llm-docs"] === true;
97
-
98
- 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
+
99
109
  const baseDir = process.cwd();
100
110
 
101
111
  // Collect all planned files: { relPath, content }
@@ -113,31 +123,40 @@ async function generate(args, flags, ctx) {
113
123
 
114
124
  // --- Route files ---
115
125
  if (genRoutes) {
116
- // Collect child tables to skip generating top-level route files for them
126
+ // Collect child tables and group by parent
117
127
  const nestedChildren = new Set();
128
+ const childrenByParent = {};
118
129
  for (const rel of relationships) {
119
130
  nestedChildren.add(rel.child);
131
+ if (!childrenByParent[rel.parent]) childrenByParent[rel.parent] = [];
132
+ childrenByParent[rel.parent].push(rel);
120
133
  }
121
134
 
122
- // One route per top-level table (skip children)
135
+ // Generate route files for each table
123
136
  for (const m of meta) {
124
137
  if (nestedChildren.has(m.table)) continue;
125
- planned.push({
126
- relPath: `routes/${m.table}.js`,
127
- content: generateRouteFile(m.table, modelsRelPath),
128
- });
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
+ }
129
153
  }
130
154
 
131
- // Child route files in subfolders: routes/<parent>/<child>.js
155
+ // Child route files inside parent folders: routes/<parent>/<child>/index.js
132
156
  for (const rel of relationships) {
133
157
  planned.push({
134
- relPath: `routes/${rel.parent}/${rel.child}.js`,
135
- content: generateChildRouteFile(
136
- rel.child,
137
- rel.parent,
138
- rel.foreignKey,
139
- `../../models`,
140
- ),
158
+ relPath: `routes/${rel.parent}/${rel.child}/index.js`,
159
+ content: generateChildRouteFile(rel.child, rel.parent, rel.foreignKey),
141
160
  });
142
161
  }
143
162
 
@@ -152,9 +171,30 @@ async function generate(args, flags, ctx) {
152
171
 
153
172
  // --- OpenAPI spec + docs route ---
154
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
+
155
195
  planned.push({
156
196
  relPath: "openapi.json",
157
- content: JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
197
+ content: JSON.stringify(spec, null, 2) + "\n",
158
198
  });
159
199
 
160
200
  // Generate Swagger UI docs route
@@ -208,16 +248,46 @@ async function generate(args, flags, ctx) {
208
248
  }
209
249
  }
210
250
 
211
- // --- LLM docs ---
212
- if (genLlmDocs) {
213
- planned.push({
214
- relPath: "llms.txt",
215
- content: generateLlmsTxt(),
216
- });
217
- planned.push({
218
- relPath: "docs/llm.md",
219
- content: generateLlmMd(),
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;
255
+
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;
267
+ }
268
+
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 },
220
276
  });
277
+
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);
286
+ }
287
+
288
+ for (const entry of saasFiles) {
289
+ planned.push(entry);
290
+ }
221
291
  }
222
292
 
223
293
  // --- Process planned files ---
@@ -252,7 +322,6 @@ async function generate(args, flags, ctx) {
252
322
  ctx.log(` created ${relPath}`);
253
323
  }
254
324
  }
255
-
256
325
  // --- Output ---
257
326
  if (flags.dryRun) {
258
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
- --llm-docs Generate only LLM documentation (llms.txt + docs/llm.md)
86
85
  --yes Accept all defaults without prompting
87
86
  --json Output machine-readable JSON
88
87
  --dry-run Report planned files without writing
@@ -117,7 +117,8 @@ function buildExpectedFiles(meta, relationships) {
117
117
  // OpenAPI spec
118
118
  expected.set(
119
119
  "openapi.json",
120
- JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
120
+ JSON.stringify(generateOpenAPISpec(meta, { relationships }), null, 2) +
121
+ "\n",
121
122
  );
122
123
 
123
124
  return expected;
@@ -544,7 +544,9 @@ function generateModelFile(m) {
544
544
  if (opt.modified_at) parts.push(`modified_at: "${opt.modified_at}"`);
545
545
  optionStr = `\n { ${parts.join(", ")} },`;
546
546
  }
547
- return `const { db, model } = require("db-model-router");
547
+ return `import dbModelRouter from "db-model-router";
548
+
549
+ const { db, model } = dbModelRouter;
548
550
 
549
551
  const ${varName} = model(
550
552
  db,
@@ -554,7 +556,7 @@ const ${varName} = model(
554
556
  ${uniqueStr},${optionStr}
555
557
  );
556
558
 
557
- module.exports = ${varName};
559
+ export default ${varName};
558
560
  `;
559
561
  }
560
562
 
@@ -563,11 +565,14 @@ function generateIndexFile(models) {
563
565
  let exports = "";
564
566
  for (const m of models) {
565
567
  const varName = safeVarName(m.table);
566
- imports += `const ${varName} = require("./${m.table}");\n`;
568
+ imports += `import ${varName} from "./${m.table}.js";\n`;
567
569
  exports += ` ${varName},\n`;
568
570
  }
569
571
  return `${imports}
570
- module.exports = {
572
+ export {
573
+ ${exports}};
574
+
575
+ export default {
571
576
  ${exports}};
572
577
  `;
573
578
  }