db-model-router 1.0.0 → 1.0.3

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,159 @@
1
+ "use strict";
2
+
3
+ const inquirer = require("inquirer");
4
+
5
+ const VALID_FRAMEWORKS = ["ultimate-express", "express"];
6
+ const VALID_DATABASES = [
7
+ "mysql",
8
+ "postgres",
9
+ "sqlite3",
10
+ "mongodb",
11
+ "mssql",
12
+ "cockroachdb",
13
+ "oracle",
14
+ "redis",
15
+ "dynamodb",
16
+ ];
17
+ const VALID_SESSIONS = ["memory", "redis", "database"];
18
+
19
+ /**
20
+ * Parse CLI arguments into a partial answers object.
21
+ * Supports: --framework, --database, --session, --rateLimiting, --helmet, --logger
22
+ * Boolean flags accept: true/false, yes/no, 1/0, or just --flag (implies true)
23
+ * @param {string[]} argv
24
+ * @returns {Partial<import('./types').InitAnswers>}
25
+ */
26
+ function parseInitArgs(argv) {
27
+ const args = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg.startsWith("--")) {
31
+ const key = arg.slice(2);
32
+ const next = argv[i + 1];
33
+ if (next && !next.startsWith("--")) {
34
+ args[key] = next;
35
+ i++;
36
+ } else {
37
+ args[key] = "true";
38
+ }
39
+ }
40
+ }
41
+
42
+ const partial = {};
43
+
44
+ if (args.framework && VALID_FRAMEWORKS.includes(args.framework)) {
45
+ partial.framework = args.framework;
46
+ }
47
+ if (args.database && VALID_DATABASES.includes(args.database)) {
48
+ partial.database = args.database;
49
+ }
50
+ // Accept --db as alias for --database
51
+ if (!partial.database && args.db && VALID_DATABASES.includes(args.db)) {
52
+ partial.database = args.db;
53
+ }
54
+ if (args.session && VALID_SESSIONS.includes(args.session)) {
55
+ partial.session = args.session;
56
+ }
57
+
58
+ // Boolean flags
59
+ for (const flag of ["rateLimiting", "helmet", "logger"]) {
60
+ if (args[flag] !== undefined) {
61
+ partial[flag] = parseBool(args[flag]);
62
+ }
63
+ }
64
+
65
+ return partial;
66
+ }
67
+
68
+ /**
69
+ * Parse a string as a boolean.
70
+ * @param {string} val
71
+ * @returns {boolean}
72
+ */
73
+ function parseBool(val) {
74
+ return ["true", "yes", "1"].includes(String(val).toLowerCase());
75
+ }
76
+
77
+ /**
78
+ * Run the interactive prompt flow to collect user preferences.
79
+ * Any values already present in `prefilledAnswers` are skipped (not prompted).
80
+ * When all 6 values are provided, no prompts are shown at all.
81
+ * @param {Partial<import('./types').InitAnswers>} [prefilledAnswers={}]
82
+ * @returns {Promise<import('./types').InitAnswers>}
83
+ */
84
+ async function promptUser(prefilledAnswers) {
85
+ const prefilled = prefilledAnswers || {};
86
+
87
+ const questions = [];
88
+
89
+ if (prefilled.framework === undefined) {
90
+ questions.push({
91
+ type: "list",
92
+ name: "framework",
93
+ message: "Select your Express framework:",
94
+ choices: VALID_FRAMEWORKS,
95
+ default: "ultimate-express",
96
+ });
97
+ }
98
+
99
+ if (prefilled.database === undefined) {
100
+ questions.push({
101
+ type: "list",
102
+ name: "database",
103
+ message: "Select your database:",
104
+ choices: VALID_DATABASES,
105
+ });
106
+ }
107
+
108
+ if (prefilled.session === undefined) {
109
+ questions.push({
110
+ type: "list",
111
+ name: "session",
112
+ message: "Select your session store:",
113
+ choices: VALID_SESSIONS,
114
+ });
115
+ }
116
+
117
+ if (prefilled.rateLimiting === undefined) {
118
+ questions.push({
119
+ type: "confirm",
120
+ name: "rateLimiting",
121
+ message: "Enable rate limiting?",
122
+ default: false,
123
+ });
124
+ }
125
+
126
+ if (prefilled.helmet === undefined) {
127
+ questions.push({
128
+ type: "confirm",
129
+ name: "helmet",
130
+ message: "Enable Helmet security headers?",
131
+ default: false,
132
+ });
133
+ }
134
+
135
+ if (prefilled.logger === undefined) {
136
+ questions.push({
137
+ type: "confirm",
138
+ name: "logger",
139
+ message: "Enable request/response logger?",
140
+ default: false,
141
+ });
142
+ }
143
+
144
+ // If all values are prefilled, skip prompts entirely
145
+ if (questions.length === 0) {
146
+ return /** @type {import('./types').InitAnswers} */ (prefilled);
147
+ }
148
+
149
+ const prompted = await inquirer.prompt(questions);
150
+ return Object.assign({}, prefilled, prompted);
151
+ }
152
+
153
+ module.exports = {
154
+ promptUser,
155
+ parseInitArgs,
156
+ VALID_FRAMEWORKS,
157
+ VALID_DATABASES,
158
+ VALID_SESSIONS,
159
+ };
@@ -0,0 +1,281 @@
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 {
9
+ generateAppJs,
10
+ generateEnvFile,
11
+ generateEnvExample,
12
+ generateLoggerMiddleware,
13
+ generateMigrateScript,
14
+ generateAddMigrationScript,
15
+ generateInitialMigration,
16
+ generateSessionMigration,
17
+ generateGitignore,
18
+ } = require("./init/generators");
19
+
20
+ const { collectDependencies, getScripts } = require("./init/dependencies");
21
+ const { promptUser, parseInitArgs } = require("./init/prompt");
22
+
23
+ /**
24
+ * Ensure a package.json exists in the current directory.
25
+ * Uses `npm init -y` for non-interactive creation. Exits with code 1 on failure.
26
+ */
27
+ function ensurePackageJson() {
28
+ if (fs.existsSync("package.json")) {
29
+ return;
30
+ }
31
+ try {
32
+ execSync("npm init -y", { stdio: "inherit" });
33
+ } catch (err) {
34
+ console.error("Error: npm init failed or was aborted.");
35
+ process.exit(1);
36
+ }
37
+ if (!fs.existsSync("package.json")) {
38
+ console.error("Error: package.json was not created. Aborting.");
39
+ process.exit(1);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Write a file, but warn (and skip) if it already exists.
45
+ * @param {string} filePath
46
+ * @param {string} content
47
+ * @returns {boolean} true if written, false if skipped
48
+ */
49
+ function safeWriteFile(filePath, content) {
50
+ if (fs.existsSync(filePath)) {
51
+ console.log(` Skipped ${filePath} (already exists)`);
52
+ return false;
53
+ }
54
+ fs.writeFileSync(filePath, content);
55
+ console.log(` Created ${filePath}`);
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Generate all project files based on user answers.
61
+ * Creates directories and writes files. Skips files that already exist.
62
+ * Returns the list of generated filenames for the summary.
63
+ * @param {import('./init/types').InitAnswers} answers
64
+ * @returns {{ files: string[], migrationFiles: string[] }}
65
+ */
66
+ function generateFiles(answers) {
67
+ const files = [];
68
+ const migrationFiles = [];
69
+
70
+ // Create directories
71
+ if (!fs.existsSync("middleware")) {
72
+ fs.mkdirSync("middleware", { recursive: true });
73
+ }
74
+ if (!fs.existsSync("migrations")) {
75
+ fs.mkdirSync("migrations", { recursive: true });
76
+ }
77
+
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");
81
+ if (safeWriteFile(".env.example", generateEnvExample(answers)))
82
+ 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
+ if (safeWriteFile(".gitignore", generateGitignore()))
93
+ files.push(".gitignore");
94
+
95
+ // Initial migration
96
+ const initialMigration = generateInitialMigration(answers);
97
+ const initialPath = path.join("migrations", initialMigration.filename);
98
+ if (safeWriteFile(initialPath, initialMigration.content)) {
99
+ migrationFiles.push(initialMigration.filename);
100
+ }
101
+
102
+ // Conditional session migration
103
+ const sessionMigration = generateSessionMigration(answers);
104
+ if (sessionMigration !== null) {
105
+ const sessionPath = path.join("migrations", sessionMigration.filename);
106
+ if (safeWriteFile(sessionPath, sessionMigration.content)) {
107
+ migrationFiles.push(sessionMigration.filename);
108
+ }
109
+ }
110
+
111
+ return { files, migrationFiles };
112
+ }
113
+
114
+ /**
115
+ * Update package.json with scripts and dependencies from the answers.
116
+ * @param {import('./init/types').InitAnswers} answers
117
+ */
118
+ function updatePackageJson(answers) {
119
+ let raw;
120
+ try {
121
+ raw = fs.readFileSync("package.json", "utf8");
122
+ } catch (err) {
123
+ console.error("Error: Could not read package.json.");
124
+ process.exit(1);
125
+ }
126
+
127
+ let pkg;
128
+ try {
129
+ pkg = JSON.parse(raw);
130
+ } catch (err) {
131
+ console.error(
132
+ "Error: package.json contains invalid JSON. Please fix it manually and re-run.",
133
+ );
134
+ process.exit(1);
135
+ }
136
+
137
+ const { dependencies, devDependencies } = collectDependencies(answers);
138
+ const scripts = getScripts();
139
+
140
+ pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
141
+ pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
142
+ pkg.devDependencies = Object.assign(
143
+ {},
144
+ pkg.devDependencies || {},
145
+ devDependencies,
146
+ );
147
+
148
+ fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
149
+ console.log(" Updated package.json");
150
+ }
151
+
152
+ /**
153
+ * Run npm install. On failure, print manual install instructions and exit with code 1.
154
+ */
155
+ function runInstall() {
156
+ console.log("\nInstalling dependencies...\n");
157
+ try {
158
+ execSync("npm install", { stdio: "inherit" });
159
+ } catch (err) {
160
+ console.error(
161
+ "\nError: npm install failed. Please run the following command manually:",
162
+ );
163
+ console.error(" npm install");
164
+ process.exit(1);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Print a summary of generated files and next-step instructions.
170
+ * Uses the file lists captured during generation (no re-calling generators).
171
+ * @param {{ files: string[], migrationFiles: string[] }} generated
172
+ */
173
+ function printSummary(generated) {
174
+ console.log("\n✔ Project scaffolded successfully!\n");
175
+ console.log("Generated files:");
176
+
177
+ for (const f of generated.files) {
178
+ console.log(` ${f}`);
179
+ }
180
+ if (generated.migrationFiles.length > 0) {
181
+ console.log(" migrations/");
182
+ for (const m of generated.migrationFiles) {
183
+ console.log(` └── ${m}`);
184
+ }
185
+ }
186
+
187
+ console.log("\nNext steps:");
188
+ console.log(" 1. Edit .env with your database credentials");
189
+ console.log(" 2. Run: npm run dev");
190
+ }
191
+
192
+ /**
193
+ * Main orchestrator.
194
+ */
195
+ async function main() {
196
+ const cliArgs = parseInitArgs(process.argv.slice(2));
197
+
198
+ // If --help flag, print usage and exit
199
+ if (process.argv.includes("--help")) {
200
+ printUsage();
201
+ process.exit(0);
202
+ }
203
+
204
+ ensurePackageJson();
205
+
206
+ let answers;
207
+ try {
208
+ answers = await promptUser(cliArgs);
209
+ } catch (err) {
210
+ // Handle Ctrl+C (inquirer throws on user cancel)
211
+ console.log("\nAborted.");
212
+ process.exit(1);
213
+ }
214
+
215
+ let generated;
216
+ try {
217
+ generated = generateFiles(answers);
218
+ } catch (err) {
219
+ if (err.code === "EACCES" || err.code === "EPERM") {
220
+ console.error(
221
+ `Error: Permission denied writing files. Check directory permissions.\n ${err.message}`,
222
+ );
223
+ } else {
224
+ console.error(`Error: Failed to generate files.\n ${err.message}`);
225
+ }
226
+ process.exit(1);
227
+ }
228
+
229
+ updatePackageJson(answers);
230
+ runInstall();
231
+ printSummary(generated);
232
+ }
233
+
234
+ /**
235
+ * Print CLI usage information.
236
+ */
237
+ function printUsage() {
238
+ console.log(`
239
+ Usage: db-model-router-init [options]
240
+
241
+ Scaffolds a complete Express-based REST API project. When all options are
242
+ provided, runs non-interactively (no prompts). Missing options are prompted.
243
+
244
+ Options:
245
+ --framework <name> Express framework: ultimate-express, express
246
+ --database <name> Database: mysql, postgres, sqlite3, mongodb, mssql,
247
+ cockroachdb, oracle, redis, dynamodb
248
+ --db <name> Alias for --database
249
+ --session <type> Session store: memory, redis, database
250
+ --rateLimiting Enable rate limiting (express-rate-limit)
251
+ --helmet Enable Helmet security headers
252
+ --logger Enable request/response logger (express-mung)
253
+ --help Show this help message
254
+
255
+ Examples:
256
+ # Fully non-interactive (LLM-friendly)
257
+ db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
258
+
259
+ # Partial — only prompts for missing values
260
+ db-model-router-init --database mysql --session memory
261
+
262
+ # Interactive (no flags)
263
+ db-model-router-init
264
+ `);
265
+ }
266
+
267
+ if (require.main === module) {
268
+ main().catch((err) => {
269
+ console.error("Error:", err.message);
270
+ process.exit(1);
271
+ });
272
+ }
273
+
274
+ module.exports = {
275
+ ensurePackageJson,
276
+ generateFiles,
277
+ updatePackageJson,
278
+ runInstall,
279
+ printSummary,
280
+ main,
281
+ };
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { parseFlags, OutputContext } = require("./flags");
5
+
6
+ const initCmd = require("./commands/init");
7
+ const inspectCmd = require("./commands/inspect");
8
+ const generateCmd = require("./commands/generate");
9
+ const doctorCmd = require("./commands/doctor");
10
+ const diffCmd = require("./commands/diff");
11
+
12
+ /**
13
+ * Map of subcommand names to their handler functions.
14
+ */
15
+ const COMMANDS = {
16
+ init: initCmd,
17
+ inspect: inspectCmd,
18
+ generate: generateCmd,
19
+ doctor: doctorCmd,
20
+ diff: diffCmd,
21
+ };
22
+
23
+ /**
24
+ * Descriptions for each subcommand, used in help output.
25
+ */
26
+ const COMMAND_DESCRIPTIONS = {
27
+ init: "Scaffold a new project from a schema file or interactively",
28
+ inspect: "Introspect a live database and produce a schema file",
29
+ generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
30
+ doctor: "Validate schema, check dependencies, and verify file sync",
31
+ diff: "Preview changes between current files and what the schema would produce",
32
+ };
33
+
34
+ /**
35
+ * Print help message listing all available subcommands.
36
+ */
37
+ function printHelp() {
38
+ console.log("Usage: db-model-router <command> [options]\n");
39
+ console.log("Commands:");
40
+ for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
41
+ console.log(` ${name.padEnd(12)} ${desc}`);
42
+ }
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");
49
+ }
50
+
51
+ /**
52
+ * Print error for unknown subcommand and list valid subcommands.
53
+ * @param {string} cmd - The unknown subcommand
54
+ */
55
+ function printUnknown(cmd) {
56
+ const valid = Object.keys(COMMANDS).join(", ");
57
+ console.error(`Unknown command: ${cmd}`);
58
+ console.error(`Valid commands: ${valid}`);
59
+ }
60
+
61
+ /**
62
+ * Main CLI entry point.
63
+ * @param {string[]} argv - process.argv.slice(2) style array
64
+ */
65
+ async function main(argv) {
66
+ const { subcommand, flags, args } = parseFlags(argv);
67
+
68
+ if (!subcommand || flags.help) {
69
+ printHelp();
70
+ return;
71
+ }
72
+
73
+ if (!COMMANDS[subcommand]) {
74
+ printUnknown(subcommand);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+
79
+ const ctx = new OutputContext(flags);
80
+ await COMMANDS[subcommand](args, flags, ctx);
81
+ ctx.flush();
82
+ }
83
+
84
+ // When run directly as a script
85
+ if (require.main === module) {
86
+ main(process.argv.slice(2)).catch((err) => {
87
+ console.error(err.message || err);
88
+ process.exitCode = 1;
89
+ });
90
+ }
91
+
92
+ module.exports = main;
93
+ module.exports.COMMANDS = COMMANDS;
94
+ module.exports.printHelp = printHelp;
95
+ module.exports.printUnknown = printUnknown;
@@ -280,6 +280,7 @@ module.exports = function model(
280
280
  data,
281
281
  getPayloadValidator("CREATE", modelStructure, primary_key, false),
282
282
  );
283
+ const originalData = { ...data };
283
284
  data = RemoveUnknownData(modelStructure, [data]);
284
285
  data = jsonStringify(data);
285
286
  updateResult = await db.upsert(table, data, unique);
@@ -288,12 +289,10 @@ module.exports = function model(
288
289
  [[primary_key, "=", updateResult.id]],
289
290
  ]);
290
291
  return getResult.count > 0 ? getResult["data"][0] : null;
291
- } else if (data.hasOwnProperty(primary_key)) {
292
- const result = await db.get(
293
- table,
294
- [[[primary_key, "=", data[primary_key]]]],
295
- option.safeDelete,
296
- );
292
+ } else if (originalData.hasOwnProperty(primary_key)) {
293
+ const result = await db.get(table, [
294
+ [[primary_key, "=", originalData[primary_key]]],
295
+ ]);
297
296
  if (result.count > 0) return result["data"][0];
298
297
  else return null;
299
298
  }
@@ -156,6 +156,14 @@ module.exports = function route(model, override = {}) {
156
156
  });
157
157
  })
