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.
- package/README.md +150 -11
- package/TODO.md +0 -15
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/README.md +223 -0
- package/db-manager/adapter-proxy.js +361 -0
- package/db-manager/demo/cockroachdb.env +6 -0
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +7 -0
- package/db-manager/demo/mongodb.env +4 -0
- package/db-manager/demo/mssql.env +6 -0
- package/db-manager/demo/mysql.env +6 -0
- package/db-manager/demo/oracle.env +6 -0
- package/db-manager/demo/postgres.env +6 -0
- package/db-manager/demo/redis.env +4 -0
- package/db-manager/demo/seeds/cockroachdb.sql +32 -0
- package/db-manager/demo/seeds/mssql.sql +32 -0
- package/db-manager/demo/seeds/mysql.sql +32 -0
- package/db-manager/demo/seeds/oracle.sql +43 -0
- package/db-manager/demo/seeds/postgres.sql +32 -0
- package/db-manager/demo/seeds/sqlite3.sql +32 -0
- package/db-manager/demo/sqlite3.env +2 -0
- package/db-manager/metadata-db.js +170 -0
- package/db-manager/public/.gitkeep +1 -0
- package/db-manager/public/css/style.css +1413 -0
- package/db-manager/public/js/app.js +1370 -0
- package/db-manager/routes/api.js +388 -0
- package/db-manager/routes/views.js +61 -0
- package/db-manager/server.js +39 -0
- package/db-manager/utils/build-filter-config.js +18 -0
- package/db-manager/utils/csv-export.js +59 -0
- package/db-manager/utils/export-filename.js +39 -0
- package/db-manager/utils/filter-tables.js +20 -0
- package/db-manager/utils/parse-filters.js +93 -0
- package/db-manager/utils/sort-state.js +35 -0
- package/db-manager/views/.gitkeep +1 -0
- package/db-manager/views/dashboard.ejs +53 -0
- package/db-manager/views/history.ejs +52 -0
- package/db-manager/views/index.ejs +35 -0
- package/db-manager/views/layout.ejs +31 -0
- package/db-manager/views/partials/data-panel.ejs +74 -0
- package/db-manager/views/partials/header.ejs +36 -0
- package/db-manager/views/partials/sidebar.ejs +30 -0
- package/db-manager/views/query.ejs +58 -0
- package/dbmr.schema.json +22 -44
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +14 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +39 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +28 -0
- package/demo/commons/migrate.js +68 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +338 -0
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/20260509170349_create_tables.sql +193 -0
- package/demo/models/addresses.js +24 -0
- package/demo/models/cart_items.js +20 -0
- package/demo/models/carts.js +18 -0
- package/demo/models/categories.js +22 -0
- package/demo/models/coupons.js +25 -0
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +23 -0
- package/demo/models/orders.js +27 -0
- package/demo/models/payments.js +23 -0
- package/demo/models/product_images.js +20 -0
- package/demo/models/product_reviews.js +22 -0
- package/demo/models/product_variants.js +22 -0
- package/demo/models/products.js +32 -0
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +21 -0
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +23 -0
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +17 -0
- package/demo/openapi.json +7000 -0
- package/demo/package-lock.json +2810 -0
- package/demo/package.json +43 -0
- package/demo/routes/addresses/index.js +6 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +7 -0
- package/demo/routes/carts/index.js +6 -0
- package/demo/routes/categories/index.js +6 -0
- package/demo/routes/coupons/index.js +6 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +54 -0
- package/demo/routes/orders/index.js +6 -0
- package/demo/routes/orders/order_items/index.js +7 -0
- package/demo/routes/orders/payments/index.js +7 -0
- package/demo/routes/orders/shipments/index.js +7 -0
- package/demo/routes/products/index.js +6 -0
- package/demo/routes/products/product_images/index.js +7 -0
- package/demo/routes/products/product_reviews/index.js +7 -0
- package/demo/routes/products/product_variants/index.js +7 -0
- package/demo/routes/roles/index.js +75 -0
- package/demo/routes/roles/permissions/index.js +47 -0
- package/demo/routes/tenants/index.js +45 -0
- package/demo/routes/users/index.js +45 -0
- package/demo/routes/wishlists/index.js +6 -0
- package/demo/seeds/saas-seed.js +329 -0
- package/docker-compose.yml +61 -0
- package/package.json +120 -113
- package/scripts/demo-create.js +1 -1
- package/skill/SKILL.md +119 -3
- package/src/cli/commands/db-manager.js +134 -0
- package/src/cli/commands/generate.js +106 -60
- package/src/cli/commands/help.js +0 -1
- package/src/cli/generate-route.js +60 -21
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +6 -0
- package/src/cli/init.js +8 -0
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +108 -0
- package/src/cli/saas/generate-saas-migrations.js +480 -0
- package/src/cli/saas/generate-saas-models.js +211 -0
- package/src/cli/saas/generate-saas-openapi.js +419 -0
- package/src/cli/saas/generate-saas-routes.js +435 -0
- package/src/cli/saas/generate-saas-seeds.js +243 -0
- package/src/cli/saas/generate-saas-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- package/src/index.js +2 -0
- package/src/mssql/db.js +41 -3
- package/src/mysql/db.js +3 -0
- package/src/postgres/db.js +6 -0
- package/src/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. **
|
|
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] [--
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
//
|
|
135
|
+
// Generate route files for each table
|
|
124
136
|
for (const m of meta) {
|
|
125
137
|
if (nestedChildren.has(m.table)) continue;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
// ---
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
const
|
|
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 (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
242
|
-
|
|
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) {
|
package/src/cli/commands/help.js
CHANGED
|
@@ -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
|
|
29
|
+
import express from "express";
|
|
30
|
+
import { ${varName} } from "#models";
|
|
30
31
|
|
|
32
|
+
const router = express.Router({ mergeParams: true });
|
|
31
33
|
const { route } = dbModelRouter;
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
router.use("/", route(${varName}));
|
|
36
|
+
|
|
37
|
+
export default router;
|
|
34
38
|
`;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Generate a parent route file that includes its own CRUD and mounts child routes.
|
|
43
|
+
* e.g., routes/orders/index.js mounts order_items under /:order_id/items
|
|
44
|
+
*
|
|
45
|
+
* @param {string} tableName - Parent table name
|
|
46
|
+
* @param {Array<{child, foreignKey}>} children - Child relationships for this parent
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function generateParentRouteFile(tableName, children) {
|
|
50
|
+
const varName = safeVarName(tableName);
|
|
51
|
+
let code = `import dbModelRouter from "db-model-router";
|
|
52
|
+
import express from "express";
|
|
53
|
+
import { ${varName} } from "#models";
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
// Import child routes
|
|
57
|
+
for (const child of children) {
|
|
58
|
+
const childVar = safeVarName(child.child);
|
|
59
|
+
code += `import ${childVar}Route from "./${child.child}/index.js";\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
code += `
|
|
63
|
+
const router = express.Router({ mergeParams: true });
|
|
64
|
+
const { route } = dbModelRouter;
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
// Mount child routes BEFORE own CRUD to prevent path clashing
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
const childVar = safeVarName(child.child);
|
|
71
|
+
code += `router.use("/:${child.foreignKey}/${child.child}", ${childVar}Route);\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
code += `
|
|
75
|
+
// CRUD routes for ${tableName}
|
|
76
|
+
router.use("/", route(${varName}));
|
|
77
|
+
|
|
78
|
+
export default router;
|
|
79
|
+
`;
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
|
|
37
83
|
/**
|
|
38
84
|
* Generate a child route file that scopes queries by parent FK.
|
|
39
|
-
* e.g.,
|
|
85
|
+
* e.g., routes/orders/items/index.js — filters items where order_id = :order_id
|
|
40
86
|
*/
|
|
41
87
|
function generateChildRouteFile(
|
|
42
88
|
childTable,
|
|
@@ -46,12 +92,16 @@ function generateChildRouteFile(
|
|
|
46
92
|
) {
|
|
47
93
|
const varName = safeVarName(childTable);
|
|
48
94
|
return `import dbModelRouter from "db-model-router";
|
|
49
|
-
import
|
|
95
|
+
import express from "express";
|
|
96
|
+
import { ${varName} } from "#models";
|
|
50
97
|
|
|
98
|
+
const router = express.Router({ mergeParams: true });
|
|
51
99
|
const { route } = dbModelRouter;
|
|
52
100
|
|
|
53
101
|
// Child route: scoped by parent ${parentTable} via ${fkColumn}
|
|
54
|
-
|
|
102
|
+
router.use("/", route(${varName}, { ${fkColumn}: "params.${fkColumn}" }));
|
|
103
|
+
|
|
104
|
+
export default router;
|
|
55
105
|
`;
|
|
56
106
|
}
|
|
57
107
|
|
|
@@ -67,7 +117,7 @@ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
|
|
|
67
117
|
* @param {{ includeDocs?: boolean }} [options]
|
|
68
118
|
*/
|
|
69
119
|
function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
70
|
-
let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
|
|
120
|
+
let imports = `import express from "express";\n\nconst router = express.Router({ mergeParams: true });\n\n`;
|
|
71
121
|
|
|
72
122
|
// Collect child tables that are nested under parents
|
|
73
123
|
const nestedChildren = new Set();
|
|
@@ -75,17 +125,11 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
75
125
|
nestedChildren.add(rel.child);
|
|
76
126
|
}
|
|
77
127
|
|
|
78
|
-
// Import top-level routes only (
|
|
128
|
+
// Import top-level routes only (children are mounted inside parent folders)
|
|
79
129
|
for (const table of tableNames) {
|
|
80
130
|
if (nestedChildren.has(table)) continue;
|
|
81
131
|
const varName = safeVarName(table);
|
|
82
|
-
imports += `import ${varName}Route from "./${table}.js";\n`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Import child routes from subfolders
|
|
86
|
-
for (const rel of relationships) {
|
|
87
|
-
const varName = safeVarName(rel.child);
|
|
88
|
-
imports += `import ${varName}ChildRoute from "./${rel.parent}/${rel.child}.js";\n`;
|
|
132
|
+
imports += `import ${varName}Route from "./${table}/index.js";\n`;
|
|
89
133
|
}
|
|
90
134
|
|
|
91
135
|
// Import docs route if openapi is generated
|
|
@@ -100,13 +144,7 @@ function generateRoutesIndexFile(tableNames, relationships = [], options = {}) {
|
|
|
100
144
|
imports += `router.use("/docs", docsRoute);\n`;
|
|
101
145
|
}
|
|
102
146
|
|
|
103
|
-
// Mount
|
|
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,
|