db-model-router 1.0.3 → 1.0.4

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.
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Per-command detailed help text.
5
+ * Each key matches a subcommand name from main.js.
6
+ */
7
+ const COMMAND_HELP = {
8
+ init: `Usage: db-model-router init [options]
9
+
10
+ Scaffold a new project from a schema file or interactively.
11
+ Creates app.js, .env, commons/, route/, middleware/, and migrations/.
12
+
13
+ Options:
14
+ --from <path> Read adapter, framework, and options from a schema file
15
+ --framework <name> Express framework: express, ultimate-express
16
+ --database <name> Database adapter: mysql, mariadb, postgres, sqlite3, mongodb,
17
+ mssql, cockroachdb, oracle, redis, dynamodb
18
+ --db <name> Alias for --database
19
+ --session <type> Session store: memory, redis, database
20
+ --output <dir> Directory for backend source files (relative to cwd).
21
+ package.json and app.js stay in root; commons/, route/,
22
+ middleware/, and migrations/ go inside this folder.
23
+ --rateLimiting Enable rate limiting (default: yes)
24
+ --helmet Enable Helmet security headers (default: yes)
25
+ --logger Enable Winston + Loki logger for Grafana (default: yes)
26
+ --yes Accept all defaults without prompting
27
+ --json Output machine-readable JSON
28
+ --dry-run Preview planned files without writing
29
+ --no-install Skip npm install after scaffolding
30
+ --help Show this help message
31
+
32
+ Generated files:
33
+ app.js Express app entry point
34
+ .env / .env.example Environment configuration
35
+ .gitignore Git ignore rules
36
+ <output>/commons/db.js Database init, connect, and global.db
37
+ <output>/commons/session.js Session configuration
38
+ <output>/commons/migrate.js Migration runner (also runs as script)
39
+ <output>/commons/add_migration.js Migration creation helper (also runs as script)
40
+ <output>/commons/security.js Helmet, rate limiting, custom headers
41
+ <output>/middleware/logger.js Winston + Loki request logger
42
+ <output>/route/index.js Central route mounting
43
+ <output>/route/health.js GET /health endpoint
44
+ <output>/migrations/ Initial migration files
45
+
46
+ Examples:
47
+ db-model-router init --from dbmr.schema.json --yes --no-install
48
+ db-model-router init --framework express --database postgres --output backend --yes
49
+ db-model-router init --database mysql --session redis --helmet --rateLimiting
50
+ db-model-router init --dry-run`,
51
+
52
+ inspect: `Usage: db-model-router inspect [options]
53
+
54
+ Introspect a live database and produce a dbmr.schema.json file.
55
+ Connects to the database, reads table structures, and outputs a schema.
56
+
57
+ Options:
58
+ --type <adapter> Database adapter (required): mysql, postgres, sqlite3,
59
+ mssql, oracle, cockroachdb
60
+ --env <path> Path to .env file for connection parameters
61
+ --out <path> Output file path (default: dbmr.schema.json)
62
+ --tables <list> Comma-separated list of tables to include (omit for all)
63
+ --yes Accept all defaults without prompting
64
+ --json Output schema as JSON to stdout (no file write)
65
+ --dry-run Output schema to stdout without writing file
66
+ --help Show this help message
67
+
68
+ Examples:
69
+ db-model-router inspect --type postgres --env .env
70
+ db-model-router inspect --type sqlite3 --out schema.json --tables users,posts
71
+ db-model-router inspect --type mysql --json`,
72
+
73
+ generate: `Usage: db-model-router generate [options]
74
+
75
+ Generate models, routes, tests, OpenAPI spec, and LLM docs from a schema file.
76
+ When no artifact flags are provided, all artifact types are generated.
77
+
78
+ Options:
79
+ --from <path> Path to schema file (default: dbmr.schema.json)
80
+ --models Generate only model files
81
+ --routes Generate only route files (including child routes and index)
82
+ --openapi Generate only OpenAPI spec
83
+ --tests Generate only test files
84
+ --llm-docs Generate only LLM documentation (llms.txt + docs/llm.md)
85
+ --yes Accept all defaults without prompting
86
+ --json Output machine-readable JSON
87
+ --dry-run Report planned files without writing
88
+ --help Show this help message
89
+
90
+ Generated files:
91
+ models/<table>.js Model with CRUD operations
92
+ routes/<table>.js Express route handlers
93
+ routes/<child>_child_of_<parent>.js Child route (scoped by FK)
94
+ routes/index.js Route mounting index
95
+ test/<table>.test.js CRUD endpoint tests
96
+ openapi.json OpenAPI 3.0 spec
97
+ llms.txt LLM quick reference
98
+ docs/llm.md Full LLM reference
99
+
100
+ Examples:
101
+ db-model-router generate --from dbmr.schema.json
102
+ db-model-router generate --models --dry-run
103
+ db-model-router generate --routes --tests
104
+ db-model-router generate --from dbmr.schema.json --json`,
105
+
106
+ doctor: `Usage: db-model-router doctor [options]
107
+
108
+ Validate schema, check adapter driver dependencies, and verify generated
109
+ files are in sync with the schema.
110
+
111
+ Options:
112
+ --from <path> Path to schema file (default: dbmr.schema.json)
113
+ --yes Accept all defaults without prompting
114
+ --json Output machine-readable JSON
115
+ --help Show this help message
116
+
117
+ Checks performed:
118
+ 1. Schema validation Syntax and structure of dbmr.schema.json
119
+ 2. Dependency check Adapter driver present in package.json
120
+ 3. Sync check Generated files match what the schema would produce
121
+
122
+ Examples:
123
+ db-model-router doctor --from dbmr.schema.json
124
+ db-model-router doctor --json`,
125
+
126
+ diff: `Usage: db-model-router diff [options]
127
+
128
+ Preview changes between the current generated files and what the schema
129
+ would produce. Read-only — does not modify any files on disk.
130
+
131
+ Options:
132
+ --from <path> Path to schema file (default: dbmr.schema.json)
133
+ --yes Accept all defaults without prompting
134
+ --json Output machine-readable JSON
135
+ --help Show this help message
136
+
137
+ Output shows:
138
+ + Added New files that would be created
139
+ ~ Modified Files with changes (includes line diffs)
140
+ - Deleted Extra files that would be removed
141
+
142
+ Examples:
143
+ db-model-router diff --from dbmr.schema.json
144
+ db-model-router diff --json`,
145
+ };
146
+
147
+ /**
148
+ * Help command handler.
149
+ *
150
+ * When called with a command name in args (e.g. `help init`), prints
151
+ * detailed help for that command. Otherwise prints the general overview.
152
+ *
153
+ * @param {object} args - Parsed key-value args
154
+ * @param {object} flags - Universal flags
155
+ * @param {import('../flags').OutputContext} ctx - Output context
156
+ * @param {object} options - Injected dependencies
157
+ * @param {Function} options.printHelp - General help printer from main.js
158
+ */
159
+ async function help(args, flags, ctx, options) {
160
+ // The command to get help for is the first positional arg captured
161
+ // by parseFlags as a key-value. We also check args._command which
162
+ // main.js will inject.
163
+ const topic = args._command;
164
+
165
+ if (topic && COMMAND_HELP[topic]) {
166
+ ctx.log(COMMAND_HELP[topic]);
167
+ } else if (topic) {
168
+ ctx.log(`Unknown command: ${topic}\n`);
169
+ ctx.log(`Available commands: ${Object.keys(COMMAND_HELP).join(", ")}\n`);
170
+ ctx.log(`Run "db-model-router help <command>" for detailed help.`);
171
+ } else {
172
+ // No topic — print general help
173
+ if (options && options.printHelp) {
174
+ options.printHelp();
175
+ }
176
+ }
177
+ }
178
+
179
+ module.exports = help;
180
+ module.exports.COMMAND_HELP = COMMAND_HELP;
@@ -19,9 +19,10 @@ const DEFAULT_ANSWERS = {
19
19
  framework: "express",
20
20
  database: "postgres",
21
21
  session: "memory",
22
- rateLimiting: false,
23
- helmet: false,
24
- logger: false,
22
+ rateLimiting: true,
23
+ helmet: true,
24
+ logger: true,
25
+ loki: false,
25
26
  };
