@db-bridge/core 1.0.0 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3000 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +477 -8
- package/dist/index.js +2547 -16
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,3000 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'util';
|
|
3
|
+
import { resolve, basename, extname, join } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { mkdir, writeFile, readdir, readFile } from 'fs/promises';
|
|
8
|
+
import { hostname } from 'os';
|
|
9
|
+
|
|
10
|
+
var CONFIG_FILES = [
|
|
11
|
+
"db-bridge.config.ts",
|
|
12
|
+
"db-bridge.config.js",
|
|
13
|
+
"db-bridge.config.mjs",
|
|
14
|
+
"dbbridge.config.ts",
|
|
15
|
+
"dbbridge.config.js"
|
|
16
|
+
];
|
|
17
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
18
|
+
let configPath = null;
|
|
19
|
+
for (const filename of CONFIG_FILES) {
|
|
20
|
+
const fullPath = resolve(cwd, filename);
|
|
21
|
+
if (existsSync(fullPath)) {
|
|
22
|
+
configPath = fullPath;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!configPath) {
|
|
27
|
+
throw new Error(`Configuration file not found. Create one of: ${CONFIG_FILES.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
31
|
+
const module = await import(fileUrl);
|
|
32
|
+
const config = module.default || module;
|
|
33
|
+
validateConfig(config);
|
|
34
|
+
return applyDefaults(config);
|
|
35
|
+
} catch (error2) {
|
|
36
|
+
if (error2.message.includes("Configuration file not found")) {
|
|
37
|
+
throw error2;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Failed to load config from ${configPath}: ${error2.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function validateConfig(config) {
|
|
43
|
+
if (!config || typeof config !== "object") {
|
|
44
|
+
throw new Error("Configuration must be an object");
|
|
45
|
+
}
|
|
46
|
+
const cfg = config;
|
|
47
|
+
const connection = cfg["connection"];
|
|
48
|
+
if (!connection || typeof connection !== "object") {
|
|
49
|
+
throw new Error('Configuration must have a "connection" object');
|
|
50
|
+
}
|
|
51
|
+
const conn = connection;
|
|
52
|
+
const dialect = conn["dialect"];
|
|
53
|
+
const host = conn["host"];
|
|
54
|
+
const database = conn["database"];
|
|
55
|
+
if (!["mysql", "postgresql"].includes(dialect)) {
|
|
56
|
+
throw new Error('connection.dialect must be "mysql" or "postgresql"');
|
|
57
|
+
}
|
|
58
|
+
if (!host || typeof host !== "string") {
|
|
59
|
+
throw new Error("connection.host is required");
|
|
60
|
+
}
|
|
61
|
+
if (!database || typeof database !== "string") {
|
|
62
|
+
throw new Error("connection.database is required");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function applyDefaults(config) {
|
|
66
|
+
const defaultPort = config.connection.dialect === "mysql" ? 3306 : 5432;
|
|
67
|
+
return {
|
|
68
|
+
...config,
|
|
69
|
+
connection: {
|
|
70
|
+
...config.connection,
|
|
71
|
+
port: config.connection.port ?? defaultPort,
|
|
72
|
+
user: config.connection.user ?? "root",
|
|
73
|
+
password: config.connection.password ?? ""
|
|
74
|
+
},
|
|
75
|
+
migrations: {
|
|
76
|
+
directory: "./src/migrations",
|
|
77
|
+
tableName: "db_migrations",
|
|
78
|
+
lockTableName: "db_migrations_lock",
|
|
79
|
+
...config.migrations
|
|
80
|
+
},
|
|
81
|
+
seeds: {
|
|
82
|
+
directory: "./src/seeds",
|
|
83
|
+
...config.seeds
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var MIGRATION_PATTERN = /^(\d{14})_(.+)\.(ts|js|mjs)$/;
|
|
88
|
+
var MigrationLoader = class {
|
|
89
|
+
directory;
|
|
90
|
+
extensions;
|
|
91
|
+
constructor(directory, extensions = [".ts", ".js", ".mjs"]) {
|
|
92
|
+
this.directory = directory;
|
|
93
|
+
this.extensions = extensions;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Scan directory for migration files
|
|
97
|
+
*/
|
|
98
|
+
async scanDirectory() {
|
|
99
|
+
const files = [];
|
|
100
|
+
try {
|
|
101
|
+
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isFile()) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const ext = extname(entry.name);
|
|
107
|
+
if (!this.extensions.includes(ext)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const match = entry.name.match(MIGRATION_PATTERN);
|
|
111
|
+
if (!match) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const [, timestamp, description] = match;
|
|
115
|
+
files.push({
|
|
116
|
+
name: basename(entry.name, ext),
|
|
117
|
+
path: join(this.directory, entry.name),
|
|
118
|
+
timestamp,
|
|
119
|
+
description: description.replaceAll("_", " ")
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
files.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
123
|
+
return files;
|
|
124
|
+
} catch (error2) {
|
|
125
|
+
if (error2.code === "ENOENT") {
|
|
126
|
+
throw new Error(`Migration directory not found: ${this.directory}`);
|
|
127
|
+
}
|
|
128
|
+
throw error2;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Load a single migration file
|
|
133
|
+
*/
|
|
134
|
+
async loadMigration(file) {
|
|
135
|
+
try {
|
|
136
|
+
const fileUrl = pathToFileURL(file.path).href;
|
|
137
|
+
const module = await import(fileUrl);
|
|
138
|
+
const migration = module.default || module;
|
|
139
|
+
if (typeof migration.up !== "function") {
|
|
140
|
+
throw new TypeError(`Migration ${file.name} is missing 'up' function`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof migration.down !== "function") {
|
|
143
|
+
throw new TypeError(`Migration ${file.name} is missing 'down' function`);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
name: file.name,
|
|
147
|
+
up: migration.up,
|
|
148
|
+
down: migration.down,
|
|
149
|
+
transactional: migration.transactional ?? true,
|
|
150
|
+
phase: migration.phase
|
|
151
|
+
};
|
|
152
|
+
} catch (error2) {
|
|
153
|
+
throw new Error(`Failed to load migration ${file.name}: ${error2.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Load all migrations from directory
|
|
158
|
+
*/
|
|
159
|
+
async loadAll() {
|
|
160
|
+
const files = await this.scanDirectory();
|
|
161
|
+
const migrations = [];
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const migration = await this.loadMigration(file);
|
|
164
|
+
migrations.push(migration);
|
|
165
|
+
}
|
|
166
|
+
return migrations;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get migration files (metadata only, without loading)
|
|
170
|
+
*/
|
|
171
|
+
async getMigrationFiles() {
|
|
172
|
+
return this.scanDirectory();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Calculate checksum for a migration file
|
|
176
|
+
*/
|
|
177
|
+
async calculateChecksum(file) {
|
|
178
|
+
const content = await readFile(file.path, "utf8");
|
|
179
|
+
const normalized = content.replaceAll("\r\n", "\n").trim();
|
|
180
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Calculate checksums for all migration files
|
|
184
|
+
*/
|
|
185
|
+
async calculateAllChecksums() {
|
|
186
|
+
const files = await this.scanDirectory();
|
|
187
|
+
const checksums = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const checksum = await this.calculateChecksum(file);
|
|
190
|
+
checksums.set(file.name, checksum);
|
|
191
|
+
}
|
|
192
|
+
return checksums;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Generate a new migration filename
|
|
196
|
+
*/
|
|
197
|
+
static generateFilename(description) {
|
|
198
|
+
const now = /* @__PURE__ */ new Date();
|
|
199
|
+
const timestamp = [
|
|
200
|
+
now.getFullYear(),
|
|
201
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
202
|
+
String(now.getDate()).padStart(2, "0"),
|
|
203
|
+
String(now.getHours()).padStart(2, "0"),
|
|
204
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
205
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
206
|
+
].join("");
|
|
207
|
+
const sanitizedDescription = description.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "");
|
|
208
|
+
return `${timestamp}_${sanitizedDescription}.ts`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get migration template content
|
|
212
|
+
*/
|
|
213
|
+
static getMigrationTemplate(name) {
|
|
214
|
+
return `/**
|
|
215
|
+
* Migration: ${name}
|
|
216
|
+
* Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
import type { SchemaBuilder } from '@db-bridge/core';
|
|
220
|
+
|
|
221
|
+
export default {
|
|
222
|
+
name: '${name}',
|
|
223
|
+
|
|
224
|
+
async up(schema: SchemaBuilder): Promise<void> {
|
|
225
|
+
// Write your migration here
|
|
226
|
+
// Example:
|
|
227
|
+
// await schema.createTable('users', (table) => {
|
|
228
|
+
// table.increments('id');
|
|
229
|
+
// table.string('email', 255).unique().notNull();
|
|
230
|
+
// table.timestamps();
|
|
231
|
+
// });
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async down(schema: SchemaBuilder): Promise<void> {
|
|
235
|
+
// Reverse the migration
|
|
236
|
+
// Example:
|
|
237
|
+
// await schema.dropTableIfExists('users');
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/schema/dialects/MySQLDialect.ts
|
|
245
|
+
var MySQLDialect = class {
|
|
246
|
+
dialect = "mysql";
|
|
247
|
+
/**
|
|
248
|
+
* Quote an identifier (table/column name)
|
|
249
|
+
*/
|
|
250
|
+
quoteIdentifier(name) {
|
|
251
|
+
return `\`${name.replaceAll("`", "``")}\``;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Quote a value for SQL
|
|
255
|
+
*/
|
|
256
|
+
quoteValue(value) {
|
|
257
|
+
if (value === null || value === void 0) {
|
|
258
|
+
return "NULL";
|
|
259
|
+
}
|
|
260
|
+
if (typeof value === "boolean") {
|
|
261
|
+
return value ? "1" : "0";
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === "number") {
|
|
264
|
+
return String(value);
|
|
265
|
+
}
|
|
266
|
+
if (typeof value === "string") {
|
|
267
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
268
|
+
}
|
|
269
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Generate CREATE TABLE statement
|
|
273
|
+
*/
|
|
274
|
+
createTable(definition) {
|
|
275
|
+
const parts = [];
|
|
276
|
+
for (const column of definition.columns) {
|
|
277
|
+
parts.push(this.columnToSQL(column));
|
|
278
|
+
}
|
|
279
|
+
if (definition.primaryKey && definition.primaryKey.length > 0) {
|
|
280
|
+
const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
281
|
+
parts.push(`PRIMARY KEY (${pkColumns})`);
|
|
282
|
+
}
|
|
283
|
+
for (const index of definition.indexes) {
|
|
284
|
+
parts.push(this.indexToSQL(index));
|
|
285
|
+
}
|
|
286
|
+
for (const fk of definition.foreignKeys) {
|
|
287
|
+
parts.push(this.foreignKeyToSQL(fk));
|
|
288
|
+
}
|
|
289
|
+
let sql = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
|
|
290
|
+
${parts.join(",\n ")}
|
|
291
|
+
)`;
|
|
292
|
+
const options = [];
|
|
293
|
+
if (definition.engine) {
|
|
294
|
+
options.push(`ENGINE=${definition.engine}`);
|
|
295
|
+
} else {
|
|
296
|
+
options.push("ENGINE=InnoDB");
|
|
297
|
+
}
|
|
298
|
+
if (definition.charset) {
|
|
299
|
+
options.push(`DEFAULT CHARSET=${definition.charset}`);
|
|
300
|
+
} else {
|
|
301
|
+
options.push("DEFAULT CHARSET=utf8mb4");
|
|
302
|
+
}
|
|
303
|
+
if (definition.collation) {
|
|
304
|
+
options.push(`COLLATE=${definition.collation}`);
|
|
305
|
+
}
|
|
306
|
+
if (definition.comment) {
|
|
307
|
+
options.push(`COMMENT=${this.quoteValue(definition.comment)}`);
|
|
308
|
+
}
|
|
309
|
+
if (options.length > 0) {
|
|
310
|
+
sql += ` ${options.join(" ")}`;
|
|
311
|
+
}
|
|
312
|
+
return sql;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Generate DROP TABLE statement
|
|
316
|
+
*/
|
|
317
|
+
dropTable(tableName) {
|
|
318
|
+
return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Generate DROP TABLE IF EXISTS statement
|
|
322
|
+
*/
|
|
323
|
+
dropTableIfExists(tableName) {
|
|
324
|
+
return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Generate RENAME TABLE statement
|
|
328
|
+
*/
|
|
329
|
+
renameTable(from, to) {
|
|
330
|
+
return `RENAME TABLE ${this.quoteIdentifier(from)} TO ${this.quoteIdentifier(to)}`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Generate ALTER TABLE statements
|
|
334
|
+
*/
|
|
335
|
+
alterTable(definition) {
|
|
336
|
+
const statements = [];
|
|
337
|
+
const tableName = this.quoteIdentifier(definition.tableName);
|
|
338
|
+
for (const op of definition.operations) {
|
|
339
|
+
switch (op.type) {
|
|
340
|
+
case "addColumn": {
|
|
341
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case "dropColumn": {
|
|
345
|
+
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case "renameColumn": {
|
|
349
|
+
statements.push(
|
|
350
|
+
`ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
|
|
351
|
+
);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "modifyColumn": {
|
|
355
|
+
statements.push(`ALTER TABLE ${tableName} MODIFY COLUMN ${this.columnToSQL(op.column)}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "addIndex": {
|
|
359
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.indexToSQL(op.index)}`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "dropIndex": {
|
|
363
|
+
statements.push(`ALTER TABLE ${tableName} DROP INDEX ${this.quoteIdentifier(op.name)}`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case "addForeignKey": {
|
|
367
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "dropForeignKey": {
|
|
371
|
+
statements.push(
|
|
372
|
+
`ALTER TABLE ${tableName} DROP FOREIGN KEY ${this.quoteIdentifier(op.name)}`
|
|
373
|
+
);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "addPrimary": {
|
|
377
|
+
const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
378
|
+
statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "dropPrimary": {
|
|
382
|
+
statements.push(`ALTER TABLE ${tableName} DROP PRIMARY KEY`);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return statements;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Generate query to check if table exists
|
|
391
|
+
*/
|
|
392
|
+
hasTable(tableName) {
|
|
393
|
+
return `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Generate query to check if column exists
|
|
397
|
+
*/
|
|
398
|
+
hasColumn(tableName, columnName) {
|
|
399
|
+
return `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ${this.quoteValue(tableName)} AND column_name = ${this.quoteValue(columnName)} LIMIT 1`;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Convert column definition to SQL
|
|
403
|
+
*/
|
|
404
|
+
columnToSQL(column) {
|
|
405
|
+
const parts = [this.quoteIdentifier(column.name)];
|
|
406
|
+
parts.push(this.columnTypeToSQL(column));
|
|
407
|
+
if (column.unsigned) {
|
|
408
|
+
parts.push("UNSIGNED");
|
|
409
|
+
}
|
|
410
|
+
if (column.nullable) {
|
|
411
|
+
parts.push("NULL");
|
|
412
|
+
} else {
|
|
413
|
+
parts.push("NOT NULL");
|
|
414
|
+
}
|
|
415
|
+
if (column.defaultRaw) {
|
|
416
|
+
parts.push(`DEFAULT ${column.defaultRaw}`);
|
|
417
|
+
} else if (column.defaultValue !== void 0) {
|
|
418
|
+
parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
|
|
419
|
+
}
|
|
420
|
+
if (column.autoIncrement) {
|
|
421
|
+
parts.push("AUTO_INCREMENT");
|
|
422
|
+
}
|
|
423
|
+
if (column.primary && !column.autoIncrement) {
|
|
424
|
+
parts.push("PRIMARY KEY");
|
|
425
|
+
} else if (column.primary && column.autoIncrement) {
|
|
426
|
+
parts.push("PRIMARY KEY");
|
|
427
|
+
}
|
|
428
|
+
if (column.unique && !column.primary) {
|
|
429
|
+
parts.push("UNIQUE");
|
|
430
|
+
}
|
|
431
|
+
if (column.comment) {
|
|
432
|
+
parts.push(`COMMENT ${this.quoteValue(column.comment)}`);
|
|
433
|
+
}
|
|
434
|
+
if (column.first) {
|
|
435
|
+
parts.push("FIRST");
|
|
436
|
+
} else if (column.after) {
|
|
437
|
+
parts.push(`AFTER ${this.quoteIdentifier(column.after)}`);
|
|
438
|
+
}
|
|
439
|
+
return parts.join(" ");
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Convert column type to MySQL type
|
|
443
|
+
*/
|
|
444
|
+
columnTypeToSQL(column) {
|
|
445
|
+
switch (column.type) {
|
|
446
|
+
case "increments": {
|
|
447
|
+
return "INT UNSIGNED AUTO_INCREMENT";
|
|
448
|
+
}
|
|
449
|
+
case "bigIncrements": {
|
|
450
|
+
return "BIGINT UNSIGNED AUTO_INCREMENT";
|
|
451
|
+
}
|
|
452
|
+
case "integer": {
|
|
453
|
+
return "INT";
|
|
454
|
+
}
|
|
455
|
+
case "bigInteger": {
|
|
456
|
+
return "BIGINT";
|
|
457
|
+
}
|
|
458
|
+
case "smallInteger": {
|
|
459
|
+
return "SMALLINT";
|
|
460
|
+
}
|
|
461
|
+
case "tinyInteger": {
|
|
462
|
+
return "TINYINT";
|
|
463
|
+
}
|
|
464
|
+
case "float": {
|
|
465
|
+
return "FLOAT";
|
|
466
|
+
}
|
|
467
|
+
case "double": {
|
|
468
|
+
return "DOUBLE";
|
|
469
|
+
}
|
|
470
|
+
case "decimal": {
|
|
471
|
+
const precision = column.precision ?? 10;
|
|
472
|
+
const scale = column.scale ?? 2;
|
|
473
|
+
return `DECIMAL(${precision},${scale})`;
|
|
474
|
+
}
|
|
475
|
+
case "string": {
|
|
476
|
+
return `VARCHAR(${column.length ?? 255})`;
|
|
477
|
+
}
|
|
478
|
+
case "text": {
|
|
479
|
+
return "TEXT";
|
|
480
|
+
}
|
|
481
|
+
case "mediumText": {
|
|
482
|
+
return "MEDIUMTEXT";
|
|
483
|
+
}
|
|
484
|
+
case "longText": {
|
|
485
|
+
return "LONGTEXT";
|
|
486
|
+
}
|
|
487
|
+
case "boolean": {
|
|
488
|
+
return "TINYINT(1)";
|
|
489
|
+
}
|
|
490
|
+
case "date": {
|
|
491
|
+
return "DATE";
|
|
492
|
+
}
|
|
493
|
+
case "datetime": {
|
|
494
|
+
return "DATETIME";
|
|
495
|
+
}
|
|
496
|
+
case "timestamp": {
|
|
497
|
+
return "TIMESTAMP";
|
|
498
|
+
}
|
|
499
|
+
case "time": {
|
|
500
|
+
return "TIME";
|
|
501
|
+
}
|
|
502
|
+
case "json":
|
|
503
|
+
case "jsonb": {
|
|
504
|
+
return "JSON";
|
|
505
|
+
}
|
|
506
|
+
case "uuid": {
|
|
507
|
+
return "CHAR(36)";
|
|
508
|
+
}
|
|
509
|
+
case "binary": {
|
|
510
|
+
return "BLOB";
|
|
511
|
+
}
|
|
512
|
+
case "enum": {
|
|
513
|
+
if (column.enumValues && column.enumValues.length > 0) {
|
|
514
|
+
const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
|
|
515
|
+
return `ENUM(${values})`;
|
|
516
|
+
}
|
|
517
|
+
return "VARCHAR(255)";
|
|
518
|
+
}
|
|
519
|
+
default: {
|
|
520
|
+
return "VARCHAR(255)";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Convert index definition to SQL
|
|
526
|
+
*/
|
|
527
|
+
indexToSQL(index) {
|
|
528
|
+
const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
529
|
+
const name = index.name ? this.quoteIdentifier(index.name) : "";
|
|
530
|
+
if (index.type === "fulltext") {
|
|
531
|
+
return `FULLTEXT INDEX ${name} (${columns})`;
|
|
532
|
+
}
|
|
533
|
+
if (index.unique) {
|
|
534
|
+
return `UNIQUE INDEX ${name} (${columns})`;
|
|
535
|
+
}
|
|
536
|
+
return `INDEX ${name} (${columns})`;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Convert foreign key definition to SQL
|
|
540
|
+
*/
|
|
541
|
+
foreignKeyToSQL(fk) {
|
|
542
|
+
const parts = ["CONSTRAINT"];
|
|
543
|
+
if (fk.name) {
|
|
544
|
+
parts.push(this.quoteIdentifier(fk.name));
|
|
545
|
+
}
|
|
546
|
+
parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
|
|
547
|
+
parts.push(
|
|
548
|
+
`REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
|
|
549
|
+
);
|
|
550
|
+
if (fk.onDelete) {
|
|
551
|
+
parts.push(`ON DELETE ${fk.onDelete}`);
|
|
552
|
+
}
|
|
553
|
+
if (fk.onUpdate) {
|
|
554
|
+
parts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
555
|
+
}
|
|
556
|
+
return parts.join(" ");
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// src/schema/dialects/PostgreSQLDialect.ts
|
|
561
|
+
var PostgreSQLDialect = class {
|
|
562
|
+
dialect = "postgresql";
|
|
563
|
+
/**
|
|
564
|
+
* Quote an identifier (table/column name)
|
|
565
|
+
*/
|
|
566
|
+
quoteIdentifier(name) {
|
|
567
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Quote a value for SQL
|
|
571
|
+
*/
|
|
572
|
+
quoteValue(value) {
|
|
573
|
+
if (value === null || value === void 0) {
|
|
574
|
+
return "NULL";
|
|
575
|
+
}
|
|
576
|
+
if (typeof value === "boolean") {
|
|
577
|
+
return value ? "TRUE" : "FALSE";
|
|
578
|
+
}
|
|
579
|
+
if (typeof value === "number") {
|
|
580
|
+
return String(value);
|
|
581
|
+
}
|
|
582
|
+
if (typeof value === "string") {
|
|
583
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
584
|
+
}
|
|
585
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Generate CREATE TABLE statement
|
|
589
|
+
*/
|
|
590
|
+
createTable(definition) {
|
|
591
|
+
const parts = [];
|
|
592
|
+
for (const column of definition.columns) {
|
|
593
|
+
parts.push(this.columnToSQL(column));
|
|
594
|
+
}
|
|
595
|
+
if (definition.primaryKey && definition.primaryKey.length > 0) {
|
|
596
|
+
const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
597
|
+
parts.push(`PRIMARY KEY (${pkColumns})`);
|
|
598
|
+
}
|
|
599
|
+
for (const fk of definition.foreignKeys) {
|
|
600
|
+
parts.push(this.foreignKeyToSQL(fk));
|
|
601
|
+
}
|
|
602
|
+
const sql = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
|
|
603
|
+
${parts.join(",\n ")}
|
|
604
|
+
)`;
|
|
605
|
+
const statements = [sql];
|
|
606
|
+
if (definition.comment) {
|
|
607
|
+
statements.push(
|
|
608
|
+
`COMMENT ON TABLE ${this.quoteIdentifier(definition.name)} IS ${this.quoteValue(definition.comment)}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return statements.join(";\n");
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Generate DROP TABLE statement
|
|
615
|
+
*/
|
|
616
|
+
dropTable(tableName) {
|
|
617
|
+
return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Generate DROP TABLE IF EXISTS statement
|
|
621
|
+
*/
|
|
622
|
+
dropTableIfExists(tableName) {
|
|
623
|
+
return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Generate ALTER TABLE ... RENAME statement
|
|
627
|
+
*/
|
|
628
|
+
renameTable(from, to) {
|
|
629
|
+
return `ALTER TABLE ${this.quoteIdentifier(from)} RENAME TO ${this.quoteIdentifier(to)}`;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Generate ALTER TABLE statements
|
|
633
|
+
*/
|
|
634
|
+
alterTable(definition) {
|
|
635
|
+
const statements = [];
|
|
636
|
+
const tableName = this.quoteIdentifier(definition.tableName);
|
|
637
|
+
for (const op of definition.operations) {
|
|
638
|
+
switch (op.type) {
|
|
639
|
+
case "addColumn": {
|
|
640
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case "dropColumn": {
|
|
644
|
+
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case "renameColumn": {
|
|
648
|
+
statements.push(
|
|
649
|
+
`ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
|
|
650
|
+
);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
case "modifyColumn": {
|
|
654
|
+
const col = op.column;
|
|
655
|
+
statements.push(
|
|
656
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} TYPE ${this.columnTypeToSQL(col)}`
|
|
657
|
+
);
|
|
658
|
+
if (col.nullable) {
|
|
659
|
+
statements.push(
|
|
660
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} DROP NOT NULL`
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
statements.push(
|
|
664
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET NOT NULL`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
if (col.defaultRaw) {
|
|
668
|
+
statements.push(
|
|
669
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${col.defaultRaw}`
|
|
670
|
+
);
|
|
671
|
+
} else if (col.defaultValue !== void 0) {
|
|
672
|
+
statements.push(
|
|
673
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${this.quoteValue(col.defaultValue)}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "addIndex": {
|
|
679
|
+
statements.push(this.createIndex(definition.tableName, op.index));
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "dropIndex": {
|
|
683
|
+
statements.push(`DROP INDEX ${this.quoteIdentifier(op.name)}`);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "addForeignKey": {
|
|
687
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "dropForeignKey": {
|
|
691
|
+
statements.push(
|
|
692
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(op.name)}`
|
|
693
|
+
);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
case "addPrimary": {
|
|
697
|
+
const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
698
|
+
statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case "dropPrimary": {
|
|
702
|
+
statements.push(
|
|
703
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(`${definition.tableName}_pkey`)}`
|
|
704
|
+
);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return statements;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Generate query to check if table exists
|
|
713
|
+
*/
|
|
714
|
+
hasTable(tableName) {
|
|
715
|
+
return `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Generate query to check if column exists
|
|
719
|
+
*/
|
|
720
|
+
hasColumn(tableName, columnName) {
|
|
721
|
+
return `SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${this.quoteValue(tableName)} AND column_name = ${this.quoteValue(columnName)} LIMIT 1`;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Create index statement
|
|
725
|
+
*/
|
|
726
|
+
createIndex(tableName, index) {
|
|
727
|
+
const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
728
|
+
const indexName = index.name || `idx_${tableName}_${index.columns.join("_")}`;
|
|
729
|
+
const unique = index.unique ? "UNIQUE " : "";
|
|
730
|
+
return `CREATE ${unique}INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(tableName)} (${columns})`;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Convert column definition to SQL
|
|
734
|
+
*/
|
|
735
|
+
columnToSQL(column) {
|
|
736
|
+
const parts = [this.quoteIdentifier(column.name)];
|
|
737
|
+
parts.push(this.columnTypeToSQL(column));
|
|
738
|
+
if (!column.nullable) {
|
|
739
|
+
parts.push("NOT NULL");
|
|
740
|
+
}
|
|
741
|
+
if (column.defaultRaw) {
|
|
742
|
+
parts.push(`DEFAULT ${column.defaultRaw}`);
|
|
743
|
+
} else if (column.defaultValue !== void 0) {
|
|
744
|
+
parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
|
|
745
|
+
}
|
|
746
|
+
if (column.primary && column.type !== "increments" && column.type !== "bigIncrements") {
|
|
747
|
+
parts.push("PRIMARY KEY");
|
|
748
|
+
}
|
|
749
|
+
if (column.unique && !column.primary) {
|
|
750
|
+
parts.push("UNIQUE");
|
|
751
|
+
}
|
|
752
|
+
return parts.join(" ");
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Convert column type to PostgreSQL type
|
|
756
|
+
*/
|
|
757
|
+
columnTypeToSQL(column) {
|
|
758
|
+
switch (column.type) {
|
|
759
|
+
case "increments": {
|
|
760
|
+
return "SERIAL PRIMARY KEY";
|
|
761
|
+
}
|
|
762
|
+
case "bigIncrements": {
|
|
763
|
+
return "BIGSERIAL PRIMARY KEY";
|
|
764
|
+
}
|
|
765
|
+
case "integer": {
|
|
766
|
+
return "INTEGER";
|
|
767
|
+
}
|
|
768
|
+
case "bigInteger": {
|
|
769
|
+
return "BIGINT";
|
|
770
|
+
}
|
|
771
|
+
case "smallInteger": {
|
|
772
|
+
return "SMALLINT";
|
|
773
|
+
}
|
|
774
|
+
case "tinyInteger": {
|
|
775
|
+
return "SMALLINT";
|
|
776
|
+
}
|
|
777
|
+
// PostgreSQL doesn't have TINYINT
|
|
778
|
+
case "float": {
|
|
779
|
+
return "REAL";
|
|
780
|
+
}
|
|
781
|
+
case "double": {
|
|
782
|
+
return "DOUBLE PRECISION";
|
|
783
|
+
}
|
|
784
|
+
case "decimal": {
|
|
785
|
+
const precision = column.precision ?? 10;
|
|
786
|
+
const scale = column.scale ?? 2;
|
|
787
|
+
return `NUMERIC(${precision},${scale})`;
|
|
788
|
+
}
|
|
789
|
+
case "string": {
|
|
790
|
+
return `VARCHAR(${column.length ?? 255})`;
|
|
791
|
+
}
|
|
792
|
+
case "text":
|
|
793
|
+
case "mediumText":
|
|
794
|
+
case "longText": {
|
|
795
|
+
return "TEXT";
|
|
796
|
+
}
|
|
797
|
+
case "boolean": {
|
|
798
|
+
return "BOOLEAN";
|
|
799
|
+
}
|
|
800
|
+
case "date": {
|
|
801
|
+
return "DATE";
|
|
802
|
+
}
|
|
803
|
+
case "datetime":
|
|
804
|
+
case "timestamp": {
|
|
805
|
+
return "TIMESTAMP";
|
|
806
|
+
}
|
|
807
|
+
case "time": {
|
|
808
|
+
return "TIME";
|
|
809
|
+
}
|
|
810
|
+
case "json": {
|
|
811
|
+
return "JSON";
|
|
812
|
+
}
|
|
813
|
+
case "jsonb": {
|
|
814
|
+
return "JSONB";
|
|
815
|
+
}
|
|
816
|
+
case "uuid": {
|
|
817
|
+
return "UUID";
|
|
818
|
+
}
|
|
819
|
+
case "binary": {
|
|
820
|
+
return "BYTEA";
|
|
821
|
+
}
|
|
822
|
+
case "enum": {
|
|
823
|
+
if (column.enumValues && column.enumValues.length > 0) {
|
|
824
|
+
const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
|
|
825
|
+
return `VARCHAR(255) CHECK (${this.quoteIdentifier(column.name)} IN (${values}))`;
|
|
826
|
+
}
|
|
827
|
+
return "VARCHAR(255)";
|
|
828
|
+
}
|
|
829
|
+
default: {
|
|
830
|
+
return "VARCHAR(255)";
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Convert foreign key definition to SQL
|
|
836
|
+
*/
|
|
837
|
+
foreignKeyToSQL(fk) {
|
|
838
|
+
const parts = ["CONSTRAINT"];
|
|
839
|
+
if (fk.name) {
|
|
840
|
+
parts.push(this.quoteIdentifier(fk.name));
|
|
841
|
+
} else {
|
|
842
|
+
parts.push(this.quoteIdentifier(`fk_${fk.column}_${fk.table}`));
|
|
843
|
+
}
|
|
844
|
+
parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
|
|
845
|
+
parts.push(
|
|
846
|
+
`REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
|
|
847
|
+
);
|
|
848
|
+
if (fk.onDelete) {
|
|
849
|
+
parts.push(`ON DELETE ${fk.onDelete}`);
|
|
850
|
+
}
|
|
851
|
+
if (fk.onUpdate) {
|
|
852
|
+
parts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
853
|
+
}
|
|
854
|
+
return parts.join(" ");
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/migrations/MigrationLock.ts
|
|
859
|
+
var MigrationLock = class {
|
|
860
|
+
adapter;
|
|
861
|
+
tableName;
|
|
862
|
+
timeout;
|
|
863
|
+
dialect;
|
|
864
|
+
lockId;
|
|
865
|
+
isLocked = false;
|
|
866
|
+
constructor(adapter, options) {
|
|
867
|
+
this.adapter = adapter;
|
|
868
|
+
this.tableName = options.tableName ?? "db_migrations_lock";
|
|
869
|
+
this.timeout = options.timeout ?? 6e4;
|
|
870
|
+
this.lockId = `${hostname()}_${process.pid}_${Date.now()}`;
|
|
871
|
+
this.dialect = options.dialect === "mysql" ? new MySQLDialect() : new PostgreSQLDialect();
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Initialize the lock table
|
|
875
|
+
*/
|
|
876
|
+
async initialize() {
|
|
877
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
878
|
+
if (this.dialect.dialect === "mysql") {
|
|
879
|
+
await this.adapter.execute(`
|
|
880
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
881
|
+
id INT PRIMARY KEY,
|
|
882
|
+
is_locked TINYINT NOT NULL DEFAULT 0,
|
|
883
|
+
locked_at TIMESTAMP NULL,
|
|
884
|
+
locked_by VARCHAR(255) NULL
|
|
885
|
+
) ENGINE=InnoDB
|
|
886
|
+
`);
|
|
887
|
+
} else {
|
|
888
|
+
await this.adapter.execute(`
|
|
889
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
890
|
+
id INT PRIMARY KEY,
|
|
891
|
+
is_locked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
892
|
+
locked_at TIMESTAMP NULL,
|
|
893
|
+
locked_by VARCHAR(255) NULL
|
|
894
|
+
)
|
|
895
|
+
`);
|
|
896
|
+
}
|
|
897
|
+
const checkSql = this.dialect.dialect === "mysql" ? `SELECT 1 FROM ${tableName} WHERE id = 1` : `SELECT 1 FROM ${tableName} WHERE id = 1`;
|
|
898
|
+
const result = await this.adapter.query(checkSql);
|
|
899
|
+
if (result.rows.length === 0) {
|
|
900
|
+
if (this.dialect.dialect === "mysql") {
|
|
901
|
+
await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, 0)`);
|
|
902
|
+
} else {
|
|
903
|
+
await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, FALSE)`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Acquire the migration lock
|
|
909
|
+
*/
|
|
910
|
+
async acquire() {
|
|
911
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
912
|
+
const startTime = Date.now();
|
|
913
|
+
while (Date.now() - startTime < this.timeout) {
|
|
914
|
+
try {
|
|
915
|
+
if (this.dialect.dialect === "mysql") {
|
|
916
|
+
const result = await this.adapter.execute(
|
|
917
|
+
`
|
|
918
|
+
UPDATE ${tableName}
|
|
919
|
+
SET is_locked = 1,
|
|
920
|
+
locked_at = NOW(),
|
|
921
|
+
locked_by = ?
|
|
922
|
+
WHERE id = 1 AND is_locked = 0
|
|
923
|
+
`,
|
|
924
|
+
[this.lockId]
|
|
925
|
+
);
|
|
926
|
+
if ((result.affectedRows ?? 0) > 0) {
|
|
927
|
+
this.isLocked = true;
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
const result = await this.adapter.execute(
|
|
932
|
+
`
|
|
933
|
+
UPDATE ${tableName}
|
|
934
|
+
SET is_locked = TRUE,
|
|
935
|
+
locked_at = NOW(),
|
|
936
|
+
locked_by = $1
|
|
937
|
+
WHERE id = 1 AND is_locked = FALSE
|
|
938
|
+
RETURNING id
|
|
939
|
+
`,
|
|
940
|
+
[this.lockId]
|
|
941
|
+
);
|
|
942
|
+
if (result.rows.length > 0) {
|
|
943
|
+
this.isLocked = true;
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const staleResult = await this.adapter.query(`SELECT locked_at, locked_by FROM ${tableName} WHERE id = 1`);
|
|
948
|
+
if (staleResult.rows.length > 0) {
|
|
949
|
+
const lockedAt = staleResult.rows[0].locked_at;
|
|
950
|
+
if (lockedAt && Date.now() - new Date(lockedAt).getTime() > this.timeout) {
|
|
951
|
+
console.warn(
|
|
952
|
+
`Releasing stale migration lock held by ${staleResult.rows[0].locked_by}`
|
|
953
|
+
);
|
|
954
|
+
await this.forceRelease();
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
await this.sleep(1e3);
|
|
959
|
+
} catch (error2) {
|
|
960
|
+
console.error("Error acquiring migration lock:", error2);
|
|
961
|
+
throw error2;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Release the migration lock
|
|
968
|
+
*/
|
|
969
|
+
async release() {
|
|
970
|
+
if (!this.isLocked) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
974
|
+
try {
|
|
975
|
+
if (this.dialect.dialect === "mysql") {
|
|
976
|
+
await this.adapter.execute(
|
|
977
|
+
`
|
|
978
|
+
UPDATE ${tableName}
|
|
979
|
+
SET is_locked = 0,
|
|
980
|
+
locked_at = NULL,
|
|
981
|
+
locked_by = NULL
|
|
982
|
+
WHERE id = 1 AND locked_by = ?
|
|
983
|
+
`,
|
|
984
|
+
[this.lockId]
|
|
985
|
+
);
|
|
986
|
+
} else {
|
|
987
|
+
await this.adapter.execute(
|
|
988
|
+
`
|
|
989
|
+
UPDATE ${tableName}
|
|
990
|
+
SET is_locked = FALSE,
|
|
991
|
+
locked_at = NULL,
|
|
992
|
+
locked_by = NULL
|
|
993
|
+
WHERE id = 1 AND locked_by = $1
|
|
994
|
+
`,
|
|
995
|
+
[this.lockId]
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
this.isLocked = false;
|
|
999
|
+
} catch (error2) {
|
|
1000
|
+
console.error("Error releasing migration lock:", error2);
|
|
1001
|
+
throw error2;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Force release the lock (for stale locks)
|
|
1006
|
+
*/
|
|
1007
|
+
async forceRelease() {
|
|
1008
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
1009
|
+
if (this.dialect.dialect === "mysql") {
|
|
1010
|
+
await this.adapter.execute(`
|
|
1011
|
+
UPDATE ${tableName}
|
|
1012
|
+
SET is_locked = 0,
|
|
1013
|
+
locked_at = NULL,
|
|
1014
|
+
locked_by = NULL
|
|
1015
|
+
WHERE id = 1
|
|
1016
|
+
`);
|
|
1017
|
+
} else {
|
|
1018
|
+
await this.adapter.execute(`
|
|
1019
|
+
UPDATE ${tableName}
|
|
1020
|
+
SET is_locked = FALSE,
|
|
1021
|
+
locked_at = NULL,
|
|
1022
|
+
locked_by = NULL
|
|
1023
|
+
WHERE id = 1
|
|
1024
|
+
`);
|
|
1025
|
+
}
|
|
1026
|
+
this.isLocked = false;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Check if lock is currently held
|
|
1030
|
+
*/
|
|
1031
|
+
async isHeld() {
|
|
1032
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
1033
|
+
const result = await this.adapter.query(
|
|
1034
|
+
`SELECT is_locked FROM ${tableName} WHERE id = 1`
|
|
1035
|
+
);
|
|
1036
|
+
if (result.rows.length === 0) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
const isLocked = result.rows[0].is_locked;
|
|
1040
|
+
return isLocked === 1 || isLocked === true;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Get lock info
|
|
1044
|
+
*/
|
|
1045
|
+
async getLockInfo() {
|
|
1046
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
1047
|
+
const result = await this.adapter.query(`SELECT is_locked, locked_at, locked_by FROM ${tableName} WHERE id = 1`);
|
|
1048
|
+
if (result.rows.length === 0) {
|
|
1049
|
+
return { isLocked: false, lockedAt: null, lockedBy: null };
|
|
1050
|
+
}
|
|
1051
|
+
const row = result.rows[0];
|
|
1052
|
+
return {
|
|
1053
|
+
isLocked: row.is_locked === 1 || row.is_locked === true,
|
|
1054
|
+
lockedAt: row.locked_at,
|
|
1055
|
+
lockedBy: row.locked_by
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Execute a function with lock
|
|
1060
|
+
*/
|
|
1061
|
+
async withLock(fn) {
|
|
1062
|
+
const acquired = await this.acquire();
|
|
1063
|
+
if (!acquired) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`Could not acquire migration lock within ${this.timeout}ms. Another migration may be running.`
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
return await fn();
|
|
1070
|
+
} finally {
|
|
1071
|
+
await this.release();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
sleep(ms) {
|
|
1075
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// src/schema/ColumnBuilder.ts
|
|
1080
|
+
var ColumnBuilder = class {
|
|
1081
|
+
definition;
|
|
1082
|
+
constructor(name, type) {
|
|
1083
|
+
this.definition = {
|
|
1084
|
+
name,
|
|
1085
|
+
type,
|
|
1086
|
+
nullable: type !== "increments" && type !== "bigIncrements",
|
|
1087
|
+
unsigned: false,
|
|
1088
|
+
autoIncrement: type === "increments" || type === "bigIncrements",
|
|
1089
|
+
primary: type === "increments" || type === "bigIncrements",
|
|
1090
|
+
unique: false,
|
|
1091
|
+
index: false,
|
|
1092
|
+
first: false
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Set column length (for string types)
|
|
1097
|
+
*/
|
|
1098
|
+
length(length) {
|
|
1099
|
+
this.definition.length = length;
|
|
1100
|
+
return this;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Set precision and scale (for decimal types)
|
|
1104
|
+
*/
|
|
1105
|
+
precision(precision, scale) {
|
|
1106
|
+
this.definition.precision = precision;
|
|
1107
|
+
this.definition.scale = scale;
|
|
1108
|
+
return this;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Mark column as nullable
|
|
1112
|
+
*/
|
|
1113
|
+
nullable() {
|
|
1114
|
+
this.definition.nullable = true;
|
|
1115
|
+
return this;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Mark column as not nullable
|
|
1119
|
+
*/
|
|
1120
|
+
notNull() {
|
|
1121
|
+
this.definition.nullable = false;
|
|
1122
|
+
return this;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Alias for notNull()
|
|
1126
|
+
*/
|
|
1127
|
+
notNullable() {
|
|
1128
|
+
return this.notNull();
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Set default value
|
|
1132
|
+
*/
|
|
1133
|
+
default(value) {
|
|
1134
|
+
this.definition.defaultValue = value;
|
|
1135
|
+
return this;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Set default value as raw SQL
|
|
1139
|
+
*/
|
|
1140
|
+
defaultRaw(sql) {
|
|
1141
|
+
this.definition.defaultRaw = sql;
|
|
1142
|
+
return this;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Set default to current timestamp
|
|
1146
|
+
*/
|
|
1147
|
+
defaultNow() {
|
|
1148
|
+
this.definition.defaultRaw = "CURRENT_TIMESTAMP";
|
|
1149
|
+
return this;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Mark column as unsigned (MySQL)
|
|
1153
|
+
*/
|
|
1154
|
+
unsigned() {
|
|
1155
|
+
this.definition.unsigned = true;
|
|
1156
|
+
return this;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Mark column as auto increment
|
|
1160
|
+
*/
|
|
1161
|
+
autoIncrement() {
|
|
1162
|
+
this.definition.autoIncrement = true;
|
|
1163
|
+
return this;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Mark column as primary key
|
|
1167
|
+
*/
|
|
1168
|
+
primary() {
|
|
1169
|
+
this.definition.primary = true;
|
|
1170
|
+
return this;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Mark column as unique
|
|
1174
|
+
*/
|
|
1175
|
+
unique() {
|
|
1176
|
+
this.definition.unique = true;
|
|
1177
|
+
return this;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Add index to column
|
|
1181
|
+
*/
|
|
1182
|
+
index() {
|
|
1183
|
+
this.definition.index = true;
|
|
1184
|
+
return this;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Add comment to column
|
|
1188
|
+
*/
|
|
1189
|
+
comment(comment) {
|
|
1190
|
+
this.definition.comment = comment;
|
|
1191
|
+
return this;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Position column after another column (MySQL)
|
|
1195
|
+
*/
|
|
1196
|
+
after(columnName) {
|
|
1197
|
+
this.definition.after = columnName;
|
|
1198
|
+
return this;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Position column first (MySQL)
|
|
1202
|
+
*/
|
|
1203
|
+
first() {
|
|
1204
|
+
this.definition.first = true;
|
|
1205
|
+
return this;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Set enum values
|
|
1209
|
+
*/
|
|
1210
|
+
values(values) {
|
|
1211
|
+
this.definition.enumValues = values;
|
|
1212
|
+
return this;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Add foreign key reference
|
|
1216
|
+
*/
|
|
1217
|
+
references(column) {
|
|
1218
|
+
const fkBuilder = new ForeignKeyBuilder(this, column);
|
|
1219
|
+
return fkBuilder;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Set foreign key definition (internal)
|
|
1223
|
+
*/
|
|
1224
|
+
setForeignKey(fk) {
|
|
1225
|
+
this.definition.references = fk;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Get the column definition
|
|
1229
|
+
*/
|
|
1230
|
+
getDefinition() {
|
|
1231
|
+
return { ...this.definition };
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
var ForeignKeyBuilder = class {
|
|
1235
|
+
columnBuilder;
|
|
1236
|
+
fkDefinition;
|
|
1237
|
+
constructor(columnBuilder, referenceColumn) {
|
|
1238
|
+
this.columnBuilder = columnBuilder;
|
|
1239
|
+
this.fkDefinition = {
|
|
1240
|
+
column: columnBuilder.getDefinition().name,
|
|
1241
|
+
referenceColumn
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Set the referenced table
|
|
1246
|
+
*/
|
|
1247
|
+
on(tableName) {
|
|
1248
|
+
this.fkDefinition.table = tableName;
|
|
1249
|
+
this.applyToColumn();
|
|
1250
|
+
return this;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Alias for on()
|
|
1254
|
+
*/
|
|
1255
|
+
inTable(tableName) {
|
|
1256
|
+
return this.on(tableName);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Set ON DELETE action
|
|
1260
|
+
*/
|
|
1261
|
+
onDelete(action) {
|
|
1262
|
+
this.fkDefinition.onDelete = action;
|
|
1263
|
+
this.applyToColumn();
|
|
1264
|
+
return this;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Set ON UPDATE action
|
|
1268
|
+
*/
|
|
1269
|
+
onUpdate(action) {
|
|
1270
|
+
this.fkDefinition.onUpdate = action;
|
|
1271
|
+
this.applyToColumn();
|
|
1272
|
+
return this;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Set constraint name
|
|
1276
|
+
*/
|
|
1277
|
+
name(name) {
|
|
1278
|
+
this.fkDefinition.name = name;
|
|
1279
|
+
this.applyToColumn();
|
|
1280
|
+
return this;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Apply the foreign key definition to the column
|
|
1284
|
+
*/
|
|
1285
|
+
applyToColumn() {
|
|
1286
|
+
if (this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
1287
|
+
this.columnBuilder.setForeignKey(this.fkDefinition);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Get the column builder for chaining
|
|
1292
|
+
*/
|
|
1293
|
+
getColumnBuilder() {
|
|
1294
|
+
return this.columnBuilder;
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// src/schema/TableBuilder.ts
|
|
1299
|
+
var TableBuilder = class {
|
|
1300
|
+
tableName;
|
|
1301
|
+
columns = [];
|
|
1302
|
+
indexes = [];
|
|
1303
|
+
foreignKeys = [];
|
|
1304
|
+
primaryKeyColumns;
|
|
1305
|
+
tableEngine;
|
|
1306
|
+
tableCharset;
|
|
1307
|
+
tableCollation;
|
|
1308
|
+
tableComment;
|
|
1309
|
+
constructor(tableName) {
|
|
1310
|
+
this.tableName = tableName;
|
|
1311
|
+
}
|
|
1312
|
+
// ============================================
|
|
1313
|
+
// Column Types
|
|
1314
|
+
// ============================================
|
|
1315
|
+
/**
|
|
1316
|
+
* Auto-incrementing integer primary key
|
|
1317
|
+
*/
|
|
1318
|
+
increments(name = "id") {
|
|
1319
|
+
return this.addColumn(name, "increments");
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Auto-incrementing big integer primary key
|
|
1323
|
+
*/
|
|
1324
|
+
bigIncrements(name = "id") {
|
|
1325
|
+
return this.addColumn(name, "bigIncrements");
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Integer column
|
|
1329
|
+
*/
|
|
1330
|
+
integer(name) {
|
|
1331
|
+
return this.addColumn(name, "integer");
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Big integer column
|
|
1335
|
+
*/
|
|
1336
|
+
bigInteger(name) {
|
|
1337
|
+
return this.addColumn(name, "bigInteger");
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Small integer column
|
|
1341
|
+
*/
|
|
1342
|
+
smallInteger(name) {
|
|
1343
|
+
return this.addColumn(name, "smallInteger");
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Tiny integer column
|
|
1347
|
+
*/
|
|
1348
|
+
tinyInteger(name) {
|
|
1349
|
+
return this.addColumn(name, "tinyInteger");
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Float column
|
|
1353
|
+
*/
|
|
1354
|
+
float(name) {
|
|
1355
|
+
return this.addColumn(name, "float");
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Double column
|
|
1359
|
+
*/
|
|
1360
|
+
double(name) {
|
|
1361
|
+
return this.addColumn(name, "double");
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Decimal column
|
|
1365
|
+
*/
|
|
1366
|
+
decimal(name, precision = 10, scale = 2) {
|
|
1367
|
+
return this.addColumn(name, "decimal").precision(precision, scale);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* String (VARCHAR) column
|
|
1371
|
+
*/
|
|
1372
|
+
string(name, length = 255) {
|
|
1373
|
+
return this.addColumn(name, "string").length(length);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Text column
|
|
1377
|
+
*/
|
|
1378
|
+
text(name) {
|
|
1379
|
+
return this.addColumn(name, "text");
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Medium text column
|
|
1383
|
+
*/
|
|
1384
|
+
mediumText(name) {
|
|
1385
|
+
return this.addColumn(name, "mediumText");
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Long text column
|
|
1389
|
+
*/
|
|
1390
|
+
longText(name) {
|
|
1391
|
+
return this.addColumn(name, "longText");
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Boolean column
|
|
1395
|
+
*/
|
|
1396
|
+
boolean(name) {
|
|
1397
|
+
return this.addColumn(name, "boolean");
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Date column
|
|
1401
|
+
*/
|
|
1402
|
+
date(name) {
|
|
1403
|
+
return this.addColumn(name, "date");
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Datetime column
|
|
1407
|
+
*/
|
|
1408
|
+
datetime(name) {
|
|
1409
|
+
return this.addColumn(name, "datetime");
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Timestamp column
|
|
1413
|
+
*/
|
|
1414
|
+
timestamp(name) {
|
|
1415
|
+
return this.addColumn(name, "timestamp");
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Time column
|
|
1419
|
+
*/
|
|
1420
|
+
time(name) {
|
|
1421
|
+
return this.addColumn(name, "time");
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* JSON column
|
|
1425
|
+
*/
|
|
1426
|
+
json(name) {
|
|
1427
|
+
return this.addColumn(name, "json");
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* JSONB column (PostgreSQL)
|
|
1431
|
+
*/
|
|
1432
|
+
jsonb(name) {
|
|
1433
|
+
return this.addColumn(name, "jsonb");
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* UUID column
|
|
1437
|
+
*/
|
|
1438
|
+
uuid(name) {
|
|
1439
|
+
return this.addColumn(name, "uuid");
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Binary column
|
|
1443
|
+
*/
|
|
1444
|
+
binary(name) {
|
|
1445
|
+
return this.addColumn(name, "binary");
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Enum column
|
|
1449
|
+
*/
|
|
1450
|
+
enum(name, values) {
|
|
1451
|
+
return this.addColumn(name, "enum").values(values);
|
|
1452
|
+
}
|
|
1453
|
+
// ============================================
|
|
1454
|
+
// Shortcut Methods
|
|
1455
|
+
// ============================================
|
|
1456
|
+
/**
|
|
1457
|
+
* Add created_at and updated_at timestamp columns
|
|
1458
|
+
*/
|
|
1459
|
+
timestamps() {
|
|
1460
|
+
this.timestamp("created_at").notNull().defaultNow();
|
|
1461
|
+
this.timestamp("updated_at").notNull().defaultNow();
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Add deleted_at timestamp column for soft deletes
|
|
1465
|
+
*/
|
|
1466
|
+
softDeletes() {
|
|
1467
|
+
return this.timestamp("deleted_at").nullable();
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Add foreign id column with foreign key
|
|
1471
|
+
*/
|
|
1472
|
+
foreignId(name) {
|
|
1473
|
+
const tableName = `${name.replace(/_id$/, "")}s`;
|
|
1474
|
+
const column = this.integer(name).unsigned().notNull();
|
|
1475
|
+
this.foreign(name).references("id").on(tableName);
|
|
1476
|
+
return column;
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Add UUID primary key column
|
|
1480
|
+
*/
|
|
1481
|
+
uuidPrimary(name = "id") {
|
|
1482
|
+
return this.uuid(name).primary().notNull();
|
|
1483
|
+
}
|
|
1484
|
+
// ============================================
|
|
1485
|
+
// Indexes
|
|
1486
|
+
// ============================================
|
|
1487
|
+
/**
|
|
1488
|
+
* Add an index
|
|
1489
|
+
*/
|
|
1490
|
+
index(columns, name) {
|
|
1491
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1492
|
+
this.indexes.push({
|
|
1493
|
+
name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
|
|
1494
|
+
columns: columnArray,
|
|
1495
|
+
unique: false
|
|
1496
|
+
});
|
|
1497
|
+
return this;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Add a unique index
|
|
1501
|
+
*/
|
|
1502
|
+
unique(columns, name) {
|
|
1503
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1504
|
+
this.indexes.push({
|
|
1505
|
+
name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
|
|
1506
|
+
columns: columnArray,
|
|
1507
|
+
unique: true
|
|
1508
|
+
});
|
|
1509
|
+
return this;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Add a fulltext index (MySQL)
|
|
1513
|
+
*/
|
|
1514
|
+
fulltext(columns, name) {
|
|
1515
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1516
|
+
this.indexes.push({
|
|
1517
|
+
name: name || `ft_${this.tableName}_${columnArray.join("_")}`,
|
|
1518
|
+
columns: columnArray,
|
|
1519
|
+
unique: false,
|
|
1520
|
+
type: "fulltext"
|
|
1521
|
+
});
|
|
1522
|
+
return this;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Set primary key columns
|
|
1526
|
+
*/
|
|
1527
|
+
primary(columns) {
|
|
1528
|
+
this.primaryKeyColumns = Array.isArray(columns) ? columns : [columns];
|
|
1529
|
+
return this;
|
|
1530
|
+
}
|
|
1531
|
+
// ============================================
|
|
1532
|
+
// Foreign Keys
|
|
1533
|
+
// ============================================
|
|
1534
|
+
/**
|
|
1535
|
+
* Add a foreign key constraint
|
|
1536
|
+
*/
|
|
1537
|
+
foreign(column) {
|
|
1538
|
+
return new ForeignKeyChain(this, column);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Add foreign key (internal)
|
|
1542
|
+
*/
|
|
1543
|
+
addForeignKey(fk) {
|
|
1544
|
+
this.foreignKeys.push(fk);
|
|
1545
|
+
}
|
|
1546
|
+
// ============================================
|
|
1547
|
+
// Table Options
|
|
1548
|
+
// ============================================
|
|
1549
|
+
/**
|
|
1550
|
+
* Set table engine (MySQL)
|
|
1551
|
+
*/
|
|
1552
|
+
engine(engine) {
|
|
1553
|
+
this.tableEngine = engine;
|
|
1554
|
+
return this;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Set table charset (MySQL)
|
|
1558
|
+
*/
|
|
1559
|
+
charset(charset) {
|
|
1560
|
+
this.tableCharset = charset;
|
|
1561
|
+
return this;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Set table collation (MySQL)
|
|
1565
|
+
*/
|
|
1566
|
+
collation(collation) {
|
|
1567
|
+
this.tableCollation = collation;
|
|
1568
|
+
return this;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Set table comment
|
|
1572
|
+
*/
|
|
1573
|
+
comment(comment) {
|
|
1574
|
+
this.tableComment = comment;
|
|
1575
|
+
return this;
|
|
1576
|
+
}
|
|
1577
|
+
// ============================================
|
|
1578
|
+
// Internal Methods
|
|
1579
|
+
// ============================================
|
|
1580
|
+
/**
|
|
1581
|
+
* Add a column and return its builder
|
|
1582
|
+
*/
|
|
1583
|
+
addColumn(name, type) {
|
|
1584
|
+
const builder = new ColumnBuilder(name, type);
|
|
1585
|
+
this.columns.push(builder.getDefinition());
|
|
1586
|
+
const index = this.columns.length - 1;
|
|
1587
|
+
const proxy = new Proxy(builder, {
|
|
1588
|
+
get: (target, prop, receiver) => {
|
|
1589
|
+
const result = Reflect.get(target, prop, receiver);
|
|
1590
|
+
if (typeof result === "function") {
|
|
1591
|
+
return (...args) => {
|
|
1592
|
+
const returnValue = result.apply(target, args);
|
|
1593
|
+
this.columns[index] = target.getDefinition();
|
|
1594
|
+
return returnValue === target ? proxy : returnValue;
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
return result;
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
return proxy;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Get the table definition
|
|
1604
|
+
*/
|
|
1605
|
+
getDefinition() {
|
|
1606
|
+
return {
|
|
1607
|
+
name: this.tableName,
|
|
1608
|
+
columns: [...this.columns],
|
|
1609
|
+
indexes: [...this.indexes],
|
|
1610
|
+
foreignKeys: [...this.foreignKeys],
|
|
1611
|
+
primaryKey: this.primaryKeyColumns,
|
|
1612
|
+
engine: this.tableEngine,
|
|
1613
|
+
charset: this.tableCharset,
|
|
1614
|
+
collation: this.tableCollation,
|
|
1615
|
+
comment: this.tableComment
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
var ForeignKeyChain = class {
|
|
1620
|
+
tableBuilder;
|
|
1621
|
+
fkDefinition;
|
|
1622
|
+
constructor(tableBuilder, column) {
|
|
1623
|
+
this.tableBuilder = tableBuilder;
|
|
1624
|
+
this.fkDefinition = { column };
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Set the referenced column
|
|
1628
|
+
*/
|
|
1629
|
+
references(column) {
|
|
1630
|
+
this.fkDefinition.referenceColumn = column;
|
|
1631
|
+
return this;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Set the referenced table
|
|
1635
|
+
*/
|
|
1636
|
+
on(tableName) {
|
|
1637
|
+
this.fkDefinition.table = tableName;
|
|
1638
|
+
this.apply();
|
|
1639
|
+
return this;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Alias for on()
|
|
1643
|
+
*/
|
|
1644
|
+
inTable(tableName) {
|
|
1645
|
+
return this.on(tableName);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Set ON DELETE action
|
|
1649
|
+
*/
|
|
1650
|
+
onDelete(action) {
|
|
1651
|
+
this.fkDefinition.onDelete = action;
|
|
1652
|
+
this.apply();
|
|
1653
|
+
return this;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Set ON UPDATE action
|
|
1657
|
+
*/
|
|
1658
|
+
onUpdate(action) {
|
|
1659
|
+
this.fkDefinition.onUpdate = action;
|
|
1660
|
+
this.apply();
|
|
1661
|
+
return this;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Set constraint name
|
|
1665
|
+
*/
|
|
1666
|
+
name(name) {
|
|
1667
|
+
this.fkDefinition.name = name;
|
|
1668
|
+
this.apply();
|
|
1669
|
+
return this;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Apply the foreign key to the table
|
|
1673
|
+
*/
|
|
1674
|
+
apply() {
|
|
1675
|
+
if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
1676
|
+
if (!this.fkDefinition.name) {
|
|
1677
|
+
this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
|
|
1678
|
+
}
|
|
1679
|
+
this.tableBuilder.addForeignKey(this.fkDefinition);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
// src/schema/SchemaBuilder.ts
|
|
1685
|
+
var SchemaBuilder = class {
|
|
1686
|
+
dialectInstance;
|
|
1687
|
+
adapter;
|
|
1688
|
+
constructor(options) {
|
|
1689
|
+
this.adapter = options.adapter;
|
|
1690
|
+
switch (options.dialect) {
|
|
1691
|
+
case "mysql": {
|
|
1692
|
+
this.dialectInstance = new MySQLDialect();
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
case "postgresql": {
|
|
1696
|
+
this.dialectInstance = new PostgreSQLDialect();
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
default: {
|
|
1700
|
+
throw new Error(`Unsupported dialect: ${options.dialect}`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Get the dialect instance
|
|
1706
|
+
*/
|
|
1707
|
+
get dialect() {
|
|
1708
|
+
return this.dialectInstance;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Create a new table
|
|
1712
|
+
*/
|
|
1713
|
+
async createTable(tableName, callback) {
|
|
1714
|
+
const builder = new TableBuilder(tableName);
|
|
1715
|
+
callback(builder);
|
|
1716
|
+
const definition = builder.getDefinition();
|
|
1717
|
+
const sql = this.dialectInstance.createTable(definition);
|
|
1718
|
+
await this.execute(sql);
|
|
1719
|
+
if (this.dialectInstance.dialect === "postgresql" && definition.indexes.length > 0) {
|
|
1720
|
+
const pgDialect = this.dialectInstance;
|
|
1721
|
+
for (const index of definition.indexes) {
|
|
1722
|
+
const indexSql = pgDialect.createIndex(tableName, index);
|
|
1723
|
+
await this.execute(indexSql);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Create a table if it doesn't exist
|
|
1729
|
+
*/
|
|
1730
|
+
async createTableIfNotExists(tableName, callback) {
|
|
1731
|
+
const exists = await this.hasTable(tableName);
|
|
1732
|
+
if (!exists) {
|
|
1733
|
+
await this.createTable(tableName, callback);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Drop a table
|
|
1738
|
+
*/
|
|
1739
|
+
async dropTable(tableName) {
|
|
1740
|
+
const sql = this.dialectInstance.dropTable(tableName);
|
|
1741
|
+
await this.execute(sql);
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Drop a table if it exists
|
|
1745
|
+
*/
|
|
1746
|
+
async dropTableIfExists(tableName) {
|
|
1747
|
+
const sql = this.dialectInstance.dropTableIfExists(tableName);
|
|
1748
|
+
await this.execute(sql);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Rename a table
|
|
1752
|
+
*/
|
|
1753
|
+
async renameTable(from, to) {
|
|
1754
|
+
const sql = this.dialectInstance.renameTable(from, to);
|
|
1755
|
+
await this.execute(sql);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Check if a table exists
|
|
1759
|
+
*/
|
|
1760
|
+
async hasTable(tableName) {
|
|
1761
|
+
const sql = this.dialectInstance.hasTable(tableName);
|
|
1762
|
+
const result = await this.query(sql);
|
|
1763
|
+
return result.length > 0;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Check if a column exists in a table
|
|
1767
|
+
*/
|
|
1768
|
+
async hasColumn(tableName, columnName) {
|
|
1769
|
+
const sql = this.dialectInstance.hasColumn(tableName, columnName);
|
|
1770
|
+
const result = await this.query(sql);
|
|
1771
|
+
return result.length > 0;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Alter a table
|
|
1775
|
+
*/
|
|
1776
|
+
async alterTable(tableName, callback) {
|
|
1777
|
+
const builder = new AlterTableBuilder(tableName);
|
|
1778
|
+
callback(builder);
|
|
1779
|
+
const definition = builder.getDefinition();
|
|
1780
|
+
const statements = this.dialectInstance.alterTable(definition);
|
|
1781
|
+
for (const sql of statements) {
|
|
1782
|
+
await this.execute(sql);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Execute raw SQL
|
|
1787
|
+
*/
|
|
1788
|
+
async raw(sql, params) {
|
|
1789
|
+
await this.execute(sql, params);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Execute SQL statement
|
|
1793
|
+
*/
|
|
1794
|
+
async execute(sql, params) {
|
|
1795
|
+
if (this.adapter) {
|
|
1796
|
+
await this.adapter.execute(sql, params);
|
|
1797
|
+
} else {
|
|
1798
|
+
console.log("SQL:", sql);
|
|
1799
|
+
if (params && params.length > 0) {
|
|
1800
|
+
console.log("Params:", params);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Execute SQL query
|
|
1806
|
+
*/
|
|
1807
|
+
async query(sql) {
|
|
1808
|
+
if (this.adapter) {
|
|
1809
|
+
const result = await this.adapter.query(sql);
|
|
1810
|
+
return result.rows;
|
|
1811
|
+
}
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Generate SQL without executing (for preview/dry-run)
|
|
1816
|
+
*/
|
|
1817
|
+
generateCreateTableSQL(tableName, callback) {
|
|
1818
|
+
const builder = new TableBuilder(tableName);
|
|
1819
|
+
callback(builder);
|
|
1820
|
+
return this.dialectInstance.createTable(builder.getDefinition());
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Generate ALTER TABLE SQL without executing
|
|
1824
|
+
*/
|
|
1825
|
+
generateAlterTableSQL(tableName, callback) {
|
|
1826
|
+
const builder = new AlterTableBuilder(tableName);
|
|
1827
|
+
callback(builder);
|
|
1828
|
+
return this.dialectInstance.alterTable(builder.getDefinition());
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
var AlterTableBuilder = class {
|
|
1832
|
+
tableName;
|
|
1833
|
+
operations = [];
|
|
1834
|
+
constructor(tableName) {
|
|
1835
|
+
this.tableName = tableName;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Add a new column
|
|
1839
|
+
*/
|
|
1840
|
+
addColumn(name, type, options) {
|
|
1841
|
+
this.operations.push({
|
|
1842
|
+
type: "addColumn",
|
|
1843
|
+
column: {
|
|
1844
|
+
name,
|
|
1845
|
+
type,
|
|
1846
|
+
nullable: true,
|
|
1847
|
+
unsigned: false,
|
|
1848
|
+
autoIncrement: false,
|
|
1849
|
+
primary: false,
|
|
1850
|
+
unique: false,
|
|
1851
|
+
index: false,
|
|
1852
|
+
first: false,
|
|
1853
|
+
...options
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
return this;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Drop a column
|
|
1860
|
+
*/
|
|
1861
|
+
dropColumn(name) {
|
|
1862
|
+
this.operations.push({ type: "dropColumn", name });
|
|
1863
|
+
return this;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Rename a column
|
|
1867
|
+
*/
|
|
1868
|
+
renameColumn(from, to) {
|
|
1869
|
+
this.operations.push({ type: "renameColumn", from, to });
|
|
1870
|
+
return this;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Modify a column
|
|
1874
|
+
*/
|
|
1875
|
+
modifyColumn(name, type, options) {
|
|
1876
|
+
this.operations.push({
|
|
1877
|
+
type: "modifyColumn",
|
|
1878
|
+
column: {
|
|
1879
|
+
name,
|
|
1880
|
+
type,
|
|
1881
|
+
nullable: true,
|
|
1882
|
+
unsigned: false,
|
|
1883
|
+
autoIncrement: false,
|
|
1884
|
+
primary: false,
|
|
1885
|
+
unique: false,
|
|
1886
|
+
index: false,
|
|
1887
|
+
first: false,
|
|
1888
|
+
...options
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
return this;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Add an index
|
|
1895
|
+
*/
|
|
1896
|
+
addIndex(columns, name) {
|
|
1897
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1898
|
+
this.operations.push({
|
|
1899
|
+
type: "addIndex",
|
|
1900
|
+
index: {
|
|
1901
|
+
name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
|
|
1902
|
+
columns: columnArray,
|
|
1903
|
+
unique: false
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
return this;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Add a unique index
|
|
1910
|
+
*/
|
|
1911
|
+
addUnique(columns, name) {
|
|
1912
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1913
|
+
this.operations.push({
|
|
1914
|
+
type: "addIndex",
|
|
1915
|
+
index: {
|
|
1916
|
+
name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
|
|
1917
|
+
columns: columnArray,
|
|
1918
|
+
unique: true
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
return this;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Drop an index
|
|
1925
|
+
*/
|
|
1926
|
+
dropIndex(name) {
|
|
1927
|
+
this.operations.push({ type: "dropIndex", name });
|
|
1928
|
+
return this;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Add a foreign key
|
|
1932
|
+
*/
|
|
1933
|
+
addForeign(column) {
|
|
1934
|
+
return new AlterForeignKeyBuilder(this, column);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Add foreign key (internal)
|
|
1938
|
+
*/
|
|
1939
|
+
addForeignKeyOperation(fk) {
|
|
1940
|
+
this.operations.push({ type: "addForeignKey", foreignKey: fk });
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Drop a foreign key
|
|
1944
|
+
*/
|
|
1945
|
+
dropForeign(name) {
|
|
1946
|
+
this.operations.push({ type: "dropForeignKey", name });
|
|
1947
|
+
return this;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Add primary key
|
|
1951
|
+
*/
|
|
1952
|
+
addPrimary(columns) {
|
|
1953
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
1954
|
+
this.operations.push({ type: "addPrimary", columns: columnArray });
|
|
1955
|
+
return this;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Drop primary key
|
|
1959
|
+
*/
|
|
1960
|
+
dropPrimary() {
|
|
1961
|
+
this.operations.push({ type: "dropPrimary" });
|
|
1962
|
+
return this;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Get the alter table definition
|
|
1966
|
+
*/
|
|
1967
|
+
getDefinition() {
|
|
1968
|
+
return {
|
|
1969
|
+
tableName: this.tableName,
|
|
1970
|
+
operations: [...this.operations]
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
var AlterForeignKeyBuilder = class {
|
|
1975
|
+
builder;
|
|
1976
|
+
fkDefinition;
|
|
1977
|
+
constructor(builder, column) {
|
|
1978
|
+
this.builder = builder;
|
|
1979
|
+
this.fkDefinition = { column };
|
|
1980
|
+
}
|
|
1981
|
+
references(column) {
|
|
1982
|
+
this.fkDefinition.referenceColumn = column;
|
|
1983
|
+
return this;
|
|
1984
|
+
}
|
|
1985
|
+
on(tableName) {
|
|
1986
|
+
this.fkDefinition.table = tableName;
|
|
1987
|
+
this.apply();
|
|
1988
|
+
return this;
|
|
1989
|
+
}
|
|
1990
|
+
onDelete(action) {
|
|
1991
|
+
this.fkDefinition.onDelete = action;
|
|
1992
|
+
this.apply();
|
|
1993
|
+
return this;
|
|
1994
|
+
}
|
|
1995
|
+
onUpdate(action) {
|
|
1996
|
+
this.fkDefinition.onUpdate = action;
|
|
1997
|
+
this.apply();
|
|
1998
|
+
return this;
|
|
1999
|
+
}
|
|
2000
|
+
name(name) {
|
|
2001
|
+
this.fkDefinition.name = name;
|
|
2002
|
+
this.apply();
|
|
2003
|
+
return this;
|
|
2004
|
+
}
|
|
2005
|
+
apply() {
|
|
2006
|
+
if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
2007
|
+
if (!this.fkDefinition.name) {
|
|
2008
|
+
this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
|
|
2009
|
+
}
|
|
2010
|
+
this.builder.addForeignKeyOperation(this.fkDefinition);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
// src/migrations/FileMigrationRunner.ts
|
|
2016
|
+
var FileMigrationRunner = class {
|
|
2017
|
+
adapter;
|
|
2018
|
+
loader;
|
|
2019
|
+
lock;
|
|
2020
|
+
schema;
|
|
2021
|
+
options;
|
|
2022
|
+
constructor(adapter, options) {
|
|
2023
|
+
this.adapter = adapter;
|
|
2024
|
+
this.options = {
|
|
2025
|
+
directory: options.directory,
|
|
2026
|
+
tableName: options.tableName ?? "db_migrations",
|
|
2027
|
+
lockTableName: options.lockTableName ?? "db_migrations_lock",
|
|
2028
|
+
lockTimeout: options.lockTimeout ?? 6e4,
|
|
2029
|
+
validateChecksums: options.validateChecksums ?? true,
|
|
2030
|
+
dialect: options.dialect,
|
|
2031
|
+
logger: options.logger ?? console,
|
|
2032
|
+
dryRun: options.dryRun ?? false
|
|
2033
|
+
};
|
|
2034
|
+
this.loader = new MigrationLoader(options.directory);
|
|
2035
|
+
this.lock = new MigrationLock(adapter, {
|
|
2036
|
+
tableName: this.options.lockTableName,
|
|
2037
|
+
timeout: this.options.lockTimeout,
|
|
2038
|
+
dialect: options.dialect
|
|
2039
|
+
});
|
|
2040
|
+
this.schema = new SchemaBuilder({
|
|
2041
|
+
dialect: options.dialect,
|
|
2042
|
+
adapter
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Initialize migration tables
|
|
2047
|
+
*/
|
|
2048
|
+
async initialize() {
|
|
2049
|
+
if (this.options.dryRun) {
|
|
2050
|
+
this.options.logger.info("DRY RUN: Would create migration tables");
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const tableName = this.options.tableName;
|
|
2054
|
+
const hasTable = await this.schema.hasTable(tableName);
|
|
2055
|
+
if (!hasTable) {
|
|
2056
|
+
await this.schema.createTable(tableName, (table) => {
|
|
2057
|
+
table.increments("id");
|
|
2058
|
+
table.string("name", 255).notNull().unique();
|
|
2059
|
+
table.integer("batch").notNull();
|
|
2060
|
+
table.timestamp("executed_at").notNull().defaultNow();
|
|
2061
|
+
table.integer("execution_time_ms").notNull();
|
|
2062
|
+
table.string("checksum", 64).notNull();
|
|
2063
|
+
table.index("batch");
|
|
2064
|
+
table.index("executed_at");
|
|
2065
|
+
});
|
|
2066
|
+
this.options.logger.info(`Created migrations table: ${tableName}`);
|
|
2067
|
+
}
|
|
2068
|
+
await this.lock.initialize();
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Run all pending migrations
|
|
2072
|
+
*/
|
|
2073
|
+
async latest() {
|
|
2074
|
+
await this.lock.initialize();
|
|
2075
|
+
return this.lock.withLock(async () => {
|
|
2076
|
+
await this.initialize();
|
|
2077
|
+
const pending = await this.getPendingMigrations();
|
|
2078
|
+
if (pending.length === 0) {
|
|
2079
|
+
this.options.logger.info("No pending migrations");
|
|
2080
|
+
return [];
|
|
2081
|
+
}
|
|
2082
|
+
const batch = await this.getNextBatch();
|
|
2083
|
+
const executed = [];
|
|
2084
|
+
this.options.logger.info(`Running ${pending.length} migrations (batch ${batch})`);
|
|
2085
|
+
for (const migration of pending) {
|
|
2086
|
+
await this.runMigration(migration, "up", batch);
|
|
2087
|
+
executed.push(migration.name);
|
|
2088
|
+
}
|
|
2089
|
+
return executed;
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Rollback the last batch of migrations
|
|
2094
|
+
*/
|
|
2095
|
+
async rollback(steps = 1) {
|
|
2096
|
+
await this.lock.initialize();
|
|
2097
|
+
return this.lock.withLock(async () => {
|
|
2098
|
+
await this.initialize();
|
|
2099
|
+
const batches = await this.getLastBatches(steps);
|
|
2100
|
+
if (batches.length === 0) {
|
|
2101
|
+
this.options.logger.info("No migrations to rollback");
|
|
2102
|
+
return [];
|
|
2103
|
+
}
|
|
2104
|
+
const rolledBack = [];
|
|
2105
|
+
for (const batch of batches) {
|
|
2106
|
+
this.options.logger.info(`Rolling back batch ${batch.batch}`);
|
|
2107
|
+
const migrations = await this.loadMigrationsByName(
|
|
2108
|
+
batch.migrations.map((m) => m.name).reverse()
|
|
2109
|
+
);
|
|
2110
|
+
for (const migration of migrations) {
|
|
2111
|
+
await this.runMigration(migration, "down", batch.batch);
|
|
2112
|
+
rolledBack.push(migration.name);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return rolledBack;
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Reset all migrations
|
|
2120
|
+
*/
|
|
2121
|
+
async reset() {
|
|
2122
|
+
await this.lock.initialize();
|
|
2123
|
+
return this.lock.withLock(async () => {
|
|
2124
|
+
await this.initialize();
|
|
2125
|
+
const executed = await this.getExecutedMigrations();
|
|
2126
|
+
if (executed.length === 0) {
|
|
2127
|
+
this.options.logger.info("No migrations to reset");
|
|
2128
|
+
return [];
|
|
2129
|
+
}
|
|
2130
|
+
const rolledBack = [];
|
|
2131
|
+
const migrations = await this.loadMigrationsByName(executed.map((m) => m.name).reverse());
|
|
2132
|
+
for (const migration of migrations) {
|
|
2133
|
+
const record = executed.find((e) => e.name === migration.name);
|
|
2134
|
+
await this.runMigration(migration, "down", record.batch);
|
|
2135
|
+
rolledBack.push(migration.name);
|
|
2136
|
+
}
|
|
2137
|
+
return rolledBack;
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Reset and re-run all migrations
|
|
2142
|
+
*/
|
|
2143
|
+
async fresh() {
|
|
2144
|
+
await this.reset();
|
|
2145
|
+
return this.latest();
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Get migration status
|
|
2149
|
+
*/
|
|
2150
|
+
async status() {
|
|
2151
|
+
await this.initialize();
|
|
2152
|
+
const allMigrations = await this.loader.getMigrationFiles();
|
|
2153
|
+
const executed = await this.getExecutedMigrations();
|
|
2154
|
+
const executedMap = new Map(executed.map((e) => [e.name, e]));
|
|
2155
|
+
return allMigrations.map((file) => {
|
|
2156
|
+
const record = executedMap.get(file.name);
|
|
2157
|
+
return {
|
|
2158
|
+
name: file.name,
|
|
2159
|
+
batch: record?.batch ?? null,
|
|
2160
|
+
executedAt: record?.executed_at ?? null,
|
|
2161
|
+
pending: !record
|
|
2162
|
+
};
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Validate migrations
|
|
2167
|
+
*/
|
|
2168
|
+
async validate() {
|
|
2169
|
+
const errors = [];
|
|
2170
|
+
try {
|
|
2171
|
+
const migrations = await this.loader.loadAll();
|
|
2172
|
+
const executed = await this.getExecutedMigrations();
|
|
2173
|
+
for (const record of executed) {
|
|
2174
|
+
const migration = migrations.find((m) => m.name === record.name);
|
|
2175
|
+
if (!migration) {
|
|
2176
|
+
errors.push(`Migration '${record.name}' exists in database but not in codebase`);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (this.options.validateChecksums) {
|
|
2180
|
+
const checksum = this.calculateChecksum(migration);
|
|
2181
|
+
if (checksum !== record.checksum) {
|
|
2182
|
+
errors.push(
|
|
2183
|
+
`Checksum mismatch for migration '${record.name}'. The migration may have been modified.`
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
} catch (error2) {
|
|
2189
|
+
errors.push(`Validation error: ${error2.message}`);
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
valid: errors.length === 0,
|
|
2193
|
+
errors
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Get pending migrations
|
|
2198
|
+
*/
|
|
2199
|
+
async getPendingMigrations() {
|
|
2200
|
+
const allMigrations = await this.loader.loadAll();
|
|
2201
|
+
const executed = await this.getExecutedMigrations();
|
|
2202
|
+
const executedNames = new Set(executed.map((e) => e.name));
|
|
2203
|
+
return allMigrations.filter((m) => !executedNames.has(m.name));
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Get executed migrations from database
|
|
2207
|
+
*/
|
|
2208
|
+
async getExecutedMigrations() {
|
|
2209
|
+
const result = await this.adapter.query(
|
|
2210
|
+
`SELECT * FROM ${this.options.tableName} ORDER BY id ASC`
|
|
2211
|
+
);
|
|
2212
|
+
return result.rows;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Get the next batch number
|
|
2216
|
+
*/
|
|
2217
|
+
async getNextBatch() {
|
|
2218
|
+
const result = await this.adapter.query(
|
|
2219
|
+
`SELECT MAX(batch) as max_batch FROM ${this.options.tableName}`
|
|
2220
|
+
);
|
|
2221
|
+
return (result.rows[0]?.max_batch ?? 0) + 1;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Get the last N batches
|
|
2225
|
+
*/
|
|
2226
|
+
async getLastBatches(count) {
|
|
2227
|
+
const result = await this.adapter.query(
|
|
2228
|
+
`SELECT * FROM ${this.options.tableName} ORDER BY batch DESC, id DESC`
|
|
2229
|
+
);
|
|
2230
|
+
const batches = /* @__PURE__ */ new Map();
|
|
2231
|
+
for (const record of result.rows) {
|
|
2232
|
+
if (!batches.has(record.batch)) {
|
|
2233
|
+
batches.set(record.batch, []);
|
|
2234
|
+
}
|
|
2235
|
+
batches.get(record.batch).push(record);
|
|
2236
|
+
}
|
|
2237
|
+
const batchNumbers = Array.from(batches.keys()).slice(0, count);
|
|
2238
|
+
return batchNumbers.map((batch) => ({
|
|
2239
|
+
batch,
|
|
2240
|
+
migrations: batches.get(batch)
|
|
2241
|
+
}));
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Load migrations by name
|
|
2245
|
+
*/
|
|
2246
|
+
async loadMigrationsByName(names) {
|
|
2247
|
+
const allMigrations = await this.loader.loadAll();
|
|
2248
|
+
const migrationMap = new Map(allMigrations.map((m) => [m.name, m]));
|
|
2249
|
+
return names.map((name) => migrationMap.get(name)).filter((m) => m !== void 0);
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Run a single migration
|
|
2253
|
+
*/
|
|
2254
|
+
async runMigration(migration, direction, batch) {
|
|
2255
|
+
const startTime = Date.now();
|
|
2256
|
+
const action = direction === "up" ? "Running" : "Rolling back";
|
|
2257
|
+
this.options.logger.info(`${action}: ${migration.name}`);
|
|
2258
|
+
if (this.options.dryRun) {
|
|
2259
|
+
this.options.logger.info(`DRY RUN: Would ${direction} ${migration.name}`);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const transaction = migration.transactional ? await this.adapter.beginTransaction() : null;
|
|
2263
|
+
try {
|
|
2264
|
+
const schema = new SchemaBuilder({
|
|
2265
|
+
dialect: this.options.dialect,
|
|
2266
|
+
adapter: this.adapter
|
|
2267
|
+
});
|
|
2268
|
+
await migration[direction](schema);
|
|
2269
|
+
const executionTime = Date.now() - startTime;
|
|
2270
|
+
if (direction === "up") {
|
|
2271
|
+
const checksum = this.calculateChecksum(migration);
|
|
2272
|
+
await this.adapter.execute(
|
|
2273
|
+
`INSERT INTO ${this.options.tableName}
|
|
2274
|
+
(name, batch, executed_at, execution_time_ms, checksum)
|
|
2275
|
+
VALUES (?, ?, NOW(), ?, ?)`,
|
|
2276
|
+
[migration.name, batch, executionTime, checksum]
|
|
2277
|
+
);
|
|
2278
|
+
} else {
|
|
2279
|
+
await this.adapter.execute(`DELETE FROM ${this.options.tableName} WHERE name = ?`, [
|
|
2280
|
+
migration.name
|
|
2281
|
+
]);
|
|
2282
|
+
}
|
|
2283
|
+
if (transaction) {
|
|
2284
|
+
await transaction.commit();
|
|
2285
|
+
}
|
|
2286
|
+
this.options.logger.info(
|
|
2287
|
+
`${direction === "up" ? "Completed" : "Rolled back"}: ${migration.name} (${executionTime}ms)`
|
|
2288
|
+
);
|
|
2289
|
+
} catch (error2) {
|
|
2290
|
+
if (transaction) {
|
|
2291
|
+
await transaction.rollback();
|
|
2292
|
+
}
|
|
2293
|
+
throw new Error(
|
|
2294
|
+
`Failed to ${direction} migration '${migration.name}': ${error2.message}`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Calculate migration checksum
|
|
2300
|
+
*/
|
|
2301
|
+
calculateChecksum(migration) {
|
|
2302
|
+
const content = `${migration.name}:${migration.up.toString()}:${migration.down.toString()}`;
|
|
2303
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
|
|
2307
|
+
// src/cli/utils.ts
|
|
2308
|
+
async function createAdapterFromConfig(config) {
|
|
2309
|
+
const { dialect, host, port, user, password, database, ssl } = config.connection;
|
|
2310
|
+
if (dialect === "mysql") {
|
|
2311
|
+
try {
|
|
2312
|
+
const { MySQLAdapter } = await import('@db-bridge/mysql');
|
|
2313
|
+
const adapter = new MySQLAdapter();
|
|
2314
|
+
await adapter.connect({
|
|
2315
|
+
host,
|
|
2316
|
+
port,
|
|
2317
|
+
user,
|
|
2318
|
+
password,
|
|
2319
|
+
database
|
|
2320
|
+
});
|
|
2321
|
+
return adapter;
|
|
2322
|
+
} catch (error2) {
|
|
2323
|
+
if (error2.message.includes("Cannot find module")) {
|
|
2324
|
+
throw new Error(
|
|
2325
|
+
"MySQL adapter not found. Install it with: npm install @db-bridge/mysql mysql2"
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
throw error2;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (dialect === "postgresql") {
|
|
2332
|
+
try {
|
|
2333
|
+
const { PostgreSQLAdapter } = await import('@db-bridge/postgresql');
|
|
2334
|
+
const adapter = new PostgreSQLAdapter();
|
|
2335
|
+
await adapter.connect({
|
|
2336
|
+
host,
|
|
2337
|
+
port,
|
|
2338
|
+
user,
|
|
2339
|
+
password,
|
|
2340
|
+
database,
|
|
2341
|
+
ssl
|
|
2342
|
+
});
|
|
2343
|
+
return adapter;
|
|
2344
|
+
} catch (error2) {
|
|
2345
|
+
if (error2.message.includes("Cannot find module")) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
"PostgreSQL adapter not found. Install it with: npm install @db-bridge/postgresql pg"
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
throw error2;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
throw new Error(`Unsupported dialect: ${dialect}`);
|
|
2354
|
+
}
|
|
2355
|
+
async function createRunnerFromConfig(options = {}) {
|
|
2356
|
+
const config = await loadConfig();
|
|
2357
|
+
const adapter = await createAdapterFromConfig(config);
|
|
2358
|
+
const runner = new FileMigrationRunner(adapter, {
|
|
2359
|
+
directory: resolve(process.cwd(), config.migrations?.directory || "./src/migrations"),
|
|
2360
|
+
tableName: config.migrations?.tableName,
|
|
2361
|
+
lockTableName: config.migrations?.lockTableName,
|
|
2362
|
+
dialect: config.connection.dialect,
|
|
2363
|
+
dryRun: options.dryRun
|
|
2364
|
+
});
|
|
2365
|
+
return { runner, adapter, config };
|
|
2366
|
+
}
|
|
2367
|
+
function formatTable(headers, rows) {
|
|
2368
|
+
const widths = headers.map((h, i) => {
|
|
2369
|
+
const maxRow = Math.max(...rows.map((r) => (r[i] || "").length));
|
|
2370
|
+
return Math.max(h.length, maxRow);
|
|
2371
|
+
});
|
|
2372
|
+
const separator = "\u2500".repeat(widths.reduce((a, b) => a + b + 3, 1));
|
|
2373
|
+
const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(" \u2502 ");
|
|
2374
|
+
const dataRows = rows.map((row) => row.map((cell, i) => (cell || "").padEnd(widths[i])).join(" \u2502 ")).join("\n");
|
|
2375
|
+
return `\u250C${separator}\u2510
|
|
2376
|
+
\u2502 ${headerRow} \u2502
|
|
2377
|
+
\u251C${separator}\u2524
|
|
2378
|
+
${dataRows ? dataRows.split("\n").map((r) => `\u2502 ${r} \u2502`).join("\n") : `\u2502${" ".repeat(separator.length)}\u2502`}
|
|
2379
|
+
\u2514${separator}\u2518`;
|
|
2380
|
+
}
|
|
2381
|
+
function success(message) {
|
|
2382
|
+
console.log(`\u2713 ${message}`);
|
|
2383
|
+
}
|
|
2384
|
+
function error(message) {
|
|
2385
|
+
console.error(`\u2717 ${message}`);
|
|
2386
|
+
}
|
|
2387
|
+
function info(message) {
|
|
2388
|
+
console.log(`\u2139 ${message}`);
|
|
2389
|
+
}
|
|
2390
|
+
function warn(message) {
|
|
2391
|
+
console.warn(`\u26A0 ${message}`);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/cli/commands/fresh.ts
|
|
2395
|
+
async function freshCommand(options = {}) {
|
|
2396
|
+
let adapter;
|
|
2397
|
+
try {
|
|
2398
|
+
if (options.dryRun) {
|
|
2399
|
+
info("DRY RUN MODE - No changes will be made");
|
|
2400
|
+
} else {
|
|
2401
|
+
warn("This will reset ALL migrations and re-run them!");
|
|
2402
|
+
}
|
|
2403
|
+
const { runner, adapter: a } = await createRunnerFromConfig(options);
|
|
2404
|
+
adapter = a;
|
|
2405
|
+
console.log("");
|
|
2406
|
+
info("Resetting all migrations...");
|
|
2407
|
+
const executed = await runner.fresh();
|
|
2408
|
+
if (executed.length === 0) {
|
|
2409
|
+
info("No migrations to run");
|
|
2410
|
+
} else {
|
|
2411
|
+
console.log("");
|
|
2412
|
+
success(`Fresh migration complete - ran ${executed.length} migration(s):`);
|
|
2413
|
+
for (const name of executed) {
|
|
2414
|
+
console.log(` - ${name}`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
} catch (error_) {
|
|
2418
|
+
error(error_.message);
|
|
2419
|
+
process.exit(1);
|
|
2420
|
+
} finally {
|
|
2421
|
+
if (adapter) {
|
|
2422
|
+
await adapter.disconnect();
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/cli/commands/latest.ts
|
|
2428
|
+
async function latestCommand(options = {}) {
|
|
2429
|
+
let adapter;
|
|
2430
|
+
try {
|
|
2431
|
+
if (options.dryRun) {
|
|
2432
|
+
info("DRY RUN MODE - No changes will be made");
|
|
2433
|
+
}
|
|
2434
|
+
const { runner, adapter: a } = await createRunnerFromConfig(options);
|
|
2435
|
+
adapter = a;
|
|
2436
|
+
const executed = await runner.latest();
|
|
2437
|
+
if (executed.length === 0) {
|
|
2438
|
+
info("Nothing to migrate");
|
|
2439
|
+
} else {
|
|
2440
|
+
console.log("");
|
|
2441
|
+
success(`Ran ${executed.length} migration(s):`);
|
|
2442
|
+
for (const name of executed) {
|
|
2443
|
+
console.log(` - ${name}`);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} catch (error_) {
|
|
2447
|
+
error(error_.message);
|
|
2448
|
+
process.exit(1);
|
|
2449
|
+
} finally {
|
|
2450
|
+
if (adapter) {
|
|
2451
|
+
await adapter.disconnect();
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
async function makeCommand(name) {
|
|
2456
|
+
try {
|
|
2457
|
+
const config = await loadConfig();
|
|
2458
|
+
const directory = resolve(process.cwd(), config.migrations?.directory || "./src/migrations");
|
|
2459
|
+
if (!existsSync(directory)) {
|
|
2460
|
+
await mkdir(directory, { recursive: true });
|
|
2461
|
+
console.log(`Created migrations directory: ${directory}`);
|
|
2462
|
+
}
|
|
2463
|
+
const filename = MigrationLoader.generateFilename(name);
|
|
2464
|
+
const filepath = resolve(directory, filename);
|
|
2465
|
+
const migrationName = filename.replace(/\.(ts|js|mjs)$/, "");
|
|
2466
|
+
const content = MigrationLoader.getMigrationTemplate(migrationName);
|
|
2467
|
+
if (existsSync(filepath)) {
|
|
2468
|
+
error(`Migration file already exists: ${filepath}`);
|
|
2469
|
+
process.exit(1);
|
|
2470
|
+
}
|
|
2471
|
+
await writeFile(filepath, content, "utf8");
|
|
2472
|
+
success(`Created migration: ${filename}`);
|
|
2473
|
+
console.log(` Path: ${filepath}`);
|
|
2474
|
+
} catch (error_) {
|
|
2475
|
+
error(error_.message);
|
|
2476
|
+
process.exit(1);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
var SeederLoader = class {
|
|
2480
|
+
constructor(directory) {
|
|
2481
|
+
this.directory = directory;
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Load all seeder files from directory
|
|
2485
|
+
*/
|
|
2486
|
+
async loadAll() {
|
|
2487
|
+
const files = await readdir(this.directory);
|
|
2488
|
+
const seederFiles = files.filter((f) => /\.(ts|js|mjs)$/.test(f) && !f.endsWith(".d.ts")).sort().map((f) => ({
|
|
2489
|
+
name: this.getSeederName(f),
|
|
2490
|
+
path: resolve(this.directory, f)
|
|
2491
|
+
}));
|
|
2492
|
+
return seederFiles;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Load a specific seeder by path
|
|
2496
|
+
*/
|
|
2497
|
+
async load(seederPath) {
|
|
2498
|
+
const fileUrl = pathToFileURL(seederPath).href;
|
|
2499
|
+
const module = await import(fileUrl);
|
|
2500
|
+
const seeder = module.default || module;
|
|
2501
|
+
if (!seeder || typeof seeder.run !== "function") {
|
|
2502
|
+
throw new Error(`Invalid seeder: ${seederPath} - must export a run() function`);
|
|
2503
|
+
}
|
|
2504
|
+
return seeder;
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Get seeder name from filename
|
|
2508
|
+
*/
|
|
2509
|
+
getSeederName(filename) {
|
|
2510
|
+
return basename(filename).replace(/\.(ts|js|mjs)$/, "");
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Generate a new seeder filename
|
|
2514
|
+
*/
|
|
2515
|
+
static generateFilename(name) {
|
|
2516
|
+
const snakeName = name.replaceAll(/([a-z])([A-Z])/g, "$1_$2").replaceAll(/[\s-]+/g, "_").toLowerCase();
|
|
2517
|
+
return `${snakeName}_seeder.ts`;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Get seeder template
|
|
2521
|
+
*/
|
|
2522
|
+
static getSeederTemplate(name) {
|
|
2523
|
+
const className = name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2524
|
+
return `/**
|
|
2525
|
+
* ${className} Seeder
|
|
2526
|
+
*/
|
|
2527
|
+
|
|
2528
|
+
import type { Seeder, DatabaseAdapter } from '@db-bridge/core';
|
|
2529
|
+
|
|
2530
|
+
export default {
|
|
2531
|
+
async run(adapter: DatabaseAdapter): Promise<void> {
|
|
2532
|
+
// Insert seed data
|
|
2533
|
+
// await adapter.execute(\`
|
|
2534
|
+
// INSERT INTO users (name, email) VALUES
|
|
2535
|
+
// ('John Doe', 'john@example.com'),
|
|
2536
|
+
// ('Jane Doe', 'jane@example.com')
|
|
2537
|
+
// \`);
|
|
2538
|
+
},
|
|
2539
|
+
} satisfies Seeder;
|
|
2540
|
+
`;
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// src/cli/commands/make-seeder.ts
|
|
2545
|
+
async function makeSeederCommand(name) {
|
|
2546
|
+
try {
|
|
2547
|
+
const config = await loadConfig();
|
|
2548
|
+
const directory = resolve(process.cwd(), config.seeds?.directory || "./src/seeds");
|
|
2549
|
+
if (!existsSync(directory)) {
|
|
2550
|
+
await mkdir(directory, { recursive: true });
|
|
2551
|
+
console.log(`Created seeds directory: ${directory}`);
|
|
2552
|
+
}
|
|
2553
|
+
const filename = SeederLoader.generateFilename(name);
|
|
2554
|
+
const filepath = resolve(directory, filename);
|
|
2555
|
+
const seederName = filename.replace(/\.(ts|js|mjs)$/, "");
|
|
2556
|
+
const content = SeederLoader.getSeederTemplate(seederName);
|
|
2557
|
+
if (existsSync(filepath)) {
|
|
2558
|
+
error(`Seeder file already exists: ${filepath}`);
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
}
|
|
2561
|
+
await writeFile(filepath, content, "utf8");
|
|
2562
|
+
success(`Created seeder: ${filename}`);
|
|
2563
|
+
console.log(` Path: ${filepath}`);
|
|
2564
|
+
} catch (error_) {
|
|
2565
|
+
error(error_.message);
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/cli/commands/reset.ts
|
|
2571
|
+
async function resetCommand(options = {}) {
|
|
2572
|
+
let adapter;
|
|
2573
|
+
try {
|
|
2574
|
+
if (options.dryRun) {
|
|
2575
|
+
info("DRY RUN MODE - No changes will be made");
|
|
2576
|
+
} else {
|
|
2577
|
+
warn("This will rollback ALL migrations!");
|
|
2578
|
+
}
|
|
2579
|
+
const { runner, adapter: a } = await createRunnerFromConfig(options);
|
|
2580
|
+
adapter = a;
|
|
2581
|
+
const rolledBack = await runner.reset();
|
|
2582
|
+
if (rolledBack.length === 0) {
|
|
2583
|
+
info("Nothing to reset");
|
|
2584
|
+
} else {
|
|
2585
|
+
console.log("");
|
|
2586
|
+
success(`Reset ${rolledBack.length} migration(s):`);
|
|
2587
|
+
for (const name of rolledBack) {
|
|
2588
|
+
console.log(` - ${name}`);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
} catch (error_) {
|
|
2592
|
+
error(error_.message);
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
} finally {
|
|
2595
|
+
if (adapter) {
|
|
2596
|
+
await adapter.disconnect();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/cli/commands/rollback.ts
|
|
2602
|
+
async function rollbackCommand(options = {}) {
|
|
2603
|
+
let adapter;
|
|
2604
|
+
try {
|
|
2605
|
+
if (options.dryRun) {
|
|
2606
|
+
info("DRY RUN MODE - No changes will be made");
|
|
2607
|
+
}
|
|
2608
|
+
const { runner, adapter: a } = await createRunnerFromConfig(options);
|
|
2609
|
+
adapter = a;
|
|
2610
|
+
const step = options.step ?? 1;
|
|
2611
|
+
const rolledBack = await runner.rollback(step);
|
|
2612
|
+
if (rolledBack.length === 0) {
|
|
2613
|
+
info("Nothing to rollback");
|
|
2614
|
+
} else {
|
|
2615
|
+
console.log("");
|
|
2616
|
+
success(`Rolled back ${rolledBack.length} migration(s):`);
|
|
2617
|
+
for (const name of rolledBack) {
|
|
2618
|
+
console.log(` - ${name}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
} catch (error_) {
|
|
2622
|
+
error(error_.message);
|
|
2623
|
+
process.exit(1);
|
|
2624
|
+
} finally {
|
|
2625
|
+
if (adapter) {
|
|
2626
|
+
await adapter.disconnect();
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/seeds/SeederRunner.ts
|
|
2632
|
+
var SeederRunner = class {
|
|
2633
|
+
constructor(adapter, options) {
|
|
2634
|
+
this.adapter = adapter;
|
|
2635
|
+
this.options = options;
|
|
2636
|
+
this.loader = new SeederLoader(options.directory);
|
|
2637
|
+
}
|
|
2638
|
+
loader;
|
|
2639
|
+
options;
|
|
2640
|
+
/**
|
|
2641
|
+
* Run all seeders (or filtered by options)
|
|
2642
|
+
*/
|
|
2643
|
+
async run() {
|
|
2644
|
+
const files = await this.loader.loadAll();
|
|
2645
|
+
const filteredFiles = this.filterSeeders(files);
|
|
2646
|
+
const results = [];
|
|
2647
|
+
for (const file of filteredFiles) {
|
|
2648
|
+
const startTime = Date.now();
|
|
2649
|
+
try {
|
|
2650
|
+
const seeder = await this.loader.load(file.path);
|
|
2651
|
+
await seeder.run(this.adapter);
|
|
2652
|
+
results.push({
|
|
2653
|
+
name: file.name,
|
|
2654
|
+
success: true,
|
|
2655
|
+
duration: Date.now() - startTime
|
|
2656
|
+
});
|
|
2657
|
+
} catch (error2) {
|
|
2658
|
+
results.push({
|
|
2659
|
+
name: file.name,
|
|
2660
|
+
success: false,
|
|
2661
|
+
error: error2.message,
|
|
2662
|
+
duration: Date.now() - startTime
|
|
2663
|
+
});
|
|
2664
|
+
break;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return results;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Run a specific seeder by name
|
|
2671
|
+
*/
|
|
2672
|
+
async runSeeder(name) {
|
|
2673
|
+
const files = await this.loader.loadAll();
|
|
2674
|
+
const file = files.find((f) => f.name === name || f.name === `${name}_seeder`);
|
|
2675
|
+
if (!file) {
|
|
2676
|
+
return {
|
|
2677
|
+
name,
|
|
2678
|
+
success: false,
|
|
2679
|
+
error: `Seeder not found: ${name}`,
|
|
2680
|
+
duration: 0
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
const startTime = Date.now();
|
|
2684
|
+
try {
|
|
2685
|
+
const seeder = await this.loader.load(file.path);
|
|
2686
|
+
await seeder.run(this.adapter);
|
|
2687
|
+
return {
|
|
2688
|
+
name: file.name,
|
|
2689
|
+
success: true,
|
|
2690
|
+
duration: Date.now() - startTime
|
|
2691
|
+
};
|
|
2692
|
+
} catch (error2) {
|
|
2693
|
+
return {
|
|
2694
|
+
name: file.name,
|
|
2695
|
+
success: false,
|
|
2696
|
+
error: error2.message,
|
|
2697
|
+
duration: Date.now() - startTime
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Filter seeders based on options
|
|
2703
|
+
*/
|
|
2704
|
+
filterSeeders(files) {
|
|
2705
|
+
let filtered = files;
|
|
2706
|
+
if (this.options.only && this.options.only.length > 0) {
|
|
2707
|
+
filtered = filtered.filter(
|
|
2708
|
+
(f) => this.options.only.some((name) => f.name === name || f.name === `${name}_seeder`)
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
if (this.options.except && this.options.except.length > 0) {
|
|
2712
|
+
filtered = filtered.filter(
|
|
2713
|
+
(f) => !this.options.except.some((name) => f.name === name || f.name === `${name}_seeder`)
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
return filtered;
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* List all available seeders
|
|
2720
|
+
*/
|
|
2721
|
+
async list() {
|
|
2722
|
+
return this.loader.loadAll();
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// src/cli/commands/seed.ts
|
|
2727
|
+
async function seedCommand(options = {}) {
|
|
2728
|
+
let adapter;
|
|
2729
|
+
try {
|
|
2730
|
+
const config = await loadConfig();
|
|
2731
|
+
adapter = await createAdapterFromConfig(config);
|
|
2732
|
+
const directory = resolve(process.cwd(), config.seeds?.directory || "./src/seeds");
|
|
2733
|
+
const runner = new SeederRunner(adapter, { directory });
|
|
2734
|
+
if (options.class) {
|
|
2735
|
+
info(`Running seeder: ${options.class}`);
|
|
2736
|
+
const result = await runner.runSeeder(options.class);
|
|
2737
|
+
if (result.success) {
|
|
2738
|
+
success(`Seeder completed: ${result.name} (${result.duration}ms)`);
|
|
2739
|
+
} else {
|
|
2740
|
+
error(`Seeder failed: ${result.error}`);
|
|
2741
|
+
process.exit(1);
|
|
2742
|
+
}
|
|
2743
|
+
} else {
|
|
2744
|
+
info("Running all seeders...");
|
|
2745
|
+
console.log("");
|
|
2746
|
+
const results = await runner.run();
|
|
2747
|
+
if (results.length === 0) {
|
|
2748
|
+
info("No seeders found");
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
let hasErrors = false;
|
|
2752
|
+
for (const result of results) {
|
|
2753
|
+
if (result.success) {
|
|
2754
|
+
success(`${result.name} (${result.duration}ms)`);
|
|
2755
|
+
} else {
|
|
2756
|
+
error(`${result.name}: ${result.error}`);
|
|
2757
|
+
hasErrors = true;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
console.log("");
|
|
2761
|
+
if (hasErrors) {
|
|
2762
|
+
error("Seeding completed with errors");
|
|
2763
|
+
process.exit(1);
|
|
2764
|
+
} else {
|
|
2765
|
+
success(`Seeding complete - ran ${results.length} seeder(s)`);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
} catch (error_) {
|
|
2769
|
+
error(error_.message);
|
|
2770
|
+
process.exit(1);
|
|
2771
|
+
} finally {
|
|
2772
|
+
if (adapter) {
|
|
2773
|
+
await adapter.disconnect();
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// src/cli/commands/status.ts
|
|
2779
|
+
async function statusCommand() {
|
|
2780
|
+
let adapter;
|
|
2781
|
+
try {
|
|
2782
|
+
const { runner, adapter: a } = await createRunnerFromConfig();
|
|
2783
|
+
adapter = a;
|
|
2784
|
+
const status = await runner.status();
|
|
2785
|
+
if (status.length === 0) {
|
|
2786
|
+
console.log("No migrations found");
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
const headers = ["Migration", "Batch", "Status", "Executed At"];
|
|
2790
|
+
const rows = status.map((s) => [
|
|
2791
|
+
s.name,
|
|
2792
|
+
s.batch?.toString() || "-",
|
|
2793
|
+
s.pending ? "Pending" : "Executed",
|
|
2794
|
+
s.executedAt ? new Date(s.executedAt).toLocaleString() : "-"
|
|
2795
|
+
]);
|
|
2796
|
+
console.log("");
|
|
2797
|
+
console.log("Migration Status:");
|
|
2798
|
+
console.log("");
|
|
2799
|
+
console.log(formatTable(headers, rows));
|
|
2800
|
+
console.log("");
|
|
2801
|
+
const pendingCount = status.filter((s) => s.pending).length;
|
|
2802
|
+
const executedCount = status.filter((s) => !s.pending).length;
|
|
2803
|
+
console.log(
|
|
2804
|
+
`Total: ${status.length} migrations (${executedCount} executed, ${pendingCount} pending)`
|
|
2805
|
+
);
|
|
2806
|
+
} catch (error_) {
|
|
2807
|
+
error(error_.message);
|
|
2808
|
+
process.exit(1);
|
|
2809
|
+
} finally {
|
|
2810
|
+
if (adapter) {
|
|
2811
|
+
await adapter.disconnect();
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// src/cli/commands/validate.ts
|
|
2817
|
+
async function validateCommand() {
|
|
2818
|
+
let adapter;
|
|
2819
|
+
try {
|
|
2820
|
+
info("Validating migrations...");
|
|
2821
|
+
console.log("");
|
|
2822
|
+
const { runner, adapter: a } = await createRunnerFromConfig();
|
|
2823
|
+
adapter = a;
|
|
2824
|
+
const result = await runner.validate();
|
|
2825
|
+
if (result.valid) {
|
|
2826
|
+
success("All migrations are valid");
|
|
2827
|
+
} else {
|
|
2828
|
+
warn("Migration validation failed:");
|
|
2829
|
+
console.log("");
|
|
2830
|
+
for (const err of result.errors) {
|
|
2831
|
+
error(` ${err}`);
|
|
2832
|
+
}
|
|
2833
|
+
console.log("");
|
|
2834
|
+
process.exit(1);
|
|
2835
|
+
}
|
|
2836
|
+
} catch (error_) {
|
|
2837
|
+
error(error_.message);
|
|
2838
|
+
process.exit(1);
|
|
2839
|
+
} finally {
|
|
2840
|
+
if (adapter) {
|
|
2841
|
+
await adapter.disconnect();
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/cli/index.ts
|
|
2847
|
+
var VERSION = "1.0.0";
|
|
2848
|
+
var HELP = `
|
|
2849
|
+
db-bridge - Database Migration & Seeding CLI
|
|
2850
|
+
|
|
2851
|
+
Usage:
|
|
2852
|
+
db-bridge <command> [options]
|
|
2853
|
+
|
|
2854
|
+
Migration Commands:
|
|
2855
|
+
migrate:make <name> Create a new migration file
|
|
2856
|
+
migrate:latest Run all pending migrations
|
|
2857
|
+
migrate:rollback Rollback the last batch of migrations
|
|
2858
|
+
migrate:status Show migration status
|
|
2859
|
+
migrate:reset Rollback all migrations
|
|
2860
|
+
migrate:fresh Drop all tables and re-run migrations
|
|
2861
|
+
migrate:validate Validate migration checksums
|
|
2862
|
+
|
|
2863
|
+
Seed Commands:
|
|
2864
|
+
make:seeder <name> Create a new seeder file
|
|
2865
|
+
db:seed Run database seeders
|
|
2866
|
+
db:seed --class=<name> Run a specific seeder
|
|
2867
|
+
|
|
2868
|
+
Options:
|
|
2869
|
+
--help, -h Show this help message
|
|
2870
|
+
--version, -v Show version number
|
|
2871
|
+
--dry-run Show what would be done without executing
|
|
2872
|
+
--step=<n> Number of batches to rollback (for rollback command)
|
|
2873
|
+
--class=<name> Specific seeder class to run (for db:seed)
|
|
2874
|
+
|
|
2875
|
+
Examples:
|
|
2876
|
+
db-bridge migrate:make create_users_table
|
|
2877
|
+
db-bridge migrate:latest
|
|
2878
|
+
db-bridge migrate:rollback --step=2
|
|
2879
|
+
db-bridge make:seeder users
|
|
2880
|
+
db-bridge db:seed
|
|
2881
|
+
db-bridge db:seed --class=users
|
|
2882
|
+
`;
|
|
2883
|
+
async function main() {
|
|
2884
|
+
const args = process.argv.slice(2);
|
|
2885
|
+
let options = {};
|
|
2886
|
+
let command = "";
|
|
2887
|
+
let commandArgs = [];
|
|
2888
|
+
try {
|
|
2889
|
+
const { values, positionals } = parseArgs({
|
|
2890
|
+
args,
|
|
2891
|
+
options: {
|
|
2892
|
+
help: { type: "boolean", short: "h" },
|
|
2893
|
+
version: { type: "boolean", short: "v" },
|
|
2894
|
+
"dry-run": { type: "boolean" },
|
|
2895
|
+
step: { type: "string" },
|
|
2896
|
+
class: { type: "string" }
|
|
2897
|
+
},
|
|
2898
|
+
allowPositionals: true
|
|
2899
|
+
});
|
|
2900
|
+
options = {
|
|
2901
|
+
help: values.help,
|
|
2902
|
+
version: values.version,
|
|
2903
|
+
dryRun: values["dry-run"],
|
|
2904
|
+
step: values.step ? parseInt(values.step, 10) : void 0,
|
|
2905
|
+
class: values.class
|
|
2906
|
+
};
|
|
2907
|
+
command = positionals[0] || "";
|
|
2908
|
+
commandArgs = positionals.slice(1);
|
|
2909
|
+
} catch {
|
|
2910
|
+
command = args[0] || "";
|
|
2911
|
+
commandArgs = args.slice(1).filter((a) => !a.startsWith("-"));
|
|
2912
|
+
options.help = args.includes("--help") || args.includes("-h");
|
|
2913
|
+
options.version = args.includes("--version") || args.includes("-v");
|
|
2914
|
+
options.dryRun = args.includes("--dry-run");
|
|
2915
|
+
const stepArg = args.find((a) => a.startsWith("--step="));
|
|
2916
|
+
if (stepArg) {
|
|
2917
|
+
options.step = parseInt(stepArg.split("=")[1] || "1", 10);
|
|
2918
|
+
}
|
|
2919
|
+
const classArg = args.find((a) => a.startsWith("--class="));
|
|
2920
|
+
if (classArg) {
|
|
2921
|
+
options.class = classArg.split("=")[1];
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (options.help || command === "help") {
|
|
2925
|
+
console.log(HELP);
|
|
2926
|
+
process.exit(0);
|
|
2927
|
+
}
|
|
2928
|
+
if (options.version) {
|
|
2929
|
+
console.log(`db-bridge v${VERSION}`);
|
|
2930
|
+
process.exit(0);
|
|
2931
|
+
}
|
|
2932
|
+
if (!command) {
|
|
2933
|
+
console.log(HELP);
|
|
2934
|
+
process.exit(0);
|
|
2935
|
+
}
|
|
2936
|
+
try {
|
|
2937
|
+
switch (command) {
|
|
2938
|
+
case "migrate:make": {
|
|
2939
|
+
if (!commandArgs[0]) {
|
|
2940
|
+
console.error("Error: Migration name is required");
|
|
2941
|
+
console.log("Usage: db-bridge migrate:make <name>");
|
|
2942
|
+
process.exit(1);
|
|
2943
|
+
}
|
|
2944
|
+
await makeCommand(commandArgs[0]);
|
|
2945
|
+
break;
|
|
2946
|
+
}
|
|
2947
|
+
case "migrate:latest": {
|
|
2948
|
+
await latestCommand({ dryRun: options.dryRun });
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
case "migrate:rollback": {
|
|
2952
|
+
await rollbackCommand({ dryRun: options.dryRun, step: options.step });
|
|
2953
|
+
break;
|
|
2954
|
+
}
|
|
2955
|
+
case "migrate:status": {
|
|
2956
|
+
await statusCommand();
|
|
2957
|
+
break;
|
|
2958
|
+
}
|
|
2959
|
+
case "migrate:reset": {
|
|
2960
|
+
await resetCommand({ dryRun: options.dryRun });
|
|
2961
|
+
break;
|
|
2962
|
+
}
|
|
2963
|
+
case "migrate:fresh": {
|
|
2964
|
+
await freshCommand({ dryRun: options.dryRun });
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2967
|
+
case "migrate:validate": {
|
|
2968
|
+
await validateCommand();
|
|
2969
|
+
break;
|
|
2970
|
+
}
|
|
2971
|
+
case "make:seeder": {
|
|
2972
|
+
if (!commandArgs[0]) {
|
|
2973
|
+
console.error("Error: Seeder name is required");
|
|
2974
|
+
console.log("Usage: db-bridge make:seeder <name>");
|
|
2975
|
+
process.exit(1);
|
|
2976
|
+
}
|
|
2977
|
+
await makeSeederCommand(commandArgs[0]);
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
case "db:seed": {
|
|
2981
|
+
await seedCommand({ class: options.class });
|
|
2982
|
+
break;
|
|
2983
|
+
}
|
|
2984
|
+
default: {
|
|
2985
|
+
console.error(`Unknown command: ${command}`);
|
|
2986
|
+
console.log('Run "db-bridge --help" for usage information.');
|
|
2987
|
+
process.exit(1);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
} catch (error2) {
|
|
2991
|
+
console.error(`Error: ${error2.message}`);
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
main().catch((error2) => {
|
|
2996
|
+
console.error("Fatal error:", error2);
|
|
2997
|
+
process.exit(1);
|
|
2998
|
+
});
|
|
2999
|
+
//# sourceMappingURL=index.js.map
|
|
3000
|
+
//# sourceMappingURL=index.js.map
|