db-model-router 1.0.4 → 1.0.6
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 +110 -16
- package/TODO.md +15 -0
- package/dbmr.schema.json +333 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -7
- package/scripts/demo-create.js +47 -0
- package/skill/SKILL.md +464 -0
- package/skill/references/cockroachdb.md +49 -0
- package/skill/references/dynamodb.md +53 -0
- package/skill/references/mongodb.md +56 -0
- package/skill/references/mssql.md +55 -0
- package/skill/references/oracle.md +52 -0
- package/skill/references/postgres.md +50 -0
- package/skill/references/redis.md +53 -0
- package/skill/references/sqlite3.md +43 -0
- package/src/cli/commands/generate.js +95 -31
- package/src/cli/commands/help.js +12 -7
- package/src/cli/commands/init.js +2 -2
- package/src/cli/commands/inspect.js +1 -0
- package/src/cli/diff-engine.js +54 -23
- package/src/cli/generate-db-manager.js +1573 -0
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +55 -27
- package/src/cli/init/dependencies.js +3 -0
- package/src/cli/init/generators.js +37 -31
- package/src/cli/init.js +8 -8
- package/src/cli/main.js +2 -2
- package/src/cockroachdb/db.js +90 -59
- package/src/commons/route.js +20 -20
- package/src/commons/validator.js +58 -1
- package/src/dynamodb/db.js +50 -27
- package/src/mongodb/db.js +1 -0
- package/src/mssql/db.js +89 -61
- package/src/mysql/db.js +1 -0
- package/src/oracle/db.js +1 -0
- package/src/postgres/db.js +61 -41
- package/src/redis/db.js +1 -0
- package/src/schema/schema-parser.js +43 -1
- package/src/schema/schema-printer.js +7 -0
- package/src/schema/schema-validator.js +17 -0
- package/src/sqlite3/db.js +12 -0
- package/docs/SKILL.md +0 -419
- package/src/cli/commands/generate-llm-docs.js +0 -418
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Oracle Adapter
|
|
2
|
+
|
|
3
|
+
Uses [oracledb](https://www.npmjs.com/package/oracledb) (Oracle Instant Client required).
|
|
4
|
+
|
|
5
|
+
## Connection
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const { init, db, model, route } = require("db-model-router");
|
|
9
|
+
init("oracle");
|
|
10
|
+
|
|
11
|
+
db.connect({
|
|
12
|
+
host: "localhost",
|
|
13
|
+
port: 1521,
|
|
14
|
+
database: "XEPDB1",
|
|
15
|
+
user: "system",
|
|
16
|
+
password: "oracle",
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Environment Variables
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
ORACLE_HOST=localhost
|
|
24
|
+
ORACLE_PORT=1521
|
|
25
|
+
ORACLE_DB=XEPDB1
|
|
26
|
+
ORACLE_USER=system
|
|
27
|
+
ORACLE_PASSWORD=oracle
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Notes
|
|
31
|
+
|
|
32
|
+
- Requires Oracle Instant Client installed on the host
|
|
33
|
+
- Uses connection pooling with session callbacks for NLS date format
|
|
34
|
+
- MySQL-style `?` placeholders are auto-translated to `:1, :2, ...`
|
|
35
|
+
- Includes a SQL translator that converts MySQL DDL/DML to Oracle syntax
|
|
36
|
+
- `MERGE INTO ... USING DUAL` is used for upsert operations
|
|
37
|
+
- `RETURNING ... INTO :pk_out` is used to retrieve auto-generated IDs
|
|
38
|
+
- Oracle reserved words in column names are auto-quoted
|
|
39
|
+
- `CLOB` values are fetched as strings
|
|
40
|
+
|
|
41
|
+
## Table Creation
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
CREATE TABLE users (
|
|
45
|
+
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
46
|
+
name VARCHAR2(255) NOT NULL,
|
|
47
|
+
email VARCHAR2(255) NOT NULL,
|
|
48
|
+
age NUMBER NOT NULL
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
[← Back to main docs](../../README.md)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# PostgreSQL Adapter
|
|
2
|
+
|
|
3
|
+
Uses the [pg](https://www.npmjs.com/package/pg) driver with connection pooling.
|
|
4
|
+
|
|
5
|
+
## Connection
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const { init, db, model, route } = require("db-model-router");
|
|
9
|
+
init("postgres");
|
|
10
|
+
|
|
11
|
+
db.connect({
|
|
12
|
+
host: "localhost",
|
|
13
|
+
port: 5432,
|
|
14
|
+
user: "postgres",
|
|
15
|
+
password: "password",
|
|
16
|
+
database: "my_app",
|
|
17
|
+
connectionLimit: 50, // pool max
|
|
18
|
+
dateStrings: false, // set true to return dates as strings
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Environment Variables
|
|
23
|
+
|
|
24
|
+
```env
|
|
25
|
+
PG_HOST=localhost
|
|
26
|
+
PG_PORT=5432
|
|
27
|
+
PG_USER=postgres
|
|
28
|
+
PG_PASSWORD=password
|
|
29
|
+
PG_DB=test_db
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- Supports MySQL-style `?` placeholders in raw `db.query()` calls — they are auto-translated to `$1, $2, ...`
|
|
35
|
+
- `SERIAL` / `BIGSERIAL` primary keys are auto-detected via `pg_index`
|
|
36
|
+
- `ON CONFLICT` is used for upsert operations
|
|
37
|
+
- Includes a SQL translator layer that converts common MySQL DDL/DML to PostgreSQL syntax
|
|
38
|
+
|
|
39
|
+
## Table Creation
|
|
40
|
+
|
|
41
|
+
```sql
|
|
42
|
+
CREATE TABLE users (
|
|
43
|
+
id SERIAL PRIMARY KEY,
|
|
44
|
+
name VARCHAR NOT NULL,
|
|
45
|
+
email VARCHAR NOT NULL,
|
|
46
|
+
age INTEGER NOT NULL
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
[← Back to main docs](../../README.md)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Redis Adapter
|
|
2
|
+
|
|
3
|
+
Uses [ioredis](https://www.npmjs.com/package/ioredis). Records are stored as Redis hashes with key pattern `{table}:{id}`.
|
|
4
|
+
|
|
5
|
+
## Connection
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const { init, db, model, route } = require("db-model-router");
|
|
9
|
+
init("redis");
|
|
10
|
+
|
|
11
|
+
db.connect({
|
|
12
|
+
host: "localhost",
|
|
13
|
+
port: 6379,
|
|
14
|
+
password: "", // optional
|
|
15
|
+
db: 0, // Redis DB index, optional
|
|
16
|
+
primaryKey: "id", // field used as the hash key suffix
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Environment Variables
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
REDIS_HOST=localhost
|
|
24
|
+
REDIS_PORT=6379
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Notes
|
|
28
|
+
|
|
29
|
+
- Each record is a Redis hash at key `{table}:{primaryKey}`
|
|
30
|
+
- Auto-incrementing IDs use `INCR {table}:__seq`
|
|
31
|
+
- All filtering, sorting, and pagination happen in-memory (full SCAN)
|
|
32
|
+
- Redis stores all values as strings — numeric coercion is applied on read
|
|
33
|
+
- Nested objects are JSON-serialized into hash fields
|
|
34
|
+
- Best suited for small-to-medium datasets where Redis is already in the stack
|
|
35
|
+
|
|
36
|
+
## Model Definition
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const users = model(
|
|
40
|
+
db,
|
|
41
|
+
"users",
|
|
42
|
+
{
|
|
43
|
+
id: "string",
|
|
44
|
+
name: "required|string",
|
|
45
|
+
email: "required|string",
|
|
46
|
+
age: "required|integer",
|
|
47
|
+
},
|
|
48
|
+
"id",
|
|
49
|
+
["id"],
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
[← Back to main docs](../../README.md)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SQLite3 Adapter
|
|
2
|
+
|
|
3
|
+
Uses [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) for synchronous, high-performance SQLite access.
|
|
4
|
+
|
|
5
|
+
## Connection
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const { init, db, model, route } = require("db-model-router");
|
|
9
|
+
init("sqlite3");
|
|
10
|
+
|
|
11
|
+
db.connect({ database: "./data.db" });
|
|
12
|
+
// or in-memory:
|
|
13
|
+
db.connect({ database: ":memory:" });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
| Option | Description |
|
|
19
|
+
| --------------- | --------------------------- |
|
|
20
|
+
| `database` | File path or `:memory:` |
|
|
21
|
+
| `readonly` | Open in read-only mode |
|
|
22
|
+
| `fileMustExist` | Throw if file doesn't exist |
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- All operations are synchronous (wrapped to match the async model API)
|
|
27
|
+
- WAL journal mode is enabled by default for better concurrency
|
|
28
|
+
- Uses `INSERT OR IGNORE` for conflict handling
|
|
29
|
+
- `ON CONFLICT ... DO UPDATE SET` for upsert
|
|
30
|
+
- No Docker container needed — runs in-process
|
|
31
|
+
|
|
32
|
+
## Table Creation
|
|
33
|
+
|
|
34
|
+
```sql
|
|
35
|
+
CREATE TABLE users (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
name TEXT NOT NULL,
|
|
38
|
+
email TEXT NOT NULL,
|
|
39
|
+
age INTEGER NOT NULL
|
|
40
|
+
);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
[← Back to main docs](../../README.md)
|
|
@@ -13,22 +13,27 @@ const {
|
|
|
13
13
|
generateChildTestFile,
|
|
14
14
|
} = require("../generate-route");
|
|
15
15
|
const { generateOpenAPISpec } = require("../generate-openapi");
|
|
16
|
-
const {
|
|
16
|
+
const { generateMigrationFiles } = require("../generate-migration");
|
|
17
|
+
const { generateDocsRoute } = require("../generate-docs-route");
|
|
18
|
+
const { generateDbManager } = require("../generate-db-manager");
|
|
19
|
+
const { migrationTimestamp } = require("../init/generators");
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Generate command handler for the unified CLI.
|
|
20
23
|
*
|
|
21
24
|
* Reads a schema file, converts to ModelMeta[], and generates
|
|
22
|
-
* models, routes, tests,
|
|
25
|
+
* models, routes, tests, OpenAPI spec, migrations, and docs route.
|
|
23
26
|
*
|
|
24
27
|
* Supported flags:
|
|
25
|
-
* --from
|
|
26
|
-
* --models
|
|
27
|
-
* --routes
|
|
28
|
-
* --openapi
|
|
29
|
-
* --tests
|
|
30
|
-
* --
|
|
31
|
-
* --
|
|
28
|
+
* --from Path to schema file (default: dbmr.schema.json)
|
|
29
|
+
* --models Generate only model files
|
|
30
|
+
* --routes Generate only route files (including child routes and index)
|
|
31
|
+
* --openapi Generate only OpenAPI spec + docs route
|
|
32
|
+
* --tests Generate only test files
|
|
33
|
+
* --migrations Generate only migration files
|
|
34
|
+
* --db-manager Generate DB Manager UI (SQL adapters only)
|
|
35
|
+
* --dry-run Report planned files without writing
|
|
36
|
+
* --json Output JSON result via ctx
|
|
32
37
|
*
|
|
33
38
|
* When no artifact flags are provided, all artifact types are generated.
|
|
34
39
|
*
|
|
@@ -80,13 +85,16 @@ async function generate(args, flags, ctx) {
|
|
|
80
85
|
args.routes === true ||
|
|
81
86
|
args.openapi === true ||
|
|
82
87
|
args.tests === true ||
|
|
83
|
-
args
|
|
88
|
+
args.migrations === true ||
|
|
89
|
+
args["db-manager"] === true;
|
|
84
90
|
|
|
85
91
|
const genModels = !hasArtifactFlag || args.models === true;
|
|
86
92
|
const genRoutes = !hasArtifactFlag || args.routes === true;
|
|
87
93
|
const genOpenapi = !hasArtifactFlag || args.openapi === true;
|
|
88
94
|
const genTests = !hasArtifactFlag || args.tests === true;
|
|
89
|
-
const
|
|
95
|
+
const genMigrations = !hasArtifactFlag || args.migrations === true;
|
|
96
|
+
//const genDbManager = !hasArtifactFlag || args["db-manager"] === true;
|
|
97
|
+
const genDbManager = false;
|
|
90
98
|
|
|
91
99
|
const modelsRelPath = "../models";
|
|
92
100
|
const baseDir = process.cwd();
|
|
@@ -106,57 +114,93 @@ async function generate(args, flags, ctx) {
|
|
|
106
114
|
|
|
107
115
|
// --- Route files ---
|
|
108
116
|
if (genRoutes) {
|
|
109
|
-
//
|
|
117
|
+
// Collect child tables to skip generating top-level route files for them
|
|
118
|
+
const nestedChildren = new Set();
|
|
119
|
+
for (const rel of relationships) {
|
|
120
|
+
nestedChildren.add(rel.child);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// One route per top-level table (skip children)
|
|
110
124
|
for (const m of meta) {
|
|
125
|
+
if (nestedChildren.has(m.table)) continue;
|
|
111
126
|
planned.push({
|
|
112
127
|
relPath: `routes/${m.table}.js`,
|
|
113
128
|
content: generateRouteFile(m.table, modelsRelPath),
|
|
114
129
|
});
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
// Child route files
|
|
132
|
+
// Child route files in subfolders: routes/<parent>/<child>.js
|
|
118
133
|
for (const rel of relationships) {
|
|
119
134
|
planned.push({
|
|
120
|
-
relPath: `routes/${rel.
|
|
135
|
+
relPath: `routes/${rel.parent}/${rel.child}.js`,
|
|
121
136
|
content: generateChildRouteFile(
|
|
122
137
|
rel.child,
|
|
123
138
|
rel.parent,
|
|
124
139
|
rel.foreignKey,
|
|
125
|
-
|
|
140
|
+
`../../models`,
|
|
126
141
|
),
|
|
127
142
|
});
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
// Routes index file
|
|
145
|
+
// Routes index file (include docs route when openapi is being generated)
|
|
131
146
|
planned.push({
|
|
132
147
|
relPath: "routes/index.js",
|
|
133
|
-
content: generateRoutesIndexFile(tableNames, relationships
|
|
148
|
+
content: generateRoutesIndexFile(tableNames, relationships, {
|
|
149
|
+
includeDocs: genOpenapi,
|
|
150
|
+
}),
|
|
134
151
|
});
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
// --- OpenAPI spec ---
|
|
154
|
+
// --- OpenAPI spec + docs route ---
|
|
138
155
|
if (genOpenapi) {
|
|
139
156
|
planned.push({
|
|
140
157
|
relPath: "openapi.json",
|
|
141
|
-
content:
|
|
158
|
+
content:
|
|
159
|
+
JSON.stringify(generateOpenAPISpec(meta, { relationships }), null, 2) +
|
|
160
|
+
"\n",
|
|
142
161
|
});
|
|
162
|
+
|
|
163
|
+
// Generate Swagger UI docs route
|
|
164
|
+
planned.push({
|
|
165
|
+
relPath: "routes/docs.js",
|
|
166
|
+
content: generateDocsRoute(),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Migration files ---
|
|
171
|
+
if (genMigrations) {
|
|
172
|
+
const migrationFiles = generateMigrationFiles(schema);
|
|
173
|
+
const ts = migrationTimestamp(new Date());
|
|
174
|
+
for (const mf of migrationFiles) {
|
|
175
|
+
planned.push({
|
|
176
|
+
relPath: `migrations/${ts}_${mf.filename}`,
|
|
177
|
+
content: mf.content,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
143
180
|
}
|
|
144
181
|
|
|
145
182
|
// --- Test files ---
|
|
146
183
|
if (genTests) {
|
|
184
|
+
// Collect child tables to skip generating top-level test files for them
|
|
185
|
+
const nestedChildrenForTests = new Set();
|
|
186
|
+
for (const rel of relationships) {
|
|
187
|
+
nestedChildrenForTests.add(rel.child);
|
|
188
|
+
}
|
|
189
|
+
|
|
147
190
|
for (const m of meta) {
|
|
191
|
+
if (nestedChildrenForTests.has(m.table)) continue;
|
|
148
192
|
planned.push({
|
|
149
193
|
relPath: `test/${m.table}.test.js`,
|
|
150
194
|
content: generateTestFile(m.table, m.primary_key),
|
|
151
195
|
});
|
|
152
196
|
}
|
|
153
197
|
|
|
154
|
-
// Child test files
|
|
198
|
+
// Child test files in subfolders: test/<parent>/<child>.test.js
|
|
155
199
|
for (const rel of relationships) {
|
|
156
200
|
const childMeta = meta.find((m) => m.table === rel.child);
|
|
157
201
|
const pk = childMeta ? childMeta.primary_key : "id";
|
|
158
202
|
planned.push({
|
|
159
|
-
relPath: `test/${rel.
|
|
203
|
+
relPath: `test/${rel.parent}/${rel.child}.test.js`,
|
|
160
204
|
content: generateChildTestFile(
|
|
161
205
|
rel.child,
|
|
162
206
|
rel.parent,
|
|
@@ -167,16 +211,36 @@ async function generate(args, flags, ctx) {
|
|
|
167
211
|
}
|
|
168
212
|
}
|
|
169
213
|
|
|
170
|
-
// ---
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
214
|
+
// --- DB Manager ---
|
|
215
|
+
if (genDbManager) {
|
|
216
|
+
const dbmOptions = {};
|
|
217
|
+
const envPath = path.join(baseDir, ".env");
|
|
218
|
+
const envExamplePath = path.join(baseDir, ".env.example");
|
|
219
|
+
const appJsPath = path.join(baseDir, "app.js");
|
|
220
|
+
const pkgJsonPath = path.join(baseDir, "package.json");
|
|
221
|
+
|
|
222
|
+
if (fs.existsSync(envPath)) {
|
|
223
|
+
dbmOptions.envContent = fs.readFileSync(envPath, "utf8");
|
|
224
|
+
}
|
|
225
|
+
if (fs.existsSync(envExamplePath)) {
|
|
226
|
+
dbmOptions.envExampleContent = fs.readFileSync(envExamplePath, "utf8");
|
|
227
|
+
}
|
|
228
|
+
if (fs.existsSync(appJsPath)) {
|
|
229
|
+
dbmOptions.appJsContent = fs.readFileSync(appJsPath, "utf8");
|
|
230
|
+
}
|
|
231
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
232
|
+
dbmOptions.packageJsonContent = fs.readFileSync(pkgJsonPath, "utf8");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const dbmResult = generateDbManager(schema, dbmOptions);
|
|
236
|
+
|
|
237
|
+
for (const f of dbmResult.files) {
|
|
238
|
+
planned.push(f);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const w of dbmResult.warnings) {
|
|
242
|
+
ctx.log(` warning: ${w}`);
|
|
243
|
+
}
|
|
180
244
|
}
|
|
181
245
|
|
|
182
246
|
// --- Process planned files ---
|
package/src/cli/commands/help.js
CHANGED
|
@@ -8,7 +8,7 @@ const COMMAND_HELP = {
|
|
|
8
8
|
init: `Usage: db-model-router init [options]
|
|
9
9
|
|
|
10
10
|
Scaffold a new project from a schema file or interactively.
|
|
11
|
-
Creates app.js, .env, commons/,
|
|
11
|
+
Creates app.js, .env, commons/, routes/, middleware/, and migrations/.
|
|
12
12
|
|
|
13
13
|
Options:
|
|
14
14
|
--from <path> Read adapter, framework, and options from a schema file
|
|
@@ -18,7 +18,7 @@ Options:
|
|
|
18
18
|
--db <name> Alias for --database
|
|
19
19
|
--session <type> Session store: memory, redis, database
|
|
20
20
|
--output <dir> Directory for backend source files (relative to cwd).
|
|
21
|
-
package.json and app.js stay in root; commons/,
|
|
21
|
+
package.json and app.js stay in root; commons/, routes/,
|
|
22
22
|
middleware/, and migrations/ go inside this folder.
|
|
23
23
|
--rateLimiting Enable rate limiting (default: yes)
|
|
24
24
|
--helmet Enable Helmet security headers (default: yes)
|
|
@@ -39,8 +39,8 @@ Generated files:
|
|
|
39
39
|
<output>/commons/add_migration.js Migration creation helper (also runs as script)
|
|
40
40
|
<output>/commons/security.js Helmet, rate limiting, custom headers
|
|
41
41
|
<output>/middleware/logger.js Winston + Loki request logger
|
|
42
|
-
<output>/
|
|
43
|
-
<output>/
|
|
42
|
+
<output>/routes/index.js Central route mounting
|
|
43
|
+
<output>/routes/health.js GET /health endpoint
|
|
44
44
|
<output>/migrations/ Initial migration files
|
|
45
45
|
|
|
46
46
|
Examples:
|
|
@@ -79,9 +79,10 @@ Options:
|
|
|
79
79
|
--from <path> Path to schema file (default: dbmr.schema.json)
|
|
80
80
|
--models Generate only model files
|
|
81
81
|
--routes Generate only route files (including child routes and index)
|
|
82
|
-
--openapi Generate only OpenAPI spec
|
|
82
|
+
--openapi Generate only OpenAPI spec + Swagger UI docs route
|
|
83
83
|
--tests Generate only test files
|
|
84
|
-
--
|
|
84
|
+
--migrations Generate only database migration files
|
|
85
|
+
--db-manager Generate DB Manager UI (SQL adapters only)
|
|
85
86
|
--yes Accept all defaults without prompting
|
|
86
87
|
--json Output machine-readable JSON
|
|
87
88
|
--dry-run Report planned files without writing
|
|
@@ -90,8 +91,11 @@ Options:
|
|
|
90
91
|
Generated files:
|
|
91
92
|
models/<table>.js Model with CRUD operations
|
|
92
93
|
routes/<table>.js Express route handlers
|
|
93
|
-
routes/<child
|
|
94
|
+
routes/<parent>/<child>.js Child route (scoped by FK)
|
|
95
|
+
routes/docs.js Swagger UI at /docs
|
|
94
96
|
routes/index.js Route mounting index
|
|
97
|
+
migrations/<timestamp>_create_tables.sql Database migration (SQL adapters)
|
|
98
|
+
migrations/<timestamp>_create_<t>.js Database migration (NoSQL adapters)
|
|
95
99
|
test/<table>.test.js CRUD endpoint tests
|
|
96
100
|
openapi.json OpenAPI 3.0 spec
|
|
97
101
|
llms.txt LLM quick reference
|
|
@@ -101,6 +105,7 @@ Examples:
|
|
|
101
105
|
db-model-router generate --from dbmr.schema.json
|
|
102
106
|
db-model-router generate --models --dry-run
|
|
103
107
|
db-model-router generate --routes --tests
|
|
108
|
+
db-model-router generate --migrations
|
|
104
109
|
db-model-router generate --from dbmr.schema.json --json`,
|
|
105
110
|
|
|
106
111
|
doctor: `Usage: db-model-router doctor [options]
|
package/src/cli/commands/init.js
CHANGED
|
@@ -165,8 +165,8 @@ function planFiles(answers, outputDir) {
|
|
|
165
165
|
`${prefix}commons/add_migration.js`,
|
|
166
166
|
`${prefix}commons/security.js`,
|
|
167
167
|
`${prefix}commons/db.js`,
|
|
168
|
-
`${prefix}
|
|
169
|
-
`${prefix}
|
|
168
|
+
`${prefix}routes/health.js`,
|
|
169
|
+
`${prefix}routes/index.js`,
|
|
170
170
|
`${prefix}migrations/<timestamp>_create_migrations_table` +
|
|
171
171
|
(isSql(answers.database) ? ".sql" : ".js"),
|
|
172
172
|
);
|
package/src/cli/diff-engine.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
generateChildTestFile,
|
|
12
12
|
} = require("./generate-route.js");
|
|
13
13
|
const { generateOpenAPISpec } = require("./generate-openapi.js");
|
|
14
|
+
const { generateDocsRoute } = require("./generate-docs-route.js");
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Simple line-by-line diff between two strings.
|
|
@@ -52,54 +53,63 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
52
53
|
const modelsRelPath = "../models";
|
|
53
54
|
const tableNames = meta.map((m) => m.table).sort();
|
|
54
55
|
|
|
56
|
+
// Collect child tables
|
|
57
|
+
const nestedChildren = new Set();
|
|
58
|
+
for (const rel of relationships) {
|
|
59
|
+
nestedChildren.add(rel.child);
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
// Model files
|
|
56
63
|
for (const m of meta) {
|
|
57
64
|
expected.set(`models/${m.table}.js`, generateModelFile(m));
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
// Route files (
|
|
67
|
+
// Route files (top-level only, skip children)
|
|
61
68
|
for (const m of meta) {
|
|
69
|
+
if (nestedChildren.has(m.table)) continue;
|
|
62
70
|
expected.set(
|
|
63
71
|
`routes/${m.table}.js`,
|
|
64
72
|
generateRouteFile(m.table, modelsRelPath),
|
|
65
73
|
);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
// Child route files
|
|
76
|
+
// Child route files in subfolders: routes/<parent>/<child>.js
|
|
69
77
|
for (const rel of relationships) {
|
|
70
|
-
const childMeta = meta.find((m) => m.table === rel.child);
|
|
71
|
-
const pk = childMeta ? childMeta.primary_key : "id";
|
|
72
78
|
expected.set(
|
|
73
|
-
`routes/${rel.
|
|
79
|
+
`routes/${rel.parent}/${rel.child}.js`,
|
|
74
80
|
generateChildRouteFile(
|
|
75
81
|
rel.child,
|
|
76
82
|
rel.parent,
|
|
77
83
|
rel.foreignKey,
|
|
78
|
-
|
|
84
|
+
`../../models`,
|
|
79
85
|
),
|
|
80
86
|
);
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
// Routes index file
|
|
89
|
+
// Routes index file (with docs route)
|
|
84
90
|
expected.set(
|
|
85
91
|
"routes/index.js",
|
|
86
|
-
generateRoutesIndexFile(tableNames, relationships),
|
|
92
|
+
generateRoutesIndexFile(tableNames, relationships, { includeDocs: true }),
|
|
87
93
|
);
|
|
88
94
|
|
|
89
|
-
//
|
|
95
|
+
// Docs route (Swagger UI)
|
|
96
|
+
expected.set("routes/docs.js", generateDocsRoute());
|
|
97
|
+
|
|
98
|
+
// Test files (top-level only, skip children)
|
|
90
99
|
for (const m of meta) {
|
|
100
|
+
if (nestedChildren.has(m.table)) continue;
|
|
91
101
|
expected.set(
|
|
92
102
|
`test/${m.table}.test.js`,
|
|
93
103
|
generateTestFile(m.table, m.primary_key),
|
|
94
104
|
);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
// Child test files
|
|
107
|
+
// Child test files in subfolders: test/<parent>/<child>.test.js
|
|
98
108
|
for (const rel of relationships) {
|
|
99
109
|
const childMeta = meta.find((m) => m.table === rel.child);
|
|
100
110
|
const pk = childMeta ? childMeta.primary_key : "id";
|
|
101
111
|
expected.set(
|
|
102
|
-
`test/${rel.
|
|
112
|
+
`test/${rel.parent}/${rel.child}.test.js`,
|
|
103
113
|
generateChildTestFile(rel.child, rel.parent, rel.foreignKey, pk),
|
|
104
114
|
);
|
|
105
115
|
}
|
|
@@ -107,7 +117,8 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
107
117
|
// OpenAPI spec
|
|
108
118
|
expected.set(
|
|
109
119
|
"openapi.json",
|
|
110
|
-
JSON.stringify(generateOpenAPISpec(meta), null, 2) +
|
|
120
|
+
JSON.stringify(generateOpenAPISpec(meta, { relationships }), null, 2) +
|
|
121
|
+
"\n",
|
|
111
122
|
);
|
|
112
123
|
|
|
113
124
|
return expected;
|
|
@@ -115,7 +126,7 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
115
126
|
|
|
116
127
|
/**
|
|
117
128
|
* Scan known artifact directories on disk and return a set of relative paths
|
|
118
|
-
* that exist.
|
|
129
|
+
* that exist. Recursively scans subdirectories.
|
|
119
130
|
*
|
|
120
131
|
* @param {string} baseDir
|
|
121
132
|
* @returns {Set<string>}
|
|
@@ -123,22 +134,42 @@ function buildExpectedFiles(meta, relationships) {
|
|
|
123
134
|
function scanDiskFiles(baseDir) {
|
|
124
135
|
const files = new Set();
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
function scanDir(dir, prefix) {
|
|
138
|
+
const fullDir = path.join(baseDir, dir);
|
|
139
|
+
if (!fs.existsSync(fullDir)) return;
|
|
140
|
+
for (const entry of fs.readdirSync(fullDir, { withFileTypes: true })) {
|
|
141
|
+
const relPath = prefix
|
|
142
|
+
? `${prefix}/${entry.name}`
|
|
143
|
+
: `${dir}/${entry.name}`;
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
scanDir(path.join(dir, entry.name), relPath);
|
|
146
|
+
} else if (entry.name.endsWith(".js")) {
|
|
147
|
+
files.add(relPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
scanDir("models");
|
|
153
|
+
scanDir("routes");
|
|
131
154
|
|
|
132
|
-
|
|
155
|
+
// For test dir, only include .test.js files
|
|
156
|
+
function scanTestDir(dir, prefix) {
|
|
133
157
|
const fullDir = path.join(baseDir, dir);
|
|
134
|
-
if (!fs.existsSync(fullDir))
|
|
135
|
-
for (const
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
if (!fs.existsSync(fullDir)) return;
|
|
159
|
+
for (const entry of fs.readdirSync(fullDir, { withFileTypes: true })) {
|
|
160
|
+
const relPath = prefix
|
|
161
|
+
? `${prefix}/${entry.name}`
|
|
162
|
+
: `${dir}/${entry.name}`;
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
scanTestDir(path.join(dir, entry.name), relPath);
|
|
165
|
+
} else if (entry.name.endsWith(".test.js")) {
|
|
166
|
+
files.add(relPath);
|
|
138
167
|
}
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
|
|
171
|
+
scanTestDir("test");
|
|
172
|
+
|
|
142
173
|
// Check for openapi.json at root
|
|
143
174
|
const openapiPath = path.join(baseDir, "openapi.json");
|
|
144
175
|
if (fs.existsSync(openapiPath)) {
|