db-model-router 1.0.2 → 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,240 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { parseSchema } = require("../../schema/schema-parser");
6
+ const { schemaToModelMeta } = require("../../schema/schema-to-meta");
7
+ const { generateModelFile } = require("../generate-model");
8
+ const {
9
+ generateRouteFile,
10
+ generateChildRouteFile,
11
+ generateRoutesIndexFile,
12
+ generateTestFile,
13
+ generateChildTestFile,
14
+ } = require("../generate-route");
15
+ const { generateOpenAPISpec } = require("../generate-openapi");
16
+ const { generateLlmsTxt, generateLlmMd } = require("./generate-llm-docs");
17
+
18
+ /**
19
+ * Generate command handler for the unified CLI.
20
+ *
21
+ * Reads a schema file, converts to ModelMeta[], and generates
22
+ * models, routes, tests, and OpenAPI spec files.
23
+ *
24
+ * Supported flags:
25
+ * --from Path to schema file (default: dbmr.schema.json)
26
+ * --models Generate only model files
27
+ * --routes Generate only route files (including child routes and index)
28
+ * --openapi Generate only OpenAPI spec
29
+ * --tests Generate only test files
30
+ * --dry-run Report planned files without writing
31
+ * --json Output JSON result via ctx
32
+ *
33
+ * When no artifact flags are provided, all artifact types are generated.
34
+ *
35
+ * @param {object} args - Parsed key-value args
36
+ * @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
37
+ * @param {import('../flags').OutputContext} ctx - Output context
38
+ */
39
+ async function generate(args, flags, ctx) {
40
+ const schemaPath = path.resolve(args.from || "dbmr.schema.json");
41
+
42
+ if (!fs.existsSync(schemaPath)) {
43
+ const msg = `Schema file not found: ${args.from || "dbmr.schema.json"}`;
44
+ if (flags.json) {
45
+ ctx.result({ error: true, code: "SCHEMA_NOT_FOUND", message: msg });
46
+ } else {
47
+ ctx.log(`Error: ${msg}`);
48
+ }
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ let schema;
54
+ try {
55
+ const raw = fs.readFileSync(schemaPath, "utf8");
56
+ schema = parseSchema(raw);
57
+ } catch (err) {
58
+ const msg = `Schema parse error: ${err.message}`;
59
+ if (flags.json) {
60
+ ctx.result({
61
+ error: true,
62
+ code: "SCHEMA_VALIDATION",
63
+ message: msg,
64
+ errors: err.errors || [],
65
+ });
66
+ } else {
67
+ ctx.log(`Error: ${msg}`);
68
+ }
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ const meta = schemaToModelMeta(schema);
74
+ const relationships = schema.relationships || [];
75
+ const tableNames = meta.map((m) => m.table).sort();
76
+
77
+ // Determine which artifact types to generate
78
+ const hasArtifactFlag =
79
+ args.models === true ||
80
+ args.routes === true ||
81
+ args.openapi === true ||
82
+ args.tests === true ||
83
+ args["llm-docs"] === true;
84
+
85
+ const genModels = !hasArtifactFlag || args.models === true;
86
+ const genRoutes = !hasArtifactFlag || args.routes === true;
87
+ const genOpenapi = !hasArtifactFlag || args.openapi === true;
88
+ const genTests = !hasArtifactFlag || args.tests === true;
89
+ const genLlmDocs = !hasArtifactFlag || args["llm-docs"] === true;
90
+
91
+ const modelsRelPath = "../models";
92
+ const baseDir = process.cwd();
93
+
94
+ // Collect all planned files: { relPath, content }
95
+ const planned = [];
96
+
97
+ // --- Model files ---
98
+ if (genModels) {
99
+ for (const m of meta) {
100
+ planned.push({
101
+ relPath: `models/${m.table}.js`,
102
+ content: generateModelFile(m),
103
+ });
104
+ }
105
+ }
106
+
107
+ // --- Route files ---
108
+ if (genRoutes) {
109
+ // One route per table
110
+ for (const m of meta) {
111
+ planned.push({
112
+ relPath: `routes/${m.table}.js`,
113
+ content: generateRouteFile(m.table, modelsRelPath),
114
+ });
115
+ }
116
+
117
+ // Child route files (one per relationship)
118
+ for (const rel of relationships) {
119
+ planned.push({
120
+ relPath: `routes/${rel.child}_child_of_${rel.parent}.js`,
121
+ content: generateChildRouteFile(
122
+ rel.child,
123
+ rel.parent,
124
+ rel.foreignKey,
125
+ modelsRelPath,
126
+ ),
127
+ });
128
+ }
129
+
130
+ // Routes index file
131
+ planned.push({
132
+ relPath: "routes/index.js",
133
+ content: generateRoutesIndexFile(tableNames, relationships),
134
+ });
135
+ }
136
+
137
+ // --- OpenAPI spec ---
138
+ if (genOpenapi) {
139
+ planned.push({
140
+ relPath: "openapi.json",
141
+ content: JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
142
+ });
143
+ }
144
+
145
+ // --- Test files ---
146
+ if (genTests) {
147
+ for (const m of meta) {
148
+ planned.push({
149
+ relPath: `test/${m.table}.test.js`,
150
+ content: generateTestFile(m.table, m.primary_key),
151
+ });
152
+ }
153
+
154
+ // Child test files (one per relationship)
155
+ for (const rel of relationships) {
156
+ const childMeta = meta.find((m) => m.table === rel.child);
157
+ const pk = childMeta ? childMeta.primary_key : "id";
158
+ planned.push({
159
+ relPath: `test/${rel.child}_child_of_${rel.parent}.test.js`,
160
+ content: generateChildTestFile(
161
+ rel.child,
162
+ rel.parent,
163
+ rel.foreignKey,
164
+ pk,
165
+ ),
166
+ });
167
+ }
168
+ }
169
+
170
+ // --- LLM docs ---
171
+ if (genLlmDocs) {
172
+ planned.push({
173
+ relPath: "llms.txt",
174
+ content: generateLlmsTxt(),
175
+ });
176
+ planned.push({
177
+ relPath: "docs/llm.md",
178
+ content: generateLlmMd(),
179
+ });
180
+ }
181
+
182
+ // --- Process planned files ---
183
+ const results = [];
184
+
185
+ for (const { relPath, content } of planned) {
186
+ const fullPath = path.join(baseDir, relPath);
187
+
188
+ if (flags.dryRun) {
189
+ results.push({ path: relPath, status: "planned" });
190
+ continue;
191
+ }
192
+
193
+ // Check if file exists and content matches (skip-unchanged)
194
+ if (fs.existsSync(fullPath)) {
195
+ const existing = fs.readFileSync(fullPath, "utf8");
196
+ if (existing === content) {
197
+ results.push({ path: relPath, status: "unchanged" });
198
+ ctx.log(` unchanged ${relPath}`);
199
+ continue;
200
+ }
201
+ // File exists but content differs — overwrite
202
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
203
+ fs.writeFileSync(fullPath, content, "utf8");
204
+ results.push({ path: relPath, status: "overwritten" });
205
+ ctx.log(` overwritten ${relPath}`);
206
+ } else {
207
+ // File does not exist — create
208
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
209
+ fs.writeFileSync(fullPath, content, "utf8");
210
+ results.push({ path: relPath, status: "created" });
211
+ ctx.log(` created ${relPath}`);
212
+ }
213
+ }
214
+
215
+ // --- Output ---
216
+ if (flags.dryRun) {
217
+ if (flags.json) {
218
+ ctx.result({ files: results });
219
+ } else {
220
+ ctx.log("Dry run — the following files would be generated:");
221
+ for (const r of results) {
222
+ ctx.log(` ${r.path}`);
223
+ }
224
+ ctx.log(`\n${results.length} file(s) planned.`);
225
+ }
226
+ } else if (flags.json) {
227
+ ctx.result({ files: results });
228
+ } else {
229
+ const created = results.filter((r) => r.status === "created").length;
230
+ const overwritten = results.filter(
231
+ (r) => r.status === "overwritten",
232
+ ).length;
233
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
234
+ ctx.log(
235
+ `\nDone. ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
236
+ );
237
+ }
238
+ }
239
+
240
+ module.exports = generate;
@@ -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;
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { parseSchema } = require("../../schema/schema-parser");
6
+ const {
7
+ generateFiles,
8
+ updatePackageJson,
9
+ runInstall,
10
+ printSummary,
11
+ ensurePackageJson,
12
+ } = require("../init");
13
+ const { promptUser } = require("../init/prompt");
14
+
15
+ /**
16
+ * Default answers used when --yes is provided and no schema is available.
17
+ */
18
+ const DEFAULT_ANSWERS = {
19
+ framework: "express",
20
+ database: "postgres",
21
+ session: "memory",
22
+ rateLimiting: true,
23
+ helmet: true,
24
+ logger: true,
25
+ loki: false,
26
+ };
27
+
28
+ /**
29
+ * Init command handler for the unified CLI.
30
+ *
31
+ * Scaffolds a new project from a schema file or interactively.
32
+ *
33
+ * @param {object} args - Parsed positional/key-value args (e.g. { from, framework, database })
34
+ * @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
35
+ * @param {import('../flags').OutputContext} ctx - Output context for --json support
36
+ */
37
+ async function init(args, flags, ctx) {
38
+ let answers;
39
+
40
+ if (args.from) {
41
+ // --from points to a schema file: read adapter/framework from it
42
+ const schemaPath = path.resolve(args.from);
43
+ if (!fs.existsSync(schemaPath)) {
44
+ throw new Error(`Schema file not found: ${args.from}`);
45
+ }
46
+ const raw = fs.readFileSync(schemaPath, "utf8");
47
+ const schema = parseSchema(raw);
48
+
49
+ answers = {
50
+ framework: schema.framework,
51
+ database: schema.adapter,
52
+ session: (schema.options && schema.options.session) || "memory",
53
+ rateLimiting: !!(schema.options && schema.options.rateLimiting),
54
+ helmet: !!(schema.options && schema.options.helmet),
55
+ logger: !!(schema.options && schema.options.logger),
56
+ loki: !!(schema.options && schema.options.loki),
57
+ };
58
+ } else if (flags.yes) {
59
+ // --yes with no schema: use defaults, but allow CLI overrides
60
+ answers = Object.assign({}, DEFAULT_ANSWERS);
61
+ if (args.framework) answers.framework = args.framework;
62
+ if (args.database) answers.database = args.database;
63
+ } else {
64
+ // Interactive: build prefilled from CLI args, prompt for the rest
65
+ const prefilled = {};
66
+ if (args.framework) prefilled.framework = args.framework;
67
+ if (args.database) prefilled.database = args.database;
68
+ answers = await promptUser(prefilled);
69
+ }
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
+
75
+ // --dry-run: report planned files without writing
76
+ if (flags.dryRun) {
77
+ const planned = planFiles(answers, outputDir);
78
+ if (flags.json) {
79
+ ctx.result({
80
+ files: planned,
81
+ dependencies: { installed: false },
82
+ actions: ["dry-run"],
83
+ });
84
+ } else {
85
+ ctx.log("Dry run — the following files would be created:");
86
+ for (const f of planned) {
87
+ ctx.log(` ${f}`);
88
+ }
89
+ ctx.log("\nNo files were written.");
90
+ }
91
+ return;
92
+ }
93
+
94
+ // Ensure package.json exists
95
+ ensurePackageJson();
96
+
97
+ // Generate project files
98
+ const generated = generateFiles(answers, outputDir);
99
+
100
+ // Update package.json with deps and scripts
101
+ updatePackageJson(answers, outputDir);
102
+
103
+ // npm install (unless --no-install)
104
+ const installed = !flags.noInstall;
105
+ if (installed) {
106
+ runInstall();
107
+ }
108
+
109
+ // Output
110
+ const allFiles = [
111
+ ...generated.files,
112
+ ...generated.migrationFiles.map((m) => {
113
+ const base = outputDir || ".";
114
+ return base === "." ? `migrations/${m}` : `${base}/migrations/${m}`;
115
+ }),
116
+ ];
117
+
118
+ if (flags.json) {
119
+ ctx.result({
120
+ files: allFiles,
121
+ dependencies: { installed },
122
+ actions: installed ? ["scaffolded", "installed"] : ["scaffolded"],
123
+ });
124
+ } else {
125
+ printSummary(generated);
126
+ if (!installed) {
127
+ ctx.log(
128
+ "\nSkipped npm install (--no-install). Run `npm install` manually.",
129
+ );
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Compute the list of files that would be created (for --dry-run).
136
+ * This mirrors the file list from generateFiles() without writing anything.
137
+ *
138
+ * @param {object} answers
139
+ * @param {string} [outputDir] - relative output directory for source files
140
+ * @returns {string[]}
141
+ */
142
+ function planFiles(answers, outputDir) {
143
+ const { isSql } = require("../init/generators");
144
+ const srcBase = outputDir || ".";
145
+ const prefix = srcBase === "." ? "" : srcBase + "/";
146
+
147
+ const files = [
148
+ "app.js",
149
+ ".env",
150
+ ".env.example",
151
+ ".gitignore",
152
+ "Dockerfile",
153
+ ".dockerignore",
154
+ ];
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
+
174
+ if (answers.session === "database" && isSql(answers.database)) {
175
+ files.push(`${prefix}migrations/<timestamp>_create_sessions_table.sql`);
176
+ }
177
+
178
+ return files;
179
+ }
180
+
181
+ module.exports = init;