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.
@@ -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
+ };