db-model-router 1.0.13 → 1.0.15

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.
package/README.md CHANGED
@@ -394,6 +394,7 @@ Generate models, routes, tests, OpenAPI spec, and LLM docs from a schema file. A
394
394
  | Flag / Arg | Description |
395
395
  | ------------------------ | ------------------------------------------------- |
396
396
  | `--from <path>` | Path to schema file (default: `dbmr.schema.json`) |
397
+ | `--output <dir>` | Directory for generated files (default: cwd) |
397
398
  | `--models=false` | Disable model file generation |
398
399
  | `--routes=false` | Disable route file generation |
399
400
  | `--openapi=false` | Disable OpenAPI spec generation |
@@ -424,6 +425,7 @@ db-model-router generate --tests=false --dry-run # skip tests
424
425
  db-model-router generate --saas-structure=false # skip SaaS generation
425
426
  db-model-router generate --openapi=false --tests=false # skip OpenAPI and tests
426
427
  db-model-router generate --from dbmr.schema.json --json
428
+ db-model-router generate --from dbmr.schema.json --output ./backend
427
429
  ```
428
430
 
429
431
  #### `doctor`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "db-model-router",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Generative API Creation using mysql2 and express libraries in node js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -270,7 +270,7 @@ db-model-router inspect --type postgres --env .env [--out schema.json] [--tables
270
270
  ### `generate` — Generate code from schema
271
271
 
272
272
  ```bash
