@db-bridge/core 1.1.5 → 1.2.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/LICENSE +20 -20
- package/dist/cli/index.js +419 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +162 -114
- package/dist/index.js +327 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Berke Erdoğan
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Berke Erdoğan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'util';
|
|
3
|
-
import { resolve, basename, extname, join } from 'path';
|
|
3
|
+
import { resolve, dirname, basename, extname, join } from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
@@ -81,10 +81,15 @@ function applyDefaults(config) {
|
|
|
81
81
|
seeds: {
|
|
82
82
|
directory: "./src/seeds",
|
|
83
83
|
...config.seeds
|
|
84
|
+
},
|
|
85
|
+
types: {
|
|
86
|
+
output: "./src/types/database.ts",
|
|
87
|
+
camelCase: false,
|
|
88
|
+
...config.types
|
|
84
89
|
}
|
|
85
90
|
};
|
|
86
91
|
}
|
|
87
|
-
var MIGRATION_PATTERN = /^(\d{14})_(.+)\.(ts|js|mjs)$/;
|
|
92
|
+
var MIGRATION_PATTERN = /^(?:([_a-z]+)_)?(\d{14})_(.+)\.(ts|js|mjs)$/;
|
|
88
93
|
var MigrationLoader = class {
|
|
89
94
|
directory;
|
|
90
95
|
extensions;
|
|
@@ -111,12 +116,13 @@ var MigrationLoader = class {
|
|
|
111
116
|
if (!match) {
|
|
112
117
|
continue;
|
|
113
118
|
}
|
|
114
|
-
const [, timestamp, description] = match;
|
|
119
|
+
const [, prefix, timestamp, description] = match;
|
|
115
120
|
files.push({
|
|
116
121
|
name: basename(entry.name, ext),
|
|
117
122
|
path: join(this.directory, entry.name),
|
|
118
123
|
timestamp,
|
|
119
|
-
description: description.replaceAll("_", " ")
|
|
124
|
+
description: description.replaceAll("_", " "),
|
|
125
|
+
prefix
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
files.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
@@ -193,8 +199,10 @@ var MigrationLoader = class {
|
|
|
193
199
|
}
|
|
194
200
|
/**
|
|
195
201
|
* Generate a new migration filename
|
|
202
|
+
* @param description - Migration description
|
|
203
|
+
* @param prefix - Optional prefix (e.g., 'auth' -> auth_20250119_xxx.ts)
|
|
196
204
|
*/
|
|
197
|
-
static generateFilename(description) {
|
|
205
|
+
static generateFilename(description, prefix) {
|
|
198
206
|
const now = /* @__PURE__ */ new Date();
|
|
199
207
|
const timestamp = [
|
|
200
208
|
now.getFullYear(),
|
|
@@ -205,7 +213,8 @@ var MigrationLoader = class {
|
|
|
205
213
|
String(now.getSeconds()).padStart(2, "0")
|
|
206
214
|
].join("");
|
|
207
215
|
const sanitizedDescription = description.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "");
|
|
208
|
-
|
|
216
|
+
const sanitizedPrefix = prefix ? prefix.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "") : null;
|
|
217
|
+
return sanitizedPrefix ? `${sanitizedPrefix}_${timestamp}_${sanitizedDescription}.ts` : `${timestamp}_${sanitizedDescription}.ts`;
|
|
209
218
|
}
|
|
210
219
|
/**
|
|
211
220
|
* Get migration template content
|
|
@@ -1072,7 +1081,7 @@ var MigrationLock = class {
|
|
|
1072
1081
|
}
|
|
1073
1082
|
}
|
|
1074
1083
|
sleep(ms) {
|
|
1075
|
-
return new Promise((
|
|
1084
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1076
1085
|
}
|
|
1077
1086
|
};
|
|
1078
1087
|
|
|
@@ -1685,8 +1694,11 @@ var ForeignKeyChain = class {
|
|
|
1685
1694
|
var SchemaBuilder = class {
|
|
1686
1695
|
dialectInstance;
|
|
1687
1696
|
adapter;
|
|
1697
|
+
collectMode;
|
|
1698
|
+
collectedStatements = [];
|
|
1688
1699
|
constructor(options) {
|
|
1689
1700
|
this.adapter = options.adapter;
|
|
1701
|
+
this.collectMode = options.collectMode ?? false;
|
|
1690
1702
|
switch (options.dialect) {
|
|
1691
1703
|
case "mysql": {
|
|
1692
1704
|
this.dialectInstance = new MySQLDialect();
|
|
@@ -1792,6 +1804,14 @@ var SchemaBuilder = class {
|
|
|
1792
1804
|
* Execute SQL statement
|
|
1793
1805
|
*/
|
|
1794
1806
|
async execute(sql, params) {
|
|
1807
|
+
if (this.collectMode) {
|
|
1808
|
+
let statement = sql;
|
|
1809
|
+
if (params && params.length > 0) {
|
|
1810
|
+
statement += ` -- params: ${JSON.stringify(params)}`;
|
|
1811
|
+
}
|
|
1812
|
+
this.collectedStatements.push(statement);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1795
1815
|
if (this.adapter) {
|
|
1796
1816
|
await this.adapter.execute(sql, params);
|
|
1797
1817
|
} else {
|
|
@@ -1801,6 +1821,18 @@ var SchemaBuilder = class {
|
|
|
1801
1821
|
}
|
|
1802
1822
|
}
|
|
1803
1823
|
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Get collected SQL statements (only in collectMode)
|
|
1826
|
+
*/
|
|
1827
|
+
getCollectedStatements() {
|
|
1828
|
+
return [...this.collectedStatements];
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Clear collected SQL statements
|
|
1832
|
+
*/
|
|
1833
|
+
clearCollectedStatements() {
|
|
1834
|
+
this.collectedStatements = [];
|
|
1835
|
+
}
|
|
1804
1836
|
/**
|
|
1805
1837
|
* Execute SQL query
|
|
1806
1838
|
*/
|
|
@@ -2256,7 +2288,23 @@ var FileMigrationRunner = class {
|
|
|
2256
2288
|
const action = direction === "up" ? "Running" : "Rolling back";
|
|
2257
2289
|
this.options.logger.info(`${action}: ${migration.name}`);
|
|
2258
2290
|
if (this.options.dryRun) {
|
|
2259
|
-
|
|
2291
|
+
const schema = new SchemaBuilder({
|
|
2292
|
+
dialect: this.options.dialect,
|
|
2293
|
+
collectMode: true
|
|
2294
|
+
});
|
|
2295
|
+
await migration[direction](schema);
|
|
2296
|
+
const statements = schema.getCollectedStatements();
|
|
2297
|
+
if (statements.length > 0) {
|
|
2298
|
+
this.options.logger.info(`DRY RUN: ${direction.toUpperCase()} ${migration.name}`);
|
|
2299
|
+
this.options.logger.info("SQL statements that would be executed:");
|
|
2300
|
+
for (const sql of statements) {
|
|
2301
|
+
this.options.logger.info(` ${sql}`);
|
|
2302
|
+
}
|
|
2303
|
+
} else {
|
|
2304
|
+
this.options.logger.info(
|
|
2305
|
+
`DRY RUN: ${direction.toUpperCase()} ${migration.name} (no SQL statements)`
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2260
2308
|
return;
|
|
2261
2309
|
}
|
|
2262
2310
|
const transaction = migration.transactional ? await this.adapter.beginTransaction() : null;
|
|
@@ -2424,6 +2472,263 @@ async function freshCommand(options = {}) {
|
|
|
2424
2472
|
}
|
|
2425
2473
|
}
|
|
2426
2474
|
|
|
2475
|
+
// src/types/TypeGenerator.ts
|
|
2476
|
+
var SQL_TYPE_MAP = {
|
|
2477
|
+
// Integers
|
|
2478
|
+
tinyint: "number",
|
|
2479
|
+
smallint: "number",
|
|
2480
|
+
mediumint: "number",
|
|
2481
|
+
int: "number",
|
|
2482
|
+
integer: "number",
|
|
2483
|
+
bigint: "number",
|
|
2484
|
+
// Floats
|
|
2485
|
+
float: "number",
|
|
2486
|
+
double: "number",
|
|
2487
|
+
decimal: "number",
|
|
2488
|
+
numeric: "number",
|
|
2489
|
+
real: "number",
|
|
2490
|
+
// Strings
|
|
2491
|
+
char: "string",
|
|
2492
|
+
varchar: "string",
|
|
2493
|
+
tinytext: "string",
|
|
2494
|
+
text: "string",
|
|
2495
|
+
mediumtext: "string",
|
|
2496
|
+
longtext: "string",
|
|
2497
|
+
enum: "string",
|
|
2498
|
+
set: "string",
|
|
2499
|
+
// Binary
|
|
2500
|
+
binary: "Buffer",
|
|
2501
|
+
varbinary: "Buffer",
|
|
2502
|
+
tinyblob: "Buffer",
|
|
2503
|
+
blob: "Buffer",
|
|
2504
|
+
mediumblob: "Buffer",
|
|
2505
|
+
longblob: "Buffer",
|
|
2506
|
+
// Date/Time
|
|
2507
|
+
date: "Date",
|
|
2508
|
+
datetime: "Date",
|
|
2509
|
+
timestamp: "Date",
|
|
2510
|
+
time: "string",
|
|
2511
|
+
year: "number",
|
|
2512
|
+
// Boolean
|
|
2513
|
+
boolean: "boolean",
|
|
2514
|
+
bool: "boolean",
|
|
2515
|
+
// JSON
|
|
2516
|
+
json: "Record<string, unknown>",
|
|
2517
|
+
jsonb: "Record<string, unknown>",
|
|
2518
|
+
// UUID (PostgreSQL)
|
|
2519
|
+
uuid: "string",
|
|
2520
|
+
// Arrays (PostgreSQL)
|
|
2521
|
+
array: "unknown[]"
|
|
2522
|
+
};
|
|
2523
|
+
var TypeGenerator = class {
|
|
2524
|
+
constructor(adapter, dialect) {
|
|
2525
|
+
this.adapter = adapter;
|
|
2526
|
+
this.dialect = dialect;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Generate TypeScript interfaces from database schema
|
|
2530
|
+
*/
|
|
2531
|
+
async generate(options = {}) {
|
|
2532
|
+
const tables = await this.getTables(options);
|
|
2533
|
+
const output = [];
|
|
2534
|
+
if (options.header) {
|
|
2535
|
+
output.push(options.header);
|
|
2536
|
+
output.push("");
|
|
2537
|
+
} else {
|
|
2538
|
+
output.push("/**");
|
|
2539
|
+
output.push(" * Auto-generated TypeScript types from database schema");
|
|
2540
|
+
output.push(` * Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2541
|
+
output.push(" * DO NOT EDIT - This file is auto-generated");
|
|
2542
|
+
output.push(" */");
|
|
2543
|
+
output.push("");
|
|
2544
|
+
}
|
|
2545
|
+
for (const table of tables) {
|
|
2546
|
+
const tableInfo = await this.getTableInfo(table);
|
|
2547
|
+
const interfaceCode = this.generateInterface(tableInfo, options);
|
|
2548
|
+
output.push(interfaceCode);
|
|
2549
|
+
output.push("");
|
|
2550
|
+
}
|
|
2551
|
+
return output.join("\n");
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Get list of tables from database
|
|
2555
|
+
*/
|
|
2556
|
+
async getTables(options) {
|
|
2557
|
+
let tables;
|
|
2558
|
+
if (this.dialect === "mysql") {
|
|
2559
|
+
const result = await this.adapter.query(
|
|
2560
|
+
`SELECT table_name FROM information_schema.tables
|
|
2561
|
+
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'`
|
|
2562
|
+
);
|
|
2563
|
+
tables = result.rows.map((r) => r["table_name"] || r["TABLE_NAME"]).filter((t) => t !== null && t !== void 0);
|
|
2564
|
+
} else {
|
|
2565
|
+
const result = await this.adapter.query(
|
|
2566
|
+
`SELECT table_name FROM information_schema.tables
|
|
2567
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`
|
|
2568
|
+
);
|
|
2569
|
+
tables = result.rows.map((r) => r["table_name"] || r["TABLE_NAME"]).filter((t) => t !== null && t !== void 0);
|
|
2570
|
+
}
|
|
2571
|
+
if (options.tables && options.tables.length > 0) {
|
|
2572
|
+
tables = tables.filter((t) => options.tables.includes(t));
|
|
2573
|
+
}
|
|
2574
|
+
if (options.exclude && options.exclude.length > 0) {
|
|
2575
|
+
tables = tables.filter((t) => !options.exclude.includes(t));
|
|
2576
|
+
}
|
|
2577
|
+
tables = tables.filter((t) => !t.startsWith("db_migrations") && !t.startsWith("db_bridge"));
|
|
2578
|
+
return tables.sort();
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Get column information for a table
|
|
2582
|
+
*/
|
|
2583
|
+
async getTableInfo(tableName) {
|
|
2584
|
+
let columns;
|
|
2585
|
+
if (this.dialect === "mysql") {
|
|
2586
|
+
columns = await this.getMySQLColumns(tableName);
|
|
2587
|
+
} else {
|
|
2588
|
+
columns = await this.getPostgreSQLColumns(tableName);
|
|
2589
|
+
}
|
|
2590
|
+
return { name: tableName, columns };
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Get MySQL column information
|
|
2594
|
+
*/
|
|
2595
|
+
async getMySQLColumns(tableName) {
|
|
2596
|
+
const result = await this.adapter.query(`SHOW FULL COLUMNS FROM \`${tableName}\``);
|
|
2597
|
+
return result.rows.map((row) => ({
|
|
2598
|
+
name: row.Field,
|
|
2599
|
+
type: this.parseColumnType(row.Type),
|
|
2600
|
+
nullable: row.Null === "YES",
|
|
2601
|
+
defaultValue: row.Default,
|
|
2602
|
+
isPrimary: row.Key === "PRI",
|
|
2603
|
+
isAutoIncrement: row.Extra.includes("auto_increment"),
|
|
2604
|
+
comment: row.Comment || void 0
|
|
2605
|
+
}));
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Get PostgreSQL column information
|
|
2609
|
+
*/
|
|
2610
|
+
async getPostgreSQLColumns(tableName) {
|
|
2611
|
+
const result = await this.adapter.query(
|
|
2612
|
+
`
|
|
2613
|
+
SELECT
|
|
2614
|
+
c.column_name,
|
|
2615
|
+
c.data_type,
|
|
2616
|
+
c.is_nullable,
|
|
2617
|
+
c.column_default,
|
|
2618
|
+
c.is_identity,
|
|
2619
|
+
pgd.description
|
|
2620
|
+
FROM information_schema.columns c
|
|
2621
|
+
LEFT JOIN pg_catalog.pg_statio_all_tables st ON c.table_name = st.relname
|
|
2622
|
+
LEFT JOIN pg_catalog.pg_description pgd ON pgd.objoid = st.relid
|
|
2623
|
+
AND pgd.objsubid = c.ordinal_position
|
|
2624
|
+
WHERE c.table_name = $1
|
|
2625
|
+
ORDER BY c.ordinal_position
|
|
2626
|
+
`,
|
|
2627
|
+
[tableName]
|
|
2628
|
+
);
|
|
2629
|
+
return result.rows.map((row) => ({
|
|
2630
|
+
name: row.column_name,
|
|
2631
|
+
type: row.data_type,
|
|
2632
|
+
nullable: row.is_nullable === "YES",
|
|
2633
|
+
defaultValue: row.column_default,
|
|
2634
|
+
isPrimary: false,
|
|
2635
|
+
// Would need additional query
|
|
2636
|
+
isAutoIncrement: row.is_identity === "YES" || (row.column_default?.includes("nextval") ?? false),
|
|
2637
|
+
comment: row.description
|
|
2638
|
+
}));
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Parse column type (extract base type from full type string)
|
|
2642
|
+
*/
|
|
2643
|
+
parseColumnType(fullType) {
|
|
2644
|
+
const baseType = fullType.toLowerCase().replace(/\(.*\)/, "").trim();
|
|
2645
|
+
return baseType.replace(" unsigned", "");
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Map SQL type to TypeScript type
|
|
2649
|
+
*/
|
|
2650
|
+
mapType(sqlType, nullable) {
|
|
2651
|
+
const baseType = this.parseColumnType(sqlType);
|
|
2652
|
+
if (sqlType.toLowerCase().includes("tinyint(1)")) {
|
|
2653
|
+
return nullable ? "boolean | null" : "boolean";
|
|
2654
|
+
}
|
|
2655
|
+
const tsType = SQL_TYPE_MAP[baseType] || "unknown";
|
|
2656
|
+
return nullable ? `${tsType} | null` : tsType;
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Generate TypeScript interface for a table
|
|
2660
|
+
*/
|
|
2661
|
+
generateInterface(table, options) {
|
|
2662
|
+
const lines = [];
|
|
2663
|
+
const interfaceName = this.toInterfaceName(table.name);
|
|
2664
|
+
if (options.includeComments) {
|
|
2665
|
+
lines.push("/**");
|
|
2666
|
+
lines.push(` * ${interfaceName} - ${table.name} table`);
|
|
2667
|
+
lines.push(" */");
|
|
2668
|
+
}
|
|
2669
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
2670
|
+
for (const column of table.columns) {
|
|
2671
|
+
const propertyName = options.camelCase ? this.toCamelCase(column.name) : column.name;
|
|
2672
|
+
const tsType = this.mapType(column.type, column.nullable);
|
|
2673
|
+
const optional = options.optionalNullable && column.nullable ? "?" : "";
|
|
2674
|
+
if (options.includeComments && column.comment) {
|
|
2675
|
+
lines.push(` /** ${column.comment} */`);
|
|
2676
|
+
}
|
|
2677
|
+
lines.push(` ${propertyName}${optional}: ${tsType};`);
|
|
2678
|
+
}
|
|
2679
|
+
lines.push("}");
|
|
2680
|
+
return lines.join("\n");
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Convert table name to PascalCase interface name
|
|
2684
|
+
*/
|
|
2685
|
+
toInterfaceName(tableName) {
|
|
2686
|
+
return tableName.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Convert snake_case to camelCase
|
|
2690
|
+
*/
|
|
2691
|
+
toCamelCase(name) {
|
|
2692
|
+
return name.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
// src/cli/commands/generate-types.ts
|
|
2697
|
+
async function generateTypesCommand(options = {}) {
|
|
2698
|
+
let adapter;
|
|
2699
|
+
try {
|
|
2700
|
+
const config = await loadConfig();
|
|
2701
|
+
adapter = await createAdapterFromConfig(config);
|
|
2702
|
+
info("Generating TypeScript types from database schema...");
|
|
2703
|
+
console.log("");
|
|
2704
|
+
const generator = new TypeGenerator(adapter, config.connection.dialect);
|
|
2705
|
+
const types = await generator.generate({
|
|
2706
|
+
tables: options.tables ? options.tables.split(",").map((t) => t.trim()) : void 0,
|
|
2707
|
+
exclude: options.exclude ? options.exclude.split(",").map((t) => t.trim()) : void 0,
|
|
2708
|
+
camelCase: options.camelCase ?? false,
|
|
2709
|
+
includeComments: options.comments ?? true,
|
|
2710
|
+
optionalNullable: true
|
|
2711
|
+
});
|
|
2712
|
+
const outputPath = resolve(
|
|
2713
|
+
process.cwd(),
|
|
2714
|
+
options.output || config.types?.output || "./src/types/database.ts"
|
|
2715
|
+
);
|
|
2716
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2717
|
+
await writeFile(outputPath, types, "utf8");
|
|
2718
|
+
console.log("");
|
|
2719
|
+
success(`Types generated: ${outputPath}`);
|
|
2720
|
+
const interfaceCount = (types.match(/export interface/g) || []).length;
|
|
2721
|
+
info(`Generated ${interfaceCount} interface(s)`);
|
|
2722
|
+
} catch (error_) {
|
|
2723
|
+
error(error_.message);
|
|
2724
|
+
process.exit(1);
|
|
2725
|
+
} finally {
|
|
2726
|
+
if (adapter) {
|
|
2727
|
+
await adapter.disconnect();
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2427
2732
|
// src/cli/commands/latest.ts
|
|
2428
2733
|
async function latestCommand(options = {}) {
|
|
2429
2734
|
let adapter;
|
|
@@ -2460,7 +2765,8 @@ async function makeCommand(name) {
|
|
|
2460
2765
|
await mkdir(directory, { recursive: true });
|
|
2461
2766
|
console.log(`Created migrations directory: ${directory}`);
|
|
2462
2767
|
}
|
|
2463
|
-
const
|
|
2768
|
+
const prefix = config.migrations?.prefix;
|
|
2769
|
+
const filename = MigrationLoader.generateFilename(name, prefix);
|
|
2464
2770
|
const filepath = resolve(directory, filename);
|
|
2465
2771
|
const migrationName = filename.replace(/\.(ts|js|mjs)$/, "");
|
|
2466
2772
|
const content = MigrationLoader.getMigrationTemplate(migrationName);
|
|
@@ -2476,20 +2782,61 @@ async function makeCommand(name) {
|
|
|
2476
2782
|
process.exit(1);
|
|
2477
2783
|
}
|
|
2478
2784
|
}
|
|
2785
|
+
var DEFAULT_PRIORITY = 100;
|
|
2479
2786
|
var SeederLoader = class {
|
|
2480
2787
|
constructor(directory) {
|
|
2481
2788
|
this.directory = directory;
|
|
2482
2789
|
}
|
|
2483
2790
|
/**
|
|
2484
|
-
* Load all seeder files from directory
|
|
2791
|
+
* Load all seeder files from directory (sorted by priority/dependencies)
|
|
2485
2792
|
*/
|
|
2486
2793
|
async loadAll() {
|
|
2487
2794
|
const files = await readdir(this.directory);
|
|
2488
|
-
const
|
|
2795
|
+
const seederPaths = files.filter((f) => /\.(ts|js|mjs)$/.test(f) && !f.endsWith(".d.ts")).map((f) => ({
|
|
2489
2796
|
name: this.getSeederName(f),
|
|
2490
2797
|
path: resolve(this.directory, f)
|
|
2491
2798
|
}));
|
|
2492
|
-
|
|
2799
|
+
const seederFiles = [];
|
|
2800
|
+
for (const file of seederPaths) {
|
|
2801
|
+
const seeder = await this.load(file.path);
|
|
2802
|
+
seederFiles.push({
|
|
2803
|
+
name: file.name,
|
|
2804
|
+
path: file.path,
|
|
2805
|
+
priority: seeder.priority ?? DEFAULT_PRIORITY,
|
|
2806
|
+
depends: seeder.depends
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
return this.sortByDependencies(seederFiles);
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Sort seeders by dependencies (topological sort) then by priority
|
|
2813
|
+
*/
|
|
2814
|
+
sortByDependencies(files) {
|
|
2815
|
+
const fileMap = new Map(files.map((f) => [f.name, f]));
|
|
2816
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2817
|
+
const result = [];
|
|
2818
|
+
const visit = (file) => {
|
|
2819
|
+
if (visited.has(file.name)) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
visited.add(file.name);
|
|
2823
|
+
if (file.depends) {
|
|
2824
|
+
for (const dep of file.depends) {
|
|
2825
|
+
const depFile = fileMap.get(dep) || fileMap.get(`${dep}_seeder`);
|
|
2826
|
+
if (depFile) {
|
|
2827
|
+
visit(depFile);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
result.push(file);
|
|
2832
|
+
};
|
|
2833
|
+
const sortedByPriority = [...files].sort(
|
|
2834
|
+
(a, b) => (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY)
|
|
2835
|
+
);
|
|
2836
|
+
for (const file of sortedByPriority) {
|
|
2837
|
+
visit(file);
|
|
2838
|
+
}
|
|
2839
|
+
return result;
|
|
2493
2840
|
}
|
|
2494
2841
|
/**
|
|
2495
2842
|
* Load a specific seeder by path
|
|
@@ -2511,10 +2858,13 @@ var SeederLoader = class {
|
|
|
2511
2858
|
}
|
|
2512
2859
|
/**
|
|
2513
2860
|
* Generate a new seeder filename
|
|
2861
|
+
* @param name - Seeder name
|
|
2862
|
+
* @param prefix - Optional prefix (e.g., 'auth' -> auth_users_seeder.ts)
|
|
2514
2863
|
*/
|
|
2515
|
-
static generateFilename(name) {
|
|
2864
|
+
static generateFilename(name, prefix) {
|
|
2516
2865
|
const snakeName = name.replaceAll(/([a-z])([A-Z])/g, "$1_$2").replaceAll(/[\s-]+/g, "_").toLowerCase();
|
|
2517
|
-
|
|
2866
|
+
const sanitizedPrefix = prefix ? prefix.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "") : null;
|
|
2867
|
+
return sanitizedPrefix ? `${sanitizedPrefix}_${snakeName}_seeder.ts` : `${snakeName}_seeder.ts`;
|
|
2518
2868
|
}
|
|
2519
2869
|
/**
|
|
2520
2870
|
* Get seeder template
|
|
@@ -2528,10 +2878,16 @@ var SeederLoader = class {
|
|
|
2528
2878
|
import type { Seeder, DatabaseAdapter } from '@db-bridge/core';
|
|
2529
2879
|
|
|
2530
2880
|
export default {
|
|
2881
|
+
// Priority: lower runs first (default: 100)
|
|
2882
|
+
// priority: 10,
|
|
2883
|
+
|
|
2884
|
+
// Dependencies: seeders that must run before this one
|
|
2885
|
+
// depends: ['users'],
|
|
2886
|
+
|
|
2531
2887
|
async run(adapter: DatabaseAdapter): Promise<void> {
|
|
2532
2888
|
// Insert seed data
|
|
2533
2889
|
// await adapter.execute(\`
|
|
2534
|
-
// INSERT INTO
|
|
2890
|
+
// INSERT INTO ${name} (name, email) VALUES
|
|
2535
2891
|
// ('John Doe', 'john@example.com'),
|
|
2536
2892
|
// ('Jane Doe', 'jane@example.com')
|
|
2537
2893
|
// \`);
|
|
@@ -2550,7 +2906,8 @@ async function makeSeederCommand(name) {
|
|
|
2550
2906
|
await mkdir(directory, { recursive: true });
|
|
2551
2907
|
console.log(`Created seeds directory: ${directory}`);
|
|
2552
2908
|
}
|
|
2553
|
-
const
|
|
2909
|
+
const prefix = config.seeds?.prefix;
|
|
2910
|
+
const filename = SeederLoader.generateFilename(name, prefix);
|
|
2554
2911
|
const filepath = resolve(directory, filename);
|
|
2555
2912
|
const seederName = filename.replace(/\.(ts|js|mjs)$/, "");
|
|
2556
2913
|
const content = SeederLoader.getSeederTemplate(seederName);
|
|
@@ -2865,12 +3222,20 @@ Seed Commands:
|
|
|
2865
3222
|
db:seed Run database seeders
|
|
2866
3223
|
db:seed --class=<name> Run a specific seeder
|
|
2867
3224
|
|
|
3225
|
+
Type Generation:
|
|
3226
|
+
generate:types Generate TypeScript interfaces from schema
|
|
3227
|
+
|
|
2868
3228
|
Options:
|
|
2869
3229
|
--help, -h Show this help message
|
|
2870
3230
|
--version, -v Show version number
|
|
2871
3231
|
--dry-run Show what would be done without executing
|
|
2872
3232
|
--step=<n> Number of batches to rollback (for rollback command)
|
|
2873
3233
|
--class=<name> Specific seeder class to run (for db:seed)
|
|
3234
|
+
--output=<path> Output file path (for generate:types)
|
|
3235
|
+
--tables=<list> Comma-separated list of tables to include
|
|
3236
|
+
--exclude=<list> Comma-separated list of tables to exclude
|
|
3237
|
+
--camel-case Use camelCase for property names
|
|
3238
|
+
--comments Include JSDoc comments (default: true)
|
|
2874
3239
|
|
|
2875
3240
|
Examples:
|
|
2876
3241
|
db-bridge migrate:make create_users_table
|
|
@@ -2879,6 +3244,8 @@ Examples:
|
|
|
2879
3244
|
db-bridge make:seeder users
|
|
2880
3245
|
db-bridge db:seed
|
|
2881
3246
|
db-bridge db:seed --class=users
|
|
3247
|
+
db-bridge generate:types
|
|
3248
|
+
db-bridge generate:types --output=./src/types/db.ts --camel-case
|
|
2882
3249
|
`;
|
|
2883
3250
|
async function main() {
|
|
2884
3251
|
const args = process.argv.slice(2);
|
|
@@ -2893,7 +3260,12 @@ async function main() {
|
|
|
2893
3260
|
version: { type: "boolean", short: "v" },
|
|
2894
3261
|
"dry-run": { type: "boolean" },
|
|
2895
3262
|
step: { type: "string" },
|
|
2896
|
-
class: { type: "string" }
|
|
3263
|
+
class: { type: "string" },
|
|
3264
|
+
output: { type: "string" },
|
|
3265
|
+
tables: { type: "string" },
|
|
3266
|
+
exclude: { type: "string" },
|
|
3267
|
+
"camel-case": { type: "boolean" },
|
|
3268
|
+
comments: { type: "boolean" }
|
|
2897
3269
|
},
|
|
2898
3270
|
allowPositionals: true
|
|
2899
3271
|
});
|
|
@@ -2902,7 +3274,12 @@ async function main() {
|
|
|
2902
3274
|
version: values.version,
|
|
2903
3275
|
dryRun: values["dry-run"],
|
|
2904
3276
|
step: values.step ? parseInt(values.step, 10) : void 0,
|
|
2905
|
-
class: values.class
|
|
3277
|
+
class: values.class,
|
|
3278
|
+
output: values.output,
|
|
3279
|
+
tables: values.tables,
|
|
3280
|
+
exclude: values.exclude,
|
|
3281
|
+
camelCase: values["camel-case"],
|
|
3282
|
+
comments: values.comments
|
|
2906
3283
|
};
|
|
2907
3284
|
command = positionals[0] || "";
|
|
2908
3285
|
commandArgs = positionals.slice(1);
|
|
@@ -2912,6 +3289,8 @@ async function main() {
|
|
|
2912
3289
|
options.help = args.includes("--help") || args.includes("-h");
|
|
2913
3290
|
options.version = args.includes("--version") || args.includes("-v");
|
|
2914
3291
|
options.dryRun = args.includes("--dry-run");
|
|
3292
|
+
options.camelCase = args.includes("--camel-case");
|
|
3293
|
+
options.comments = args.includes("--comments");
|
|
2915
3294
|
const stepArg = args.find((a) => a.startsWith("--step="));
|
|
2916
3295
|
if (stepArg) {
|
|
2917
3296
|
options.step = parseInt(stepArg.split("=")[1] || "1", 10);
|
|
@@ -2920,6 +3299,18 @@ async function main() {
|
|
|
2920
3299
|
if (classArg) {
|
|
2921
3300
|
options.class = classArg.split("=")[1];
|
|
2922
3301
|
}
|
|
3302
|
+
const outputArg = args.find((a) => a.startsWith("--output="));
|
|
3303
|
+
if (outputArg) {
|
|
3304
|
+
options.output = outputArg.split("=")[1];
|
|
3305
|
+
}
|
|
3306
|
+
const tablesArg = args.find((a) => a.startsWith("--tables="));
|
|
3307
|
+
if (tablesArg) {
|
|
3308
|
+
options.tables = tablesArg.split("=")[1];
|
|
3309
|
+
}
|
|
3310
|
+
const excludeArg = args.find((a) => a.startsWith("--exclude="));
|
|
3311
|
+
if (excludeArg) {
|
|
3312
|
+
options.exclude = excludeArg.split("=")[1];
|
|
3313
|
+
}
|
|
2923
3314
|
}
|
|
2924
3315
|
if (options.help || command === "help") {
|
|
2925
3316
|
console.log(HELP);
|
|
@@ -2981,6 +3372,16 @@ async function main() {
|
|
|
2981
3372
|
await seedCommand({ class: options.class });
|
|
2982
3373
|
break;
|
|
2983
3374
|
}
|
|
3375
|
+
case "generate:types": {
|
|
3376
|
+
await generateTypesCommand({
|
|
3377
|
+
output: options.output,
|
|
3378
|
+
tables: options.tables,
|
|
3379
|
+
exclude: options.exclude,
|
|
3380
|
+
camelCase: options.camelCase,
|
|
3381
|
+
comments: options.comments
|
|
3382
|
+
});
|
|
3383
|
+
break;
|
|
3384
|
+
}
|
|
2984
3385
|
default: {
|
|
2985
3386
|
console.error(`Unknown command: ${command}`);
|
|
2986
3387
|
console.log('Run "db-bridge --help" for usage information.');
|