db-model-router 1.0.0
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/.env +7 -0
- package/LICENSE +201 -0
- package/README.md +505 -0
- package/docker-compose.yml +141 -0
- package/docs/README.md +208 -0
- package/docs/SKILL.md +202 -0
- package/docs/adapters/cockroachdb.md +49 -0
- package/docs/adapters/dynamodb.md +53 -0
- package/docs/adapters/mongodb.md +56 -0
- package/docs/adapters/mssql.md +55 -0
- package/docs/adapters/oracle.md +52 -0
- package/docs/adapters/postgres.md +50 -0
- package/docs/adapters/redis.md +53 -0
- package/docs/adapters/sqlite3.md +43 -0
- package/package.json +109 -0
- package/src/cli/generate-app.js +359 -0
- package/src/cli/generate-model.js +760 -0
- package/src/cli/generate-openapi.js +237 -0
- package/src/cli/generate-route.js +346 -0
- package/src/cockroachdb/db.js +563 -0
- package/src/commons/function.js +165 -0
- package/src/commons/model.js +444 -0
- package/src/commons/route.js +214 -0
- package/src/commons/validator.js +172 -0
- package/src/dynamodb/db.js +552 -0
- package/src/index.js +57 -0
- package/src/mongodb/db.js +381 -0
- package/src/mssql/db.js +461 -0
- package/src/mysql/db.js +527 -0
- package/src/oracle/db.js +855 -0
- package/src/oracle/sql_translator.js +406 -0
- package/src/postgres/db.js +666 -0
- package/src/postgres/ddl_translator.js +69 -0
- package/src/postgres/sql_translator.js +396 -0
- package/src/redis/db.js +448 -0
- package/src/serve.js +90 -0
- package/src/sqlite3/db.js +346 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const DB_TYPE_MAP = {
|
|
7
|
+
mysql: "mysql",
|
|
8
|
+
postgres: "postgres",
|
|
9
|
+
postgresql: "postgres",
|
|
10
|
+
sqlite3: "sqlite3",
|
|
11
|
+
mssql: "mssql",
|
|
12
|
+
oracle: "oracle",
|
|
13
|
+
cockroachdb: "cockroachdb",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_TYPES = Object.keys(DB_TYPE_MAP);
|
|
17
|
+
|
|
18
|
+
// --- Introspection queries per adapter ---
|
|
19
|
+
|
|
20
|
+
async function introspectMySQL(db) {
|
|
21
|
+
const tables = await db.query("SHOW TABLES");
|
|
22
|
+
const tableNames = tables.map((r) => Object.values(r)[0]);
|
|
23
|
+
const models = [];
|
|
24
|
+
for (const table of tableNames) {
|
|
25
|
+
const columns = await db.query("SHOW COLUMNS FROM `" + table + "`");
|
|
26
|
+
const pk = columns.find((c) => c.Key === "PRI");
|
|
27
|
+
const pkName = pk ? pk.Field : "id";
|
|
28
|
+
const indexes = await db.query("SHOW INDEX FROM `" + table + "`");
|
|
29
|
+
const uniqueCols = getUniqueColumns(
|
|
30
|
+
indexes,
|
|
31
|
+
"Key_name",
|
|
32
|
+
"Column_name",
|
|
33
|
+
pkName,
|
|
34
|
+
);
|
|
35
|
+
const allColNames = columns.map((c) => c.Field);
|
|
36
|
+
const option = detectOptionColumns(allColNames);
|
|
37
|
+
const structure = {};
|
|
38
|
+
for (const col of columns) {
|
|
39
|
+
if (pk && col.Field === pk.Field) continue;
|
|
40
|
+
if (isTimestampColumn(col.Field)) continue;
|
|
41
|
+
if (isSafeDeleteColumn(col.Field)) continue;
|
|
42
|
+
structure[col.Field] = buildRule(col);
|
|
43
|
+
}
|
|
44
|
+
models.push({
|
|
45
|
+
table,
|
|
46
|
+
primary_key: pkName,
|
|
47
|
+
unique: uniqueCols,
|
|
48
|
+
structure,
|
|
49
|
+
option,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return models;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Groups index rows by index name and returns the best unique key set.
|
|
57
|
+
* Prefers multi-column unique indexes over single-column ones.
|
|
58
|
+
* Excludes the PRIMARY key index.
|
|
59
|
+
* Falls back to [pkName] if no unique indexes found.
|
|
60
|
+
*/
|
|
61
|
+
function getUniqueColumns(indexRows, indexNameKey, columnNameKey, pkName) {
|
|
62
|
+
const groups = {};
|
|
63
|
+
for (const row of indexRows) {
|
|
64
|
+
const idxName = row[indexNameKey];
|
|
65
|
+
const colName = row[columnNameKey];
|
|
66
|
+
const isUnique =
|
|
67
|
+
row.unique === 1 ||
|
|
68
|
+
row.unique === true ||
|
|
69
|
+
row.Non_unique === 0 ||
|
|
70
|
+
row.is_unique === true;
|
|
71
|
+
if (!isUnique) continue;
|
|
72
|
+
if (idxName === "PRIMARY" || idxName === "pk" || idxName === pkName)
|
|
73
|
+
continue;
|
|
74
|
+
if (!groups[idxName]) groups[idxName] = [];
|
|
75
|
+
groups[idxName].push(colName);
|
|
76
|
+
}
|
|
77
|
+
const allGroups = Object.values(groups);
|
|
78
|
+
if (allGroups.length === 0) return [pkName];
|
|
79
|
+
// Prefer multi-column unique index, otherwise take the first one
|
|
80
|
+
const multi = allGroups.find((g) => g.length > 1);
|
|
81
|
+
if (multi) return multi;
|
|
82
|
+
// Flatten all single-column unique indexes
|
|
83
|
+
const flat = allGroups.flat();
|
|
84
|
+
return flat.length > 0 ? flat : [pkName];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function introspectPostgres(db, schema = "public") {
|
|
88
|
+
const tables = await db.query(
|
|
89
|
+
`/* PG_NATIVE */ SELECT tablename FROM pg_tables WHERE schemaname = $1`,
|
|
90
|
+
[schema],
|
|
91
|
+
);
|
|
92
|
+
const tableNames = tables.map((r) => r.tablename);
|
|
93
|
+
const models = [];
|
|
94
|
+
for (const table of tableNames) {
|
|
95
|
+
const columns = await db.query(
|
|
96
|
+
`/* PG_NATIVE */ SELECT column_name, data_type, is_nullable, column_default
|
|
97
|
+
FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2
|
|
98
|
+
ORDER BY ordinal_position`,
|
|
99
|
+
[schema, table],
|
|
100
|
+
);
|
|
101
|
+
const pkResult = await db.query(
|
|
102
|
+
`/* PG_NATIVE */ SELECT a.attname FROM pg_index i
|
|
103
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
104
|
+
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
|
|
105
|
+
[table],
|
|
106
|
+
);
|
|
107
|
+
const pk = pkResult.length > 0 ? pkResult[0].attname : "id";
|
|
108
|
+
// Get unique indexes grouped by index name
|
|
109
|
+
const uniqueIdxResult = await db.query(
|
|
110
|
+
`/* PG_NATIVE */ SELECT ic.relname AS index_name, a.attname AS column_name
|
|
111
|
+
FROM pg_index i
|
|
112
|
+
JOIN pg_class ic ON ic.oid = i.indexrelid
|
|
113
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
114
|
+
WHERE i.indrelid = $1::regclass AND i.indisunique AND NOT i.indisprimary
|
|
115
|
+
ORDER BY ic.relname, a.attnum`,
|
|
116
|
+
[table],
|
|
117
|
+
);
|
|
118
|
+
const uniqueCols = groupUniqueIndexes(
|
|
119
|
+
uniqueIdxResult,
|
|
120
|
+
"index_name",
|
|
121
|
+
"column_name",
|
|
122
|
+
pk,
|
|
123
|
+
);
|
|
124
|
+
const allColNames = columns.map((c) => c.column_name);
|
|
125
|
+
const option = detectOptionColumns(allColNames);
|
|
126
|
+
const structure = {};
|
|
127
|
+
for (const col of columns) {
|
|
128
|
+
if (col.column_name === pk) continue;
|
|
129
|
+
if (isTimestampColumn(col.column_name)) continue;
|
|
130
|
+
if (isSafeDeleteColumn(col.column_name)) continue;
|
|
131
|
+
structure[col.column_name] = buildRulePg(col);
|
|
132
|
+
}
|
|
133
|
+
models.push({
|
|
134
|
+
table,
|
|
135
|
+
primary_key: pk,
|
|
136
|
+
unique: uniqueCols,
|
|
137
|
+
structure,
|
|
138
|
+
option,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return models;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Groups unique index query results by index name and picks the best set.
|
|
146
|
+
* Prefers multi-column unique indexes. Falls back to [pkName].
|
|
147
|
+
*/
|
|
148
|
+
function groupUniqueIndexes(rows, indexNameKey, columnNameKey, pkName) {
|
|
149
|
+
const groups = {};
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
const idxName = row[indexNameKey];
|
|
152
|
+
const colName = row[columnNameKey];
|
|
153
|
+
if (!groups[idxName]) groups[idxName] = [];
|
|
154
|
+
groups[idxName].push(colName);
|
|
155
|
+
}
|
|
156
|
+
const allGroups = Object.values(groups);
|
|
157
|
+
if (allGroups.length === 0) return [pkName];
|
|
158
|
+
const multi = allGroups.find((g) => g.length > 1);
|
|
159
|
+
if (multi) return multi;
|
|
160
|
+
const flat = allGroups.flat();
|
|
161
|
+
return flat.length > 0 ? flat : [pkName];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function introspectSQLite3(db) {
|
|
165
|
+
const tables = db
|
|
166
|
+
.query(
|
|
167
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
|
168
|
+
)
|
|
169
|
+
.map((r) => r.name);
|
|
170
|
+
const models = [];
|
|
171
|
+
for (const table of tables) {
|
|
172
|
+
const columns = db.query("PRAGMA table_info(`" + table + "`)");
|
|
173
|
+
const pk = columns.find((c) => c.pk === 1);
|
|
174
|
+
const pkName = pk ? pk.name : "id";
|
|
175
|
+
const uniqueIdxs = db.query("PRAGMA index_list(`" + table + "`)");
|
|
176
|
+
// Group unique index columns by index name
|
|
177
|
+
const idxGroups = {};
|
|
178
|
+
for (const idx of uniqueIdxs) {
|
|
179
|
+
if (idx.unique) {
|
|
180
|
+
const info = db.query("PRAGMA index_info(`" + idx.name + "`)");
|
|
181
|
+
idxGroups[idx.name] = info.map((i) => i.name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const allGroups = Object.values(idxGroups);
|
|
185
|
+
let uniqueCols;
|
|
186
|
+
if (allGroups.length === 0) {
|
|
187
|
+
uniqueCols = [pkName];
|
|
188
|
+
} else {
|
|
189
|
+
const multi = allGroups.find((g) => g.length > 1);
|
|
190
|
+
if (multi) {
|
|
191
|
+
uniqueCols = multi;
|
|
192
|
+
} else {
|
|
193
|
+
uniqueCols = allGroups.flat();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const allColNames = columns.map((c) => c.name);
|
|
197
|
+
const option = detectOptionColumns(allColNames);
|
|
198
|
+
const structure = {};
|
|
199
|
+
for (const col of columns) {
|
|
200
|
+
if (pk && col.name === pk.name) continue;
|
|
201
|
+
if (isTimestampColumn(col.name)) continue;
|
|
202
|
+
if (isSafeDeleteColumn(col.name)) continue;
|
|
203
|
+
structure[col.name] = buildRuleSqlite(col);
|
|
204
|
+
}
|
|
205
|
+
models.push({
|
|
206
|
+
table,
|
|
207
|
+
primary_key: pkName,
|
|
208
|
+
unique: uniqueCols,
|
|
209
|
+
structure,
|
|
210
|
+
option,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return models;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function introspectMSSQL(db) {
|
|
217
|
+
const tables = await db.query(
|
|
218
|
+
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'",
|
|
219
|
+
);
|
|
220
|
+
const tableNames = (tables.recordset || tables).map((r) => r.TABLE_NAME);
|
|
221
|
+
const models = [];
|
|
222
|
+
for (const table of tableNames) {
|
|
223
|
+
const columns = await db.query(
|
|
224
|
+
`SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
|
225
|
+
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '${table}'
|
|
226
|
+
ORDER BY ORDINAL_POSITION`,
|
|
227
|
+
);
|
|
228
|
+
const colRows = columns.recordset || columns;
|
|
229
|
+
const pkResult = await db.query(
|
|
230
|
+
`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
231
|
+
WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + CONSTRAINT_NAME), 'IsPrimaryKey') = 1
|
|
232
|
+
AND TABLE_NAME = '${table}'`,
|
|
233
|
+
);
|
|
234
|
+
const pkRows = pkResult.recordset || pkResult;
|
|
235
|
+
const pk = pkRows.length > 0 ? pkRows[0].COLUMN_NAME : "id";
|
|
236
|
+
// Get unique constraints grouped by constraint name (excluding PK)
|
|
237
|
+
const uniqueResult = await db.query(
|
|
238
|
+
`SELECT tc.CONSTRAINT_NAME, col.COLUMN_NAME
|
|
239
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
240
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE col ON tc.CONSTRAINT_NAME = col.CONSTRAINT_NAME
|
|
241
|
+
WHERE tc.TABLE_NAME = '${table}' AND tc.CONSTRAINT_TYPE = 'UNIQUE'
|
|
242
|
+
ORDER BY tc.CONSTRAINT_NAME, col.ORDINAL_POSITION`,
|
|
243
|
+
);
|
|
244
|
+
const uniqueRows = uniqueResult.recordset || uniqueResult;
|
|
245
|
+
const uniqueCols = groupUniqueIndexes(
|
|
246
|
+
uniqueRows,
|
|
247
|
+
"CONSTRAINT_NAME",
|
|
248
|
+
"COLUMN_NAME",
|
|
249
|
+
pk,
|
|
250
|
+
);
|
|
251
|
+
const allColNames = colRows.map((c) => c.COLUMN_NAME);
|
|
252
|
+
const option = detectOptionColumns(allColNames);
|
|
253
|
+
const structure = {};
|
|
254
|
+
for (const col of colRows) {
|
|
255
|
+
if (col.COLUMN_NAME === pk) continue;
|
|
256
|
+
if (isTimestampColumn(col.COLUMN_NAME)) continue;
|
|
257
|
+
if (isSafeDeleteColumn(col.COLUMN_NAME)) continue;
|
|
258
|
+
structure[col.COLUMN_NAME] = buildRuleMssql(col);
|
|
259
|
+
}
|
|
260
|
+
models.push({
|
|
261
|
+
table,
|
|
262
|
+
primary_key: pk,
|
|
263
|
+
unique: uniqueCols,
|
|
264
|
+
structure,
|
|
265
|
+
option,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return models;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function introspectOracle(db) {
|
|
272
|
+
const tables = await db.query(
|
|
273
|
+
"/* ORACLE_NATIVE */ SELECT table_name FROM user_tables ORDER BY table_name",
|
|
274
|
+
[],
|
|
275
|
+
);
|
|
276
|
+
const tableNames = tables.map((r) => r.table_name || r.TABLE_NAME);
|
|
277
|
+
const models = [];
|
|
278
|
+
for (const table of tableNames) {
|
|
279
|
+
const columns = await db.query(
|
|
280
|
+
`/* ORACLE_NATIVE */ SELECT column_name, data_type, nullable, data_default
|
|
281
|
+
FROM user_tab_columns WHERE table_name = :1 ORDER BY column_id`,
|
|
282
|
+
[table.toUpperCase()],
|
|
283
|
+
);
|
|
284
|
+
const pkResult = await db.query(
|
|
285
|
+
`/* ORACLE_NATIVE */ SELECT cols.column_name FROM user_constraints cons
|
|
286
|
+
JOIN user_cons_columns cols ON cons.constraint_name = cols.constraint_name
|
|
287
|
+
WHERE cons.table_name = :1 AND cons.constraint_type = 'P'`,
|
|
288
|
+
[table.toUpperCase()],
|
|
289
|
+
);
|
|
290
|
+
const pkRows = pkResult.map((r) =>
|
|
291
|
+
(r.column_name || r.COLUMN_NAME || "").toLowerCase(),
|
|
292
|
+
);
|
|
293
|
+
const pk = pkRows.length > 0 ? pkRows[0] : "id";
|
|
294
|
+
// Get unique constraints grouped by constraint name (excluding PK)
|
|
295
|
+
const uniqueResult = await db.query(
|
|
296
|
+
`/* ORACLE_NATIVE */ SELECT cons.constraint_name, cols.column_name
|
|
297
|
+
FROM user_constraints cons
|
|
298
|
+
JOIN user_cons_columns cols ON cons.constraint_name = cols.constraint_name
|
|
299
|
+
WHERE cons.table_name = :1 AND cons.constraint_type = 'U'
|
|
300
|
+
ORDER BY cons.constraint_name, cols.position`,
|
|
301
|
+
[table.toUpperCase()],
|
|
302
|
+
);
|
|
303
|
+
const uniqueRows = uniqueResult.map((r) => ({
|
|
304
|
+
constraint_name: (
|
|
305
|
+
r.constraint_name ||
|
|
306
|
+
r.CONSTRAINT_NAME ||
|
|
307
|
+
""
|
|
308
|
+
).toLowerCase(),
|
|
309
|
+
column_name: (r.column_name || r.COLUMN_NAME || "").toLowerCase(),
|
|
310
|
+
}));
|
|
311
|
+
const uniqueCols = groupUniqueIndexes(
|
|
312
|
+
uniqueRows,
|
|
313
|
+
"constraint_name",
|
|
314
|
+
"column_name",
|
|
315
|
+
pk,
|
|
316
|
+
);
|
|
317
|
+
const allColNames = columns.map((c) =>
|
|
318
|
+
(c.column_name || c.COLUMN_NAME || "").toLowerCase(),
|
|
319
|
+
);
|
|
320
|
+
const option = detectOptionColumns(allColNames);
|
|
321
|
+
const structure = {};
|
|
322
|
+
for (const col of columns) {
|
|
323
|
+
const colName = (col.column_name || col.COLUMN_NAME || "").toLowerCase();
|
|
324
|
+
if (colName === pk) continue;
|
|
325
|
+
if (isTimestampColumn(colName)) continue;
|
|
326
|
+
if (isSafeDeleteColumn(colName)) continue;
|
|
327
|
+
structure[colName] = buildRuleOracle(col);
|
|
328
|
+
}
|
|
329
|
+
models.push({
|
|
330
|
+
table: table.toLowerCase(),
|
|
331
|
+
primary_key: pk,
|
|
332
|
+
unique: uniqueCols,
|
|
333
|
+
structure,
|
|
334
|
+
option,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return models;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function introspectCockroachDB(db) {
|
|
341
|
+
// CockroachDB is PG-compatible, reuse postgres introspection
|
|
342
|
+
return introspectPostgres(db);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- Timestamp column detection ---
|
|
346
|
+
|
|
347
|
+
const CREATED_AT_VARIANTS = new Set([
|
|
348
|
+
"created_at",
|
|
349
|
+
"createdat",
|
|
350
|
+
"created",
|
|
351
|
+
"create_date",
|
|
352
|
+
"createdate",
|
|
353
|
+
"creation_date",
|
|
354
|
+
"creationdate",
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
const MODIFIED_AT_VARIANTS = new Set([
|
|
358
|
+
"modified_at",
|
|
359
|
+
"modifiedat",
|
|
360
|
+
"modified",
|
|
361
|
+
"updated_at",
|
|
362
|
+
"updatedat",
|
|
363
|
+
"updated",
|
|
364
|
+
"update_date",
|
|
365
|
+
"updatedate",
|
|
366
|
+
"modification_date",
|
|
367
|
+
"modificationdate",
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
const TIMESTAMP_COLUMNS = new Set([
|
|
371
|
+
...CREATED_AT_VARIANTS,
|
|
372
|
+
...MODIFIED_AT_VARIANTS,
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
const SAFE_DELETE_VARIANTS = new Set([
|
|
376
|
+
"is_deleted",
|
|
377
|
+
"isdeleted",
|
|
378
|
+
"deleted",
|
|
379
|
+
"is_removed",
|
|
380
|
+
"isremoved",
|
|
381
|
+
"removed",
|
|
382
|
+
"soft_deleted",
|
|
383
|
+
"softdeleted",
|
|
384
|
+
"is_active",
|
|
385
|
+
"isactive",
|
|
386
|
+
"is_archived",
|
|
387
|
+
"isarchived",
|
|
388
|
+
"archived",
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
function isTimestampColumn(name) {
|
|
392
|
+
return TIMESTAMP_COLUMNS.has(name.toLowerCase());
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isSafeDeleteColumn(name) {
|
|
396
|
+
return SAFE_DELETE_VARIANTS.has(name.toLowerCase());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isCreatedAtColumn(name) {
|
|
400
|
+
return CREATED_AT_VARIANTS.has(name.toLowerCase());
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isModifiedAtColumn(name) {
|
|
404
|
+
return MODIFIED_AT_VARIANTS.has(name.toLowerCase());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Scan column names and return detected option fields:
|
|
409
|
+
* { safeDelete, created_at, modified_at }
|
|
410
|
+
* Each is the actual column name from the DB, or null if not found.
|
|
411
|
+
*/
|
|
412
|
+
function detectOptionColumns(columnNames) {
|
|
413
|
+
let safeDelete = null;
|
|
414
|
+
let created_at = null;
|
|
415
|
+
let modified_at = null;
|
|
416
|
+
for (const name of columnNames) {
|
|
417
|
+
const lower = name.toLowerCase();
|
|
418
|
+
if (!safeDelete && isSafeDeleteColumn(lower)) safeDelete = name;
|
|
419
|
+
if (!created_at && isCreatedAtColumn(lower)) created_at = name;
|
|
420
|
+
if (!modified_at && isModifiedAtColumn(lower)) modified_at = name;
|
|
421
|
+
}
|
|
422
|
+
return { safeDelete, created_at, modified_at };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --- Rule builders per DB type ---
|
|
426
|
+
|
|
427
|
+
function buildRule(col) {
|
|
428
|
+
// MySQL SHOW COLUMNS format: { Field, Type, Null, Key, Default, Extra }
|
|
429
|
+
const nullable = col.Null === "YES";
|
|
430
|
+
const hasDefault =
|
|
431
|
+
col.Default !== null || (col.Extra && col.Extra.includes("auto_increment"));
|
|
432
|
+
const type = mysqlTypeToValidator(col.Type);
|
|
433
|
+
return (nullable || hasDefault ? "" : "required|") + type;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function buildRulePg(col) {
|
|
437
|
+
const nullable = col.is_nullable === "YES";
|
|
438
|
+
const hasDefault =
|
|
439
|
+
col.column_default !== null && col.column_default !== undefined;
|
|
440
|
+
const type = pgTypeToValidator(col.data_type);
|
|
441
|
+
return (nullable || hasDefault ? "" : "required|") + type;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function buildRuleSqlite(col) {
|
|
445
|
+
const nullable = col.notnull === 0;
|
|
446
|
+
const hasDefault = col.dflt_value !== null && col.dflt_value !== undefined;
|
|
447
|
+
const type = sqliteTypeToValidator(col.type);
|
|
448
|
+
return (nullable || hasDefault ? "" : "required|") + type;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function buildRuleMssql(col) {
|
|
452
|
+
const nullable = col.IS_NULLABLE === "YES";
|
|
453
|
+
const hasDefault =
|
|
454
|
+
col.COLUMN_DEFAULT !== null && col.COLUMN_DEFAULT !== undefined;
|
|
455
|
+
const type = mssqlTypeToValidator(col.DATA_TYPE);
|
|
456
|
+
return (nullable || hasDefault ? "" : "required|") + type;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function buildRuleOracle(col) {
|
|
460
|
+
const nullable = (col.nullable || col.NULLABLE || "Y") === "Y";
|
|
461
|
+
const rawDefault = col.data_default || col.DATA_DEFAULT;
|
|
462
|
+
const hasDefault =
|
|
463
|
+
rawDefault !== null &&
|
|
464
|
+
rawDefault !== undefined &&
|
|
465
|
+
String(rawDefault).trim() !== "";
|
|
466
|
+
const dataType = (col.data_type || col.DATA_TYPE || "").toUpperCase();
|
|
467
|
+
const type = oracleTypeToValidator(dataType);
|
|
468
|
+
return (nullable || hasDefault ? "" : "required|") + type;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function mysqlTypeToValidator(t) {
|
|
472
|
+
t = t.toLowerCase();
|
|
473
|
+
if (/int/.test(t)) return "integer";
|
|
474
|
+
if (/float|double|decimal|numeric/.test(t)) return "numeric";
|
|
475
|
+
if (/json/.test(t)) return "object";
|
|
476
|
+
if (/text|char|varchar|enum|set/.test(t)) return "string";
|
|
477
|
+
if (/blob|binary/.test(t)) return "string";
|
|
478
|
+
if (/date|time|year/.test(t)) return "string";
|
|
479
|
+
if (/bool/.test(t)) return "integer";
|
|
480
|
+
return "string";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function pgTypeToValidator(t) {
|
|
484
|
+
t = t.toLowerCase();
|
|
485
|
+
if (/int|serial/.test(t)) return "integer";
|
|
486
|
+
if (/numeric|decimal|real|double|float|money/.test(t)) return "numeric";
|
|
487
|
+
if (/json/.test(t)) return "object";
|
|
488
|
+
if (/bool/.test(t)) return "integer";
|
|
489
|
+
if (/char|text|varchar|uuid/.test(t)) return "string";
|
|
490
|
+
if (/date|time|interval/.test(t)) return "string";
|
|
491
|
+
return "string";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function sqliteTypeToValidator(t) {
|
|
495
|
+
t = (t || "").toLowerCase();
|
|
496
|
+
if (/int/.test(t)) return "integer";
|
|
497
|
+
if (/real|float|double|numeric|decimal/.test(t)) return "numeric";
|
|
498
|
+
if (/json/.test(t)) return "object";
|
|
499
|
+
if (/blob/.test(t)) return "string";
|
|
500
|
+
return "string";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function mssqlTypeToValidator(t) {
|
|
504
|
+
t = (t || "").toLowerCase();
|
|
505
|
+
if (/int|smallint|tinyint|bigint/.test(t)) return "integer";
|
|
506
|
+
if (/decimal|numeric|float|real|money/.test(t)) return "numeric";
|
|
507
|
+
if (/bit/.test(t)) return "integer";
|
|
508
|
+
if (/char|text|varchar|nchar|nvarchar|ntext/.test(t)) return "string";
|
|
509
|
+
if (/date|time|datetime/.test(t)) return "string";
|
|
510
|
+
if (/uniqueidentifier/.test(t)) return "string";
|
|
511
|
+
return "string";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function oracleTypeToValidator(t) {
|
|
515
|
+
if (/NUMBER|INTEGER|FLOAT|BINARY_FLOAT|BINARY_DOUBLE/.test(t))
|
|
516
|
+
return "numeric";
|
|
517
|
+
if (/CLOB|BLOB|RAW|LONG/.test(t)) return "string";
|
|
518
|
+
if (/DATE|TIMESTAMP/.test(t)) return "string";
|
|
519
|
+
if (/CHAR|VARCHAR|NCHAR|NVARCHAR/.test(t)) return "string";
|
|
520
|
+
return "string";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// --- Code generation ---
|
|
524
|
+
|
|
525
|
+
function safeVarName(name) {
|
|
526
|
+
// If the table name is a valid JS identifier, use it as-is
|
|
527
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return name;
|
|
528
|
+
// Otherwise fall back to a safe version
|
|
529
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function generateModelFile(m) {
|
|
533
|
+
const varName = safeVarName(m.table);
|
|
534
|
+
const structStr = JSON.stringify(m.structure, null, 4);
|
|
535
|
+
const uniqueStr = JSON.stringify(m.unique);
|
|
536
|
+
const opt = m.option || {};
|
|
537
|
+
const hasOption = opt.safeDelete || opt.created_at || opt.modified_at;
|
|
538
|
+
let optionStr = "";
|
|
539
|
+
if (hasOption) {
|
|
540
|
+
const parts = [];
|
|
541
|
+
if (opt.safeDelete) parts.push(`safeDelete: "${opt.safeDelete}"`);
|
|
542
|
+
if (opt.created_at) parts.push(`created_at: "${opt.created_at}"`);
|
|
543
|
+
if (opt.modified_at) parts.push(`modified_at: "${opt.modified_at}"`);
|
|
544
|
+
optionStr = `\n { ${parts.join(", ")} },`;
|
|
545
|
+
}
|
|
546
|
+
return `const { db, model } = require("db-model-router");
|
|
547
|
+
|
|
548
|
+
const ${varName} = model(
|
|
549
|
+
db,
|
|
550
|
+
"${m.table}",
|
|
551
|
+
${structStr},
|
|
552
|
+
"${m.primary_key}",
|
|
553
|
+
${uniqueStr},${optionStr}
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
module.exports = ${varName};
|
|
557
|
+
`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function generateIndexFile(models) {
|
|
561
|
+
let imports = "";
|
|
562
|
+
let exports = "";
|
|
563
|
+
for (const m of models) {
|
|
564
|
+
const varName = safeVarName(m.table);
|
|
565
|
+
imports += `const ${varName} = require("./${m.table}");\n`;
|
|
566
|
+
exports += ` ${varName},\n`;
|
|
567
|
+
}
|
|
568
|
+
return `${imports}
|
|
569
|
+
module.exports = {
|
|
570
|
+
${exports}};
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// --- Main CLI ---
|
|
575
|
+
|
|
576
|
+
async function main() {
|
|
577
|
+
const args = parseArgs(process.argv.slice(2));
|
|
578
|
+
|
|
579
|
+
if (args.help) {
|
|
580
|
+
printUsage();
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const dbType = DB_TYPE_MAP[(args.type || "").toLowerCase()];
|
|
585
|
+
if (!dbType) {
|
|
586
|
+
console.error(
|
|
587
|
+
`Error: Unsupported --type "${args.type}". Supported: ${SUPPORTED_TYPES.join(", ")}`,
|
|
588
|
+
);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const outputDir = path.resolve(args.output || "./models");
|
|
593
|
+
|
|
594
|
+
// Load env file if provided
|
|
595
|
+
if (args.env) {
|
|
596
|
+
require("dotenv").config({ path: path.resolve(args.env) });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const config = buildConfig(args);
|
|
600
|
+
|
|
601
|
+
console.log(`Connecting to ${dbType}...`);
|
|
602
|
+
|
|
603
|
+
const restRouter = require("../index.js");
|
|
604
|
+
restRouter.init(dbType);
|
|
605
|
+
const db = restRouter.db;
|
|
606
|
+
db.connect(config);
|
|
607
|
+
|
|
608
|
+
let models;
|
|
609
|
+
try {
|
|
610
|
+
switch (dbType) {
|
|
611
|
+
case "mysql":
|
|
612
|
+
models = await introspectMySQL(db);
|
|
613
|
+
break;
|
|
614
|
+
case "postgres":
|
|
615
|
+
models = await introspectPostgres(db, args.schema || "public");
|
|
616
|
+
break;
|
|
617
|
+
case "sqlite3":
|
|
618
|
+
models = await introspectSQLite3(db);
|
|
619
|
+
break;
|
|
620
|
+
case "mssql":
|
|
621
|
+
models = await introspectMSSQL(db);
|
|
622
|
+
break;
|
|
623
|
+
case "oracle":
|
|
624
|
+
models = await introspectOracle(db);
|
|
625
|
+
break;
|
|
626
|
+
case "cockroachdb":
|
|
627
|
+
models = await introspectCockroachDB(db);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.error("Introspection failed:", err.message);
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!models || models.length === 0) {
|
|
636
|
+
console.log("No tables found.");
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Apply --tables filter if provided
|
|
641
|
+
if (args.tables) {
|
|
642
|
+
const tableSpecs = args.tables.split(",").map((s) => s.trim());
|
|
643
|
+
const allowedTables = new Set(
|
|
644
|
+
tableSpecs.map((s) => (s.includes(".") ? s.split(".").pop() : s)),
|
|
645
|
+
);
|
|
646
|
+
// Also include parent tables referenced in dot notation
|
|
647
|
+
for (const spec of tableSpecs) {
|
|
648
|
+
if (spec.includes(".")) {
|
|
649
|
+
const parts = spec.split(".");
|
|
650
|
+
for (const p of parts) allowedTables.add(p);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
models = models.filter((m) => allowedTables.has(m.table));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Write files
|
|
657
|
+
if (!fs.existsSync(outputDir)) {
|
|
658
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const m of models) {
|
|
662
|
+
const filePath = path.join(outputDir, m.table + ".js");
|
|
663
|
+
fs.writeFileSync(filePath, generateModelFile(m));
|
|
664
|
+
console.log(` Created ${filePath}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const indexPath = path.join(outputDir, "index.js");
|
|
668
|
+
fs.writeFileSync(indexPath, generateIndexFile(models));
|
|
669
|
+
console.log(` Created ${indexPath}`);
|
|
670
|
+
|
|
671
|
+
console.log(`\nGenerated ${models.length} model(s) in ${outputDir}`);
|
|
672
|
+
|
|
673
|
+
// Disconnect
|
|
674
|
+
if (db.disconnect) await db.disconnect();
|
|
675
|
+
else if (db.close) await db.close();
|
|
676
|
+
process.exit(0);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function buildConfig(args) {
|
|
680
|
+
return {
|
|
681
|
+
host: args.host || process.env.DB_HOST || "localhost",
|
|
682
|
+
port: args.port || process.env.DB_PORT,
|
|
683
|
+
database: args.database || process.env.DB_NAME,
|
|
684
|
+
user: args.user || process.env.DB_USER,
|
|
685
|
+
password: args.password || process.env.DB_PASS,
|
|
686
|
+
// sqlite3
|
|
687
|
+
filename: args.database || process.env.DB_NAME,
|
|
688
|
+
// mssql
|
|
689
|
+
server: args.host || process.env.DB_HOST || "localhost",
|
|
690
|
+
options: { encrypt: false, trustServerCertificate: true },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function parseArgs(argv) {
|
|
695
|
+
const args = {};
|
|
696
|
+
for (let i = 0; i < argv.length; i++) {
|
|
697
|
+
const arg = argv[i];
|
|
698
|
+
if (arg.startsWith("--")) {
|
|
699
|
+
const key = arg.slice(2);
|
|
700
|
+
const next = argv[i + 1];
|
|
701
|
+
if (next && !next.startsWith("--")) {
|
|
702
|
+
args[key] = next;
|
|
703
|
+
i++;
|
|
704
|
+
} else {
|
|
705
|
+
args[key] = true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return args;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function printUsage() {
|
|
713
|
+
console.log(`
|
|
714
|
+
Usage: rest-router-generate-model --type <db_type> [options]
|
|
715
|
+
|
|
716
|
+
Options:
|
|
717
|
+
--type Database type (${SUPPORTED_TYPES.join(", ")})
|
|
718
|
+
--host Database host (default: localhost)
|
|
719
|
+
--port Database port
|
|
720
|
+
--database Database name (or file path for sqlite3)
|
|
721
|
+
--user Database user
|
|
722
|
+
--password Database password
|
|
723
|
+
--schema Schema name (postgres only, default: public)
|
|
724
|
+
--output Output directory (default: ./models)
|
|
725
|
+
--tables Comma-separated list of tables to generate (default: all)
|
|
726
|
+
Use dot notation for parent-child: posts.comments
|
|
727
|
+
--env Path to .env file to load
|
|
728
|
+
--help Show this help message
|
|
729
|
+
|
|
730
|
+
Examples:
|
|
731
|
+
rest-router-generate-model --type mysql --host localhost --database mydb --user root --password secret
|
|
732
|
+
rest-router-generate-model --type sqlite3 --database ./myapp.db --output ./src/models
|
|
733
|
+
rest-router-generate-model --type postgres --env .env --output ./models
|
|
734
|
+
`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (require.main === module) {
|
|
738
|
+
main().catch((err) => {
|
|
739
|
+
console.error("Error:", err.message);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Export for programmatic use / testing
|
|
745
|
+
module.exports = {
|
|
746
|
+
introspectMySQL,
|
|
747
|
+
introspectPostgres,
|
|
748
|
+
introspectSQLite3,
|
|
749
|
+
introspectMSSQL,
|
|
750
|
+
introspectOracle,
|
|
751
|
+
introspectCockroachDB,
|
|
752
|
+
generateModelFile,
|
|
753
|
+
generateIndexFile,
|
|
754
|
+
isTimestampColumn,
|
|
755
|
+
isSafeDeleteColumn,
|
|
756
|
+
isCreatedAtColumn,
|
|
757
|
+
isModifiedAtColumn,
|
|
758
|
+
detectOptionColumns,
|
|
759
|
+
safeVarName,
|
|
760
|
+
};
|