26
27
 
27
28
  /**
@@ -52,6 +53,7 @@ async function init(args, flags, ctx) {
52
53
  rateLimiting: !!(schema.options && schema.options.rateLimiting),
53
54
  helmet: !!(schema.options && schema.options.helmet),
54
55
  logger: !!(schema.options && schema.options.logger),
56
+ loki: !!(schema.options && schema.options.loki),
55
57
  };
56
58
  } else if (flags.yes) {
57
59
  // --yes with no schema: use defaults, but allow CLI overrides
@@ -66,9 +68,13 @@ async function init(args, flags, ctx) {
66
68
  answers = await promptUser(prefilled);
67
69
  }
68
70
 
71
+ // Resolve --output directory (relative to cwd)
72
+ // CLI --output flag takes precedence, then interactive prompt answer
73
+ const outputDir = args.output || answers.output || "";
74
+
69
75
  // --dry-run: report planned files without writing
70
76
  if (flags.dryRun) {
71
- const planned = planFiles(answers);
77
+ const planned = planFiles(answers, outputDir);
72
78
  if (flags.json) {
73
79
  ctx.result({
74
80
  files: planned,
@@ -89,10 +95,10 @@ async function init(args, flags, ctx) {
89
95
  ensurePackageJson();
90
96
 
91
97
  // Generate project files
92
- const generated = generateFiles(answers);
98
+ const generated = generateFiles(answers, outputDir);
93
99
 
94
100
  // Update package.json with deps and scripts
95
- updatePackageJson(answers);
101
+ updatePackageJson(answers, outputDir);
96
102
 
97
103
  // npm install (unless --no-install)
98
104
  const installed = !flags.noInstall;
@@ -103,7 +109,10 @@ async function init(args, flags, ctx) {
103
109
  // Output
104
110
  const allFiles = [
105
111
  ...generated.files,
106
- ...generated.migrationFiles.map((m) => `migrations/${m}`),
112
+ ...generated.migrationFiles.map((m) => {
113
+ const base = outputDir || ".";
114
+ return base === "." ? `migrations/${m}` : `${base}/migrations/${m}`;
115
+ }),
107
116
  ];
108
117
 
109
118
  if (flags.json) {
@@ -127,24 +136,43 @@ async function init(args, flags, ctx) {
127
136
  * This mirrors the file list from generateFiles() without writing anything.
128
137
  *
129
138
  * @param {object} answers
139
+ * @param {string} [outputDir] - relative output directory for source files
130
140
  * @returns {string[]}
131
141
  */