273
- db-model-router generate --from dbmr.schema.json [--models=false] [--routes=false] [--openapi=false] [--tests=false] [--migrations=false] [--saas-structure=false]
273
+ db-model-router generate --from dbmr.schema.json [--output <dir>] [--models=false] [--routes=false] [--openapi=false] [--tests=false] [--migrations=false] [--saas-structure=false]
274
274
  ```
275
275
 
276
276
  All artifact types are **enabled by default**. Use `--flag=false` to disable specific ones.
@@ -6,6 +6,7 @@ const { parseSchema } = require("../../schema/schema-parser");
6
6
  const { SchemaValidationError } = require("../../schema/schema-validator");
7
7
  const { schemaToModelMeta } = require("../../schema/schema-to-meta");
8
8
  const { computeDiff } = require("../diff-engine");
9
+ const { generateSaasStructure } = require("../generate-saas-structure");
9
10
 
10
11
  /**
11
12
  * Adapter-to-driver mapping.
@@ -111,13 +112,53 @@ async function doctor(args, flags, ctx) {
111
112
 
112
113
  if (schema) {
113
114
  const meta = schemaToModelMeta(schema);
114
- const relationships = schema.relationships || [];
115
- const diff = computeDiff(baseDir, meta, relationships);
115
+
116
+ // Derive route relationships from parent fields (same logic as generate)
117
+ const routeRelationships = [];
118
+ for (const [tableName, tableDef] of Object.entries(schema.tables || {})) {
119
+ if (tableDef.parent) {
120
+ const parentTable = schema.tables[tableDef.parent];
121
+ if (parentTable) {
122
+ routeRelationships.push({
123
+ parent: tableDef.parent,
124
+ child: tableName,
125
+ foreignKey: parentTable.pk,
126
+ });
127
+ }
128
+ }
129
+ }
130
+
131
+ const tableNames = meta.map((m) => m.table).sort();
132
+
133
+ // Detect whether OpenAPI docs were generated
134
+ const includeDocs = fs.existsSync(path.join(baseDir, "openapi.json"));
135
+
136
+ // Detect whether SaaS structure was generated
137
+ const saasFiles = [];
138
+ if (fs.existsSync(path.join(baseDir, "routes", "auth", "index.js"))) {
139
+ const adapter = schema.adapter;
140
+ saasFiles.push(
141
+ ...generateSaasStructure(adapter, {
142
+ tableNames,
143
+ relationships: routeRelationships,
144
+ routeOptions: { includeDocs },
145
+ }),
146
+ );
147
+ }
148
+
149
+ const diffOptions = { includeDocs, saasFiles };
150
+ const diff = computeDiff(baseDir, meta, routeRelationships, diffOptions);
151
+
152
+ // Filter out known init-scaffold files that doctor should ignore
153
+ const initWhitelist = new Set(["routes/health.js"]);
154
+ const filteredDeleted = diff.deleted.filter(
155
+ (f) => !initWhitelist.has(f),
156
+ );
116
157
 
117
158
  if (
118
159
  diff.added.length > 0 ||
119
160
  diff.modified.length > 0 ||
120
- diff.deleted.length > 0
161
+ filteredDeleted.length > 0
121
162
  ) {
122
163
  sync.ok = false;
123
164
  for (const f of diff.added) {
@@ -126,7 +167,7 @@ async function doctor(args, flags, ctx) {
126
167
  for (const m of diff.modified) {
127
168
  sync.outOfSync.push({ file: m.file, status: "modified" });
128
169
  }
129
- for (const f of diff.deleted) {
170
+ for (const f of filteredDeleted) {
130
171
  sync.outOfSync.push({ file: f, status: "extra" });
131
172
  }
132
173
  }
@@ -156,7 +156,7 @@ async function generate(args, flags, ctx) {
156
156
  genSaas = args["saas-structure"] !== false;
157
157
  }
158
158
 
159
- const baseDir = process.cwd();
159
+ const baseDir = path.resolve(args.output || process.cwd());
160
160
 
161
161
  // Collect all planned files: { relPath, content }
162
162
  const planned = [];
@@ -338,6 +338,7 @@ async function generate(args, flags, ctx) {
338
338
  tableNames,
339
339
  relationships: routeRelationships,
340
340
  routeOptions: { includeDocs: genOpenapi },
341
+ baseDir,
341
342
  });
342
343
 
343
344
  // The SaaS generator produces a combined routes/index.js that includes
@@ -11,6 +11,7 @@ const {
11
11
  ensurePackageJson,
12
12
  } = require("../init");
13
13
  const { promptUser } = require("../init/prompt");
14
+ const generateCmd = require("./generate");
14
15
 
15
16
  /**
16
17
  * Default answers used when --yes is provided and no schema is available.
@@ -86,6 +87,12 @@ async function init(args, flags, ctx) {
86
87
  for (const f of planned) {
87
88
  ctx.log(` ${f}`);
88
89
  }
90
+ }
91
+ // Also preview schema-generated artifacts when --from is used
92
+ if (args.from) {
93
+ await generateCmd(args, flags, ctx);
94
+ }
95
+ if (!flags.json) {
89
96
  ctx.log("\nNo files were written.");
90
97
  }
91
98
  return;
@@ -106,6 +113,12 @@ async function init(args, flags, ctx) {
106
113
  runInstall();
107
114
  }
108
115
 
116
+ // When --from points to a schema, also generate models, routes, tests, etc.
117
+ if (args.from) {
118
+ await generateCmd(args, flags, ctx);
119
+ if (process.exitCode) return; // bail if generate reported an error
120
+ }
121
+
109
122
  // Output
110
123
  const allFiles = [
111
124
  ...generated.files,
@@ -49,7 +49,8 @@ function lineDiff(expected, actual) {
49
49
  * @param {Array<{parent, child, foreignKey}>} relationships
50
50
  * @returns {Map<string, string>}
51
51
  */
52
- function buildExpectedFiles(meta, relationships) {
52
+ function buildExpectedFiles(meta, relationships, options = {}) {
53
+ const { includeDocs = true, saasFiles = [] } = options;
53
54
  const expected = new Map();
54
55
  const tableNames = meta.map((m) => m.table).sort();
55
56
 
@@ -114,14 +115,16 @@ function buildExpectedFiles(meta, relationships) {
114
115
  }
115
116
  }
116
117
 
117
- // Routes index file (with docs route)
118
+ // Routes index file
118
119
  expected.set(
119
120
  "routes/index.js",
120
- generateRoutesIndexFile(tableNames, relationships, { includeDocs: true }),
121
+ generateRoutesIndexFile(tableNames, relationships, { includeDocs }),
121
122
  );
122
123
 
123
124
  // Docs route (Swagger UI)
124
- expected.set("routes/docs.js", generateDocsRoute());
125
+ if (includeDocs) {
126
+ expected.set("routes/docs.js", generateDocsRoute());
127
+ }
125
128
 
126
129
  // Test files at correct nested paths
127
130
  for (const m of meta) {
@@ -154,6 +157,11 @@ function buildExpectedFiles(meta, relationships) {
154
157
  "\n",
155
158
  );
156
159
 
160
+ // Merge SaaS expected files last so they overwrite schema files where needed
161
+ for (const entry of saasFiles) {
162
+ expected.set(entry.relPath, entry.content);
163
+ }
164
+
157
165
  return expected;
158
166
  }
159
167
 
@@ -220,8 +228,8 @@ function scanDiskFiles(baseDir) {
220
228
  * @param {Array<{parent, child, foreignKey}>} relationships
221
229
  * @returns {{ added: string[], modified: Array<{file: string, diff: string}>, deleted: string[] }}
222
230
  */
223
- function computeDiff(baseDir, meta, relationships) {
224
- const expected = buildExpectedFiles(meta, relationships);
231
+ function computeDiff(baseDir, meta, relationships, options = {}) {
232
+ const expected = buildExpectedFiles(meta, relationships, options);
225
233
  const diskFiles = scanDiskFiles(baseDir);
226
234
 
227
235
  const added = [];
@@ -29,8 +29,8 @@ const { generateSaasTests } = require("./saas/generate-saas-tests");
29
29
  *
30
30
  * @returns {string} Updated .gitignore content
31
31
  */
32
- function getGitignoreContent() {
33
- const gitignorePath = path.join(process.cwd(), ".gitignore");
32
+ function getGitignoreContent(baseDir) {
33
+ const gitignorePath = path.join(baseDir || process.cwd(), ".gitignore");
34
34
  let content = "";
35
35
  if (fs.existsSync(gitignorePath)) {
36
36
  content = fs.readFileSync(gitignorePath, "utf8");
@@ -121,7 +121,7 @@ function generateSaasStructure(adapter, options) {
121
121
  }
122
122
 
123
123
  // 8. .gitignore update (add credentials.md)
124
- planned.push({ relPath: ".gitignore", content: getGitignoreContent() });
124
+ planned.push({ relPath: ".gitignore", content: getGitignoreContent(opts.baseDir) });
125
125
 
126
126
  return planned;
127
127
  }
package/src/cli/init.js CHANGED
@@ -239,14 +239,17 @@ function updatePackageJson(answers, outputDir) {
239
239
  const { dependencies, devDependencies } = collectDependencies(answers);
240
240
  const scripts = getScripts(outputDir);
241
241
 
242
+ const normalizedOutput = outputDir ? outputDir.replace(/\/+$/, "") : "";
243
+ const importPrefix = normalizedOutput ? `./${normalizedOutput}/` : "./";
244
+
242
245
  pkg.type = "module";
243
246
  pkg.imports = {
244
247
  "#root/*.js": "./*.js",
245
- "#models": "./models/index.js",
246
- "#models/*.js": "./models/*.js",
247
- "#routes/*.js": "./routes/*.js",
248
- "#commons/*.js": "./commons/*.js",
249
- "#middleware/*.js": "./middleware/*.js",
248
+ "#models": `${importPrefix}models/index.js`,
249
+ "#models/*.js": `${importPrefix}models/*.js`,
250
+ "#routes/*.js": `${importPrefix}routes/*.js`,
251
+ "#commons/*.js": `${importPrefix}commons/*.js`,
252
+ "#middleware/*.js": `${importPrefix}middleware/*.js`,
250
253
  };
251
254
  pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
252
255
  pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
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");
@@ -63,6 +63,7 @@ const COMMAND_FLAGS = {
63
63
  ],
64
64
  generate: [
65
65
  ["--from <path>", "Schema file (default: dbmr.schema.json)"],
66
+ ["--output <dir>", "Directory for generated files (default: cwd)"],
66
67
  ["--models", "Generate only model files"],
67
68
  ["--routes", "Generate only route files"],
68
69
  ["--openapi", "Generate only OpenAPI spec"],
@@ -304,8 +304,9 @@ function where(filter, safeDelete = null) {
304
304
  ),
305
305
  );
306
306
  } else if (j[1] === "like" || j[1] === "not like") {
307
+ const pgOp = j[1] === "like" ? "ILIKE" : "NOT ILIKE";
307
308
  bindIdx++;
308
- conditionAnd.push(`${escapeId(j[0])} ${j[1]} $${bindIdx}`);
309
+ conditionAnd.push(`${escapeId(j[0])} ${pgOp} $${bindIdx}`);
309
310
  value.push("%" + j[2] + "%");
310
311
  } else {
311
312
  bindIdx++;