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 +2 -0
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/src/cli/commands/doctor.js +45 -4
- package/src/cli/commands/generate.js +2 -1
- package/src/cli/commands/init.js +13 -0
- package/src/cli/diff-engine.js +14 -6
- package/src/cli/generate-saas-structure.js +3 -3
- package/src/cli/init.js +8 -5
- package/src/cli/main.js +2 -1
- package/src/postgres/db.js +2 -1
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
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/src/cli/commands/init.js
CHANGED
|
@@ -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,
|
package/src/cli/diff-engine.js
CHANGED
|
@@ -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
|
|
118
|
+
// Routes index file
|
|
118
119
|
expected.set(
|
|
119
120
|
"routes/index.js",
|
|
120
|
-
generateRoutesIndexFile(tableNames, relationships, { includeDocs
|
|
121
|
+
generateRoutesIndexFile(tableNames, relationships, { includeDocs }),
|
|
121
122
|
);
|
|
122
123
|
|
|
123
124
|
// Docs route (Swagger UI)
|
|
124
|
-
|
|
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":
|
|
246
|
-
"#models/*.js":
|
|
247
|
-
"#routes/*.js":
|
|
248
|
-
"#commons/*.js":
|
|
249
|
-
"#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"],
|
package/src/postgres/db.js
CHANGED
|
@@ -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])} ${
|
|
309
|
+
conditionAnd.push(`${escapeId(j[0])} ${pgOp} $${bindIdx}`);
|
|
309
310
|
value.push("%" + j[2] + "%");
|
|
310
311
|
} else {
|
|
311
312
|
bindIdx++;
|