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.
@@ -5,6 +5,7 @@ const inquirer = require("inquirer");
5
5
  const VALID_FRAMEWORKS = ["ultimate-express", "express"];
6
6
  const VALID_DATABASES = [
7
7
  "mysql",
8
+ "mariadb",
8
9
  "postgres",
9
10
  "sqlite3",
10
11
  "mongodb",
@@ -56,12 +57,17 @@ function parseInitArgs(argv) {
56
57
  }
57
58
 
58
59
  // Boolean flags
59
- for (const flag of ["rateLimiting", "helmet", "logger"]) {
60
+ for (const flag of ["rateLimiting", "helmet", "logger", "loki"]) {
60
61
  if (args[flag] !== undefined) {
61
62
  partial[flag] = parseBool(args[flag]);
62
63
  }
63
64
  }
64
65
 
66
+ // Output directory
67
+ if (args.output !== undefined && args.output !== "true") {
68
+ partial.output = args.output;
69
+ }
70
+
65
71
  return partial;
66
72
  }
67
73
 
@@ -114,12 +120,22 @@ async function promptUser(prefilledAnswers) {
114
120
  });
115
121
  }
116
122
 
123
+ if (prefilled.output === undefined) {
124
+ questions.push({
125
+ type: "input",
126
+ name: "output",
127
+ message:
128
+ "Output directory for backend source files (leave empty for root):",
129
+ default: "",
130
+ });
131
+ }
132
+
117
133
  if (prefilled.rateLimiting === undefined) {
118
134
  questions.push({
119
135
  type: "confirm",
120
136
  name: "rateLimiting",
121
137
  message: "Enable rate limiting?",
122
- default: false,
138
+ default: true,
123
139
  });
124
140
  }
125
141
 
@@ -128,7 +144,7 @@ async function promptUser(prefilledAnswers) {
128
144
  type: "confirm",
129
145
  name: "helmet",
130
146
  message: "Enable Helmet security headers?",
131
- default: false,
147
+ default: true,
132
148
  });
133
149
  }
134
150
 
@@ -136,8 +152,8 @@ async function promptUser(prefilledAnswers) {
136
152
  questions.push({
137
153
  type: "confirm",
138
154
  name: "logger",
139
- message: "Enable request/response logger?",
140
- default: false,
155
+ message: "Enable request/response logger (Winston)?",
156
+ default: true,
141
157
  });
142
158
  }
143
159
 
@@ -147,6 +163,22 @@ async function promptUser(prefilledAnswers) {
147
163
  }
148
164
 
149
165
  const prompted = await inquirer.prompt(questions);
166
+
167
+ // Follow-up: if logger is enabled, ask about Loki
168
+ if (prompted.logger && prefilled.loki === undefined) {
169
+ const lokiAnswer = await inquirer.prompt([
170
+ {
171
+ type: "confirm",
172
+ name: "loki",
173
+ message: "Send logs to Grafana Loki?",
174
+ default: false,
175
+ },
176
+ ]);
177
+ prompted.loki = lokiAnswer.loki;
178
+ } else if (!prompted.logger && prefilled.logger === undefined) {
179
+ prompted.loki = false;
180
+ }
181
+
150
182
  return Object.assign({}, prefilled, prompted);
151
183
  }
152
184
 
package/src/cli/init.js CHANGED
@@ -7,14 +7,26 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const {
9
9
  generateAppJs,
10
+ generateAppJsV2,
10
11
  generateEnvFile,
11
12
  generateEnvExample,
12
13
  generateLoggerMiddleware,
13
- generateMigrateScript,
14
- generateAddMigrationScript,
15
14
  generateInitialMigration,
16
15
  generateSessionMigration,
17
16
  generateGitignore,
17
+ generateDockerfile,
18
+ generateDockerignore,
19
+ generateGrafanaDatasources,
20
+ generateDockerCompose,
21
+ generateCloudBeaverDataSources,
22
+ generateSessionJs,
23
+ generateMigrateModule,
24
+ generateAddMigrationModule,
25
+ generateSecurityJs,
26
+ generateHealthRoute,
27
+ generateRouteIndexFile,
28
+ generateDbModule,
29
+ randomPassword,
18
30
  } = require("./init/generators");
