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,175 @@
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
+ const helpCmd = require("./commands/help");
12
+
13
+ /**
14
+ * Map of subcommand names to their handler functions.
15
+ */
16
+ const COMMANDS = {
17
+ init: initCmd,
18
+ inspect: inspectCmd,
19
+ generate: generateCmd,
20
+ doctor: doctorCmd,
21
+ diff: diffCmd,
22
+ help: helpCmd,
23
+ };
24
+
25
+ /**
26
+ * Descriptions for each subcommand, used in help output.
27
+ */
28
+ const COMMAND_DESCRIPTIONS = {
29
+ init: "Scaffold a new project from a schema file or interactively",
30
+ inspect: "Introspect a live database and produce a schema file",
31
+ generate: "Generate models, routes, tests, and OpenAPI spec from a schema",
32
+ doctor: "Validate schema, check dependencies, and verify file sync",
33
+ diff: "Preview changes between current files and what the schema would produce",
34
+ help: "Show help for a command",
35
+ };
36
+
37
+ /**
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.
75
+ */
76
+ function printHelp() {
77
+ console.log("Usage: db-model-router <command> [options]\n");
78
+
79
+ console.log("Commands:\n");
80
+ for (const [name, desc] of Object.entries(COMMAND_DESCRIPTIONS)) {
81
+ if (name === "help") {
82
+ console.log(` ${name.padEnd(12)} ${desc}`);
83
+ continue;
84
+ }
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
+ }
93
+ }
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.');
102
+ }
103
+
104
+ /**
105
+ * Print error for unknown subcommand and list valid subcommands.
106
+ * @param {string} cmd - The unknown subcommand
107
+ */
108
+ function printUnknown(cmd) {
109
+ const valid = Object.keys(COMMANDS).join(", ");
110
+ console.error(`Unknown command: ${cmd}`);
111
+ console.error(`Valid commands: ${valid}`);
112
+ }
113
+
114
+ /**
115
+ * Main CLI entry point.
116
+ * @param {string[]} argv - process.argv.slice(2) style array
117
+ */
118
+ async function main(argv) {
119
+ const { subcommand, flags, args } = parseFlags(argv);
120
+
121
+ if (!subcommand) {
122
+ printHelp();
123
+ return;
124
+ }
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
+
153
+ if (!COMMANDS[subcommand]) {
154
+ printUnknown(subcommand);
155
+ process.exitCode = 1;
156
+ return;
157
+ }
158
+
159
+ const ctx = new OutputContext(flags);
160
+ await COMMANDS[subcommand](args, flags, ctx);
161
+ ctx.flush();
162
+ }
163
+
164
+ // When run directly as a script
165
+ if (require.main === module) {
166
+ main(process.argv.slice(2)).catch((err) => {
167
+ console.error(err.message || err);
168
+ process.exitCode = 1;
169
+ });
170
+ }
171
+
172
+ module.exports = main;
173
+ module.exports.COMMANDS = COMMANDS;
174
+ module.exports.printHelp = printHelp;
175
+ 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)
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",
@@ -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 };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Serialize a parsed schema (internal representation from parseSchema())
5
+ * back into a JSON string.
6
+ *
7
+ * - Tables are sorted alphabetically by name.
8
+ * - Relationships are sorted by [parent, child].
9
+ * - Output uses 2-space indentation with a trailing newline.
10
+ * - Optional fields (options, unique, softDelete, relationships) are preserved.
11
+ *
12
+ * @param {object} schema — internal representation from parseSchema()
13
+ * @returns {string} — JSON with 2-space indent + trailing newline
14
+ */
15
+ function printSchema(schema) {
16
+ const output = {};
17
+
18
+ output.adapter = schema.adapter;
19
+ output.framework = schema.framework;
20
+
21
+ // Preserve options if present and non-empty
22
+ if (schema.options && Object.keys(schema.options).length > 0) {
23
+ output.options = schema.options;
24
+ }
25
+
26
+ // Sort tables alphabetically by name
27
+ const sortedTableNames = Object.keys(schema.tables).sort();
28
+ const tables = {};
29
+ for (const name of sortedTableNames) {
30
+ const table = schema.tables[name];
31
+ const tableDef = {
32
+ columns: table.columns,
33
+ pk: table.pk || "id",
34
+ };
35
+
36
+ // Preserve unique if not the default [pk]
37
+ const defaultUnique = [table.pk || "id"];
38
+ const hasCustomUnique =
39
+ table.unique &&
40
+ (table.unique.length !== defaultUnique.length ||
41
+ table.unique.some((v, i) => v !== defaultUnique[i]));
42
+ if (hasCustomUnique) {
43
+ tableDef.unique = table.unique;
44
+ }
45
+
46
+ // Preserve softDelete if set
47
+ if (table.softDelete !== null && table.softDelete !== undefined) {
48
+ tableDef.softDelete = table.softDelete;
49
+ }
50
+
51
+ // Preserve timestamps if not the default { created_at: null, modified_at: null }
52
+ if (table.timestamps) {
53
+ const hasCustomTimestamps =
54
+ table.timestamps.created_at !== null ||
55
+ table.timestamps.modified_at !== null;
56
+ if (hasCustomTimestamps) {
57
+ tableDef.timestamps = table.timestamps;
58
+ }
59
+ }
60
+
61
+ tables[name] = tableDef;
62
+ }
63
+ output.tables = tables;
64
+
65
+ // Sort relationships by [parent, child] and include if non-empty
66
+ if (schema.relationships && schema.relationships.length > 0) {
67
+ output.relationships = [...schema.relationships].sort((a, b) => {
68
+ const cmp = a.parent.localeCompare(b.parent);
69
+ if (cmp !== 0) return cmp;
70
+ return a.child.localeCompare(b.child);
71
+ });
72
+ }
73
+
74
+ return JSON.stringify(output, null, 2) + "\n";
75
+ }
76
+
77
+ module.exports = { printSchema };
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Convert a parsed schema into the model metadata array used by
5
+ * generateModelFile(), generateRouteFile(), generateOpenAPISpec().
6
+ *
7
+ * Each ModelMeta matches the shape returned by the existing introspection
8
+ * functions:
9
+ * { table, structure, primary_key, unique, option }
10
+ *
11
+ * - The primary key column is excluded from `structure`.
12
+ * - Timestamp columns (created_at, modified_at values) are excluded from `structure`.
13
+ * - The softDelete column is excluded from `structure`.
14
+ * - Output is sorted alphabetically by table name.
15
+ *
16
+ * @param {{ adapter: string, framework: string, tables: object, relationships: Array, options: object }} schema
17
+ * @returns {Array<{ table: string, structure: object, primary_key: string, unique: string[], option: { safeDelete: string|null, created_at: string|null, modified_at: string|null } }>}
18
+ */
19
+ function schemaToModelMeta(schema) {
20
+ const tableNames = Object.keys(schema.tables).sort();
21
+ return tableNames.map((tableName) => {
22
+ const tableDef = schema.tables[tableName];
23
+
24
+ // Build the set of columns to exclude from structure
25
+ const excludeSet = new Set();
26
+
27
+ // Exclude primary key
28
+ excludeSet.add(tableDef.pk);
29
+
30
+ // Exclude timestamp columns
31
+ if (tableDef.timestamps) {
32
+ if (tableDef.timestamps.created_at) {
33
+ excludeSet.add(tableDef.timestamps.created_at);
34
+ }
35
+ if (tableDef.timestamps.modified_at) {
36
+ excludeSet.add(tableDef.timestamps.modified_at);
37
+ }
38
+ }
39
+
40
+ // Exclude softDelete column
41
+ if (tableDef.softDelete) {
42
+ excludeSet.add(tableDef.softDelete);
43
+ }
44
+
45
+ // Build structure, excluding the above columns
46
+ const structure = {};
47
+ for (const [colName, rule] of Object.entries(tableDef.columns)) {
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;
53
+ structure[colName] = rule;
54
+ }
55
+ }
56
+
57
+ // Map option fields
58
+ const option = {
59
+ safeDelete: tableDef.softDelete || null,
60
+ created_at: tableDef.timestamps
61
+ ? tableDef.timestamps.created_at || null
62
+ : null,
63
+ modified_at: tableDef.timestamps
64
+ ? tableDef.timestamps.modified_at || null
65
+ : null,
66
+ };
67
+
68
+ return {
69
+ table: tableName,
70
+ structure,
71
+ primary_key: tableDef.pk,
72
+ unique: [...tableDef.unique],
73
+ option,
74
+ };
75
+ });
76
+ }
77
+
78
+ module.exports = { schemaToModelMeta };