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.
- package/README.md +154 -203
- package/docker-compose.yml +13 -9
- package/docs/SKILL.md +196 -24
- package/docs/adapters/cockroachdb.md +1 -1
- package/docs/adapters/dynamodb.md +1 -1
- package/docs/adapters/mongodb.md +1 -1
- package/docs/adapters/mssql.md +1 -1
- package/docs/adapters/oracle.md +1 -1
- package/docs/adapters/postgres.md +1 -1
- package/docs/adapters/redis.md +1 -1
- package/docs/adapters/sqlite3.md +1 -1
- package/package.json +7 -12
- package/src/cli/commands/diff.js +114 -0
- package/src/cli/commands/doctor.js +181 -0
- package/src/cli/commands/generate-llm-docs.js +418 -0
- package/src/cli/commands/generate.js +240 -0
- package/src/cli/commands/init.js +153 -0
- package/src/cli/commands/inspect.js +205 -0
- package/src/cli/diff-engine.js +198 -0
- package/src/cli/flags.js +112 -0
- package/src/cli/generate-model.js +4 -4
- package/src/cli/generate-openapi.js +5 -1
- package/src/cli/generate-route.js +242 -7
- package/src/cli/init/dependencies.js +83 -0
- package/src/cli/init/generators.js +782 -0
- package/src/cli/init/prompt.js +159 -0
- package/src/cli/init.js +281 -0
- package/src/cli/main.js +95 -0
- package/src/commons/model.js +5 -6
- package/src/commons/route.js +24 -0
- package/src/schema/schema-parser.js +78 -0
- package/src/schema/schema-printer.js +81 -0
- package/src/schema/schema-to-meta.js +74 -0
- package/src/schema/schema-validator.js +253 -0
- package/src/serve.js +7 -10
- package/docs/README.md +0 -208
- package/src/cli/generate-app.js +0 -359
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { generateModelFile } = require("./generate-model.js");
|
|
6
|
+
const {
|
|
7
|
+
generateRouteFile,
|
|
8
|
+
generateChildRouteFile,
|
|
9
|
+
generateRoutesIndexFile,
|
|
10
|
+
generateTestFile,
|
|
11
|
+
generateChildTestFile,
|
|
12
|
+
} = require("./generate-route.js");
|
|
13
|
+
const { generateOpenAPISpec } = require("./generate-openapi.js");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Simple line-by-line diff between two strings.
|
|
17
|
+
* Returns a human-readable unified-style diff string.
|
|
18
|
+
*/
|
|
19
|
+
function lineDiff(expected, actual) {
|
|
20
|
+
const expectedLines = expected.split("\n");
|
|
21
|
+
const actualLines = actual.split("\n");
|
|
22
|
+
const lines = [];
|
|
23
|
+
const maxLen = Math.max(expectedLines.length, actualLines.length);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < maxLen; i++) {
|
|
26
|
+
const exp = i < expectedLines.length ? expectedLines[i] : undefined;
|
|
27
|
+
const act = i < actualLines.length ? actualLines[i] : undefined;
|
|
28
|
+
|
|
29
|
+
if (exp === act) continue;
|
|
30
|
+
if (act !== undefined && exp === undefined) {
|
|
31
|
+
lines.push(`+${i + 1}: ${act}`);
|
|
32
|
+
} else if (exp !== undefined && act === undefined) {
|
|
33
|
+
lines.push(`-${i + 1}: ${exp}`);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push(`-${i + 1}: ${act}`);
|
|
36
|
+
lines.push(`+${i + 1}: ${exp}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a map of relative file path → expected content for all artifacts
|
|
44
|
+
* that the schema would generate.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array<{table, structure, primary_key, unique, option}>} meta
|
|
47
|
+
* @param {Array<{parent, child, foreignKey}>} relationships
|
|
48
|
+
* @returns {Map<string, string>}
|
|
49
|
+
*/
|
|
50
|
+
function buildExpectedFiles(meta, relationships) {
|
|
51
|
+
const expected = new Map();
|
|
52
|
+
const modelsRelPath = "../models";
|
|
53
|
+
const tableNames = meta.map((m) => m.table).sort();
|
|
54
|
+
|
|
55
|
+
// Model files
|
|
56
|
+
for (const m of meta) {
|
|
57
|
+
expected.set(`models/${m.table}.js`, generateModelFile(m));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Route files (one per table)
|
|
61
|
+
for (const m of meta) {
|
|
62
|
+
expected.set(
|
|
63
|
+
`routes/${m.table}.js`,
|
|
64
|
+
generateRouteFile(m.table, modelsRelPath),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Child route files (one per relationship)
|
|
69
|
+
for (const rel of relationships) {
|
|
70
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
71
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
72
|
+
expected.set(
|
|
73
|
+
`routes/${rel.child}_child_of_${rel.parent}.js`,
|
|
74
|
+
generateChildRouteFile(
|
|
75
|
+
rel.child,
|
|
76
|
+
rel.parent,
|
|
77
|
+
rel.foreignKey,
|
|
78
|
+
modelsRelPath,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Routes index file
|
|
84
|
+
expected.set(
|
|
85
|
+
"routes/index.js",
|
|
86
|
+
generateRoutesIndexFile(tableNames, relationships),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Test files (one per table)
|
|
90
|
+
for (const m of meta) {
|
|
91
|
+
expected.set(
|
|
92
|
+
`test/${m.table}.test.js`,
|
|
93
|
+
generateTestFile(m.table, m.primary_key),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Child test files (one per relationship)
|
|
98
|
+
for (const rel of relationships) {
|
|
99
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
100
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
101
|
+
expected.set(
|
|
102
|
+
`test/${rel.child}_child_of_${rel.parent}.test.js`,
|
|
103
|
+
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// OpenAPI spec
|
|
108
|
+
expected.set(
|
|
109
|
+
"openapi.json",
|
|
110
|
+
JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return expected;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Scan known artifact directories on disk and return a set of relative paths
|
|
118
|
+
* that exist.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} baseDir
|
|
121
|
+
* @returns {Set<string>}
|
|
122
|
+
*/
|
|
123
|
+
function scanDiskFiles(baseDir) {
|
|
124
|
+
const files = new Set();
|
|
125
|
+
|
|
126
|
+
const dirs = [
|
|
127
|
+
{ dir: "models", ext: ".js" },
|
|
128
|
+
{ dir: "routes", ext: ".js" },
|
|
129
|
+
{ dir: "test", ext: ".test.js" },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const { dir, ext } of dirs) {
|
|
133
|
+
const fullDir = path.join(baseDir, dir);
|
|
134
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
135
|
+
for (const file of fs.readdirSync(fullDir)) {
|
|
136
|
+
if (file.endsWith(ext)) {
|
|
137
|
+
files.add(`${dir}/${file}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for openapi.json at root
|
|
143
|
+
const openapiPath = path.join(baseDir, "openapi.json");
|
|
144
|
+
if (fs.existsSync(openapiPath)) {
|
|
145
|
+
files.add("openapi.json");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return files;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compare expected generated content against actual files on disk.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} baseDir — project root
|
|
155
|
+
* @param {Array<{table, structure, primary_key, unique, option}>} meta — from schema
|
|
156
|
+
* @param {Array<{parent, child, foreignKey}>} relationships
|
|
157
|
+
* @returns {{ added: string[], modified: Array<{file: string, diff: string}>, deleted: string[] }}
|
|
158
|
+
*/
|
|
159
|
+
function computeDiff(baseDir, meta, relationships) {
|
|
160
|
+
const expected = buildExpectedFiles(meta, relationships);
|
|
161
|
+
const diskFiles = scanDiskFiles(baseDir);
|
|
162
|
+
|
|
163
|
+
const added = [];
|
|
164
|
+
const modified = [];
|
|
165
|
+
const deleted = [];
|
|
166
|
+
|
|
167
|
+
// Check expected files against disk
|
|
168
|
+
for (const [relPath, expectedContent] of expected) {
|
|
169
|
+
const fullPath = path.join(baseDir, relPath);
|
|
170
|
+
if (!fs.existsSync(fullPath)) {
|
|
171
|
+
added.push(relPath);
|
|
172
|
+
} else {
|
|
173
|
+
const actualContent = fs.readFileSync(fullPath, "utf8");
|
|
174
|
+
if (actualContent !== expectedContent) {
|
|
175
|
+
modified.push({
|
|
176
|
+
file: relPath,
|
|
177
|
+
diff: lineDiff(expectedContent, actualContent),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// unchanged — not reported
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check disk files not in expected set → deleted
|
|
185
|
+
for (const diskFile of diskFiles) {
|
|
186
|
+
if (!expected.has(diskFile)) {
|
|
187
|
+
deleted.push(diskFile);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
added: added.sort(),
|
|
193
|
+
modified: modified.sort((a, b) => a.file.localeCompare(b.file)),
|
|
194
|
+
deleted: deleted.sort(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { computeDiff, buildExpectedFiles, lineDiff };
|
package/src/cli/flags.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Universal flag parser and OutputContext for the db-model-router CLI.
|
|
5
|
+
*
|
|
6
|
+
* Parses --yes, --json, --dry-run, --no-install, --help from argv.
|
|
7
|
+
* Extracts the subcommand (first non-flag argument).
|
|
8
|
+
* Collects remaining key-value flags into an args object.
|
|
9
|
+
*
|
|
10
|
+
* @module cli/flags
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse CLI argv into subcommand, flags, and args.
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} argv - process.argv.slice(2) style array
|
|
17
|
+
* @returns {{ subcommand: string|null, flags: Flags, args: object }}
|
|
18
|
+
*/
|
|
19
|
+
function parseFlags(argv) {
|
|
20
|
+
const flags = {
|
|
21
|
+
yes: false,
|
|
22
|
+
json: false,
|
|
23
|
+
dryRun: false,
|
|
24
|
+
noInstall: false,
|
|
25
|
+
help: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const args = {};
|
|
29
|
+
let subcommand = null;
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const arg = argv[i];
|
|
33
|
+
|
|
34
|
+
if (arg === "--yes") {
|
|
35
|
+
flags.yes = true;
|
|
36
|
+
} else if (arg === "--json") {
|
|
37
|
+
flags.json = true;
|
|
38
|
+
} else if (arg === "--dry-run") {
|
|
39
|
+
flags.dryRun = true;
|
|
40
|
+
} else if (arg === "--no-install") {
|
|
41
|
+
flags.noInstall = true;
|
|
42
|
+
} else if (arg === "--help") {
|
|
43
|
+
flags.help = true;
|
|
44
|
+
} else if (arg.startsWith("--")) {
|
|
45
|
+
// Key-value flag: --from schema.json → { from: "schema.json" }
|
|
46
|
+
const key = arg.slice(2);
|
|
47
|
+
const next = argv[i + 1];
|
|
48
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
49
|
+
args[key] = next;
|
|
50
|
+
i++; // skip the value
|
|
51
|
+
} else {
|
|
52
|
+
args[key] = true;
|
|
53
|
+
}
|
|
54
|
+
} else if (subcommand === null) {
|
|
55
|
+
subcommand = arg;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { subcommand, flags, args };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OutputContext controls CLI output behavior based on flags.
|
|
64
|
+
*
|
|
65
|
+
* When --json is active:
|
|
66
|
+
* - log() is a no-op (suppresses human-readable output)
|
|
67
|
+
* - result() accumulates data
|
|
68
|
+
* - flush() prints the accumulated JSON to stdout
|
|
69
|
+
*
|
|
70
|
+
* When --json is NOT active:
|
|
71
|
+
* - log() prints to stdout
|
|
72
|
+
* - result() is a no-op
|
|
73
|
+
* - flush() is a no-op
|
|
74
|
+
*/
|
|
75
|
+
class OutputContext {
|
|
76
|
+
constructor(flags) {
|
|
77
|
+
this._json = !!(flags && flags.json);
|
|
78
|
+
this._results = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log a human-readable message. No-op when --json is active.
|
|
83
|
+
* @param {string} msg
|
|
84
|
+
*/
|
|
85
|
+
log(msg) {
|
|
86
|
+
if (!this._json) {
|
|
87
|
+
console.log(msg);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Accumulate a result object for JSON output.
|
|
93
|
+
* @param {*} data
|
|
94
|
+
*/
|
|
95
|
+
result(data) {
|
|
96
|
+
this._results.push(data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Flush accumulated JSON results to stdout if --json is active.
|
|
101
|
+
* Prints a single JSON object (or the last result if only one was accumulated).
|
|
102
|
+
*/
|
|
103
|
+
flush() {
|
|
104
|
+
if (this._json && this._results.length > 0) {
|
|
105
|
+
const output =
|
|
106
|
+
this._results.length === 1 ? this._results[0] : this._results;
|
|
107
|
+
console.log(JSON.stringify(output));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { parseFlags, OutputContext };
|
|
@@ -711,7 +711,7 @@ function parseArgs(argv) {
|
|
|
711
711
|
|
|
712
712
|
function printUsage() {
|
|
713
713
|
console.log(`
|
|
714
|
-
Usage:
|
|
714
|
+
Usage: db-model-router-generate-model --type <db_type> [options]
|
|
715
715
|
|
|
716
716
|
Options:
|
|
717
717
|
--type Database type (${SUPPORTED_TYPES.join(", ")})
|
|
@@ -728,9 +728,9 @@ Options:
|
|
|
728
728
|
--help Show this help message
|
|
729
729
|
|
|
730
730
|
Examples:
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
731
|
+
db-model-router-generate-model --type mysql --host localhost --database mydb --user root --password secret
|
|
732
|
+
db-model-router-generate-model --type sqlite3 --database ./myapp.db --output ./src/models
|
|
733
|
+
db-model-router-generate-model --type postgres --env .env --output ./models
|
|
734
734
|
`);
|
|
735
735
|
}
|
|
736
736
|
|
|
@@ -8,7 +8,11 @@ function generateOpenAPISpec(models, options = {}) {
|
|
|
8
8
|
|
|
9
9
|
const spec = {
|
|
10
10
|
openapi: "3.0.3",
|
|
11
|
-
info: {
|
|
11
|
+
info: {
|
|
12
|
+
title,
|
|
13
|
+
version,
|
|
14
|
+
description: "Auto-generated by db-model-router CLI",
|
|
15
|
+
},
|
|
12
16
|
paths: {},
|
|
13
17
|
components: { schemas: {} },
|
|
14
18
|
};
|
|
@@ -108,6 +108,192 @@ function generateSimpleRoutesIndexFile(tableNames) {
|
|
|
108
108
|
return generateRoutesIndexFile(tableNames, []);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Generate a test file for a route covering all CRUD methods.
|
|
113
|
+
* Uses supertest + the app's express setup.
|
|
114
|
+
*/
|
|
115
|
+
function generateTestFile(tableName, pk) {
|
|
116
|
+
const varName = safeVarName(tableName);
|
|
117
|
+
return `const assert = require("assert");
|
|
118
|
+
const express = require("express");
|
|
119
|
+
const request = require("supertest");
|
|
120
|
+
const { route } = require("db-model-router");
|
|
121
|
+
|
|
122
|
+
// Adjust the path to your model file as needed
|
|
123
|
+
const ${varName} = require("../models/${tableName}");
|
|
124
|
+
|
|
125
|
+
function createApp() {
|
|
126
|
+
const app = express();
|
|
127
|
+
app.use(express.json());
|
|
128
|
+
app.use("/${tableName}", route(${varName}));
|
|
129
|
+
return app;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe("${tableName} routes", function () {
|
|
133
|
+
let app;
|
|
134
|
+
|
|
135
|
+
before(function () {
|
|
136
|
+
app = createApp();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("GET /${tableName}/", function () {
|
|
140
|
+
it("should list records", async function () {
|
|
141
|
+
const res = await request(app).get("/${tableName}/");
|
|
142
|
+
assert.strictEqual(res.status, 200);
|
|
143
|
+
assert.ok(Array.isArray(res.body.data));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("POST /${tableName}/add", function () {
|
|
148
|
+
it("should insert a single record", async function () {
|
|
149
|
+
const res = await request(app)
|
|
150
|
+
.post("/${tableName}/add")
|
|
151
|
+
.send({});
|
|
152
|
+
assert.ok([200, 201, 400].includes(res.status));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("POST /${tableName}/", function () {
|
|
157
|
+
it("should bulk insert records", async function () {
|
|
158
|
+
const res = await request(app)
|
|
159
|
+
.post("/${tableName}/")
|
|
160
|
+
.send({ data: [] });
|
|
161
|
+
assert.ok([200, 201, 400].includes(res.status));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("GET /${tableName}/:${pk}", function () {
|
|
166
|
+
it("should get a record by ID", async function () {
|
|
167
|
+
const res = await request(app).get("/${tableName}/1");
|
|
168
|
+
assert.ok([200, 404].includes(res.status));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("PUT /${tableName}/:${pk}", function () {
|
|
173
|
+
it("should update a record", async function () {
|
|
174
|
+
const res = await request(app)
|
|
175
|
+
.put("/${tableName}/1")
|
|
176
|
+
.send({});
|
|
177
|
+
assert.ok([200, 400, 404].includes(res.status));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("PATCH /${tableName}/:${pk}", function () {
|
|
182
|
+
it("should partially update a record", async function () {
|
|
183
|
+
const res = await request(app)
|
|
184
|
+
.patch("/${tableName}/1")
|
|
185
|
+
.send({});
|
|
186
|
+
assert.ok([200, 400, 404].includes(res.status));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("DELETE /${tableName}/:${pk}", function () {
|
|
191
|
+
it("should delete a record", async function () {
|
|
192
|
+
const res = await request(app).delete("/${tableName}/1");
|
|
193
|
+
assert.ok([200, 204, 404].includes(res.status));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("PUT /${tableName}/", function () {
|
|
198
|
+
it("should bulk update records", async function () {
|
|
199
|
+
const res = await request(app)
|
|
200
|
+
.put("/${tableName}/")
|
|
201
|
+
.send({ data: [] });
|
|
202
|
+
assert.ok([200, 400].includes(res.status));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("DELETE /${tableName}/", function () {
|
|
207
|
+
it("should bulk delete records", async function () {
|
|
208
|
+
const res = await request(app)
|
|
209
|
+
.delete("/${tableName}/")
|
|
210
|
+
.send({});
|
|
211
|
+
assert.ok([200, 204, 400].includes(res.status));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generate a child route test file that tests the nested parent/:fk/child endpoints.
|
|
220
|
+
*/
|
|
221
|
+
function generateChildTestFile(childTable, parentTable, fkColumn, pk) {
|
|
222
|
+
const childVar = safeVarName(childTable);
|
|
223
|
+
return `const assert = require("assert");
|
|
224
|
+
const express = require("express");
|
|
225
|
+
const request = require("supertest");
|
|
226
|
+
const { route } = require("db-model-router");
|
|
227
|
+
|
|
228
|
+
const ${childVar} = require("../models/${childTable}");
|
|
229
|
+
|
|
230
|
+
function createApp() {
|
|
231
|
+
const app = express();
|
|
232
|
+
app.use(express.json());
|
|
233
|
+
app.use("/${parentTable}/:${fkColumn}/${childTable}", route(${childVar}, { ${fkColumn}: "params.${fkColumn}" }));
|
|
234
|
+
return app;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
describe("${childTable} (child of ${parentTable}) routes", function () {
|
|
238
|
+
let app;
|
|
239
|
+
const parentId = 1;
|
|
240
|
+
|
|
241
|
+
before(function () {
|
|
242
|
+
app = createApp();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("GET /${parentTable}/:${fkColumn}/${childTable}/", function () {
|
|
246
|
+
it("should list child records scoped by parent", async function () {
|
|
247
|
+
const res = await request(app).get(\`/${parentTable}/\${parentId}/${childTable}/\`);
|
|
248
|
+
assert.strictEqual(res.status, 200);
|
|
249
|
+
assert.ok(Array.isArray(res.body.data));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("POST /${parentTable}/:${fkColumn}/${childTable}/add", function () {
|
|
254
|
+
it("should insert a child record", async function () {
|
|
255
|
+
const res = await request(app)
|
|
256
|
+
.post(\`/${parentTable}/\${parentId}/${childTable}/add\`)
|
|
257
|
+
.send({});
|
|
258
|
+
assert.ok([200, 201, 400].includes(res.status));
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("GET /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
|
|
263
|
+
it("should get a child record by ID", async function () {
|
|
264
|
+
const res = await request(app).get(\`/${parentTable}/\${parentId}/${childTable}/1\`);
|
|
265
|
+
assert.ok([200, 404].includes(res.status));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("PUT /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
|
|
270
|
+
it("should update a child record", async function () {
|
|
271
|
+
const res = await request(app)
|
|
272
|
+
.put(\`/${parentTable}/\${parentId}/${childTable}/1\`)
|
|
273
|
+
.send({});
|
|
274
|
+
assert.ok([200, 400, 404].includes(res.status));
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("PATCH /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
|
|
279
|
+
it("should partially update a child record", async function () {
|
|
280
|
+
const res = await request(app)
|
|
281
|
+
.patch(\`/${parentTable}/\${parentId}/${childTable}/1\`)
|
|
282
|
+
.send({});
|
|
283
|
+
assert.ok([200, 400, 404].includes(res.status));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("DELETE /${parentTable}/:${fkColumn}/${childTable}/:${pk}", function () {
|
|
288
|
+
it("should delete a child record", async function () {
|
|
289
|
+
const res = await request(app).delete(\`/${parentTable}/\${parentId}/${childTable}/1\`);
|
|
290
|
+
assert.ok([200, 204, 404].includes(res.status));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
111
297
|
/**
|
|
112
298
|
* Read model directory to discover table names from generated model files.
|
|
113
299
|
* Looks for .js files that are not index.js.
|
|
@@ -143,7 +329,7 @@ async function main() {
|
|
|
143
329
|
if (!dbType) {
|
|
144
330
|
console.error(
|
|
145
331
|
`Error: No models found in "${modelsDir}" and no --type provided to generate them.\n` +
|
|
146
|
-
`Either generate models first with
|
|
332
|
+
`Either generate models first with db-model-router-generate-model, or provide --type to auto-generate.`,
|
|
147
333
|
);
|
|
148
334
|
process.exit(1);
|
|
149
335
|
}
|
|
@@ -262,6 +448,51 @@ async function main() {
|
|
|
262
448
|
// OpenAPI generation is optional, don't fail
|
|
263
449
|
}
|
|
264
450
|
|
|
451
|
+
// Generate test files for all routes
|
|
452
|
+
const testsDir = path.resolve(path.dirname(routesDir), "tests");
|
|
453
|
+
if (!fs.existsSync(testsDir)) {
|
|
454
|
+
fs.mkdirSync(testsDir, { recursive: true });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const table of tableNames) {
|
|
458
|
+
// Try to extract PK from model file
|
|
459
|
+
let pk = "id";
|
|
460
|
+
const modelPath = path.join(modelsDir, table + ".js");
|
|
461
|
+
if (fs.existsSync(modelPath)) {
|
|
462
|
+
const meta = parseModelFile(fs.readFileSync(modelPath, "utf8"), table);
|
|
463
|
+
if (meta && meta.primary_key) pk = meta.primary_key;
|
|
464
|
+
}
|
|
465
|
+
const testPath = path.join(testsDir, table + ".test.js");
|
|
466
|
+
fs.writeFileSync(testPath, generateTestFile(table, pk));
|
|
467
|
+
console.log(` Created ${testPath}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Generate child route test files
|
|
471
|
+
for (const rel of relationships) {
|
|
472
|
+
let pk = "id";
|
|
473
|
+
const modelPath = path.join(modelsDir, rel.child + ".js");
|
|
474
|
+
if (fs.existsSync(modelPath)) {
|
|
475
|
+
const meta = parseModelFile(
|
|
476
|
+
fs.readFileSync(modelPath, "utf8"),
|
|
477
|
+
rel.child,
|
|
478
|
+
);
|
|
479
|
+
if (meta && meta.primary_key) pk = meta.primary_key;
|
|
480
|
+
}
|
|
481
|
+
const testPath = path.join(
|
|
482
|
+
testsDir,
|
|
483
|
+
`${rel.child}_child_of_${rel.parent}.test.js`,
|
|
484
|
+
);
|
|
485
|
+
fs.writeFileSync(
|
|
486
|
+
testPath,
|
|
487
|
+
generateChildTestFile(rel.child, rel.parent, rel.fkColumn, pk),
|
|
488
|
+
);
|
|
489
|
+
console.log(` Created ${testPath}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log(
|
|
493
|
+
`Generated ${tableNames.length + relationships.length} test file(s) in ${testsDir}`,
|
|
494
|
+
);
|
|
495
|
+
|
|
265
496
|
process.exit(0);
|
|
266
497
|
}
|
|
267
498
|
|
|
@@ -271,7 +502,9 @@ async function main() {
|
|
|
271
502
|
function parseModelFile(content, tableName) {
|
|
272
503
|
try {
|
|
273
504
|
// Extract structure JSON
|
|
274
|
-
const structMatch = content.match(
|
|
505
|
+
const structMatch = content.match(
|
|
506
|
+
/model\(\s*\n?\s*db,\s*\n?\s*"[^"]+",\s*\n?\s*(\{[\s\S]*?\}),/,
|
|
507
|
+
);
|
|
275
508
|
if (!structMatch) return null;
|
|
276
509
|
const structure = JSON.parse(structMatch[1]);
|
|
277
510
|
// Extract primary key
|
|
@@ -281,7 +514,7 @@ function parseModelFile(content, tableName) {
|
|
|
281
514
|
} catch (e) {
|
|
282
515
|
return null;
|
|
283
516
|
}
|
|
284
|
-
|
|
517
|
+
}
|
|
285
518
|
function parseArgs(argv) {
|
|
286
519
|
const args = {};
|
|
287
520
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -302,7 +535,7 @@ function parseArgs(argv) {
|
|
|
302
535
|
|
|
303
536
|
function printUsage() {
|
|
304
537
|
console.log(`
|
|
305
|
-
Usage:
|
|
538
|
+
Usage: db-model-router-generate-route [options]
|
|
306
539
|
|
|
307
540
|
Options:
|
|
308
541
|
--models Path to models directory (default: ./models)
|
|
@@ -320,13 +553,13 @@ Options:
|
|
|
320
553
|
|
|
321
554
|
Examples:
|
|
322
555
|
# Generate routes from existing models
|
|
323
|
-
|
|
556
|
+
db-model-router-generate-route --models ./models --output ./routes
|
|
324
557
|
|
|
325
558
|
# Auto-generate models + routes in one step
|
|
326
|
-
|
|
559
|
+
db-model-router-generate-route --type mysql --env .env --models ./models --output ./routes
|
|
327
560
|
|
|
328
561
|
# SQLite3 example
|
|
329
|
-
|
|
562
|
+
db-model-router-generate-route --type sqlite3 --database ./myapp.db
|
|
330
563
|
`);
|
|
331
564
|
}
|
|
332
565
|
|
|
@@ -341,6 +574,8 @@ module.exports = {
|
|
|
341
574
|
generateRouteFile,
|
|
342
575
|
generateChildRouteFile,
|
|
343
576
|
generateRoutesIndexFile,
|
|
577
|
+
generateTestFile,
|
|
578
|
+
generateChildTestFile,
|
|
344
579
|
discoverModels,
|
|
345
580
|
safeVarName,
|
|
346
581
|
};
|