db-model-router 1.0.17 → 1.0.19
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/.serena/project.yml +57 -30
- package/README.md +117 -104
- package/dbmr.postgres.schema.json +5 -1
- package/dbmr.postgres.test.schema.json +5 -1
- package/dbmr.schema.json +5 -1
- package/docs/dbmr-schema-spec.md +28 -4
- package/package.json +1 -1
- package/scripts/demo-create.js +56 -87
- package/skill/SKILL.md +77 -46
- package/src/cli/commands/generate.js +32 -115
- package/src/cli/commands/help.js +55 -62
- package/src/cli/commands/init.js +95 -72
- package/src/cli/flags.js +4 -1
- package/src/cli/init/generators.js +2 -2
- package/src/cli/init.js +13 -77
- package/src/cli/main.js +4 -25
- package/src/cli/saas/generate-saas-migrations.js +2 -1
- package/src/schema/schema-parser.js +18 -1
- package/src/schema/schema-validator.js +75 -0
- package/src/cli/init/prompt.js +0 -191
package/scripts/demo-create.js
CHANGED
|
@@ -1,87 +1,56 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
7
|
-
|
|
8
|
-
const ROOT = path.resolve(__dirname, "..");
|
|
9
|
-
const DEMO = path.join(ROOT, "demo");
|
|
10
|
-
|
|
11
|
-
const ADAPTER = process.argv[2] || "sqlite3";
|
|
12
|
-
const SUPPORTED_ADAPTERS = ["sqlite3", "postgres", "mysql", "mariadb"];
|
|
13
|
-
|
|
14
|
-
if (!SUPPORTED_ADAPTERS.includes(ADAPTER)) {
|
|
15
|
-
console.error(`Error: Unsupported adapter "${ADAPTER}".`);
|
|
16
|
-
console.error(`Supported: ${SUPPORTED_ADAPTERS.join(", ")}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 1. Clear demo folder
|
|
21
|
-
if (fs.existsSync(DEMO)) {
|
|
22
|
-
fs.rmSync(DEMO, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
fs.mkdirSync(DEMO, { recursive: true });
|
|
25
|
-
|
|
26
|
-
const run = (cmd, cwd) => {
|
|
27
|
-
console.log(`\n> ${cmd}`);
|
|
28
|
-
execSync(cmd, { cwd, stdio: "inherit" });
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// 2. Scaffold project with sqlite3
|
|
59
|
-
run(
|
|
60
|
-
"node ../src/cli/main.js init --database sqlite3 --yes --no-install",
|
|
61
|
-
DEMO,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// 3. Copy schema and patch adapter to sqlite3
|
|
65
|
-
const schema = JSON.parse(
|
|
66
|
-
fs.readFileSync(path.join(ROOT, "dbmr.schema.json"), "utf8"),
|
|
67
|
-
);
|
|
68
|
-
schema.adapter = "sqlite3";
|
|
69
|
-
if (schema.options) {
|
|
70
|
-
delete schema.options.session;
|
|
71
|
-
delete schema.options.loki;
|
|
72
|
-
}
|
|
73
|
-
fs.writeFileSync(
|
|
74
|
-
path.join(DEMO, "dbmr.schema.json"),
|
|
75
|
-
JSON.stringify(schema, null, 2) + "\n",
|
|
76
|
-
);
|
|
77
|
-
console.log("\n> Copied and patched dbmr.schema.json (adapter → sqlite3)");
|
|
78
|
-
|
|
79
|
-
// 4. Generate models, routes, tests, openapi from schema
|
|
80
|
-
run("node ../src/cli/main.js generate --from dbmr.schema.json", DEMO);
|
|
81
|
-
|
|
82
|
-
// 5. Install dependencies
|
|
83
|
-
run("npm install", DEMO);
|
|
84
|
-
|
|
85
|
-
console.log("\n✔ Demo project ready in ./demo");
|
|
86
|
-
console.log("cd demo && npm run migrate && npm run dev\n");
|
|
87
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
9
|
+
const DEMO = path.join(ROOT, "demo");
|
|
10
|
+
|
|
11
|
+
const ADAPTER = process.argv[2] || "sqlite3";
|
|
12
|
+
const SUPPORTED_ADAPTERS = ["sqlite3", "postgres", "mysql", "mariadb"];
|
|
13
|
+
|
|
14
|
+
if (!SUPPORTED_ADAPTERS.includes(ADAPTER)) {
|
|
15
|
+
console.error(`Error: Unsupported adapter "${ADAPTER}".`);
|
|
16
|
+
console.error(`Supported: ${SUPPORTED_ADAPTERS.join(", ")}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. Clear demo folder
|
|
21
|
+
if (fs.existsSync(DEMO)) {
|
|
22
|
+
fs.rmSync(DEMO, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(DEMO, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const run = (cmd, cwd) => {
|
|
27
|
+
console.log(`\n> ${cmd}`);
|
|
28
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 2. Copy schema into the demo folder
|
|
32
|
+
const schemaPath =
|
|
33
|
+
ADAPTER === "postgres"
|
|
34
|
+
? path.join(ROOT, "dbmr.postgres.test.schema.json")
|
|
35
|
+
: path.join(ROOT, "dbmr.schema.json");
|
|
36
|
+
|
|
37
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
38
|
+
|
|
39
|
+
// Patch adapter-specific options for the sqlite3 demo
|
|
40
|
+
if (ADAPTER === "sqlite3") {
|
|
41
|
+
schema.adapter = "sqlite3";
|
|
42
|
+
if (schema.options) {
|
|
43
|
+
delete schema.options.session;
|
|
44
|
+
delete schema.options.loki;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const schemaDest = path.join(DEMO, "dbmr.schema.json");
|
|
49
|
+
fs.writeFileSync(schemaDest, JSON.stringify(schema, null, 2) + "\n");
|
|
50
|
+
console.log(`\n> Copied ${schemaPath} → ${schemaDest}`);
|
|
51
|
+
|
|
52
|
+
// 3. Scaffold + generate from schema + install in one init run
|
|
53
|
+
run("node ../src/cli/main.js init --yes", DEMO);
|
|
54
|
+
|
|
55
|
+
console.log("\n✔ Demo project ready in ./demo");
|
|
56
|
+
console.log("cd demo && npm run migrate && npm run dev\n");
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: db-model-router
|
|
3
|
-
description: Use this skill whenever the user wants to build a REST API with Node.js/Express backed by any database — including MySQL, PostgreSQL, SQLite, MongoDB, MSSQL, Oracle, Redis, DynamoDB, or CockroachDB. Trigger on any mention of db-model-router, or when the user asks to scaffold, generate, or wire up a CRUD API, models, or routes for a Node/Express backend. Also trigger when the user asks about connecting to a specific database with db-model-router, model definitions, filter syntax, bulk operations, schema-driven generation, or CLI commands like init/
|
|
3
|
+
description: Use this skill whenever the user wants to build a REST API with Node.js/Express backed by any database — including MySQL, PostgreSQL, SQLite, MongoDB, MSSQL, Oracle, Redis, DynamoDB, or CockroachDB. Trigger on any mention of db-model-router, or when the user asks to scaffold, generate, or wire up a CRUD API, models, or routes for a Node/Express backend. Also trigger when the user asks about connecting to a specific database with db-model-router, model definitions, filter syntax, bulk operations, schema-driven generation, or CLI commands like init/inspect/doctor/diff/db-manager.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# db-model-router — LLM Skill Reference
|
|
@@ -28,21 +28,21 @@ MySQL/MariaDB use `mysql2` — no separate reference file needed (see Connection
|
|
|
28
28
|
|
|
29
29
|
## LLM Workflow (follow this order for new projects)
|
|
30
30
|
|
|
31
|
-
1. **
|
|
32
|
-
2. **
|
|
33
|
-
3. **
|
|
34
|
-
4. **
|
|
35
|
-
5. **
|
|
36
|
-
6. **Generate SaaS structure** (if multi-tenant): `db-model-router generate --from dbmr.schema.json --saas-structure`
|
|
37
|
-
7. **Run**: `npm run dev`
|
|
31
|
+
1. **Write `dbmr.schema.json`** — adapter, framework, `options` (session, rateLimiting, helmet, logger, loki, output, saasStructure, apiBasePath, port), `tables`, `relationships`.
|
|
32
|
+
2. **Build the whole project (first buildout only)**: `db-model-router init` (reads `./dbmr.schema.json` → app.js + models + routes + migrations + tests + OpenAPI + optional SaaS). Add `--no-install` to skip npm install, `--dry-run` to preview.
|
|
33
|
+
3. **Start infra**: `npm run docker:up`
|
|
34
|
+
4. **Migrate**: `npm run migrate`
|
|
35
|
+
5. **Run**: `npm run dev`
|
|
38
36
|
|
|
39
|
-
>
|
|
37
|
+
> `init` is **first-buildout only**. It refuses to run if `app.js` already exists in cwd. To add new tables/models/routes after buildout, do it manually — add a migration (`node commons/add_migration.js <name>`), add `models/<table>.js`, mount `routes/<table>/index.js` in `routes/index.js`. See `db-model-router help init`.
|
|
38
|
+
> When `options.saasStructure` is `true` (default), skip defining `users`, `tenants`, `roles`, `role_permissions` in your schema — they're auto-generated with auth middleware, permission system, and tenant isolation.
|
|
40
39
|
|
|
41
|
-
For existing databases, use `inspect` first
|
|
40
|
+
For existing databases, use `inspect` first, then `init`:
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
43
|
db-model-router inspect --type postgres --env .env # → writes dbmr.schema.json
|
|
45
|
-
|
|
44
|
+
# edit dbmr.schema.json (set options, parents, etc.) then:
|
|
45
|
+
db-model-router init # → full project from schema
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
---
|
|
@@ -252,56 +252,61 @@ db-model-router <command> [options]
|
|
|
252
252
|
db-model-router help <command>
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
### `init` —
|
|
255
|
+
### `init` — First-buildout-only project scaffold
|
|
256
256
|
|
|
257
257
|
```bash
|
|
258
|
-
#
|
|
259
|
-
db-model-router init
|
|
260
|
-
--rateLimiting --helmet --logger --yes
|
|
258
|
+
# Build the full project from ./dbmr.schema.json (first buildout only)
|
|
259
|
+
db-model-router init
|
|
261
260
|
|
|
262
|
-
#
|
|
263
|
-
db-model-router init
|
|
261
|
+
# Specific schema path
|
|
262
|
+
db-model-router init ./my.schema.json
|
|
264
263
|
|
|
265
|
-
#
|
|
266
|
-
db-model-router init --
|
|
264
|
+
# Preview / skip install
|
|
265
|
+
db-model-router init --dry-run
|
|
266
|
+
db-model-router init --no-install
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
**First buildout only** — refuses if `app.js` already exists in cwd. No config flags; all config lives in `options` of `dbmr.schema.json`:
|
|
270
|
+
|
|
271
|
+
| Option | Default | Description |
|
|
272
|
+
| --------------- | ----------- | --------------------------------------------------------------------------- |
|
|
273
|
+
| `output` | `null` (cwd)| Directory for backend source files (package.json/app.js stay in root) |
|
|
274
|
+
| `saasStructure` | `true` | Generate the multi-tenant SaaS backend (tables, middleware, routes, seeds)|
|
|
275
|
+
| `apiBasePath` | `"/api"` | API mount path (→ `.env` `API_BASE_PATH`) |
|
|
276
|
+
| `port` | `3000` | Server port (→ `.env` `PORT`) |
|
|
277
|
+
| `session` | `"memory"` | `memory` / `redis` / `database` |
|
|
278
|
+
| `rateLimiting` | `true` | Enable `express-rate-limit` |
|
|
279
|
+
| `helmet` | `true` | Enable Helmet security headers |
|
|
280
|
+
| `logger` | `true` | Enable Winston request logger |
|
|
281
|
+
| `loki` | `false` | Enable Grafana Loki transport + Loki/Grafana in docker-compose |
|
|
270
282
|
|
|
271
283
|
Generated structure (ESM, `"type":"module"`):
|
|
272
284
|
|
|
273
285
|
```
|
|
274
286
|
app.js Express entry point
|
|
275
|
-
.env / .env.example Env config (random passwords)
|
|
287
|
+
.env / .env.example Env config (PORT, API_BASE_PATH, random passwords)
|
|
276
288
|
docker-compose.yml DB + optional Loki/Grafana
|
|
277
289
|
<output>/commons/db.js Database init + global.db
|
|
278
|
-
<output>/commons/migrate.js
|
|
279
|
-
<output>/
|
|
280
|
-
<output>/
|
|
281
|
-
<output>/
|
|
290
|
+
<output>/commons/migrate.js Migration runner
|
|
291
|
+
<output>/commons/add_migration.js Migration creator helper
|
|
292
|
+
<output>/routes/index.js Central route mounting (SaaS + product routes)
|
|
293
|
+
<output>/routes/health.js GET /health endpoint
|
|
294
|
+
<output>/routes/docs.js Swagger UI at /docs
|
|
295
|
+
<output>/routes/<table>/index.js CRUD route per table (nested by parent)
|
|
296
|
+
<output>/models/<table>.js Model per table
|
|
297
|
+
<output>/migrations/ CREATE TABLE migrations
|
|
298
|
+
<output>/test/<table>.test.js CRUD endpoint tests
|
|
299
|
+
<output>/openapi.json OpenAPI 3.0 spec
|
|
300
|
+
<output>/seeds/saas-seed.js (when saasStructure=true)
|
|
282
301
|
```
|
|
283
302
|
|
|
284
|
-
Docker services auto-generated: database, Redis (if session=redis), Loki + Grafana (if
|
|
303
|
+
Docker services auto-generated: database, Redis (if session=redis), Loki + Grafana (if options.loki).
|
|
285
304
|
|
|
286
305
|
Scripts: `start`, `dev`, `test`, `migrate`, `add_migration`, `docker:build`, `docker:up`, `docker:down`.
|
|
287
306
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
db-model-router inspect --type postgres --env .env [--out schema.json] [--tables t1,t2]
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### `generate` — Generate code from schema
|
|
295
|
-
|
|
296
|
-
```bash
|
|
297
|
-
db-model-router generate --from dbmr.schema.json [--output <dir>] [--models=false] [--routes=false] [--openapi=false] [--tests=false] [--migrations=false] [--saas-structure=false]
|
|
298
|
-
```
|
|
307
|
+
#### `options.saasStructure` (default: enabled)
|
|
299
308
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
#### `--saas-structure` (default: enabled)
|
|
303
|
-
|
|
304
|
-
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:
|
|
309
|
+
SaaS structure is generated unless `options.saasStructure` is `false`. It produces a complete multi-tenant SaaS backend on top of schema-generated code:
|
|
305
310
|
|
|
306
311
|
- **Tables**: `tenants`, `users`, `roles`, `role_permissions`, `webhooks`, `webhook_logs`
|
|
307
312
|
- **Middleware**: `authenticate.js`, `tenantIsolation.js`, `hasPermission.js`
|
|
@@ -310,10 +315,31 @@ SaaS structure is always generated unless explicitly disabled with `--saas-struc
|
|
|
310
315
|
- **Seeds**: Super Admin (all permissions, global scope) + Tenant Admin role template
|
|
311
316
|
- **Migrations**: Single consolidated `.sql` file with all SaaS tables
|
|
312
317
|
|
|
313
|
-
> **Critical**: Since
|
|
318
|
+
> **Critical**: Since `saasStructure` defaults to `true`, `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`).
|
|
314
319
|
|
|
315
320
|
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.
|
|
316
321
|
|
|
322
|
+
#### Adding tables/models/routes after buildout (manual — do NOT re-run `init`)
|
|
323
|
+
|
|
324
|
+
1. **Migration**: `node commons/add_migration.js <name>` → write `CREATE TABLE` (SQL) or `create_<table>.js` (NoSQL) inside the new timestamped file.
|
|
325
|
+
2. **Model**: add `models/<table>.js` mirroring an existing model:
|
|
326
|
+
```js
|
|
327
|
+
const model = require('#commons/model');
|
|
328
|
+
const db = require('#commons/db');
|
|
329
|
+
module.exports = model(db, '<table>', structure, '<pk>', uniqueKeys, option);
|
|
330
|
+
```
|
|
331
|
+
3. **Route**: add `routes/<table>/index.js` via `route(model)`, then mount in `routes/index.js`:
|
|
332
|
+
```js
|
|
333
|
+
router.use('/<endpoint>', require('./<table>/index.js'));
|
|
334
|
+
```
|
|
335
|
+
4. Run the new migration, then restart the server.
|
|
336
|
+
|
|
337
|
+
### `inspect` — Introspect existing DB → schema
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
db-model-router inspect --type postgres --env .env [--out schema.json] [--tables t1,t2]
|
|
341
|
+
```
|
|
342
|
+
|
|
317
343
|
### `doctor` — Validate schema + check file sync
|
|
318
344
|
|
|
319
345
|
```bash
|
|
@@ -326,7 +352,7 @@ db-model-router doctor [--from dbmr.schema.json] [--json]
|
|
|
326
352
|
db-model-router diff [--from dbmr.schema.json] [--json]
|
|
327
353
|
```
|
|
328
354
|
|
|
329
|
-
Universal flags (all commands): `--
|
|
355
|
+
Universal flags (all commands): `--json`, `--dry-run`, `--no-install`, `--help`
|
|
330
356
|
|
|
331
357
|
### `db-manager` — Launch database management UI
|
|
332
358
|
|
|
@@ -357,7 +383,12 @@ Requires a `.env` file with `DB_TYPE` and connection variables.
|
|
|
357
383
|
"session": "redis",
|
|
358
384
|
"rateLimiting": true,
|
|
359
385
|
"helmet": true,
|
|
360
|
-
"logger": true
|
|
386
|
+
"logger": true,
|
|
387
|
+
"loki": false,
|
|
388
|
+
"output": null,
|
|
389
|
+
"saasStructure": true,
|
|
390
|
+
"apiBasePath": "/api",
|
|
391
|
+
"port": 3000
|
|
361
392
|
},
|
|
362
393
|
"tables": {
|
|
363
394
|
"users": {
|
|
@@ -644,6 +675,6 @@ Topic: `{KAFKA_TOPIC_PREFIX}.{table_name}` (e.g. `dbmr.users`)
|
|
|
644
675
|
12. Docker passwords are randomly generated and shared between `.env` and `docker-compose.yml`.
|
|
645
676
|
13. PK convention: `<table>_id` (e.g. `user_id`, `post_id`). Include ALL columns in schema.
|
|
646
677
|
14. Use `parent` only for domain hierarchies (e.g. `posts → comments`), not system tables.
|
|
647
|
-
15. When
|
|
678
|
+
15. When `options.saasStructure` is `true` (default), 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. `init` is first-buildout only (refuses if `app.js` exists) — add new tables/models/routes manually (migration + model + route mount in `routes/index.js`).
|
|
648
679
|
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.
|
|
649
680
|
17. `search` is a **reserved** query param — never a column filter. It only takes effect when the model has `option.search_columns` set (config-only). It OR-joins a `LIKE %term%` across those columns and AND-combines with other filters. Works on `find`/`findOne`/`list` and the `GET /:id` + `GET /` routes.
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const { parseSchema } = require("../../schema/schema-parser");
|
|
6
5
|
const { schemaToModelMeta } = require("../../schema/schema-to-meta");
|
|
7
6
|
const { generateModelFile } = require("../generate-model");
|
|
8
7
|
const {
|
|
@@ -27,6 +26,7 @@ const { generateSaasOpenAPIPaths } = require("../saas/generate-saas-openapi");
|
|
|
27
26
|
const SUPPORTED_ADAPTERS = [
|
|
28
27
|
"postgres",
|
|
29
28
|
"mysql",
|
|
29
|
+
"mariadb",
|
|
30
30
|
"sqlite3",
|
|
31
31
|
"mssql",
|
|
32
32
|
"oracle",
|
|
@@ -37,64 +37,23 @@ const SUPPORTED_ADAPTERS = [
|
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Build all schema-driven artifacts (models, routes, tests, OpenAPI spec,
|
|
41
|
+
* migrations, docs route, and optional SaaS structure) from an already-parsed
|
|
42
|
+
* schema, writing them under `baseDir`.
|
|
41
43
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* This is the internal buildout engine invoked by the `init` command. It is
|
|
45
|
+
* NOT registered as a CLI subcommand. All artifact types are always generated;
|
|
46
|
+
* the SaaS structure is gated by `schema.options.saasStructure` (default true).
|
|
44
47
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* --migrations Generate only migration files
|
|
52
|
-
* --dry-run Report planned files without writing
|
|
53
|
-
* --json Output JSON result via ctx
|
|
54
|
-
*
|
|
55
|
-
* When no artifact flags are provided, all artifact types are generated.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} args - Parsed key-value args
|
|
58
|
-
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
59
|
-
* @param {import('../flags').OutputContext} ctx - Output context
|
|
48
|
+
* @param {object} opts
|
|
49
|
+
* @param {object} opts.schema - Parsed schema from parseSchema()
|
|
50
|
+
* @param {string} opts.baseDir - Absolute directory to write artifacts into
|
|
51
|
+
* @param {import('../flags').OutputContext} opts.ctx - Output context for --json support
|
|
52
|
+
* @param {object} opts.flags - Universal flags: { json, dryRun, ... }
|
|
53
|
+
* @returns {Promise<{ files: Array<{ path: string, status: string }> }>}
|
|
60
54
|
*/
|
|
61
|
-
async function
|
|
62
|
-
// --- Standard schema-based generation ---
|
|
63
|
-
const schemaPath = path.resolve(args.from || "dbmr.schema.json");
|
|
64
|
-
|
|
65
|
-
if (!fs.existsSync(schemaPath)) {
|
|
66
|
-
const msg = `Schema file not found: ${args.from || "dbmr.schema.json"}`;
|
|
67
|
-
if (flags.json) {
|
|
68
|
-
ctx.result({ error: true, code: "SCHEMA_NOT_FOUND", message: msg });
|
|
69
|
-
} else {
|
|
70
|
-
ctx.log(`Error: ${msg}`);
|
|
71
|
-
}
|
|
72
|
-
process.exitCode = 1;
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let schema;
|
|
77
|
-
try {
|
|
78
|
-
const raw = fs.readFileSync(schemaPath, "utf8");
|
|
79
|
-
schema = parseSchema(raw);
|
|
80
|
-
} catch (err) {
|
|
81
|
-
const msg = `Schema parse error: ${err.message}`;
|
|
82
|
-
if (flags.json) {
|
|
83
|
-
ctx.result({
|
|
84
|
-
error: true,
|
|
85
|
-
code: "SCHEMA_VALIDATION",
|
|
86
|
-
message: msg,
|
|
87
|
-
errors: err.errors || [],
|
|
88
|
-
});
|
|
89
|
-
} else {
|
|
90
|
-
ctx.log(`Error: ${msg}`);
|
|
91
|
-
}
|
|
92
|
-
process.exitCode = 1;
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
55
|
+
async function buildSchemaArtifacts({ schema, baseDir, ctx, flags }) {
|
|
96
56
|
const meta = schemaToModelMeta(schema);
|
|
97
|
-
const relationships = schema.relationships || [];
|
|
98
57
|
const tableNames = meta.map((m) => m.table).sort();
|
|
99
58
|
|
|
100
59
|
// For route generation, only use parent-derived relationships.
|
|
@@ -126,37 +85,13 @@ async function generate(args, flags, ctx) {
|
|
|
126
85
|
ancestors[m.table] = chain;
|
|
127
86
|
}
|
|
128
87
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
args.tests === true ||
|
|
137
|
-
args.migrations === true;
|
|
138
|
-
|
|
139
|
-
let genModels, genRoutes, genOpenapi, genTests, genMigrations, genSaas;
|
|
140
|
-
|
|
141
|
-
if (hasExplicitTrue) {
|
|
142
|
-
// Selective mode: only generate what was explicitly requested
|
|
143
|
-
genModels = args.models === true;
|
|
144
|
-
genRoutes = args.routes === true;
|
|
145
|
-
genOpenapi = args.openapi === true;
|
|
146
|
-
genTests = args.tests === true;
|
|
147
|
-
genMigrations = args.migrations === true;
|
|
148
|
-
genSaas = args["saas-structure"] === true;
|
|
149
|
-
} else {
|
|
150
|
-
// Default mode: generate all unless explicitly disabled
|
|
151
|
-
genModels = args.models !== false;
|
|
152
|
-
genRoutes = args.routes !== false;
|
|
153
|
-
genOpenapi = args.openapi !== false;
|
|
154
|
-
genTests = args.tests !== false;
|
|
155
|
-
genMigrations = args.migrations !== false;
|
|
156
|
-
genSaas = args["saas-structure"] !== false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const baseDir = path.resolve(args.output || process.cwd());
|
|
88
|
+
// Always generate every artifact type. SaaS gated by schema.options.saasStructure.
|
|
89
|
+
const genModels = true;
|
|
90
|
+
const genRoutes = true;
|
|
91
|
+
const genOpenapi = true;
|
|
92
|
+
const genTests = true;
|
|
93
|
+
const genMigrations = true;
|
|
94
|
+
const genSaas = !(schema.options && schema.options.saasStructure === false);
|
|
160
95
|
|
|
161
96
|
// Collect all planned files: { relPath, content }
|
|
162
97
|
const planned = [];
|
|
@@ -179,9 +114,6 @@ async function generate(args, flags, ctx) {
|
|
|
179
114
|
childrenByParent[rel.parent].push(rel);
|
|
180
115
|
}
|
|
181
116
|
|
|
182
|
-
// Generate exactly ONE route file per table at its correct nested path.
|
|
183
|
-
// Intermediate tables (both child and parent) get a hybrid route file
|
|
184
|
-
// that scopes their own CRUD by an ancestor FK and mounts their children.
|
|
185
117
|
for (const m of meta) {
|
|
186
118
|
const tableName = m.table;
|
|
187
119
|
const chain = ancestors[tableName];
|
|
@@ -194,7 +126,6 @@ async function generate(args, flags, ctx) {
|
|
|
194
126
|
if (hasChildren) {
|
|
195
127
|
const children = childrenByParent[tableName];
|
|
196
128
|
if (hasParent) {
|
|
197
|
-
// Intermediate node: own CRUD scoped by parent PK + mounts children
|
|
198
129
|
const immediateParent = chain[chain.length - 1];
|
|
199
130
|
const parentFk = schema.tables[immediateParent].pk;
|
|
200
131
|
planned.push({
|
|
@@ -202,14 +133,12 @@ async function generate(args, flags, ctx) {
|
|
|
202
133
|
content: generateParentRouteFile(tableName, children, parentFk),
|
|
203
134
|
});
|
|
204
135
|
} else {
|
|
205
|
-
// Root parent: own CRUD unscoped + mounts children
|
|
206
136
|
planned.push({
|
|
207
137
|
relPath,
|
|
208
138
|
content: generateParentRouteFile(tableName, children),
|
|
209
139
|
});
|
|
210
140
|
}
|
|
211
141
|
} else if (hasParent) {
|
|
212
|
-
// Leaf child
|
|
213
142
|
const immediateParent = chain[chain.length - 1];
|
|
214
143
|
const parentFk = schema.tables[immediateParent].pk;
|
|
215
144
|
planned.push({
|
|
@@ -217,7 +146,6 @@ async function generate(args, flags, ctx) {
|
|
|
217
146
|
content: generateChildRouteFile(tableName, immediateParent, parentFk),
|
|
218
147
|
});
|
|
219
148
|
} else {
|
|
220
|
-
// Root leaf
|
|
221
149
|
planned.push({
|
|
222
150
|
relPath,
|
|
223
151
|
content: generateRouteFile(tableName),
|
|
@@ -225,7 +153,6 @@ async function generate(args, flags, ctx) {
|
|
|
225
153
|
}
|
|
226
154
|
}
|
|
227
155
|
|
|
228
|
-
// Routes index file (include docs route when openapi is being generated)
|
|
229
156
|
planned.push({
|
|
230
157
|
relPath: "routes/index.js",
|
|
231
158
|
content: generateRoutesIndexFile(tableNames, routeRelationships, {
|
|
@@ -238,19 +165,12 @@ async function generate(args, flags, ctx) {
|
|
|
238
165
|
if (genOpenapi) {
|
|
239
166
|
const spec = generateOpenAPISpec(meta, { relationships: routeRelationships });
|
|
240
167
|
|
|
241
|
-
// Merge SaaS routes into the OpenAPI spec when saas-structure is active
|
|
242
|
-
// SaaS routes appear BEFORE product routes in the docs
|
|
243
168
|
if (genSaas) {
|
|
244
169
|
const saasApi = generateSaasOpenAPIPaths();
|
|
245
|
-
|
|
246
|
-
// Prepend SaaS paths before product paths
|
|
247
170
|
const productPaths = spec.paths;
|
|
248
171
|
spec.paths = { ...saasApi.paths, ...productPaths };
|
|
249
|
-
|
|
250
|
-
// Prepend SaaS schemas before product schemas
|
|
251
172
|
const productSchemas = spec.components.schemas;
|
|
252
173
|
spec.components.schemas = { ...saasApi.schemas, ...productSchemas };
|
|
253
|
-
|
|
254
174
|
if (!spec.components.securitySchemes) {
|
|
255
175
|
spec.components.securitySchemes = {};
|
|
256
176
|
}
|
|
@@ -261,8 +181,6 @@ async function generate(args, flags, ctx) {
|
|
|
261
181
|
relPath: "openapi.json",
|
|
262
182
|
content: JSON.stringify(spec, null, 2) + "\n",
|
|
263
183
|
});
|
|
264
|
-
|
|
265
|
-
// Generate Swagger UI docs route
|
|
266
184
|
planned.push({
|
|
267
185
|
relPath: "routes/docs.js",
|
|
268
186
|
content: generateDocsRoute(),
|
|
@@ -315,20 +233,19 @@ async function generate(args, flags, ctx) {
|
|
|
315
233
|
|
|
316
234
|
// --- SaaS structure files (additive, on top of schema-based generation) ---
|
|
317
235
|
if (genSaas) {
|
|
318
|
-
|
|
319
|
-
const adapter = args.adapter || schema.adapter;
|
|
236
|
+
const adapter = schema.adapter;
|
|
320
237
|
|
|
321
238
|
if (!adapter || !SUPPORTED_ADAPTERS.includes(adapter)) {
|
|
322
239
|
const msg = adapter
|
|
323
240
|
? `Invalid adapter: ${adapter}. Supported: ${SUPPORTED_ADAPTERS.join(", ")}`
|
|
324
|
-
: `Adapter is required for saas-structure generation.
|
|
241
|
+
: `Adapter is required for saas-structure generation. Set adapter in schema.`;
|
|
325
242
|
if (flags.json) {
|
|
326
243
|
ctx.result({ error: true, code: "INVALID_ADAPTER", message: msg });
|
|
327
244
|
} else {
|
|
328
245
|
ctx.log(`Error: ${msg}`);
|
|
329
246
|
}
|
|
330
247
|
process.exitCode = 1;
|
|
331
|
-
return;
|
|
248
|
+
return { files: [] };
|
|
332
249
|
}
|
|
333
250
|
|
|
334
251
|
const saasFiles = generateSaasStructure(adapter, {
|
|
@@ -342,8 +259,8 @@ async function generate(args, flags, ctx) {
|
|
|
342
259
|
});
|
|
343
260
|
|
|
344
261
|
// The SaaS generator produces a combined routes/index.js that includes
|
|
345
|
-
// both SaaS routes and dbmr schema-generated routes. Remove any
|
|
346
|
-
//
|
|
262
|
+
// both SaaS routes and dbmr schema-generated routes. Remove any previously
|
|
263
|
+
// planned routes/index.js from the schema generator.
|
|
347
264
|
const existingIndexIdx = planned.findIndex(
|
|
348
265
|
(p) => p.relPath === "routes/index.js",
|
|
349
266
|
);
|
|
@@ -384,25 +301,23 @@ async function generate(args, flags, ctx) {
|
|
|
384
301
|
ctx.log(` unchanged ${relPath}`);
|
|
385
302
|
continue;
|
|
386
303
|
}
|
|
387
|
-
// File exists but content differs — overwrite
|
|
388
304
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
389
305
|
fs.writeFileSync(fullPath, content, "utf8");
|
|
390
306
|
results.push({ path: relPath, status: "overwritten" });
|
|
391
307
|
ctx.log(` overwritten ${relPath}`);
|
|
392
308
|
} else {
|
|
393
|
-
// File does not exist — create
|
|
394
309
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
395
310
|
fs.writeFileSync(fullPath, content, "utf8");
|
|
396
311
|
results.push({ path: relPath, status: "created" });
|
|
397
312
|
ctx.log(` created ${relPath}`);
|
|
398
313
|
}
|
|
399
314
|
}
|
|
400
|
-
|
|
315
|
+
|
|
401
316
|
if (flags.dryRun) {
|
|
402
317
|
if (flags.json) {
|
|
403
318
|
ctx.result({ files: results });
|
|
404
319
|
} else {
|
|
405
|
-
ctx.log("Dry run — the following
|
|
320
|
+
ctx.log("Dry run — the following schema artifacts would be generated:");
|
|
406
321
|
for (const r of results) {
|
|
407
322
|
ctx.log(` ${r.path}`);
|
|
408
323
|
}
|
|
@@ -417,9 +332,11 @@ async function generate(args, flags, ctx) {
|
|
|
417
332
|
).length;
|
|
418
333
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
419
334
|
ctx.log(
|
|
420
|
-
`\
|
|
335
|
+
`\nSchema artifacts: ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
|
|
421
336
|
);
|
|
422
337
|
}
|
|
338
|
+
|
|
339
|
+
return { files: results };
|
|
423
340
|
}
|
|
424
341
|
|
|
425
|
-
module.exports =
|
|
342
|
+
module.exports = { buildSchemaArtifacts, SUPPORTED_ADAPTERS };
|