db-model-router 1.0.17 → 1.0.18
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 +31 -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/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 {
|
|
@@ -37,64 +36,23 @@ const SUPPORTED_ADAPTERS = [
|
|
|
37
36
|
];
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
39
|
+
* Build all schema-driven artifacts (models, routes, tests, OpenAPI spec,
|
|
40
|
+
* migrations, docs route, and optional SaaS structure) from an already-parsed
|
|
41
|
+
* schema, writing them under `baseDir`.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* This is the internal buildout engine invoked by the `init` command. It is
|
|
44
|
+
* NOT registered as a CLI subcommand. All artifact types are always generated;
|
|
45
|
+
* the SaaS structure is gated by `schema.options.saasStructure` (default true).
|
|
44
46
|
*
|
|
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
|
|
47
|
+
* @param {object} opts
|
|
48
|
+
* @param {object} opts.schema - Parsed schema from parseSchema()
|
|
49
|
+
* @param {string} opts.baseDir - Absolute directory to write artifacts into
|
|
50
|
+
* @param {import('../flags').OutputContext} opts.ctx - Output context for --json support
|
|
51
|
+
* @param {object} opts.flags - Universal flags: { json, dryRun, ... }
|
|
52
|
+
* @returns {Promise<{ files: Array<{ path: string, status: string }> }>}
|
|
60
53
|
*/
|
|
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
|
-
|
|
54
|
+
async function buildSchemaArtifacts({ schema, baseDir, ctx, flags }) {
|
|
96
55
|
const meta = schemaToModelMeta(schema);
|
|
97
|
-
const relationships = schema.relationships || [];
|
|
98
56
|
const tableNames = meta.map((m) => m.table).sort();
|
|
99
57
|
|
|
100
58
|
// For route generation, only use parent-derived relationships.
|
|
@@ -126,37 +84,13 @@ async function generate(args, flags, ctx) {
|
|
|
126
84
|
ancestors[m.table] = chain;
|
|
127
85
|
}
|
|
128
86
|
|
|
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());
|
|
87
|
+
// Always generate every artifact type. SaaS gated by schema.options.saasStructure.
|
|
88
|
+
const genModels = true;
|
|
89
|
+
const genRoutes = true;
|
|
90
|
+
const genOpenapi = true;
|
|
91
|
+
const genTests = true;
|
|
92
|
+
const genMigrations = true;
|
|
93
|
+
const genSaas = !(schema.options && schema.options.saasStructure === false);
|
|
160
94
|
|
|
161
95
|
// Collect all planned files: { relPath, content }
|
|
162
96
|
const planned = [];
|
|
@@ -179,9 +113,6 @@ async function generate(args, flags, ctx) {
|
|
|
179
113
|
childrenByParent[rel.parent].push(rel);
|
|
180
114
|
}
|
|
181
115
|
|
|
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
116
|
for (const m of meta) {
|
|
186
117
|
const tableName = m.table;
|
|
187
118
|
const chain = ancestors[tableName];
|
|
@@ -194,7 +125,6 @@ async function generate(args, flags, ctx) {
|
|
|
194
125
|
if (hasChildren) {
|
|
195
126
|
const children = childrenByParent[tableName];
|
|
196
127
|
if (hasParent) {
|
|
197
|
-
// Intermediate node: own CRUD scoped by parent PK + mounts children
|
|
198
128
|
const immediateParent = chain[chain.length - 1];
|
|
199
129
|
const parentFk = schema.tables[immediateParent].pk;
|
|
200
130
|
planned.push({
|
|
@@ -202,14 +132,12 @@ async function generate(args, flags, ctx) {
|
|
|
202
132
|
content: generateParentRouteFile(tableName, children, parentFk),
|
|
203
133
|
});
|
|
204
134
|
} else {
|
|
205
|
-
// Root parent: own CRUD unscoped + mounts children
|
|
206
135
|
planned.push({
|
|
207
136
|
relPath,
|
|
208
137
|
content: generateParentRouteFile(tableName, children),
|
|
209
138
|
});
|
|
210
139
|
}
|
|
211
140
|
} else if (hasParent) {
|
|
212
|
-
// Leaf child
|
|
213
141
|
const immediateParent = chain[chain.length - 1];
|
|
214
142
|
const parentFk = schema.tables[immediateParent].pk;
|
|
215
143
|
planned.push({
|
|
@@ -217,7 +145,6 @@ async function generate(args, flags, ctx) {
|
|
|
217
145
|
content: generateChildRouteFile(tableName, immediateParent, parentFk),
|
|
218
146
|
});
|
|
219
147
|
} else {
|
|
220
|
-
// Root leaf
|
|
221
148
|
planned.push({
|
|
222
149
|
relPath,
|
|
223
150
|
content: generateRouteFile(tableName),
|
|
@@ -225,7 +152,6 @@ async function generate(args, flags, ctx) {
|
|
|
225
152
|
}
|
|
226
153
|
}
|
|
227
154
|
|
|
228
|
-
// Routes index file (include docs route when openapi is being generated)
|
|
229
155
|
planned.push({
|
|
230
156
|
relPath: "routes/index.js",
|
|
231
157
|
content: generateRoutesIndexFile(tableNames, routeRelationships, {
|
|
@@ -238,19 +164,12 @@ async function generate(args, flags, ctx) {
|
|
|
238
164
|
if (genOpenapi) {
|
|
239
165
|
const spec = generateOpenAPISpec(meta, { relationships: routeRelationships });
|
|
240
166
|
|
|
241
|
-
// Merge SaaS routes into the OpenAPI spec when saas-structure is active
|
|
242
|
-
// SaaS routes appear BEFORE product routes in the docs
|
|
243
167
|
if (genSaas) {
|
|
244
168
|
const saasApi = generateSaasOpenAPIPaths();
|
|
245
|
-
|
|
246
|
-
// Prepend SaaS paths before product paths
|
|
247
169
|
const productPaths = spec.paths;
|
|
248
170
|
spec.paths = { ...saasApi.paths, ...productPaths };
|
|
249
|
-
|
|
250
|
-
// Prepend SaaS schemas before product schemas
|
|
251
171
|
const productSchemas = spec.components.schemas;
|
|
252
172
|
spec.components.schemas = { ...saasApi.schemas, ...productSchemas };
|
|
253
|
-
|
|
254
173
|
if (!spec.components.securitySchemes) {
|
|
255
174
|
spec.components.securitySchemes = {};
|
|
256
175
|
}
|
|
@@ -261,8 +180,6 @@ async function generate(args, flags, ctx) {
|
|
|
261
180
|
relPath: "openapi.json",
|
|
262
181
|
content: JSON.stringify(spec, null, 2) + "\n",
|
|
263
182
|
});
|
|
264
|
-
|
|
265
|
-
// Generate Swagger UI docs route
|
|
266
183
|
planned.push({
|
|
267
184
|
relPath: "routes/docs.js",
|
|
268
185
|
content: generateDocsRoute(),
|
|
@@ -315,20 +232,19 @@ async function generate(args, flags, ctx) {
|
|
|
315
232
|
|
|
316
233
|
// --- SaaS structure files (additive, on top of schema-based generation) ---
|
|
317
234
|
if (genSaas) {
|
|
318
|
-
|
|
319
|
-
const adapter = args.adapter || schema.adapter;
|
|
235
|
+
const adapter = schema.adapter;
|
|
320
236
|
|
|
321
237
|
if (!adapter || !SUPPORTED_ADAPTERS.includes(adapter)) {
|
|
322
238
|
const msg = adapter
|
|
323
239
|
? `Invalid adapter: ${adapter}. Supported: ${SUPPORTED_ADAPTERS.join(", ")}`
|
|
324
|
-
: `Adapter is required for saas-structure generation.
|
|
240
|
+
: `Adapter is required for saas-structure generation. Set adapter in schema.`;
|
|
325
241
|
if (flags.json) {
|
|
326
242
|
ctx.result({ error: true, code: "INVALID_ADAPTER", message: msg });
|
|
327
243
|
} else {
|
|
328
244
|
ctx.log(`Error: ${msg}`);
|
|
329
245
|
}
|
|
330
246
|
process.exitCode = 1;
|
|
331
|
-
return;
|
|
247
|
+
return { files: [] };
|
|
332
248
|
}
|
|
333
249
|
|
|
334
250
|
const saasFiles = generateSaasStructure(adapter, {
|
|
@@ -342,8 +258,8 @@ async function generate(args, flags, ctx) {
|
|
|
342
258
|
});
|
|
343
259
|
|
|
344
260
|
// The SaaS generator produces a combined routes/index.js that includes
|
|
345
|
-
// both SaaS routes and dbmr schema-generated routes. Remove any
|
|
346
|
-
//
|
|
261
|
+
// both SaaS routes and dbmr schema-generated routes. Remove any previously
|
|
262
|
+
// planned routes/index.js from the schema generator.
|
|
347
263
|
const existingIndexIdx = planned.findIndex(
|
|
348
264
|
(p) => p.relPath === "routes/index.js",
|
|
349
265
|
);
|
|
@@ -384,25 +300,23 @@ async function generate(args, flags, ctx) {
|
|
|
384
300
|
ctx.log(` unchanged ${relPath}`);
|
|
385
301
|
continue;
|
|
386
302
|
}
|
|
387
|
-
// File exists but content differs — overwrite
|
|
388
303
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
389
304
|
fs.writeFileSync(fullPath, content, "utf8");
|
|
390
305
|
results.push({ path: relPath, status: "overwritten" });
|
|
391
306
|
ctx.log(` overwritten ${relPath}`);
|
|
392
307
|
} else {
|
|
393
|
-
// File does not exist — create
|
|
394
308
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
395
309
|
fs.writeFileSync(fullPath, content, "utf8");
|
|
396
310
|
results.push({ path: relPath, status: "created" });
|
|
397
311
|
ctx.log(` created ${relPath}`);
|
|
398
312
|
}
|
|
399
313
|
}
|
|
400
|
-
|
|
314
|
+
|
|
401
315
|
if (flags.dryRun) {
|
|
402
316
|
if (flags.json) {
|
|
403
317
|
ctx.result({ files: results });
|
|
404
318
|
} else {
|
|
405
|
-
ctx.log("Dry run — the following
|
|
319
|
+
ctx.log("Dry run — the following schema artifacts would be generated:");
|
|
406
320
|
for (const r of results) {
|
|
407
321
|
ctx.log(` ${r.path}`);
|
|
408
322
|
}
|
|
@@ -417,9 +331,11 @@ async function generate(args, flags, ctx) {
|
|
|
417
331
|
).length;
|
|
418
332
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
419
333
|
ctx.log(
|
|
420
|
-
`\
|
|
334
|
+
`\nSchema artifacts: ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
|
|
421
335
|
);
|
|
422
336
|
}
|
|
337
|
+
|
|
338
|
+
return { files: results };
|
|
423
339
|
}
|
|
424
340
|
|
|
425
|
-
module.exports =
|
|
341
|
+
module.exports = { buildSchemaArtifacts, SUPPORTED_ADAPTERS };
|