19
31
 
20
32
  const { collectDependencies, getScripts } = require("./init/dependencies");
@@ -61,40 +73,139 @@ function safeWriteFile(filePath, content) {
61
73
  * Creates directories and writes files. Skips files that already exist.
62
74
  * Returns the list of generated filenames for the summary.
63
75
  * @param {import('./init/types').InitAnswers} answers
76
+ * @param {string} [outputDir] - relative directory for source files (e.g. "backend")
64
77
  * @returns {{ files: string[], migrationFiles: string[] }}
65
78
  */
66
- function generateFiles(answers) {
79
+ function generateFiles(answers, outputDir) {
67
80
  const files = [];
68
81
  const migrationFiles = [];
69
82
 
83
+ // Resolve source directory (outputDir-relative or cwd)
84
+ const srcBase = outputDir || ".";
85
+
86
+ // Generate random secrets (shared between .env and docker-compose)
87
+ const secrets = {
88
+ dbPass: randomPassword(),
89
+ redisPass: answers.session === "redis" ? randomPassword() : "",
90
+ sessionSecret: randomPassword(32),
91
+ };
92
+
70
93
  // Create directories
71
- if (!fs.existsSync("middleware")) {
72
- fs.mkdirSync("middleware", { recursive: true });
94
+ const dirs = [
95
+ path.join(srcBase, "middleware"),
96
+ path.join(srcBase, "migrations"),
97
+ path.join(srcBase, "commons"),
98
+ path.join(srcBase, "route"),
99
+ ];
100
+ // SQLite3 needs a data/ folder for the database file
101
+ if (answers.database === "sqlite3") {
102
+ dirs.push("data");
73
103
  }
74
- if (!fs.existsSync("migrations")) {
75
- fs.mkdirSync("migrations", { recursive: true });
104
+ for (const dir of dirs) {
105
+ if (!fs.existsSync(dir)) {
106
+ fs.mkdirSync(dir, { recursive: true });
107
+ }
76
108
  }
77
109
 
78
- // Write files (skip if they already exist)
79
- if (safeWriteFile("app.js", generateAppJs(answers))) files.push("app.js");
80
- if (safeWriteFile(".env", generateEnvFile(answers))) files.push(".env");
110
+ // Root-level files (always in cwd, not in outputDir)
111
+ // app.js uses the v2 generator that links commons/route modules
112
+ if (safeWriteFile("app.js", generateAppJsV2(answers, outputDir || "")))
113
+ files.push("app.js");
114
+ if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
115
+ files.push(".env");
81
116
  if (safeWriteFile(".env.example", generateEnvExample(answers)))
82
117
  files.push(".env.example");
83
-
84
- const loggerPath = path.join("middleware", "logger.js");
85
- if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
86
- files.push("middleware/logger.js");
87
-
88
- if (safeWriteFile("migrate.js", generateMigrateScript(answers)))
89
- files.push("migrate.js");
90
- if (safeWriteFile("add_migration.js", generateAddMigrationScript(answers)))
91
- files.push("add_migration.js");
92
118
  if (safeWriteFile(".gitignore", generateGitignore()))
93
119
  files.push(".gitignore");
94
120
 
95
- // Initial migration
121
+ // docker-compose.yml (if the database needs Docker)
122
+ const dockerCompose = generateDockerCompose(answers, secrets);
123
+ if (dockerCompose !== null) {
124
+ if (safeWriteFile("docker-compose.yml", dockerCompose))
125
+ files.push("docker-compose.yml");
126
+ }
127
+
128
+ // CloudBeaver data-sources.json (auto-connect config)
129
+ const cbDataSources = generateCloudBeaverDataSources(answers, secrets);
130
+ if (cbDataSources !== null) {
131
+ const cbDir = ".cloudbeaver";
132
+ if (!fs.existsSync(cbDir)) {
133
+ fs.mkdirSync(cbDir, { recursive: true });
134
+ }
135
+ const cbPath = path.join(cbDir, "data-sources.json");
136
+ if (safeWriteFile(cbPath, cbDataSources))
137
+ files.push(".cloudbeaver/data-sources.json");
138
+ }
139
+
140
+ // Grafana datasource provisioning (when loki is enabled)
141
+ if (answers.loki) {
142
+ const grafanaDir = ".grafana";
143
+ if (!fs.existsSync(grafanaDir)) {
144
+ fs.mkdirSync(grafanaDir, { recursive: true });
145
+ }
146
+ const grafanaPath = path.join(grafanaDir, "datasources.yml");
147
+ if (safeWriteFile(grafanaPath, generateGrafanaDatasources()))
148
+ files.push(".grafana/datasources.yml");
149
+ }
150
+
151
+ // Dockerfile and .dockerignore
152
+ if (safeWriteFile("Dockerfile", generateDockerfile(answers, outputDir)))
153
+ files.push("Dockerfile");
154
+ if (safeWriteFile(".dockerignore", generateDockerignore()))
155
+ files.push(".dockerignore");
156
+
157
+ // Source files inside outputDir (or cwd if no outputDir)
158
+ const loggerPath = path.join(srcBase, "middleware", "logger.js");
159
+ if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
160
+ files.push(path.join(srcBase, "middleware/logger.js"));
161
+
162
+ // commons/session.js
163
+ const sessionPath = path.join(srcBase, "commons", "session.js");
164
+ if (safeWriteFile(sessionPath, generateSessionJs(answers)))
165
+ files.push(path.join(srcBase, "commons/session.js"));
166
+
167
+ // commons/migrate.js
168
+ const migratePath = path.join(srcBase, "commons", "migrate.js");
169
+ if (safeWriteFile(migratePath, generateMigrateModule(answers, outputDir)))
170
+ files.push(path.join(srcBase, "commons/migrate.js"));
171
+
172
+ // commons/add_migration.js
173
+ const addMigrationPath = path.join(srcBase, "commons", "add_migration.js");
174
+ if (
175
+ safeWriteFile(
176
+ addMigrationPath,
177
+ generateAddMigrationModule(answers, outputDir),
178
+ )
179
+ )
180
+ files.push(path.join(srcBase, "commons/add_migration.js"));
181
+
182
+ // commons/security.js
183
+ const securityPath = path.join(srcBase, "commons", "security.js");
184
+ if (safeWriteFile(securityPath, generateSecurityJs(answers)))
185
+ files.push(path.join(srcBase, "commons/security.js"));
186
+
187
+ // commons/db.js
188
+ const dbPath = path.join(srcBase, "commons", "db.js");
189
+ if (safeWriteFile(dbPath, generateDbModule(answers)))
190
+ files.push(path.join(srcBase, "commons/db.js"));
191
+
192
+ // route/health.js
193
+ const healthPath = path.join(srcBase, "route", "health.js");
194
+ if (safeWriteFile(healthPath, generateHealthRoute()))
195
+ files.push(path.join(srcBase, "route/health.js"));
196
+
197
+ // route/index.js
198
+ const routeIndexPath = path.join(srcBase, "route", "index.js");
199
+ if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
200
+ files.push(path.join(srcBase, "route/index.js"));
201
+
202
+ // Initial migration (inside outputDir/migrations)
96
203
  const initialMigration = generateInitialMigration(answers);
97
- const initialPath = path.join("migrations", initialMigration.filename);
204
+ const initialPath = path.join(
205
+ srcBase,
206
+ "migrations",
207
+ initialMigration.filename,
208
+ );
98
209
  if (safeWriteFile(initialPath, initialMigration.content)) {
99
210
  migrationFiles.push(initialMigration.filename);
100
211
  }
@@ -102,8 +213,12 @@ function generateFiles(answers) {
102
213
  // Conditional session migration
103
214
  const sessionMigration = generateSessionMigration(answers);
104
215
  if (sessionMigration !== null) {
105
- const sessionPath = path.join("migrations", sessionMigration.filename);
106
- if (safeWriteFile(sessionPath, sessionMigration.content)) {
216
+ const sessionMigPath = path.join(
217
+ srcBase,
218
+ "migrations",
219
+ sessionMigration.filename,
220
+ );
221
+ if (safeWriteFile(sessionMigPath, sessionMigration.content)) {
107
222
  migrationFiles.push(sessionMigration.filename);
108
223
  }
109
224
  }
@@ -114,8 +229,9 @@ function generateFiles(answers) {
114
229
  /**
115
230
  * Update package.json with scripts and dependencies from the answers.
116
231
  * @param {import('./init/types').InitAnswers} answers
232
+ * @param {string} [outputDir] - relative output directory for source files
117
233
  */
118
- function updatePackageJson(answers) {
234
+ function updatePackageJson(answers, outputDir) {
119
235
  let raw;
120
236
  try {
121
237
  raw = fs.readFileSync("package.json", "utf8");
@@ -135,8 +251,9 @@ function updatePackageJson(answers) {
135
251
  }
136
252
 
137
253
  const { dependencies, devDependencies } = collectDependencies(answers);
138
- const scripts = getScripts();
254
+ const scripts = getScripts(outputDir);
139
255
 
256
+ pkg.type = "module";
140
257
  pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
141
258
  pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
142
259
  pkg.devDependencies = Object.assign(
@@ -247,6 +364,9 @@ Options:
247
364
  cockroachdb, oracle, redis, dynamodb
248
365
  --db <name> Alias for --database
249
366
  --session <type> Session store: memory, redis, database
367
+ --output <dir> Directory for backend source files (e.g. --output backend).
368
+ package.json stays in root; index.js, commons/, route/,
369
+ middleware/, and migrations/ go inside the output folder.
250
370
  --rateLimiting Enable rate limiting (express-rate-limit)
251
371
  --helmet Enable Helmet security headers
252
372
  --logger Enable request/response logger (express-mung)
@@ -256,6 +376,9 @@ Examples:
256
376
  # Fully non-interactive (LLM-friendly)
257
377
  db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
258
378
 
379
+ # With output directory
380
+ db-model-router-init --framework express --database postgres --output backend --yes
381
+
259
382
  # Partial — only prompts for missing values
260
383
  db-model-router-init --database mysql --session memory
261
384
 
package/src/cli/main.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  const { parseFlags, OutputContext } = require("./flags");
@@ -8,6 +8,7 @@ const inspectCmd = require("./commands/inspect");
8
8
  const generateCmd = require("./commands/generate");
9
9
  const doctorCmd = require("./commands/doctor");
10
10
  const diffCmd = require("./commands/diff");
11
+ const helpCmd = require("./commands/help");
11
12
 
12
13
  /**
13
14
  * Map of subcommand names to their handler functions.
@@ -18,6 +19,7 @@ const COMMANDS = {
18
19
  generate: generateCmd,
19
20
  doctor: doctorCmd,
20
21
  diff: diffCmd,
22
+ help: helpCmd,
21
23
  };
22
24
 
23
25
  /**
@@ -29,23 +31,74 @@ const COMMAND_DESCRIPTIONS = {
29
31
  generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
30
32
  doctor: "Validate schema, check dependencies, and verify file sync",
31
33
  diff: "Preview changes between current files and what the schema would produce",
34
+ help: "Show help for a command",
32
35
  };
33
36
 
34
37
  /**
35
- * Print help message listing all available subcommands.
38
+ * Per-command flag summaries shown in the general help overview.
39
+ */
40
+ const COMMAND_FLAGS = {
41
+ init: [
42
+ ["--from <path>", "Read config from a schema file"],
43
+ ["--framework <name>", "express, ultimate-express"],
44
+ [
45
+ "--database <name>",
46
+ "mysql, mariadb, postgres, sqlite3, mongodb, mssql, cockroachdb, oracle, redis, dynamodb",
47
+ ],
48
+ ["--db <name>", "Alias for --database"],
49
+ ["--session <type>", "memory, redis, database"],
50
+ ["--output <dir>", "Directory for backend source files"],
51
+ ["--rateLimiting", "Enable rate limiting (default: yes)"],
52
+ ["--helmet", "Enable Helmet security headers (default: yes)"],
53
+ ["--logger", "Enable Winston + Loki logger (default: yes)"],
54
+ ],
55
+ inspect: [
56
+ ["--type <adapter>", "Database adapter (required)"],
57
+ ["--env <path>", "Path to .env file"],
58
+ ["--out <path>", "Output file (default: dbmr.schema.json)"],
59
+ ["--tables <list>", "Comma-separated table filter"],
60
+ ],
61
+ generate: [
62
+ ["--from <path>", "Schema file (default: dbmr.schema.json)"],
63
+ ["--models", "Generate only model files"],
64
+ ["--routes", "Generate only route files"],
65
+ ["--openapi", "Generate only OpenAPI spec"],
66
+ ["--tests", "Generate only test files"],
67
+ ["--llm-docs", "Generate only LLM documentation"],
68
+ ],
69
+ doctor: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
70
+ diff: [["--from <path>", "Schema file (default: dbmr.schema.json)"]],
71
+ };
72
+
73
+ /**
74
+ * Print help message listing all available subcommands with their flags.
36
75
  */
37
76
  function printHelp() {
38
77
  console.log("Usage: db-model-router <command> [options]\n");
39
- console.log("Commands:");
78
+
79
+ console.log("Commands:\n");
40
80
  for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
81
+ if (name === "help") {
82
+ console.log(` ${name.padEnd(12)} ${desc}`);
83
+ continue;
84
+ }
41
85
  console.log(` ${name.padEnd(12)} ${desc}`);
86
+ const flags = COMMAND_FLAGS[name];
87
+ if (flags) {
88
+ for (const [flag, info] of flags) {
89
+ console.log(` ${flag.padEnd(22)} ${info}`);
90
+ }
91
+ console.log("");
92
+ }
42
93
  }
43
- console.log("\nGlobal flags:");
44
- console.log(" --yes Accept all defaults without prompting");
45
- console.log(" --json Output machine-readable JSON");
46
- console.log(" --dry-run Preview actions without side effects");
47
- console.log(" --no-install Skip npm install step");
48
- console.log(" --help Show help for a command");
94
+
95
+ console.log("Global flags (all commands):");
96
+ console.log(" --yes Accept all defaults without prompting");
97
+ console.log(" --json Output machine-readable JSON");
98
+ console.log(" --dry-run Preview actions without side effects");
99
+ console.log(" --no-install Skip npm install step");
100
+ console.log(" --help Show help for a command");
101
+ console.log('\nRun "db-model-router help <command>" for detailed usage.');
49
102
  }
50
103
 
51
104
  /**
@@ -65,11 +118,38 @@ function printUnknown(cmd) {
65
118
  async function main(argv) {
66
119
  const { subcommand, flags, args } = parseFlags(argv);
67
120
 
68
- if (!subcommand || flags.help) {
121
+ if (!subcommand) {
69
122
  printHelp();
70
123
  return;
71
124
  }
72
125
 
126
+ // `help <command>` — extract the topic from the second positional arg
127
+ if (subcommand === "help") {
128
+ // Re-parse to grab the second positional word as the help topic.
129
+ // parseFlags puts the first positional into subcommand; the second
130
+ // positional ends up as a key in args (if it looks like a flag value)
131
+ // or is lost. So we grab it directly from argv.
132
+ const topic =
133
+ argv.find((a, i) => i > 0 && !a.startsWith("-") && argv[0] === "help") ||
134
+ argv.find(
135
+ (a, i) => i > 0 && !a.startsWith("-") && argv.indexOf("help") < i,
136
+ );
137
+ args._command = topic || null;
138
+ const ctx = new OutputContext(flags);
139
+ await helpCmd(args, flags, ctx, { printHelp });
140
+ ctx.flush();
141
+ return;
142
+ }
143
+
144
+ // `<command> --help` — show detailed help for that command
145
+ if (flags.help) {
146
+ args._command = subcommand;
147
+ const ctx = new OutputContext(flags);
148
+ await helpCmd(args, flags, ctx, { printHelp });
149
+ ctx.flush();
150
+ return;
151
+ }
152
+
73
153
  if (!COMMANDS[subcommand]) {
74
154
  printUnknown(subcommand);
75
155
  process.exitCode = 1;
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ const model = require("./commons/model.js");
2
2
  const route = require("./commons/route.js");
3
3
  const routers = {
4
4
  mysql: "./mysql/db.js",
5
+ mariadb: "./mysql/db.js",
5
6
  postgresql: "./postgres/db.js",
6
7
  postgres: "./postgres/db.js",
7
8
  oracle: "./oracle/db.js",
@@ -29,6 +30,7 @@ function init(DB_TYPE) {
29
30
  if (err.code === "MODULE_NOT_FOUND") {
30
31
  const driverMap = {
31
32
  mysql: "mysql2",
33
+ mariadb: "mysql2",
32
34
  postgresql: "pg",
33
35
  postgres: "pg",
34
36
  oracle: "oracledb",
@@ -30,13 +30,9 @@ function printSchema(schema) {
30
30
  const table = schema.tables[name];
31
31
  const tableDef = {
32
32
  columns: table.columns,
33
+ pk: table.pk || "id",
33
34
  };
34
35
 
35
- // Include pk if not the default "id"
36
- if (table.pk && table.pk !== "id") {
37
- tableDef.pk = table.pk;
38
- }
39
-
40
36
  // Preserve unique if not the default [pk]
41
37
  const defaultUnique = [table.pk || "id"];
42
38
  const hasCustomUnique =
@@ -46,6 +46,10 @@ function schemaToModelMeta(schema) {
46
46
  const structure = {};
47
47
  for (const [colName, rule] of Object.entries(tableDef.columns)) {
48
48
  if (!excludeSet.has(colName)) {
49
+ // Strip auto_increment and datetime columns from model structure
50
+ // (DB handles these automatically)
51
+ const baseType = rule.replace(/^required\|/, "");
52
+ if (baseType === "auto_increment") continue;
49
53
  structure[colName] = rule;
50
54
  }
51
55
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const VALID_ADAPTERS = new Set([
4
4
  "mysql",
5
+ "mariadb",
5
6
  "postgres",
6
7
  "sqlite3",
7
8
  "mongodb",
@@ -14,7 +15,8 @@ const VALID_ADAPTERS = new Set([
14
15
 
15
16
  const VALID_FRAMEWORKS = new Set(["express", "ultimate-express"]);
16
17
 
17
- const COLUMN_RULE_RE = /^(required\|)?(string|integer|numeric|boolean|object)$/;
18
+ const COLUMN_RULE_RE =
19
+ /^(required\|)?(string|integer|numeric|boolean|object|datetime|auto_increment)$/;
18
20
 
19
21
  class SchemaValidationError extends Error {
20
22
  constructor(errors) {