132
- function planFiles(answers) {
142
+ function planFiles(answers, outputDir) {
133
143
  const { isSql } = require("../init/generators");
144
+ const srcBase = outputDir || ".";
145
+ const prefix = srcBase === "." ? "" : srcBase + "/";
146
+
134
147
  const files = [
135
148
  "app.js",
136
149
  ".env",
137
150
  ".env.example",
138
- "middleware/logger.js",
139
- "migrate.js",
140
- "add_migration.js",
141
151
  ".gitignore",
142
- "migrations/<timestamp>_create_migrations_table" +
143
- (isSql(answers.database) ? ".sql" : ".js"),
152
+ "Dockerfile",
153
+ ".dockerignore",
144
154
  ];
145
155
 
156
+ // docker-compose.yml for databases that need Docker
157
+ if (answers.database !== "sqlite3") {
158
+ files.push("docker-compose.yml");
159
+ }
160
+
161
+ files.push(
162
+ `${prefix}middleware/logger.js`,
163
+ `${prefix}commons/session.js`,
164
+ `${prefix}commons/migrate.js`,
165
+ `${prefix}commons/add_migration.js`,
166
+ `${prefix}commons/security.js`,
167
+ `${prefix}commons/db.js`,
168
+ `${prefix}route/health.js`,
169
+ `${prefix}route/index.js`,
170
+ `${prefix}migrations/<timestamp>_create_migrations_table` +
171
+ (isSql(answers.database) ? ".sql" : ".js"),
172
+ );
173
+
146
174
  if (answers.session === "database" && isSql(answers.database)) {
147
- files.push("migrations/<timestamp>_create_sessions_table.sql");
175
+ files.push(`${prefix}migrations/<timestamp>_create_sessions_table.sql`);
148
176
  }
149
177
 
150
178
  return files;
@@ -40,15 +40,32 @@ function modelMetaToSchema(adapter, framework, models) {
40
40
  for (const m of models) {
41
41
  const columns = {};
42
42
 
43
- // Re-add columns from structure
43
+ const pk = m.primary_key || "id";
44
+ const opt = m.option || {};
45
+
46
+ // Add PK column as auto_increment
47
+ columns[pk] = "auto_increment";
48
+
49
+ // Add timestamp columns as datetime
50
+ if (opt.created_at) {
51
+ columns[opt.created_at] = "datetime";
52
+ }
53
+ if (opt.modified_at) {
54
+ columns[opt.modified_at] = "datetime";
55
+ }
56
+
57
+ // Add softDelete column
58
+ if (opt.safeDelete) {
59
+ columns[opt.safeDelete] = "boolean";
60
+ }
61
+
62
+ // Add remaining columns from structure
44
63
  for (const [col, rule] of Object.entries(m.structure)) {
45
64
  columns[col] = rule;
46
65
  }
47
66
 
48
- const pk = m.primary_key || "id";
49
67
  const unique = m.unique && m.unique.length > 0 ? [...m.unique] : [pk];
50
68
 
51
- const opt = m.option || {};
52
69
  const softDelete = opt.safeDelete || null;
53
70
  const timestamps = {
54
71
  created_at: opt.created_at || null,
@@ -475,7 +475,7 @@ function mysqlTypeToValidator(t) {
475
475
  if (/json/.test(t)) return "object";
476
476
  if (/text|char|varchar|enum|set/.test(t)) return "string";
477
477
  if (/blob|binary/.test(t)) return "string";
478
- if (/date|time|year/.test(t)) return "string";
478
+ if (/date|time|year/.test(t)) return "datetime";
479
479
  if (/bool/.test(t)) return "integer";
480
480
  return "string";
481
481
  }
@@ -487,7 +487,7 @@ function pgTypeToValidator(t) {
487
487
  if (/json/.test(t)) return "object";
488
488
  if (/bool/.test(t)) return "integer";
489
489
  if (/char|text|varchar|uuid/.test(t)) return "string";
490
- if (/date|time|interval/.test(t)) return "string";
490
+ if (/date|time|interval/.test(t)) return "datetime";
491
491
  return "string";
492
492
  }
493
493
 
@@ -496,6 +496,7 @@ function sqliteTypeToValidator(t) {
496
496
  if (/int/.test(t)) return "integer";
497
497
  if (/real|float|double|numeric|decimal/.test(t)) return "numeric";
498
498
  if (/json/.test(t)) return "object";
499
+ if (/date|time/.test(t)) return "datetime";
499
500
  if (/blob/.test(t)) return "string";
500
501
  return "string";
501
502
  }
@@ -506,7 +507,7 @@ function mssqlTypeToValidator(t) {
506
507
  if (/decimal|numeric|float|real|money/.test(t)) return "numeric";
507
508
  if (/bit/.test(t)) return "integer";
508
509
  if (/char|text|varchar|nchar|nvarchar|ntext/.test(t)) return "string";
509
- if (/date|time|datetime/.test(t)) return "string";
510
+ if (/date|time|datetime/.test(t)) return "datetime";
510
511
  if (/uniqueidentifier/.test(t)) return "string";
511
512
  return "string";
512
513
  }
@@ -515,7 +516,7 @@ function oracleTypeToValidator(t) {
515
516
  if (/NUMBER|INTEGER|FLOAT|BINARY_FLOAT|BINARY_DOUBLE/.test(t))
516
517
  return "numeric";
517
518
  if (/CLOB|BLOB|RAW|LONG/.test(t)) return "string";
518
- if (/DATE|TIMESTAMP/.test(t)) return "string";
519
+ if (/DATE|TIMESTAMP/.test(t)) return "datetime";
519
520
  if (/CHAR|VARCHAR|NCHAR|NVARCHAR/.test(t)) return "string";
520
521
  return "string";
521
522
  }
@@ -25,10 +25,12 @@ function safeVarName(name) {
25
25
  */
26
26
  function generateRouteFile(tableName, modelsRelPath) {
27
27
  const varName = safeVarName(tableName);
28
- return `const { route } = require("db-model-router");
29
- const ${varName} = require("${modelsRelPath}/${tableName}");
28
+ return `import dbModelRouter from "db-model-router";
29
+ import ${varName} from "${modelsRelPath}/${tableName}.js";
30
30
 
31
- module.exports = route(${varName});
31
+ const { route } = dbModelRouter;
32
+
33
+ export default route(${varName});
32
34
  `;
33
35
  }
34
36
 
@@ -43,11 +45,13 @@ function generateChildRouteFile(
43
45
  modelsRelPath,
44
46
  ) {
45
47
  const varName = safeVarName(childTable);
46
- return `const { route } = require("db-model-router");
47
- const ${varName} = require("${modelsRelPath}/${childTable}");
48
+ return `import dbModelRouter from "db-model-router";
49
+ import ${varName} from "${modelsRelPath}/${childTable}.js";
50
+
51
+ const { route } = dbModelRouter;
48
52
 
49
53
  // Child route: scoped by parent ${parentTable} via ${fkColumn}
50
- module.exports = route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
54
+ export default route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
51
55
  `;
52
56
  }
53
57
 
@@ -56,7 +60,7 @@ module.exports = route(${varName}, { ${fkColumn}: "params.${fkColumn}" });
56
60
  * Supports parent-child nesting: parent/:pk/child
57
61
  */
58
62
  function generateRoutesIndexFile(tableNames, relationships = []) {
59
- let imports = `let express;\ntry { express = require("ultimate-express"); } catch (_) { express = require("express"); }\nconst router = express.Router();\n\n`;
63
+ let imports = `import express from "express";\n\nconst router = express.Router();\n\n`;
60
64
 
61
65
  // Collect child tables that are nested under parents
62
66
  const nestedChildren = new Set();
@@ -66,12 +70,12 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
66
70
 
67
71
  for (const table of tableNames) {
68
72
  const varName = safeVarName(table);
69
- imports += `const ${varName}Route = require("./${table}");\n`;
73
+ imports += `import ${varName}Route from "./${table}.js";\n`;
70
74
  }
71
75
  // Import child routes with _child suffix for nested ones
72
76
  for (const rel of relationships) {
73
77
  const varName = safeVarName(rel.child);
74
- imports += `const ${varName}ChildRoute = require("./${rel.child}_child_of_${rel.parent}");\n`;
78
+ imports += `import ${varName}ChildRoute from "./${rel.child}_child_of_${rel.parent}.js";\n`;
75
79
  }
76
80
 
77
81
  imports += "\n";
@@ -85,9 +89,7 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
85
89
 
86
90
  // Mount nested child routes under parent
87
91
  for (const rel of relationships) {
88
- const parentVar = safeVarName(rel.parent);
89
92
  const childVar = safeVarName(rel.child);
90
- // Find parent PK from model file name convention — use fkColumn without _id suffix as parent pk param
91
93
  imports += `router.use("/${rel.parent}/:${rel.fkColumn}/${rel.child}", ${childVar}ChildRoute);\n`;
92
94
  }
93
95
 
@@ -97,7 +99,7 @@ function generateRoutesIndexFile(tableNames, relationships = []) {
97
99
  imports += `router.use("/${rel.child}", ${varName}Route);\n`;
98
100
  }
99
101
 
100
- imports += "\nmodule.exports = router;\n";
102
+ imports += "\nexport default router;\n";
101
103
  return imports;
102
104
  }
103
105
 
@@ -114,13 +116,15 @@ function generateSimpleRoutesIndexFile(tableNames) {
114
116
  */
115
117
  function generateTestFile(tableName, pk) {
116
118
  const varName = safeVarName(tableName);
117
- return `const assert = require("assert");
118
- const express = require("express");
119
- const request = require("supertest");
120
- const { route } = require("db-model-router");
119
+ return `import assert from "assert";
120
+ import express from "express";
121
+ import request from "supertest";
122
+ import dbModelRouter from "db-model-router";
123
+
124
+ const { route } = dbModelRouter;
121
125
 
122
126
  // Adjust the path to your model file as needed
123
- const ${varName} = require("../models/${tableName}");
127
+ import ${varName} from "../models/${tableName}.js";
124
128
 
125
129
  function createApp() {
126
130
  const app = express();
@@ -220,12 +224,14 @@ describe("${tableName} routes", function () {
220
224
  */
221
225
  function generateChildTestFile(childTable, parentTable, fkColumn, pk) {
222
226
  const childVar = safeVarName(childTable);
223
- return `const assert = require("assert");
224
- const express = require("express");
225
- const request = require("supertest");
226
- const { route } = require("db-model-router");
227
+ return `import assert from "assert";
228
+ import express from "express";
229
+ import request from "supertest";
230
+ import dbModelRouter from "db-model-router";
231
+
232
+ const { route } = dbModelRouter;
227
233
 
228
- const ${childVar} = require("../models/${childTable}");
234
+ import ${childVar} from "../models/${childTable}.js";
229
235
 
230
236
  function createApp() {
231
237
  const app = express();
@@ -5,6 +5,7 @@
5
5
  */
6
6
  const DRIVER_MAP = {
7
7
  mysql: ["mysql2"],
8
+ mariadb: ["mysql2"],
8
9
  postgres: ["pg"],
9
10
  sqlite3: ["better-sqlite3"],
10
11
  mongodb: ["mongodb"],
@@ -53,7 +54,10 @@ function collectDependencies(answers) {
53
54
  dependencies["helmet"] = "latest";
54
55
  }
55
56
  if (answers.logger) {
56
- dependencies["express-mung"] = "latest";
57
+ dependencies["winston"] = "latest";
58
+ if (answers.loki) {
59
+ dependencies["winston-loki"] = "latest";
60
+ }
57
61
  }
58
62
 
59
63
  // Dev dependencies
@@ -63,16 +67,21 @@ function collectDependencies(answers) {
63
67
  }
64
68
 
65
69
  /**
66
- * Returns the 5 package.json scripts.
70
+ * Returns the package.json scripts.
71
+ * @param {string} [outputDir] - relative output directory for source files
67
72
  * @returns {Record<string, string>}
68
73
  */
69
- function getScripts() {
74
+ function getScripts(outputDir) {
75
+ const prefix = outputDir ? `${outputDir}/` : "";
70
76
  return {
71
77
  start: "node app.js",
72
78
  dev: "nodemon app.js",
73
79
  test: 'echo "Error: no test specified" && exit 1',
74
- migrate: "node migrate.js",
75
- add_migration: "node add_migration.js",
80
+ migrate: `node ${prefix}commons/migrate.js`,
81
+ add_migration: `node ${prefix}commons/add_migration.js`,
82
+ "docker:build": "docker build -t app .",
83
+ "docker:up": "docker compose up -d",
84
+ "docker:down": "docker compose down",
76
85
  };
77
86
  }
78
87