158
158
  .post("/", (req, res) => {
159
+ if (!req.body || !Array.isArray(req.body.data)) {
160
+ return res
161
+ .status(400)
162
+ .send({
163
+ type: "danger",
164
+ message: "Request body must contain a 'data' array",
165
+ });
166
+ }
159
167
  let payload = payloadOverride(req.body.data, req, override);
160
168
  model
161
169
  .insert({ data: payload })
@@ -167,6 +175,14 @@ module.exports = function route(model, override = {}) {
167
175
  });
168
176
  })
169
177
  .put("/", (req, res) => {
178
+ if (!req.body || !Array.isArray(req.body.data)) {
179
+ return res
180
+ .status(400)
181
+ .send({
182
+ type: "danger",
183
+ message: "Request body must contain a 'data' array",
184
+ });
185
+ }
170
186
  let payload = payloadOverride(req.body.data, req, override);
171
187
  model
172
188
  .update({ data: payload })
@@ -178,6 +194,14 @@ module.exports = function route(model, override = {}) {
178
194
  });
179
195
  })
180
196
  .delete("/", (req, res) => {
197
+ if (!req.body || !Array.isArray(req.body.data)) {
198
+ return res
199
+ .status(400)
200
+ .send({
201
+ type: "danger",
202
+ message: "Request body must contain a 'data' array",
203
+ });
204
+ }
181
205
  let payload = payloadOverride(req.body.data, req, override);
182
206
  model
183
207
  .remove(payload)
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ const { validateSchema, SchemaValidationError } = require("./schema-validator");
4
+
5
+ /**
6
+ * Parse a schema from a JSON string or plain object.
7
+ *
8
+ * - If `input` is a string, JSON.parse it (wrapping parse errors).
9
+ * - Validate via validateSchema(); throw SchemaValidationError if invalid.
10
+ * - Normalize each table with defaults for pk, unique, timestamps, softDelete.
11
+ * - Return the internal { adapter, framework, tables, relationships, options } representation.
12
+ *
13
+ * @param {string|object} input — raw JSON string or parsed object
14
+ * @returns {{ adapter: string, framework: string, tables: object, relationships: Array, options: object }}
15
+ * @throws {SchemaValidationError}
16
+ */
17
+ function parseSchema(input) {
18
+ let raw = input;
19
+
20
+ // If string, attempt JSON.parse; wrap errors
21
+ if (typeof raw === "string") {
22
+ try {
23
+ raw = JSON.parse(raw);
24
+ } catch (err) {
25
+ throw new SchemaValidationError([
26
+ {
27
+ path: "",
28
+ message: `Invalid JSON: ${err.message}`,
29
+ },
30
+ ]);
31
+ }
32
+ }
33
+
34
+ // Validate
35
+ const result = validateSchema(raw);
36
+ if (!result.valid) {
37
+ throw new SchemaValidationError(result.errors);
38
+ }
39
+
40
+ // Normalize tables
41
+ const tables = {};
42
+ for (const [tableName, tableDef] of Object.entries(raw.tables)) {
43
+ const pk = tableDef.pk || "id";
44
+ const unique = tableDef.unique !== undefined ? [...tableDef.unique] : [pk];
45
+ const timestamps =
46
+ tableDef.timestamps !== undefined
47
+ ? { ...tableDef.timestamps }
48
+ : { created_at: null, modified_at: null };
49
+ // Ensure timestamps always has both keys
50
+ if (!("created_at" in timestamps)) {
51
+ timestamps.created_at = null;
52
+ }
53
+ if (!("modified_at" in timestamps)) {
54
+ timestamps.modified_at = null;
55
+ }
56
+ const softDelete =
57
+ tableDef.softDelete !== undefined ? tableDef.softDelete : null;
58
+
59
+ tables[tableName] = {
60
+ name: tableName,
61
+ columns: { ...tableDef.columns },
62
+ pk,
63
+ unique,
64
+ softDelete,
65
+ timestamps,
66
+ };
67
+ }
68
+
69
+ return {
70
+ adapter: raw.adapter,
71
+ framework: raw.framework,
72
+ tables,
73
+ relationships: raw.relationships ? [...raw.relationships] : [],
74
+ options: raw.options ? { ...raw.options } : {},
75
+ };
76
+ }
77
+
78
+ module.exports = { parseSchema };