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.
- package/README.md +150 -11
- package/TODO.md +0 -14
- 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 +23 -45
- package/demo/.env.example +1 -0
- package/demo/app.js +3 -1
- package/demo/commons/db.js +11 -0
- package/demo/commons/migrate.js +3 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +22 -46
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
- package/demo/models/addresses.js +5 -3
- package/demo/models/cart_items.js +5 -3
- package/demo/models/carts.js +5 -3
- package/demo/models/categories.js +5 -3
- package/demo/models/coupons.js +5 -3
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +4 -2
- package/demo/models/orders.js +5 -3
- package/demo/models/payments.js +5 -3
- package/demo/models/product_images.js +4 -2
- package/demo/models/product_reviews.js +5 -3
- package/demo/models/product_variants.js +5 -3
- package/demo/models/products.js +5 -3
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +5 -3
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +12 -8
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +4 -2
- package/demo/openapi.json +1744 -616
- package/demo/package-lock.json +24 -24
- package/demo/package.json +9 -0
- package/demo/routes/{addresses.js → addresses/index.js} +1 -1
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
- package/demo/routes/{carts.js → carts/index.js} +1 -1
- package/demo/routes/{categories.js → categories/index.js} +1 -1
- package/demo/routes/{coupons.js → coupons/index.js} +1 -1
- package/demo/routes/index.js +39 -24
- package/demo/routes/{orders.js → orders/index.js} +1 -1
- package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
- package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
- package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
- package/demo/routes/{products.js → products/index.js} +1 -1
- package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
- package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
- package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
- 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.js → wishlists/index.js} +1 -1
- 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 +112 -43
- package/src/cli/commands/help.js +0 -1
- package/src/cli/diff-engine.js +2 -1
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +61 -22
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +42 -30
- 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/sqlite3/db.js +11 -0
- package/demo/docs/llm.md +0 -197
- package/demo/llms.txt +0 -70
- package/demo/routes/users.js +0 -6
- package/src/cli/commands/generate-llm-docs.js +0 -418
- /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. **
|
|
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,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
|
-
|
|
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 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
|
|
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
|
-
//
|
|
135
|
+
// Generate route files for each table
|
|
123
136
|
for (const m of meta) {
|
|
124
137
|
if (nestedChildren.has(m.table)) continue;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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(
|
|
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
|
-
// ---
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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) {
|
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
|
-
--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
|
package/src/cli/diff-engine.js
CHANGED
|
@@ -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) +
|
|
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 `
|
|
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
|
-
|
|
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 += `
|
|
568
|
+
imports += `import ${varName} from "./${m.table}.js";\n`;
|
|
567
569
|
exports += ` ${varName},\n`;
|
|
568
570
|
}
|
|
569
571
|
return `${imports}
|
|
570
|
-
|
|
572
|
+
export {
|
|
573
|
+
${exports}};
|
|
574
|
+
|
|
575
|
+
export default {
|
|
571
576
|
${exports}};
|
|
572
577
|
`;
|
|
573
578
|
}
|