db-model-router 1.0.16 → 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.
@@ -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
- if (ADAPTER === "postgres") {
32
- // --- Postgres demo flow ---
33
- const schemaPath = path.join(ROOT, "dbmr.postgres.test.schema.json");
34
-
35
- // 2. Copy Postgres schema into the demo folder
36
- const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
37
- const schemaDest = path.join(DEMO, "dbmr.schema.json");
38
- fs.writeFileSync(schemaDest, JSON.stringify(schema, null, 2) + "\n");
39
- console.log(`\n> Copied ${schemaPath} ${schemaDest}`);
40
-
41
- // 3. Scaffold project with postgres
42
- run(
43
- "node ../src/cli/main.js init --database postgres --yes --no-install",
44
- DEMO,
45
- );
46
-
47
- // 4. Generate from schema (already copied)
48
- run("node ../src/cli/main.js generate --from dbmr.schema.json", DEMO);
49
-
50
- // 5. Install dependencies
51
- run("npm install", DEMO);
52
-
53
- console.log("\n✔ Postgres demo project ready in ./demo");
54
- console.log("cd demo && npm run migrate && npm run dev\n");
55
- } else {
56
- // --- SQLite3 demo flow (default) ---
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/generate/inspect/doctor/diff.
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. **Scaffold**: `db-model-router init --framework express --database postgres --session redis --rateLimiting --helmet --logger --yes`
32
- 2. **Start infra**: `npm run docker:up`
33
- 3. **Migrations**: Write SQL/JS files into `migrations/`, then `npm run migrate`
34
- 4. **Generate models**: `db-model-router generate --from dbmr.schema.json --models`
35
- 5. **Generate routes + tests**: `db-model-router generate --from dbmr.schema.json --routes --tests`
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
- > When using `--saas-structure`, skip defining `users`, `tenants`, `roles`, `role_permissions` in your schemathey're auto-generated with auth middleware, permission system, and tenant isolation.
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
- db-model-router generate --from dbmr.schema.json # models, routes, tests, OpenAPI
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` — Scaffold project
255
+ ### `init` — First-buildout-only project scaffold
256
256
 
257
257
  ```bash
258
- # Fully non-interactive (LLM-friendly)
259
- db-model-router init --framework express --database postgres --session redis \
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
- # With Loki/Grafana logging
263
- db-model-router init --database postgres --logger --loki --yes
261
+ # Specific schema path
262
+ db-model-router init ./my.schema.json
264
263
 
265
- # From schema file
266
- db-model-router init --from dbmr.schema.json --yes --no-install
264
+ # Preview / skip install
265
+ db-model-router init --dry-run
266
+ db-model-router init --no-install
267
267
  ```
268
268
 
269
- Key flags: `--framework`, `--database` (or `--db`), `--session`, `--output`, `--rateLimiting`, `--helmet`, `--logger`, `--loki`, `--yes`, `--no-install`
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 Migration runner
279
- <output>/route/index.js Central route mounting
280
- <output>/route/health.js GET /health endpoint
281
- <output>/migrations/ Initial migration files
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 --loki).
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
- ### `inspect` Introspect existing DB → schema
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
- All artifact types are **enabled by default**. Use `--flag=false` to disable specific ones.
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 `--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`).
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): `--yes`, `--json`, `--dry-run`, `--no-install`, `--help`
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 `--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.
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
- * Generate command handler for the unified CLI.
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
- * Reads a schema file, converts to ModelMeta[], and generates
43
- * models, routes, tests, OpenAPI spec, migrations, and docs route.
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
- * Supported flags:
46
- * --from Path to schema file (default: dbmr.schema.json)
47
- * --models Generate only model files
48
- * --routes Generate only route files (including child routes and index)
49
- * --openapi Generate only OpenAPI spec + docs route
50
- * --tests Generate only test files
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 generate(args, flags, ctx) {
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
- // Determine which artifact types to generate.
130
- // If any specific artifact flag is explicitly set to true, only generate those.
131
- // Otherwise, all artifact types are generated (unless explicitly set to false).
132
- const hasExplicitTrue =
133
- args.models === true ||
134
- args.routes === true ||
135
- args.openapi === true ||
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
- // Determine adapter: from --adapter flag, or from the schema's adapter field
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. Provide --adapter or set adapter in schema.`;
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
- // previously-planned routes/index.js from the schema generator.
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
- // --- Output ---
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 files would be generated:");
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
- `\nDone. ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
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 = generate;
341
+ module.exports = { buildSchemaArtifacts, SUPPORTED_ADAPTERS };