@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
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash, pbkdf2Sync, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
|
2
2
|
import { constants, gzipSync, gunzipSync } from 'zlib';
|
|
3
3
|
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
import { readdir, readFile } from 'fs/promises';
|
|
5
|
+
import { extname, join, basename, resolve } from 'path';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
import { hostname } from 'os';
|
|
4
8
|
|
|
5
9
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
10
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -351,7 +355,7 @@ async function withTimeout(promise, timeoutMs, message) {
|
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
function sleep(ms) {
|
|
354
|
-
return new Promise((
|
|
358
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
355
359
|
}
|
|
356
360
|
|
|
357
361
|
// src/utils/validation.ts
|
|
@@ -1657,7 +1661,7 @@ var HealthChecker = class {
|
|
|
1657
1661
|
if (result.status === "healthy") {
|
|
1658
1662
|
return;
|
|
1659
1663
|
}
|
|
1660
|
-
await new Promise((
|
|
1664
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
1661
1665
|
}
|
|
1662
1666
|
throw new Error(`Health check did not become healthy within ${maxWaitTime}ms`);
|
|
1663
1667
|
}
|
|
@@ -2105,6 +2109,2533 @@ var MigrationRunner = class {
|
|
|
2105
2109
|
};
|
|
2106
2110
|
}
|
|
2107
2111
|
};
|
|
2112
|
+
var MIGRATION_PATTERN = /^(\d{14})_(.+)\.(ts|js|mjs)$/;
|
|
2113
|
+
var MigrationLoader = class {
|
|
2114
|
+
directory;
|
|
2115
|
+
extensions;
|
|
2116
|
+
constructor(directory, extensions = [".ts", ".js", ".mjs"]) {
|
|
2117
|
+
this.directory = directory;
|
|
2118
|
+
this.extensions = extensions;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Scan directory for migration files
|
|
2122
|
+
*/
|
|
2123
|
+
async scanDirectory() {
|
|
2124
|
+
const files = [];
|
|
2125
|
+
try {
|
|
2126
|
+
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
2127
|
+
for (const entry of entries) {
|
|
2128
|
+
if (!entry.isFile()) {
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const ext = extname(entry.name);
|
|
2132
|
+
if (!this.extensions.includes(ext)) {
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
const match = entry.name.match(MIGRATION_PATTERN);
|
|
2136
|
+
if (!match) {
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
const [, timestamp, description] = match;
|
|
2140
|
+
files.push({
|
|
2141
|
+
name: basename(entry.name, ext),
|
|
2142
|
+
path: join(this.directory, entry.name),
|
|
2143
|
+
timestamp,
|
|
2144
|
+
description: description.replaceAll("_", " ")
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
files.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
2148
|
+
return files;
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (error.code === "ENOENT") {
|
|
2151
|
+
throw new Error(`Migration directory not found: ${this.directory}`);
|
|
2152
|
+
}
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Load a single migration file
|
|
2158
|
+
*/
|
|
2159
|
+
async loadMigration(file) {
|
|
2160
|
+
try {
|
|
2161
|
+
const fileUrl = pathToFileURL(file.path).href;
|
|
2162
|
+
const module = await import(fileUrl);
|
|
2163
|
+
const migration = module.default || module;
|
|
2164
|
+
if (typeof migration.up !== "function") {
|
|
2165
|
+
throw new TypeError(`Migration ${file.name} is missing 'up' function`);
|
|
2166
|
+
}
|
|
2167
|
+
if (typeof migration.down !== "function") {
|
|
2168
|
+
throw new TypeError(`Migration ${file.name} is missing 'down' function`);
|
|
2169
|
+
}
|
|
2170
|
+
return {
|
|
2171
|
+
name: file.name,
|
|
2172
|
+
up: migration.up,
|
|
2173
|
+
down: migration.down,
|
|
2174
|
+
transactional: migration.transactional ?? true,
|
|
2175
|
+
phase: migration.phase
|
|
2176
|
+
};
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
throw new Error(`Failed to load migration ${file.name}: ${error.message}`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Load all migrations from directory
|
|
2183
|
+
*/
|
|
2184
|
+
async loadAll() {
|
|
2185
|
+
const files = await this.scanDirectory();
|
|
2186
|
+
const migrations = [];
|
|
2187
|
+
for (const file of files) {
|
|
2188
|
+
const migration = await this.loadMigration(file);
|
|
2189
|
+
migrations.push(migration);
|
|
2190
|
+
}
|
|
2191
|
+
return migrations;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Get migration files (metadata only, without loading)
|
|
2195
|
+
*/
|
|
2196
|
+
async getMigrationFiles() {
|
|
2197
|
+
return this.scanDirectory();
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Calculate checksum for a migration file
|
|
2201
|
+
*/
|
|
2202
|
+
async calculateChecksum(file) {
|
|
2203
|
+
const content = await readFile(file.path, "utf8");
|
|
2204
|
+
const normalized = content.replaceAll("\r\n", "\n").trim();
|
|
2205
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Calculate checksums for all migration files
|
|
2209
|
+
*/
|
|
2210
|
+
async calculateAllChecksums() {
|
|
2211
|
+
const files = await this.scanDirectory();
|
|
2212
|
+
const checksums = /* @__PURE__ */ new Map();
|
|
2213
|
+
for (const file of files) {
|
|
2214
|
+
const checksum = await this.calculateChecksum(file);
|
|
2215
|
+
checksums.set(file.name, checksum);
|
|
2216
|
+
}
|
|
2217
|
+
return checksums;
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Generate a new migration filename
|
|
2221
|
+
*/
|
|
2222
|
+
static generateFilename(description) {
|
|
2223
|
+
const now = /* @__PURE__ */ new Date();
|
|
2224
|
+
const timestamp = [
|
|
2225
|
+
now.getFullYear(),
|
|
2226
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
2227
|
+
String(now.getDate()).padStart(2, "0"),
|
|
2228
|
+
String(now.getHours()).padStart(2, "0"),
|
|
2229
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
2230
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
2231
|
+
].join("");
|
|
2232
|
+
const sanitizedDescription = description.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "");
|
|
2233
|
+
return `${timestamp}_${sanitizedDescription}.ts`;
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Get migration template content
|
|
2237
|
+
*/
|
|
2238
|
+
static getMigrationTemplate(name) {
|
|
2239
|
+
return `/**
|
|
2240
|
+
* Migration: ${name}
|
|
2241
|
+
* Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2242
|
+
*/
|
|
2243
|
+
|
|
2244
|
+
import type { SchemaBuilder } from '@db-bridge/core';
|
|
2245
|
+
|
|
2246
|
+
export default {
|
|
2247
|
+
name: '${name}',
|
|
2248
|
+
|
|
2249
|
+
async up(schema: SchemaBuilder): Promise<void> {
|
|
2250
|
+
// Write your migration here
|
|
2251
|
+
// Example:
|
|
2252
|
+
// await schema.createTable('users', (table) => {
|
|
2253
|
+
// table.increments('id');
|
|
2254
|
+
// table.string('email', 255).unique().notNull();
|
|
2255
|
+
// table.timestamps();
|
|
2256
|
+
// });
|
|
2257
|
+
},
|
|
2258
|
+
|
|
2259
|
+
async down(schema: SchemaBuilder): Promise<void> {
|
|
2260
|
+
// Reverse the migration
|
|
2261
|
+
// Example:
|
|
2262
|
+
// await schema.dropTableIfExists('users');
|
|
2263
|
+
},
|
|
2264
|
+
};
|
|
2265
|
+
`;
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
// src/schema/dialects/MySQLDialect.ts
|
|
2270
|
+
var MySQLDialect = class {
|
|
2271
|
+
dialect = "mysql";
|
|
2272
|
+
/**
|
|
2273
|
+
* Quote an identifier (table/column name)
|
|
2274
|
+
*/
|
|
2275
|
+
quoteIdentifier(name) {
|
|
2276
|
+
return `\`${name.replaceAll("`", "``")}\``;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Quote a value for SQL
|
|
2280
|
+
*/
|
|
2281
|
+
quoteValue(value) {
|
|
2282
|
+
if (value === null || value === void 0) {
|
|
2283
|
+
return "NULL";
|
|
2284
|
+
}
|
|
2285
|
+
if (typeof value === "boolean") {
|
|
2286
|
+
return value ? "1" : "0";
|
|
2287
|
+
}
|
|
2288
|
+
if (typeof value === "number") {
|
|
2289
|
+
return String(value);
|
|
2290
|
+
}
|
|
2291
|
+
if (typeof value === "string") {
|
|
2292
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2293
|
+
}
|
|
2294
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Generate CREATE TABLE statement
|
|
2298
|
+
*/
|
|
2299
|
+
createTable(definition) {
|
|
2300
|
+
const parts = [];
|
|
2301
|
+
for (const column of definition.columns) {
|
|
2302
|
+
parts.push(this.columnToSQL(column));
|
|
2303
|
+
}
|
|
2304
|
+
if (definition.primaryKey && definition.primaryKey.length > 0) {
|
|
2305
|
+
const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2306
|
+
parts.push(`PRIMARY KEY (${pkColumns})`);
|
|
2307
|
+
}
|
|
2308
|
+
for (const index of definition.indexes) {
|
|
2309
|
+
parts.push(this.indexToSQL(index));
|
|
2310
|
+
}
|
|
2311
|
+
for (const fk of definition.foreignKeys) {
|
|
2312
|
+
parts.push(this.foreignKeyToSQL(fk));
|
|
2313
|
+
}
|
|
2314
|
+
let sql2 = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
|
|
2315
|
+
${parts.join(",\n ")}
|
|
2316
|
+
)`;
|
|
2317
|
+
const options = [];
|
|
2318
|
+
if (definition.engine) {
|
|
2319
|
+
options.push(`ENGINE=${definition.engine}`);
|
|
2320
|
+
} else {
|
|
2321
|
+
options.push("ENGINE=InnoDB");
|
|
2322
|
+
}
|
|
2323
|
+
if (definition.charset) {
|
|
2324
|
+
options.push(`DEFAULT CHARSET=${definition.charset}`);
|
|
2325
|
+
} else {
|
|
2326
|
+
options.push("DEFAULT CHARSET=utf8mb4");
|
|
2327
|
+
}
|
|
2328
|
+
if (definition.collation) {
|
|
2329
|
+
options.push(`COLLATE=${definition.collation}`);
|
|
2330
|
+
}
|
|
2331
|
+
if (definition.comment) {
|
|
2332
|
+
options.push(`COMMENT=${this.quoteValue(definition.comment)}`);
|
|
2333
|
+
}
|
|
2334
|
+
if (options.length > 0) {
|
|
2335
|
+
sql2 += ` ${options.join(" ")}`;
|
|
2336
|
+
}
|
|
2337
|
+
return sql2;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Generate DROP TABLE statement
|
|
2341
|
+
*/
|
|
2342
|
+
dropTable(tableName) {
|
|
2343
|
+
return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Generate DROP TABLE IF EXISTS statement
|
|
2347
|
+
*/
|
|
2348
|
+
dropTableIfExists(tableName) {
|
|
2349
|
+
return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Generate RENAME TABLE statement
|
|
2353
|
+
*/
|
|
2354
|
+
renameTable(from, to) {
|
|
2355
|
+
return `RENAME TABLE ${this.quoteIdentifier(from)} TO ${this.quoteIdentifier(to)}`;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Generate ALTER TABLE statements
|
|
2359
|
+
*/
|
|
2360
|
+
alterTable(definition) {
|
|
2361
|
+
const statements = [];
|
|
2362
|
+
const tableName = this.quoteIdentifier(definition.tableName);
|
|
2363
|
+
for (const op of definition.operations) {
|
|
2364
|
+
switch (op.type) {
|
|
2365
|
+
case "addColumn": {
|
|
2366
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
case "dropColumn": {
|
|
2370
|
+
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
case "renameColumn": {
|
|
2374
|
+
statements.push(
|
|
2375
|
+
`ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
|
|
2376
|
+
);
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
case "modifyColumn": {
|
|
2380
|
+
statements.push(`ALTER TABLE ${tableName} MODIFY COLUMN ${this.columnToSQL(op.column)}`);
|
|
2381
|
+
break;
|
|
2382
|
+
}
|
|
2383
|
+
case "addIndex": {
|
|
2384
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.indexToSQL(op.index)}`);
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
case "dropIndex": {
|
|
2388
|
+
statements.push(`ALTER TABLE ${tableName} DROP INDEX ${this.quoteIdentifier(op.name)}`);
|
|
2389
|
+
break;
|
|
2390
|
+
}
|
|
2391
|
+
case "addForeignKey": {
|
|
2392
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
case "dropForeignKey": {
|
|
2396
|
+
statements.push(
|
|
2397
|
+
`ALTER TABLE ${tableName} DROP FOREIGN KEY ${this.quoteIdentifier(op.name)}`
|
|
2398
|
+
);
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
case "addPrimary": {
|
|
2402
|
+
const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2403
|
+
statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
|
|
2404
|
+
break;
|
|
2405
|
+
}
|
|
2406
|
+
case "dropPrimary": {
|
|
2407
|
+
statements.push(`ALTER TABLE ${tableName} DROP PRIMARY KEY`);
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return statements;
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Generate query to check if table exists
|
|
2416
|
+
*/
|
|
2417
|
+
hasTable(tableName) {
|
|
2418
|
+
return `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Generate query to check if column exists
|
|
2422
|
+
*/
|
|
2423
|
+
hasColumn(tableName, columnName) {
|
|
2424
|
+
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`;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Convert column definition to SQL
|
|
2428
|
+
*/
|
|
2429
|
+
columnToSQL(column) {
|
|
2430
|
+
const parts = [this.quoteIdentifier(column.name)];
|
|
2431
|
+
parts.push(this.columnTypeToSQL(column));
|
|
2432
|
+
if (column.unsigned) {
|
|
2433
|
+
parts.push("UNSIGNED");
|
|
2434
|
+
}
|
|
2435
|
+
if (column.nullable) {
|
|
2436
|
+
parts.push("NULL");
|
|
2437
|
+
} else {
|
|
2438
|
+
parts.push("NOT NULL");
|
|
2439
|
+
}
|
|
2440
|
+
if (column.defaultRaw) {
|
|
2441
|
+
parts.push(`DEFAULT ${column.defaultRaw}`);
|
|
2442
|
+
} else if (column.defaultValue !== void 0) {
|
|
2443
|
+
parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
|
|
2444
|
+
}
|
|
2445
|
+
if (column.autoIncrement) {
|
|
2446
|
+
parts.push("AUTO_INCREMENT");
|
|
2447
|
+
}
|
|
2448
|
+
if (column.primary && !column.autoIncrement) {
|
|
2449
|
+
parts.push("PRIMARY KEY");
|
|
2450
|
+
} else if (column.primary && column.autoIncrement) {
|
|
2451
|
+
parts.push("PRIMARY KEY");
|
|
2452
|
+
}
|
|
2453
|
+
if (column.unique && !column.primary) {
|
|
2454
|
+
parts.push("UNIQUE");
|
|
2455
|
+
}
|
|
2456
|
+
if (column.comment) {
|
|
2457
|
+
parts.push(`COMMENT ${this.quoteValue(column.comment)}`);
|
|
2458
|
+
}
|
|
2459
|
+
if (column.first) {
|
|
2460
|
+
parts.push("FIRST");
|
|
2461
|
+
} else if (column.after) {
|
|
2462
|
+
parts.push(`AFTER ${this.quoteIdentifier(column.after)}`);
|
|
2463
|
+
}
|
|
2464
|
+
return parts.join(" ");
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Convert column type to MySQL type
|
|
2468
|
+
*/
|
|
2469
|
+
columnTypeToSQL(column) {
|
|
2470
|
+
switch (column.type) {
|
|
2471
|
+
case "increments": {
|
|
2472
|
+
return "INT UNSIGNED AUTO_INCREMENT";
|
|
2473
|
+
}
|
|
2474
|
+
case "bigIncrements": {
|
|
2475
|
+
return "BIGINT UNSIGNED AUTO_INCREMENT";
|
|
2476
|
+
}
|
|
2477
|
+
case "integer": {
|
|
2478
|
+
return "INT";
|
|
2479
|
+
}
|
|
2480
|
+
case "bigInteger": {
|
|
2481
|
+
return "BIGINT";
|
|
2482
|
+
}
|
|
2483
|
+
case "smallInteger": {
|
|
2484
|
+
return "SMALLINT";
|
|
2485
|
+
}
|
|
2486
|
+
case "tinyInteger": {
|
|
2487
|
+
return "TINYINT";
|
|
2488
|
+
}
|
|
2489
|
+
case "float": {
|
|
2490
|
+
return "FLOAT";
|
|
2491
|
+
}
|
|
2492
|
+
case "double": {
|
|
2493
|
+
return "DOUBLE";
|
|
2494
|
+
}
|
|
2495
|
+
case "decimal": {
|
|
2496
|
+
const precision = column.precision ?? 10;
|
|
2497
|
+
const scale = column.scale ?? 2;
|
|
2498
|
+
return `DECIMAL(${precision},${scale})`;
|
|
2499
|
+
}
|
|
2500
|
+
case "string": {
|
|
2501
|
+
return `VARCHAR(${column.length ?? 255})`;
|
|
2502
|
+
}
|
|
2503
|
+
case "text": {
|
|
2504
|
+
return "TEXT";
|
|
2505
|
+
}
|
|
2506
|
+
case "mediumText": {
|
|
2507
|
+
return "MEDIUMTEXT";
|
|
2508
|
+
}
|
|
2509
|
+
case "longText": {
|
|
2510
|
+
return "LONGTEXT";
|
|
2511
|
+
}
|
|
2512
|
+
case "boolean": {
|
|
2513
|
+
return "TINYINT(1)";
|
|
2514
|
+
}
|
|
2515
|
+
case "date": {
|
|
2516
|
+
return "DATE";
|
|
2517
|
+
}
|
|
2518
|
+
case "datetime": {
|
|
2519
|
+
return "DATETIME";
|
|
2520
|
+
}
|
|
2521
|
+
case "timestamp": {
|
|
2522
|
+
return "TIMESTAMP";
|
|
2523
|
+
}
|
|
2524
|
+
case "time": {
|
|
2525
|
+
return "TIME";
|
|
2526
|
+
}
|
|
2527
|
+
case "json":
|
|
2528
|
+
case "jsonb": {
|
|
2529
|
+
return "JSON";
|
|
2530
|
+
}
|
|
2531
|
+
case "uuid": {
|
|
2532
|
+
return "CHAR(36)";
|
|
2533
|
+
}
|
|
2534
|
+
case "binary": {
|
|
2535
|
+
return "BLOB";
|
|
2536
|
+
}
|
|
2537
|
+
case "enum": {
|
|
2538
|
+
if (column.enumValues && column.enumValues.length > 0) {
|
|
2539
|
+
const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
|
|
2540
|
+
return `ENUM(${values})`;
|
|
2541
|
+
}
|
|
2542
|
+
return "VARCHAR(255)";
|
|
2543
|
+
}
|
|
2544
|
+
default: {
|
|
2545
|
+
return "VARCHAR(255)";
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Convert index definition to SQL
|
|
2551
|
+
*/
|
|
2552
|
+
indexToSQL(index) {
|
|
2553
|
+
const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2554
|
+
const name = index.name ? this.quoteIdentifier(index.name) : "";
|
|
2555
|
+
if (index.type === "fulltext") {
|
|
2556
|
+
return `FULLTEXT INDEX ${name} (${columns})`;
|
|
2557
|
+
}
|
|
2558
|
+
if (index.unique) {
|
|
2559
|
+
return `UNIQUE INDEX ${name} (${columns})`;
|
|
2560
|
+
}
|
|
2561
|
+
return `INDEX ${name} (${columns})`;
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Convert foreign key definition to SQL
|
|
2565
|
+
*/
|
|
2566
|
+
foreignKeyToSQL(fk) {
|
|
2567
|
+
const parts = ["CONSTRAINT"];
|
|
2568
|
+
if (fk.name) {
|
|
2569
|
+
parts.push(this.quoteIdentifier(fk.name));
|
|
2570
|
+
}
|
|
2571
|
+
parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
|
|
2572
|
+
parts.push(
|
|
2573
|
+
`REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
|
|
2574
|
+
);
|
|
2575
|
+
if (fk.onDelete) {
|
|
2576
|
+
parts.push(`ON DELETE ${fk.onDelete}`);
|
|
2577
|
+
}
|
|
2578
|
+
if (fk.onUpdate) {
|
|
2579
|
+
parts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
2580
|
+
}
|
|
2581
|
+
return parts.join(" ");
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
// src/schema/dialects/PostgreSQLDialect.ts
|
|
2586
|
+
var PostgreSQLDialect = class {
|
|
2587
|
+
dialect = "postgresql";
|
|
2588
|
+
/**
|
|
2589
|
+
* Quote an identifier (table/column name)
|
|
2590
|
+
*/
|
|
2591
|
+
quoteIdentifier(name) {
|
|
2592
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Quote a value for SQL
|
|
2596
|
+
*/
|
|
2597
|
+
quoteValue(value) {
|
|
2598
|
+
if (value === null || value === void 0) {
|
|
2599
|
+
return "NULL";
|
|
2600
|
+
}
|
|
2601
|
+
if (typeof value === "boolean") {
|
|
2602
|
+
return value ? "TRUE" : "FALSE";
|
|
2603
|
+
}
|
|
2604
|
+
if (typeof value === "number") {
|
|
2605
|
+
return String(value);
|
|
2606
|
+
}
|
|
2607
|
+
if (typeof value === "string") {
|
|
2608
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2609
|
+
}
|
|
2610
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Generate CREATE TABLE statement
|
|
2614
|
+
*/
|
|
2615
|
+
createTable(definition) {
|
|
2616
|
+
const parts = [];
|
|
2617
|
+
for (const column of definition.columns) {
|
|
2618
|
+
parts.push(this.columnToSQL(column));
|
|
2619
|
+
}
|
|
2620
|
+
if (definition.primaryKey && definition.primaryKey.length > 0) {
|
|
2621
|
+
const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2622
|
+
parts.push(`PRIMARY KEY (${pkColumns})`);
|
|
2623
|
+
}
|
|
2624
|
+
for (const fk of definition.foreignKeys) {
|
|
2625
|
+
parts.push(this.foreignKeyToSQL(fk));
|
|
2626
|
+
}
|
|
2627
|
+
const sql2 = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
|
|
2628
|
+
${parts.join(",\n ")}
|
|
2629
|
+
)`;
|
|
2630
|
+
const statements = [sql2];
|
|
2631
|
+
if (definition.comment) {
|
|
2632
|
+
statements.push(
|
|
2633
|
+
`COMMENT ON TABLE ${this.quoteIdentifier(definition.name)} IS ${this.quoteValue(definition.comment)}`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
return statements.join(";\n");
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Generate DROP TABLE statement
|
|
2640
|
+
*/
|
|
2641
|
+
dropTable(tableName) {
|
|
2642
|
+
return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Generate DROP TABLE IF EXISTS statement
|
|
2646
|
+
*/
|
|
2647
|
+
dropTableIfExists(tableName) {
|
|
2648
|
+
return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Generate ALTER TABLE ... RENAME statement
|
|
2652
|
+
*/
|
|
2653
|
+
renameTable(from, to) {
|
|
2654
|
+
return `ALTER TABLE ${this.quoteIdentifier(from)} RENAME TO ${this.quoteIdentifier(to)}`;
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Generate ALTER TABLE statements
|
|
2658
|
+
*/
|
|
2659
|
+
alterTable(definition) {
|
|
2660
|
+
const statements = [];
|
|
2661
|
+
const tableName = this.quoteIdentifier(definition.tableName);
|
|
2662
|
+
for (const op of definition.operations) {
|
|
2663
|
+
switch (op.type) {
|
|
2664
|
+
case "addColumn": {
|
|
2665
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
case "dropColumn": {
|
|
2669
|
+
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
case "renameColumn": {
|
|
2673
|
+
statements.push(
|
|
2674
|
+
`ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
|
|
2675
|
+
);
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
case "modifyColumn": {
|
|
2679
|
+
const col = op.column;
|
|
2680
|
+
statements.push(
|
|
2681
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} TYPE ${this.columnTypeToSQL(col)}`
|
|
2682
|
+
);
|
|
2683
|
+
if (col.nullable) {
|
|
2684
|
+
statements.push(
|
|
2685
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} DROP NOT NULL`
|
|
2686
|
+
);
|
|
2687
|
+
} else {
|
|
2688
|
+
statements.push(
|
|
2689
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET NOT NULL`
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
if (col.defaultRaw) {
|
|
2693
|
+
statements.push(
|
|
2694
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${col.defaultRaw}`
|
|
2695
|
+
);
|
|
2696
|
+
} else if (col.defaultValue !== void 0) {
|
|
2697
|
+
statements.push(
|
|
2698
|
+
`ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${this.quoteValue(col.defaultValue)}`
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
case "addIndex": {
|
|
2704
|
+
statements.push(this.createIndex(definition.tableName, op.index));
|
|
2705
|
+
break;
|
|
2706
|
+
}
|
|
2707
|
+
case "dropIndex": {
|
|
2708
|
+
statements.push(`DROP INDEX ${this.quoteIdentifier(op.name)}`);
|
|
2709
|
+
break;
|
|
2710
|
+
}
|
|
2711
|
+
case "addForeignKey": {
|
|
2712
|
+
statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
case "dropForeignKey": {
|
|
2716
|
+
statements.push(
|
|
2717
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(op.name)}`
|
|
2718
|
+
);
|
|
2719
|
+
break;
|
|
2720
|
+
}
|
|
2721
|
+
case "addPrimary": {
|
|
2722
|
+
const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2723
|
+
statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
case "dropPrimary": {
|
|
2727
|
+
statements.push(
|
|
2728
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(`${definition.tableName}_pkey`)}`
|
|
2729
|
+
);
|
|
2730
|
+
break;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return statements;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Generate query to check if table exists
|
|
2738
|
+
*/
|
|
2739
|
+
hasTable(tableName) {
|
|
2740
|
+
return `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Generate query to check if column exists
|
|
2744
|
+
*/
|
|
2745
|
+
hasColumn(tableName, columnName) {
|
|
2746
|
+
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`;
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Create index statement
|
|
2750
|
+
*/
|
|
2751
|
+
createIndex(tableName, index) {
|
|
2752
|
+
const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
|
|
2753
|
+
const indexName = index.name || `idx_${tableName}_${index.columns.join("_")}`;
|
|
2754
|
+
const unique = index.unique ? "UNIQUE " : "";
|
|
2755
|
+
return `CREATE ${unique}INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(tableName)} (${columns})`;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Convert column definition to SQL
|
|
2759
|
+
*/
|
|
2760
|
+
columnToSQL(column) {
|
|
2761
|
+
const parts = [this.quoteIdentifier(column.name)];
|
|
2762
|
+
parts.push(this.columnTypeToSQL(column));
|
|
2763
|
+
if (!column.nullable) {
|
|
2764
|
+
parts.push("NOT NULL");
|
|
2765
|
+
}
|
|
2766
|
+
if (column.defaultRaw) {
|
|
2767
|
+
parts.push(`DEFAULT ${column.defaultRaw}`);
|
|
2768
|
+
} else if (column.defaultValue !== void 0) {
|
|
2769
|
+
parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (column.primary && column.type !== "increments" && column.type !== "bigIncrements") {
|
|
2772
|
+
parts.push("PRIMARY KEY");
|
|
2773
|
+
}
|
|
2774
|
+
if (column.unique && !column.primary) {
|
|
2775
|
+
parts.push("UNIQUE");
|
|
2776
|
+
}
|
|
2777
|
+
return parts.join(" ");
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Convert column type to PostgreSQL type
|
|
2781
|
+
*/
|
|
2782
|
+
columnTypeToSQL(column) {
|
|
2783
|
+
switch (column.type) {
|
|
2784
|
+
case "increments": {
|
|
2785
|
+
return "SERIAL PRIMARY KEY";
|
|
2786
|
+
}
|
|
2787
|
+
case "bigIncrements": {
|
|
2788
|
+
return "BIGSERIAL PRIMARY KEY";
|
|
2789
|
+
}
|
|
2790
|
+
case "integer": {
|
|
2791
|
+
return "INTEGER";
|
|
2792
|
+
}
|
|
2793
|
+
case "bigInteger": {
|
|
2794
|
+
return "BIGINT";
|
|
2795
|
+
}
|
|
2796
|
+
case "smallInteger": {
|
|
2797
|
+
return "SMALLINT";
|
|
2798
|
+
}
|
|
2799
|
+
case "tinyInteger": {
|
|
2800
|
+
return "SMALLINT";
|
|
2801
|
+
}
|
|
2802
|
+
// PostgreSQL doesn't have TINYINT
|
|
2803
|
+
case "float": {
|
|
2804
|
+
return "REAL";
|
|
2805
|
+
}
|
|
2806
|
+
case "double": {
|
|
2807
|
+
return "DOUBLE PRECISION";
|
|
2808
|
+
}
|
|
2809
|
+
case "decimal": {
|
|
2810
|
+
const precision = column.precision ?? 10;
|
|
2811
|
+
const scale = column.scale ?? 2;
|
|
2812
|
+
return `NUMERIC(${precision},${scale})`;
|
|
2813
|
+
}
|
|
2814
|
+
case "string": {
|
|
2815
|
+
return `VARCHAR(${column.length ?? 255})`;
|
|
2816
|
+
}
|
|
2817
|
+
case "text":
|
|
2818
|
+
case "mediumText":
|
|
2819
|
+
case "longText": {
|
|
2820
|
+
return "TEXT";
|
|
2821
|
+
}
|
|
2822
|
+
case "boolean": {
|
|
2823
|
+
return "BOOLEAN";
|
|
2824
|
+
}
|
|
2825
|
+
case "date": {
|
|
2826
|
+
return "DATE";
|
|
2827
|
+
}
|
|
2828
|
+
case "datetime":
|
|
2829
|
+
case "timestamp": {
|
|
2830
|
+
return "TIMESTAMP";
|
|
2831
|
+
}
|
|
2832
|
+
case "time": {
|
|
2833
|
+
return "TIME";
|
|
2834
|
+
}
|
|
2835
|
+
case "json": {
|
|
2836
|
+
return "JSON";
|
|
2837
|
+
}
|
|
2838
|
+
case "jsonb": {
|
|
2839
|
+
return "JSONB";
|
|
2840
|
+
}
|
|
2841
|
+
case "uuid": {
|
|
2842
|
+
return "UUID";
|
|
2843
|
+
}
|
|
2844
|
+
case "binary": {
|
|
2845
|
+
return "BYTEA";
|
|
2846
|
+
}
|
|
2847
|
+
case "enum": {
|
|
2848
|
+
if (column.enumValues && column.enumValues.length > 0) {
|
|
2849
|
+
const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
|
|
2850
|
+
return `VARCHAR(255) CHECK (${this.quoteIdentifier(column.name)} IN (${values}))`;
|
|
2851
|
+
}
|
|
2852
|
+
return "VARCHAR(255)";
|
|
2853
|
+
}
|
|
2854
|
+
default: {
|
|
2855
|
+
return "VARCHAR(255)";
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Convert foreign key definition to SQL
|
|
2861
|
+
*/
|
|
2862
|
+
foreignKeyToSQL(fk) {
|
|
2863
|
+
const parts = ["CONSTRAINT"];
|
|
2864
|
+
if (fk.name) {
|
|
2865
|
+
parts.push(this.quoteIdentifier(fk.name));
|
|
2866
|
+
} else {
|
|
2867
|
+
parts.push(this.quoteIdentifier(`fk_${fk.column}_${fk.table}`));
|
|
2868
|
+
}
|
|
2869
|
+
parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
|
|
2870
|
+
parts.push(
|
|
2871
|
+
`REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
|
|
2872
|
+
);
|
|
2873
|
+
if (fk.onDelete) {
|
|
2874
|
+
parts.push(`ON DELETE ${fk.onDelete}`);
|
|
2875
|
+
}
|
|
2876
|
+
if (fk.onUpdate) {
|
|
2877
|
+
parts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
2878
|
+
}
|
|
2879
|
+
return parts.join(" ");
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
// src/migrations/MigrationLock.ts
|
|
2884
|
+
var MigrationLock = class {
|
|
2885
|
+
adapter;
|
|
2886
|
+
tableName;
|
|
2887
|
+
timeout;
|
|
2888
|
+
dialect;
|
|
2889
|
+
lockId;
|
|
2890
|
+
isLocked = false;
|
|
2891
|
+
constructor(adapter, options) {
|
|
2892
|
+
this.adapter = adapter;
|
|
2893
|
+
this.tableName = options.tableName ?? "db_migrations_lock";
|
|
2894
|
+
this.timeout = options.timeout ?? 6e4;
|
|
2895
|
+
this.lockId = `${hostname()}_${process.pid}_${Date.now()}`;
|
|
2896
|
+
this.dialect = options.dialect === "mysql" ? new MySQLDialect() : new PostgreSQLDialect();
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Initialize the lock table
|
|
2900
|
+
*/
|
|
2901
|
+
async initialize() {
|
|
2902
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
2903
|
+
if (this.dialect.dialect === "mysql") {
|
|
2904
|
+
await this.adapter.execute(`
|
|
2905
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
2906
|
+
id INT PRIMARY KEY,
|
|
2907
|
+
is_locked TINYINT NOT NULL DEFAULT 0,
|
|
2908
|
+
locked_at TIMESTAMP NULL,
|
|
2909
|
+
locked_by VARCHAR(255) NULL
|
|
2910
|
+
) ENGINE=InnoDB
|
|
2911
|
+
`);
|
|
2912
|
+
} else {
|
|
2913
|
+
await this.adapter.execute(`
|
|
2914
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
2915
|
+
id INT PRIMARY KEY,
|
|
2916
|
+
is_locked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
2917
|
+
locked_at TIMESTAMP NULL,
|
|
2918
|
+
locked_by VARCHAR(255) NULL
|
|
2919
|
+
)
|
|
2920
|
+
`);
|
|
2921
|
+
}
|
|
2922
|
+
const checkSql = this.dialect.dialect === "mysql" ? `SELECT 1 FROM ${tableName} WHERE id = 1` : `SELECT 1 FROM ${tableName} WHERE id = 1`;
|
|
2923
|
+
const result = await this.adapter.query(checkSql);
|
|
2924
|
+
if (result.rows.length === 0) {
|
|
2925
|
+
if (this.dialect.dialect === "mysql") {
|
|
2926
|
+
await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, 0)`);
|
|
2927
|
+
} else {
|
|
2928
|
+
await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, FALSE)`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Acquire the migration lock
|
|
2934
|
+
*/
|
|
2935
|
+
async acquire() {
|
|
2936
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
2937
|
+
const startTime = Date.now();
|
|
2938
|
+
while (Date.now() - startTime < this.timeout) {
|
|
2939
|
+
try {
|
|
2940
|
+
if (this.dialect.dialect === "mysql") {
|
|
2941
|
+
const result = await this.adapter.execute(
|
|
2942
|
+
`
|
|
2943
|
+
UPDATE ${tableName}
|
|
2944
|
+
SET is_locked = 1,
|
|
2945
|
+
locked_at = NOW(),
|
|
2946
|
+
locked_by = ?
|
|
2947
|
+
WHERE id = 1 AND is_locked = 0
|
|
2948
|
+
`,
|
|
2949
|
+
[this.lockId]
|
|
2950
|
+
);
|
|
2951
|
+
if ((result.affectedRows ?? 0) > 0) {
|
|
2952
|
+
this.isLocked = true;
|
|
2953
|
+
return true;
|
|
2954
|
+
}
|
|
2955
|
+
} else {
|
|
2956
|
+
const result = await this.adapter.execute(
|
|
2957
|
+
`
|
|
2958
|
+
UPDATE ${tableName}
|
|
2959
|
+
SET is_locked = TRUE,
|
|
2960
|
+
locked_at = NOW(),
|
|
2961
|
+
locked_by = $1
|
|
2962
|
+
WHERE id = 1 AND is_locked = FALSE
|
|
2963
|
+
RETURNING id
|
|
2964
|
+
`,
|
|
2965
|
+
[this.lockId]
|
|
2966
|
+
);
|
|
2967
|
+
if (result.rows.length > 0) {
|
|
2968
|
+
this.isLocked = true;
|
|
2969
|
+
return true;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const staleResult = await this.adapter.query(`SELECT locked_at, locked_by FROM ${tableName} WHERE id = 1`);
|
|
2973
|
+
if (staleResult.rows.length > 0) {
|
|
2974
|
+
const lockedAt = staleResult.rows[0].locked_at;
|
|
2975
|
+
if (lockedAt && Date.now() - new Date(lockedAt).getTime() > this.timeout) {
|
|
2976
|
+
console.warn(
|
|
2977
|
+
`Releasing stale migration lock held by ${staleResult.rows[0].locked_by}`
|
|
2978
|
+
);
|
|
2979
|
+
await this.forceRelease();
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
await this.sleep(1e3);
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
console.error("Error acquiring migration lock:", error);
|
|
2986
|
+
throw error;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return false;
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Release the migration lock
|
|
2993
|
+
*/
|
|
2994
|
+
async release() {
|
|
2995
|
+
if (!this.isLocked) {
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
2999
|
+
try {
|
|
3000
|
+
if (this.dialect.dialect === "mysql") {
|
|
3001
|
+
await this.adapter.execute(
|
|
3002
|
+
`
|
|
3003
|
+
UPDATE ${tableName}
|
|
3004
|
+
SET is_locked = 0,
|
|
3005
|
+
locked_at = NULL,
|
|
3006
|
+
locked_by = NULL
|
|
3007
|
+
WHERE id = 1 AND locked_by = ?
|
|
3008
|
+
`,
|
|
3009
|
+
[this.lockId]
|
|
3010
|
+
);
|
|
3011
|
+
} else {
|
|
3012
|
+
await this.adapter.execute(
|
|
3013
|
+
`
|
|
3014
|
+
UPDATE ${tableName}
|
|
3015
|
+
SET is_locked = FALSE,
|
|
3016
|
+
locked_at = NULL,
|
|
3017
|
+
locked_by = NULL
|
|
3018
|
+
WHERE id = 1 AND locked_by = $1
|
|
3019
|
+
`,
|
|
3020
|
+
[this.lockId]
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
this.isLocked = false;
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
console.error("Error releasing migration lock:", error);
|
|
3026
|
+
throw error;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Force release the lock (for stale locks)
|
|
3031
|
+
*/
|
|
3032
|
+
async forceRelease() {
|
|
3033
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
3034
|
+
if (this.dialect.dialect === "mysql") {
|
|
3035
|
+
await this.adapter.execute(`
|
|
3036
|
+
UPDATE ${tableName}
|
|
3037
|
+
SET is_locked = 0,
|
|
3038
|
+
locked_at = NULL,
|
|
3039
|
+
locked_by = NULL
|
|
3040
|
+
WHERE id = 1
|
|
3041
|
+
`);
|
|
3042
|
+
} else {
|
|
3043
|
+
await this.adapter.execute(`
|
|
3044
|
+
UPDATE ${tableName}
|
|
3045
|
+
SET is_locked = FALSE,
|
|
3046
|
+
locked_at = NULL,
|
|
3047
|
+
locked_by = NULL
|
|
3048
|
+
WHERE id = 1
|
|
3049
|
+
`);
|
|
3050
|
+
}
|
|
3051
|
+
this.isLocked = false;
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Check if lock is currently held
|
|
3055
|
+
*/
|
|
3056
|
+
async isHeld() {
|
|
3057
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
3058
|
+
const result = await this.adapter.query(
|
|
3059
|
+
`SELECT is_locked FROM ${tableName} WHERE id = 1`
|
|
3060
|
+
);
|
|
3061
|
+
if (result.rows.length === 0) {
|
|
3062
|
+
return false;
|
|
3063
|
+
}
|
|
3064
|
+
const isLocked = result.rows[0].is_locked;
|
|
3065
|
+
return isLocked === 1 || isLocked === true;
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Get lock info
|
|
3069
|
+
*/
|
|
3070
|
+
async getLockInfo() {
|
|
3071
|
+
const tableName = this.dialect.quoteIdentifier(this.tableName);
|
|
3072
|
+
const result = await this.adapter.query(`SELECT is_locked, locked_at, locked_by FROM ${tableName} WHERE id = 1`);
|
|
3073
|
+
if (result.rows.length === 0) {
|
|
3074
|
+
return { isLocked: false, lockedAt: null, lockedBy: null };
|
|
3075
|
+
}
|
|
3076
|
+
const row = result.rows[0];
|
|
3077
|
+
return {
|
|
3078
|
+
isLocked: row.is_locked === 1 || row.is_locked === true,
|
|
3079
|
+
lockedAt: row.locked_at,
|
|
3080
|
+
lockedBy: row.locked_by
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Execute a function with lock
|
|
3085
|
+
*/
|
|
3086
|
+
async withLock(fn) {
|
|
3087
|
+
const acquired = await this.acquire();
|
|
3088
|
+
if (!acquired) {
|
|
3089
|
+
throw new Error(
|
|
3090
|
+
`Could not acquire migration lock within ${this.timeout}ms. Another migration may be running.`
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
try {
|
|
3094
|
+
return await fn();
|
|
3095
|
+
} finally {
|
|
3096
|
+
await this.release();
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
sleep(ms) {
|
|
3100
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3101
|
+
}
|
|
3102
|
+
};
|
|
3103
|
+
|
|
3104
|
+
// src/schema/ColumnBuilder.ts
|
|
3105
|
+
var ColumnBuilder = class {
|
|
3106
|
+
definition;
|
|
3107
|
+
constructor(name, type) {
|
|
3108
|
+
this.definition = {
|
|
3109
|
+
name,
|
|
3110
|
+
type,
|
|
3111
|
+
nullable: type !== "increments" && type !== "bigIncrements",
|
|
3112
|
+
unsigned: false,
|
|
3113
|
+
autoIncrement: type === "increments" || type === "bigIncrements",
|
|
3114
|
+
primary: type === "increments" || type === "bigIncrements",
|
|
3115
|
+
unique: false,
|
|
3116
|
+
index: false,
|
|
3117
|
+
first: false
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Set column length (for string types)
|
|
3122
|
+
*/
|
|
3123
|
+
length(length) {
|
|
3124
|
+
this.definition.length = length;
|
|
3125
|
+
return this;
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* Set precision and scale (for decimal types)
|
|
3129
|
+
*/
|
|
3130
|
+
precision(precision, scale) {
|
|
3131
|
+
this.definition.precision = precision;
|
|
3132
|
+
this.definition.scale = scale;
|
|
3133
|
+
return this;
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Mark column as nullable
|
|
3137
|
+
*/
|
|
3138
|
+
nullable() {
|
|
3139
|
+
this.definition.nullable = true;
|
|
3140
|
+
return this;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Mark column as not nullable
|
|
3144
|
+
*/
|
|
3145
|
+
notNull() {
|
|
3146
|
+
this.definition.nullable = false;
|
|
3147
|
+
return this;
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Alias for notNull()
|
|
3151
|
+
*/
|
|
3152
|
+
notNullable() {
|
|
3153
|
+
return this.notNull();
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Set default value
|
|
3157
|
+
*/
|
|
3158
|
+
default(value) {
|
|
3159
|
+
this.definition.defaultValue = value;
|
|
3160
|
+
return this;
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Set default value as raw SQL
|
|
3164
|
+
*/
|
|
3165
|
+
defaultRaw(sql2) {
|
|
3166
|
+
this.definition.defaultRaw = sql2;
|
|
3167
|
+
return this;
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Set default to current timestamp
|
|
3171
|
+
*/
|
|
3172
|
+
defaultNow() {
|
|
3173
|
+
this.definition.defaultRaw = "CURRENT_TIMESTAMP";
|
|
3174
|
+
return this;
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Mark column as unsigned (MySQL)
|
|
3178
|
+
*/
|
|
3179
|
+
unsigned() {
|
|
3180
|
+
this.definition.unsigned = true;
|
|
3181
|
+
return this;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Mark column as auto increment
|
|
3185
|
+
*/
|
|
3186
|
+
autoIncrement() {
|
|
3187
|
+
this.definition.autoIncrement = true;
|
|
3188
|
+
return this;
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Mark column as primary key
|
|
3192
|
+
*/
|
|
3193
|
+
primary() {
|
|
3194
|
+
this.definition.primary = true;
|
|
3195
|
+
return this;
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Mark column as unique
|
|
3199
|
+
*/
|
|
3200
|
+
unique() {
|
|
3201
|
+
this.definition.unique = true;
|
|
3202
|
+
return this;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Add index to column
|
|
3206
|
+
*/
|
|
3207
|
+
index() {
|
|
3208
|
+
this.definition.index = true;
|
|
3209
|
+
return this;
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Add comment to column
|
|
3213
|
+
*/
|
|
3214
|
+
comment(comment) {
|
|
3215
|
+
this.definition.comment = comment;
|
|
3216
|
+
return this;
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Position column after another column (MySQL)
|
|
3220
|
+
*/
|
|
3221
|
+
after(columnName) {
|
|
3222
|
+
this.definition.after = columnName;
|
|
3223
|
+
return this;
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Position column first (MySQL)
|
|
3227
|
+
*/
|
|
3228
|
+
first() {
|
|
3229
|
+
this.definition.first = true;
|
|
3230
|
+
return this;
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Set enum values
|
|
3234
|
+
*/
|
|
3235
|
+
values(values) {
|
|
3236
|
+
this.definition.enumValues = values;
|
|
3237
|
+
return this;
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Add foreign key reference
|
|
3241
|
+
*/
|
|
3242
|
+
references(column) {
|
|
3243
|
+
const fkBuilder = new ForeignKeyBuilder(this, column);
|
|
3244
|
+
return fkBuilder;
|
|
3245
|
+
}
|
|
3246
|
+
/**
|
|
3247
|
+
* Set foreign key definition (internal)
|
|
3248
|
+
*/
|
|
3249
|
+
setForeignKey(fk) {
|
|
3250
|
+
this.definition.references = fk;
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Get the column definition
|
|
3254
|
+
*/
|
|
3255
|
+
getDefinition() {
|
|
3256
|
+
return { ...this.definition };
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
3259
|
+
var ForeignKeyBuilder = class {
|
|
3260
|
+
columnBuilder;
|
|
3261
|
+
fkDefinition;
|
|
3262
|
+
constructor(columnBuilder, referenceColumn) {
|
|
3263
|
+
this.columnBuilder = columnBuilder;
|
|
3264
|
+
this.fkDefinition = {
|
|
3265
|
+
column: columnBuilder.getDefinition().name,
|
|
3266
|
+
referenceColumn
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Set the referenced table
|
|
3271
|
+
*/
|
|
3272
|
+
on(tableName) {
|
|
3273
|
+
this.fkDefinition.table = tableName;
|
|
3274
|
+
this.applyToColumn();
|
|
3275
|
+
return this;
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Alias for on()
|
|
3279
|
+
*/
|
|
3280
|
+
inTable(tableName) {
|
|
3281
|
+
return this.on(tableName);
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Set ON DELETE action
|
|
3285
|
+
*/
|
|
3286
|
+
onDelete(action) {
|
|
3287
|
+
this.fkDefinition.onDelete = action;
|
|
3288
|
+
this.applyToColumn();
|
|
3289
|
+
return this;
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Set ON UPDATE action
|
|
3293
|
+
*/
|
|
3294
|
+
onUpdate(action) {
|
|
3295
|
+
this.fkDefinition.onUpdate = action;
|
|
3296
|
+
this.applyToColumn();
|
|
3297
|
+
return this;
|
|
3298
|
+
}
|
|
3299
|
+
/**
|
|
3300
|
+
* Set constraint name
|
|
3301
|
+
*/
|
|
3302
|
+
name(name) {
|
|
3303
|
+
this.fkDefinition.name = name;
|
|
3304
|
+
this.applyToColumn();
|
|
3305
|
+
return this;
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Apply the foreign key definition to the column
|
|
3309
|
+
*/
|
|
3310
|
+
applyToColumn() {
|
|
3311
|
+
if (this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
3312
|
+
this.columnBuilder.setForeignKey(this.fkDefinition);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Get the column builder for chaining
|
|
3317
|
+
*/
|
|
3318
|
+
getColumnBuilder() {
|
|
3319
|
+
return this.columnBuilder;
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
|
|
3323
|
+
// src/schema/TableBuilder.ts
|
|
3324
|
+
var TableBuilder = class {
|
|
3325
|
+
tableName;
|
|
3326
|
+
columns = [];
|
|
3327
|
+
indexes = [];
|
|
3328
|
+
foreignKeys = [];
|
|
3329
|
+
primaryKeyColumns;
|
|
3330
|
+
tableEngine;
|
|
3331
|
+
tableCharset;
|
|
3332
|
+
tableCollation;
|
|
3333
|
+
tableComment;
|
|
3334
|
+
constructor(tableName) {
|
|
3335
|
+
this.tableName = tableName;
|
|
3336
|
+
}
|
|
3337
|
+
// ============================================
|
|
3338
|
+
// Column Types
|
|
3339
|
+
// ============================================
|
|
3340
|
+
/**
|
|
3341
|
+
* Auto-incrementing integer primary key
|
|
3342
|
+
*/
|
|
3343
|
+
increments(name = "id") {
|
|
3344
|
+
return this.addColumn(name, "increments");
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Auto-incrementing big integer primary key
|
|
3348
|
+
*/
|
|
3349
|
+
bigIncrements(name = "id") {
|
|
3350
|
+
return this.addColumn(name, "bigIncrements");
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Integer column
|
|
3354
|
+
*/
|
|
3355
|
+
integer(name) {
|
|
3356
|
+
return this.addColumn(name, "integer");
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Big integer column
|
|
3360
|
+
*/
|
|
3361
|
+
bigInteger(name) {
|
|
3362
|
+
return this.addColumn(name, "bigInteger");
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Small integer column
|
|
3366
|
+
*/
|
|
3367
|
+
smallInteger(name) {
|
|
3368
|
+
return this.addColumn(name, "smallInteger");
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Tiny integer column
|
|
3372
|
+
*/
|
|
3373
|
+
tinyInteger(name) {
|
|
3374
|
+
return this.addColumn(name, "tinyInteger");
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Float column
|
|
3378
|
+
*/
|
|
3379
|
+
float(name) {
|
|
3380
|
+
return this.addColumn(name, "float");
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Double column
|
|
3384
|
+
*/
|
|
3385
|
+
double(name) {
|
|
3386
|
+
return this.addColumn(name, "double");
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Decimal column
|
|
3390
|
+
*/
|
|
3391
|
+
decimal(name, precision = 10, scale = 2) {
|
|
3392
|
+
return this.addColumn(name, "decimal").precision(precision, scale);
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* String (VARCHAR) column
|
|
3396
|
+
*/
|
|
3397
|
+
string(name, length = 255) {
|
|
3398
|
+
return this.addColumn(name, "string").length(length);
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Text column
|
|
3402
|
+
*/
|
|
3403
|
+
text(name) {
|
|
3404
|
+
return this.addColumn(name, "text");
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Medium text column
|
|
3408
|
+
*/
|
|
3409
|
+
mediumText(name) {
|
|
3410
|
+
return this.addColumn(name, "mediumText");
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Long text column
|
|
3414
|
+
*/
|
|
3415
|
+
longText(name) {
|
|
3416
|
+
return this.addColumn(name, "longText");
|
|
3417
|
+
}
|
|
3418
|
+
/**
|
|
3419
|
+
* Boolean column
|
|
3420
|
+
*/
|
|
3421
|
+
boolean(name) {
|
|
3422
|
+
return this.addColumn(name, "boolean");
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Date column
|
|
3426
|
+
*/
|
|
3427
|
+
date(name) {
|
|
3428
|
+
return this.addColumn(name, "date");
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Datetime column
|
|
3432
|
+
*/
|
|
3433
|
+
datetime(name) {
|
|
3434
|
+
return this.addColumn(name, "datetime");
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Timestamp column
|
|
3438
|
+
*/
|
|
3439
|
+
timestamp(name) {
|
|
3440
|
+
return this.addColumn(name, "timestamp");
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Time column
|
|
3444
|
+
*/
|
|
3445
|
+
time(name) {
|
|
3446
|
+
return this.addColumn(name, "time");
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* JSON column
|
|
3450
|
+
*/
|
|
3451
|
+
json(name) {
|
|
3452
|
+
return this.addColumn(name, "json");
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* JSONB column (PostgreSQL)
|
|
3456
|
+
*/
|
|
3457
|
+
jsonb(name) {
|
|
3458
|
+
return this.addColumn(name, "jsonb");
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* UUID column
|
|
3462
|
+
*/
|
|
3463
|
+
uuid(name) {
|
|
3464
|
+
return this.addColumn(name, "uuid");
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Binary column
|
|
3468
|
+
*/
|
|
3469
|
+
binary(name) {
|
|
3470
|
+
return this.addColumn(name, "binary");
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Enum column
|
|
3474
|
+
*/
|
|
3475
|
+
enum(name, values) {
|
|
3476
|
+
return this.addColumn(name, "enum").values(values);
|
|
3477
|
+
}
|
|
3478
|
+
// ============================================
|
|
3479
|
+
// Shortcut Methods
|
|
3480
|
+
// ============================================
|
|
3481
|
+
/**
|
|
3482
|
+
* Add created_at and updated_at timestamp columns
|
|
3483
|
+
*/
|
|
3484
|
+
timestamps() {
|
|
3485
|
+
this.timestamp("created_at").notNull().defaultNow();
|
|
3486
|
+
this.timestamp("updated_at").notNull().defaultNow();
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Add deleted_at timestamp column for soft deletes
|
|
3490
|
+
*/
|
|
3491
|
+
softDeletes() {
|
|
3492
|
+
return this.timestamp("deleted_at").nullable();
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Add foreign id column with foreign key
|
|
3496
|
+
*/
|
|
3497
|
+
foreignId(name) {
|
|
3498
|
+
const tableName = `${name.replace(/_id$/, "")}s`;
|
|
3499
|
+
const column = this.integer(name).unsigned().notNull();
|
|
3500
|
+
this.foreign(name).references("id").on(tableName);
|
|
3501
|
+
return column;
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* Add UUID primary key column
|
|
3505
|
+
*/
|
|
3506
|
+
uuidPrimary(name = "id") {
|
|
3507
|
+
return this.uuid(name).primary().notNull();
|
|
3508
|
+
}
|
|
3509
|
+
// ============================================
|
|
3510
|
+
// Indexes
|
|
3511
|
+
// ============================================
|
|
3512
|
+
/**
|
|
3513
|
+
* Add an index
|
|
3514
|
+
*/
|
|
3515
|
+
index(columns, name) {
|
|
3516
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3517
|
+
this.indexes.push({
|
|
3518
|
+
name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
|
|
3519
|
+
columns: columnArray,
|
|
3520
|
+
unique: false
|
|
3521
|
+
});
|
|
3522
|
+
return this;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Add a unique index
|
|
3526
|
+
*/
|
|
3527
|
+
unique(columns, name) {
|
|
3528
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3529
|
+
this.indexes.push({
|
|
3530
|
+
name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
|
|
3531
|
+
columns: columnArray,
|
|
3532
|
+
unique: true
|
|
3533
|
+
});
|
|
3534
|
+
return this;
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Add a fulltext index (MySQL)
|
|
3538
|
+
*/
|
|
3539
|
+
fulltext(columns, name) {
|
|
3540
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3541
|
+
this.indexes.push({
|
|
3542
|
+
name: name || `ft_${this.tableName}_${columnArray.join("_")}`,
|
|
3543
|
+
columns: columnArray,
|
|
3544
|
+
unique: false,
|
|
3545
|
+
type: "fulltext"
|
|
3546
|
+
});
|
|
3547
|
+
return this;
|
|
3548
|
+
}
|
|
3549
|
+
/**
|
|
3550
|
+
* Set primary key columns
|
|
3551
|
+
*/
|
|
3552
|
+
primary(columns) {
|
|
3553
|
+
this.primaryKeyColumns = Array.isArray(columns) ? columns : [columns];
|
|
3554
|
+
return this;
|
|
3555
|
+
}
|
|
3556
|
+
// ============================================
|
|
3557
|
+
// Foreign Keys
|
|
3558
|
+
// ============================================
|
|
3559
|
+
/**
|
|
3560
|
+
* Add a foreign key constraint
|
|
3561
|
+
*/
|
|
3562
|
+
foreign(column) {
|
|
3563
|
+
return new ForeignKeyChain(this, column);
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Add foreign key (internal)
|
|
3567
|
+
*/
|
|
3568
|
+
addForeignKey(fk) {
|
|
3569
|
+
this.foreignKeys.push(fk);
|
|
3570
|
+
}
|
|
3571
|
+
// ============================================
|
|
3572
|
+
// Table Options
|
|
3573
|
+
// ============================================
|
|
3574
|
+
/**
|
|
3575
|
+
* Set table engine (MySQL)
|
|
3576
|
+
*/
|
|
3577
|
+
engine(engine) {
|
|
3578
|
+
this.tableEngine = engine;
|
|
3579
|
+
return this;
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Set table charset (MySQL)
|
|
3583
|
+
*/
|
|
3584
|
+
charset(charset) {
|
|
3585
|
+
this.tableCharset = charset;
|
|
3586
|
+
return this;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Set table collation (MySQL)
|
|
3590
|
+
*/
|
|
3591
|
+
collation(collation) {
|
|
3592
|
+
this.tableCollation = collation;
|
|
3593
|
+
return this;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Set table comment
|
|
3597
|
+
*/
|
|
3598
|
+
comment(comment) {
|
|
3599
|
+
this.tableComment = comment;
|
|
3600
|
+
return this;
|
|
3601
|
+
}
|
|
3602
|
+
// ============================================
|
|
3603
|
+
// Internal Methods
|
|
3604
|
+
// ============================================
|
|
3605
|
+
/**
|
|
3606
|
+
* Add a column and return its builder
|
|
3607
|
+
*/
|
|
3608
|
+
addColumn(name, type) {
|
|
3609
|
+
const builder = new ColumnBuilder(name, type);
|
|
3610
|
+
this.columns.push(builder.getDefinition());
|
|
3611
|
+
const index = this.columns.length - 1;
|
|
3612
|
+
const proxy = new Proxy(builder, {
|
|
3613
|
+
get: (target, prop, receiver) => {
|
|
3614
|
+
const result = Reflect.get(target, prop, receiver);
|
|
3615
|
+
if (typeof result === "function") {
|
|
3616
|
+
return (...args) => {
|
|
3617
|
+
const returnValue = result.apply(target, args);
|
|
3618
|
+
this.columns[index] = target.getDefinition();
|
|
3619
|
+
return returnValue === target ? proxy : returnValue;
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
return result;
|
|
3623
|
+
}
|
|
3624
|
+
});
|
|
3625
|
+
return proxy;
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Get the table definition
|
|
3629
|
+
*/
|
|
3630
|
+
getDefinition() {
|
|
3631
|
+
return {
|
|
3632
|
+
name: this.tableName,
|
|
3633
|
+
columns: [...this.columns],
|
|
3634
|
+
indexes: [...this.indexes],
|
|
3635
|
+
foreignKeys: [...this.foreignKeys],
|
|
3636
|
+
primaryKey: this.primaryKeyColumns,
|
|
3637
|
+
engine: this.tableEngine,
|
|
3638
|
+
charset: this.tableCharset,
|
|
3639
|
+
collation: this.tableCollation,
|
|
3640
|
+
comment: this.tableComment
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
var ForeignKeyChain = class {
|
|
3645
|
+
tableBuilder;
|
|
3646
|
+
fkDefinition;
|
|
3647
|
+
constructor(tableBuilder, column) {
|
|
3648
|
+
this.tableBuilder = tableBuilder;
|
|
3649
|
+
this.fkDefinition = { column };
|
|
3650
|
+
}
|
|
3651
|
+
/**
|
|
3652
|
+
* Set the referenced column
|
|
3653
|
+
*/
|
|
3654
|
+
references(column) {
|
|
3655
|
+
this.fkDefinition.referenceColumn = column;
|
|
3656
|
+
return this;
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Set the referenced table
|
|
3660
|
+
*/
|
|
3661
|
+
on(tableName) {
|
|
3662
|
+
this.fkDefinition.table = tableName;
|
|
3663
|
+
this.apply();
|
|
3664
|
+
return this;
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Alias for on()
|
|
3668
|
+
*/
|
|
3669
|
+
inTable(tableName) {
|
|
3670
|
+
return this.on(tableName);
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3673
|
+
* Set ON DELETE action
|
|
3674
|
+
*/
|
|
3675
|
+
onDelete(action) {
|
|
3676
|
+
this.fkDefinition.onDelete = action;
|
|
3677
|
+
this.apply();
|
|
3678
|
+
return this;
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Set ON UPDATE action
|
|
3682
|
+
*/
|
|
3683
|
+
onUpdate(action) {
|
|
3684
|
+
this.fkDefinition.onUpdate = action;
|
|
3685
|
+
this.apply();
|
|
3686
|
+
return this;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Set constraint name
|
|
3690
|
+
*/
|
|
3691
|
+
name(name) {
|
|
3692
|
+
this.fkDefinition.name = name;
|
|
3693
|
+
this.apply();
|
|
3694
|
+
return this;
|
|
3695
|
+
}
|
|
3696
|
+
/**
|
|
3697
|
+
* Apply the foreign key to the table
|
|
3698
|
+
*/
|
|
3699
|
+
apply() {
|
|
3700
|
+
if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
3701
|
+
if (!this.fkDefinition.name) {
|
|
3702
|
+
this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
|
|
3703
|
+
}
|
|
3704
|
+
this.tableBuilder.addForeignKey(this.fkDefinition);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
|
|
3709
|
+
// src/schema/SchemaBuilder.ts
|
|
3710
|
+
var SchemaBuilder = class {
|
|
3711
|
+
dialectInstance;
|
|
3712
|
+
adapter;
|
|
3713
|
+
constructor(options) {
|
|
3714
|
+
this.adapter = options.adapter;
|
|
3715
|
+
switch (options.dialect) {
|
|
3716
|
+
case "mysql": {
|
|
3717
|
+
this.dialectInstance = new MySQLDialect();
|
|
3718
|
+
break;
|
|
3719
|
+
}
|
|
3720
|
+
case "postgresql": {
|
|
3721
|
+
this.dialectInstance = new PostgreSQLDialect();
|
|
3722
|
+
break;
|
|
3723
|
+
}
|
|
3724
|
+
default: {
|
|
3725
|
+
throw new Error(`Unsupported dialect: ${options.dialect}`);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Get the dialect instance
|
|
3731
|
+
*/
|
|
3732
|
+
get dialect() {
|
|
3733
|
+
return this.dialectInstance;
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Create a new table
|
|
3737
|
+
*/
|
|
3738
|
+
async createTable(tableName, callback) {
|
|
3739
|
+
const builder = new TableBuilder(tableName);
|
|
3740
|
+
callback(builder);
|
|
3741
|
+
const definition = builder.getDefinition();
|
|
3742
|
+
const sql2 = this.dialectInstance.createTable(definition);
|
|
3743
|
+
await this.execute(sql2);
|
|
3744
|
+
if (this.dialectInstance.dialect === "postgresql" && definition.indexes.length > 0) {
|
|
3745
|
+
const pgDialect = this.dialectInstance;
|
|
3746
|
+
for (const index of definition.indexes) {
|
|
3747
|
+
const indexSql = pgDialect.createIndex(tableName, index);
|
|
3748
|
+
await this.execute(indexSql);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Create a table if it doesn't exist
|
|
3754
|
+
*/
|
|
3755
|
+
async createTableIfNotExists(tableName, callback) {
|
|
3756
|
+
const exists2 = await this.hasTable(tableName);
|
|
3757
|
+
if (!exists2) {
|
|
3758
|
+
await this.createTable(tableName, callback);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Drop a table
|
|
3763
|
+
*/
|
|
3764
|
+
async dropTable(tableName) {
|
|
3765
|
+
const sql2 = this.dialectInstance.dropTable(tableName);
|
|
3766
|
+
await this.execute(sql2);
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Drop a table if it exists
|
|
3770
|
+
*/
|
|
3771
|
+
async dropTableIfExists(tableName) {
|
|
3772
|
+
const sql2 = this.dialectInstance.dropTableIfExists(tableName);
|
|
3773
|
+
await this.execute(sql2);
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Rename a table
|
|
3777
|
+
*/
|
|
3778
|
+
async renameTable(from, to) {
|
|
3779
|
+
const sql2 = this.dialectInstance.renameTable(from, to);
|
|
3780
|
+
await this.execute(sql2);
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Check if a table exists
|
|
3784
|
+
*/
|
|
3785
|
+
async hasTable(tableName) {
|
|
3786
|
+
const sql2 = this.dialectInstance.hasTable(tableName);
|
|
3787
|
+
const result = await this.query(sql2);
|
|
3788
|
+
return result.length > 0;
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Check if a column exists in a table
|
|
3792
|
+
*/
|
|
3793
|
+
async hasColumn(tableName, columnName) {
|
|
3794
|
+
const sql2 = this.dialectInstance.hasColumn(tableName, columnName);
|
|
3795
|
+
const result = await this.query(sql2);
|
|
3796
|
+
return result.length > 0;
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Alter a table
|
|
3800
|
+
*/
|
|
3801
|
+
async alterTable(tableName, callback) {
|
|
3802
|
+
const builder = new AlterTableBuilder(tableName);
|
|
3803
|
+
callback(builder);
|
|
3804
|
+
const definition = builder.getDefinition();
|
|
3805
|
+
const statements = this.dialectInstance.alterTable(definition);
|
|
3806
|
+
for (const sql2 of statements) {
|
|
3807
|
+
await this.execute(sql2);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Execute raw SQL
|
|
3812
|
+
*/
|
|
3813
|
+
async raw(sql2, params) {
|
|
3814
|
+
await this.execute(sql2, params);
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Execute SQL statement
|
|
3818
|
+
*/
|
|
3819
|
+
async execute(sql2, params) {
|
|
3820
|
+
if (this.adapter) {
|
|
3821
|
+
await this.adapter.execute(sql2, params);
|
|
3822
|
+
} else {
|
|
3823
|
+
console.log("SQL:", sql2);
|
|
3824
|
+
if (params && params.length > 0) {
|
|
3825
|
+
console.log("Params:", params);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Execute SQL query
|
|
3831
|
+
*/
|
|
3832
|
+
async query(sql2) {
|
|
3833
|
+
if (this.adapter) {
|
|
3834
|
+
const result = await this.adapter.query(sql2);
|
|
3835
|
+
return result.rows;
|
|
3836
|
+
}
|
|
3837
|
+
return [];
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Generate SQL without executing (for preview/dry-run)
|
|
3841
|
+
*/
|
|
3842
|
+
generateCreateTableSQL(tableName, callback) {
|
|
3843
|
+
const builder = new TableBuilder(tableName);
|
|
3844
|
+
callback(builder);
|
|
3845
|
+
return this.dialectInstance.createTable(builder.getDefinition());
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Generate ALTER TABLE SQL without executing
|
|
3849
|
+
*/
|
|
3850
|
+
generateAlterTableSQL(tableName, callback) {
|
|
3851
|
+
const builder = new AlterTableBuilder(tableName);
|
|
3852
|
+
callback(builder);
|
|
3853
|
+
return this.dialectInstance.alterTable(builder.getDefinition());
|
|
3854
|
+
}
|
|
3855
|
+
};
|
|
3856
|
+
var AlterTableBuilder = class {
|
|
3857
|
+
tableName;
|
|
3858
|
+
operations = [];
|
|
3859
|
+
constructor(tableName) {
|
|
3860
|
+
this.tableName = tableName;
|
|
3861
|
+
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Add a new column
|
|
3864
|
+
*/
|
|
3865
|
+
addColumn(name, type, options) {
|
|
3866
|
+
this.operations.push({
|
|
3867
|
+
type: "addColumn",
|
|
3868
|
+
column: {
|
|
3869
|
+
name,
|
|
3870
|
+
type,
|
|
3871
|
+
nullable: true,
|
|
3872
|
+
unsigned: false,
|
|
3873
|
+
autoIncrement: false,
|
|
3874
|
+
primary: false,
|
|
3875
|
+
unique: false,
|
|
3876
|
+
index: false,
|
|
3877
|
+
first: false,
|
|
3878
|
+
...options
|
|
3879
|
+
}
|
|
3880
|
+
});
|
|
3881
|
+
return this;
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Drop a column
|
|
3885
|
+
*/
|
|
3886
|
+
dropColumn(name) {
|
|
3887
|
+
this.operations.push({ type: "dropColumn", name });
|
|
3888
|
+
return this;
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Rename a column
|
|
3892
|
+
*/
|
|
3893
|
+
renameColumn(from, to) {
|
|
3894
|
+
this.operations.push({ type: "renameColumn", from, to });
|
|
3895
|
+
return this;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Modify a column
|
|
3899
|
+
*/
|
|
3900
|
+
modifyColumn(name, type, options) {
|
|
3901
|
+
this.operations.push({
|
|
3902
|
+
type: "modifyColumn",
|
|
3903
|
+
column: {
|
|
3904
|
+
name,
|
|
3905
|
+
type,
|
|
3906
|
+
nullable: true,
|
|
3907
|
+
unsigned: false,
|
|
3908
|
+
autoIncrement: false,
|
|
3909
|
+
primary: false,
|
|
3910
|
+
unique: false,
|
|
3911
|
+
index: false,
|
|
3912
|
+
first: false,
|
|
3913
|
+
...options
|
|
3914
|
+
}
|
|
3915
|
+
});
|
|
3916
|
+
return this;
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Add an index
|
|
3920
|
+
*/
|
|
3921
|
+
addIndex(columns, name) {
|
|
3922
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3923
|
+
this.operations.push({
|
|
3924
|
+
type: "addIndex",
|
|
3925
|
+
index: {
|
|
3926
|
+
name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
|
|
3927
|
+
columns: columnArray,
|
|
3928
|
+
unique: false
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
return this;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Add a unique index
|
|
3935
|
+
*/
|
|
3936
|
+
addUnique(columns, name) {
|
|
3937
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3938
|
+
this.operations.push({
|
|
3939
|
+
type: "addIndex",
|
|
3940
|
+
index: {
|
|
3941
|
+
name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
|
|
3942
|
+
columns: columnArray,
|
|
3943
|
+
unique: true
|
|
3944
|
+
}
|
|
3945
|
+
});
|
|
3946
|
+
return this;
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Drop an index
|
|
3950
|
+
*/
|
|
3951
|
+
dropIndex(name) {
|
|
3952
|
+
this.operations.push({ type: "dropIndex", name });
|
|
3953
|
+
return this;
|
|
3954
|
+
}
|
|
3955
|
+
/**
|
|
3956
|
+
* Add a foreign key
|
|
3957
|
+
*/
|
|
3958
|
+
addForeign(column) {
|
|
3959
|
+
return new AlterForeignKeyBuilder(this, column);
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Add foreign key (internal)
|
|
3963
|
+
*/
|
|
3964
|
+
addForeignKeyOperation(fk) {
|
|
3965
|
+
this.operations.push({ type: "addForeignKey", foreignKey: fk });
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Drop a foreign key
|
|
3969
|
+
*/
|
|
3970
|
+
dropForeign(name) {
|
|
3971
|
+
this.operations.push({ type: "dropForeignKey", name });
|
|
3972
|
+
return this;
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Add primary key
|
|
3976
|
+
*/
|
|
3977
|
+
addPrimary(columns) {
|
|
3978
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
3979
|
+
this.operations.push({ type: "addPrimary", columns: columnArray });
|
|
3980
|
+
return this;
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Drop primary key
|
|
3984
|
+
*/
|
|
3985
|
+
dropPrimary() {
|
|
3986
|
+
this.operations.push({ type: "dropPrimary" });
|
|
3987
|
+
return this;
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Get the alter table definition
|
|
3991
|
+
*/
|
|
3992
|
+
getDefinition() {
|
|
3993
|
+
return {
|
|
3994
|
+
tableName: this.tableName,
|
|
3995
|
+
operations: [...this.operations]
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
};
|
|
3999
|
+
var AlterForeignKeyBuilder = class {
|
|
4000
|
+
builder;
|
|
4001
|
+
fkDefinition;
|
|
4002
|
+
constructor(builder, column) {
|
|
4003
|
+
this.builder = builder;
|
|
4004
|
+
this.fkDefinition = { column };
|
|
4005
|
+
}
|
|
4006
|
+
references(column) {
|
|
4007
|
+
this.fkDefinition.referenceColumn = column;
|
|
4008
|
+
return this;
|
|
4009
|
+
}
|
|
4010
|
+
on(tableName) {
|
|
4011
|
+
this.fkDefinition.table = tableName;
|
|
4012
|
+
this.apply();
|
|
4013
|
+
return this;
|
|
4014
|
+
}
|
|
4015
|
+
onDelete(action) {
|
|
4016
|
+
this.fkDefinition.onDelete = action;
|
|
4017
|
+
this.apply();
|
|
4018
|
+
return this;
|
|
4019
|
+
}
|
|
4020
|
+
onUpdate(action) {
|
|
4021
|
+
this.fkDefinition.onUpdate = action;
|
|
4022
|
+
this.apply();
|
|
4023
|
+
return this;
|
|
4024
|
+
}
|
|
4025
|
+
name(name) {
|
|
4026
|
+
this.fkDefinition.name = name;
|
|
4027
|
+
this.apply();
|
|
4028
|
+
return this;
|
|
4029
|
+
}
|
|
4030
|
+
apply() {
|
|
4031
|
+
if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
|
|
4032
|
+
if (!this.fkDefinition.name) {
|
|
4033
|
+
this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
|
|
4034
|
+
}
|
|
4035
|
+
this.builder.addForeignKeyOperation(this.fkDefinition);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
|
|
4040
|
+
// src/migrations/FileMigrationRunner.ts
|
|
4041
|
+
var FileMigrationRunner = class {
|
|
4042
|
+
adapter;
|
|
4043
|
+
loader;
|
|
4044
|
+
lock;
|
|
4045
|
+
schema;
|
|
4046
|
+
options;
|
|
4047
|
+
constructor(adapter, options) {
|
|
4048
|
+
this.adapter = adapter;
|
|
4049
|
+
this.options = {
|
|
4050
|
+
directory: options.directory,
|
|
4051
|
+
tableName: options.tableName ?? "db_migrations",
|
|
4052
|
+
lockTableName: options.lockTableName ?? "db_migrations_lock",
|
|
4053
|
+
lockTimeout: options.lockTimeout ?? 6e4,
|
|
4054
|
+
validateChecksums: options.validateChecksums ?? true,
|
|
4055
|
+
dialect: options.dialect,
|
|
4056
|
+
logger: options.logger ?? console,
|
|
4057
|
+
dryRun: options.dryRun ?? false
|
|
4058
|
+
};
|
|
4059
|
+
this.loader = new MigrationLoader(options.directory);
|
|
4060
|
+
this.lock = new MigrationLock(adapter, {
|
|
4061
|
+
tableName: this.options.lockTableName,
|
|
4062
|
+
timeout: this.options.lockTimeout,
|
|
4063
|
+
dialect: options.dialect
|
|
4064
|
+
});
|
|
4065
|
+
this.schema = new SchemaBuilder({
|
|
4066
|
+
dialect: options.dialect,
|
|
4067
|
+
adapter
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Initialize migration tables
|
|
4072
|
+
*/
|
|
4073
|
+
async initialize() {
|
|
4074
|
+
if (this.options.dryRun) {
|
|
4075
|
+
this.options.logger.info("DRY RUN: Would create migration tables");
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const tableName = this.options.tableName;
|
|
4079
|
+
const hasTable = await this.schema.hasTable(tableName);
|
|
4080
|
+
if (!hasTable) {
|
|
4081
|
+
await this.schema.createTable(tableName, (table) => {
|
|
4082
|
+
table.increments("id");
|
|
4083
|
+
table.string("name", 255).notNull().unique();
|
|
4084
|
+
table.integer("batch").notNull();
|
|
4085
|
+
table.timestamp("executed_at").notNull().defaultNow();
|
|
4086
|
+
table.integer("execution_time_ms").notNull();
|
|
4087
|
+
table.string("checksum", 64).notNull();
|
|
4088
|
+
table.index("batch");
|
|
4089
|
+
table.index("executed_at");
|
|
4090
|
+
});
|
|
4091
|
+
this.options.logger.info(`Created migrations table: ${tableName}`);
|
|
4092
|
+
}
|
|
4093
|
+
await this.lock.initialize();
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Run all pending migrations
|
|
4097
|
+
*/
|
|
4098
|
+
async latest() {
|
|
4099
|
+
await this.lock.initialize();
|
|
4100
|
+
return this.lock.withLock(async () => {
|
|
4101
|
+
await this.initialize();
|
|
4102
|
+
const pending = await this.getPendingMigrations();
|
|
4103
|
+
if (pending.length === 0) {
|
|
4104
|
+
this.options.logger.info("No pending migrations");
|
|
4105
|
+
return [];
|
|
4106
|
+
}
|
|
4107
|
+
const batch = await this.getNextBatch();
|
|
4108
|
+
const executed = [];
|
|
4109
|
+
this.options.logger.info(`Running ${pending.length} migrations (batch ${batch})`);
|
|
4110
|
+
for (const migration of pending) {
|
|
4111
|
+
await this.runMigration(migration, "up", batch);
|
|
4112
|
+
executed.push(migration.name);
|
|
4113
|
+
}
|
|
4114
|
+
return executed;
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Rollback the last batch of migrations
|
|
4119
|
+
*/
|
|
4120
|
+
async rollback(steps = 1) {
|
|
4121
|
+
await this.lock.initialize();
|
|
4122
|
+
return this.lock.withLock(async () => {
|
|
4123
|
+
await this.initialize();
|
|
4124
|
+
const batches = await this.getLastBatches(steps);
|
|
4125
|
+
if (batches.length === 0) {
|
|
4126
|
+
this.options.logger.info("No migrations to rollback");
|
|
4127
|
+
return [];
|
|
4128
|
+
}
|
|
4129
|
+
const rolledBack = [];
|
|
4130
|
+
for (const batch of batches) {
|
|
4131
|
+
this.options.logger.info(`Rolling back batch ${batch.batch}`);
|
|
4132
|
+
const migrations = await this.loadMigrationsByName(
|
|
4133
|
+
batch.migrations.map((m) => m.name).reverse()
|
|
4134
|
+
);
|
|
4135
|
+
for (const migration of migrations) {
|
|
4136
|
+
await this.runMigration(migration, "down", batch.batch);
|
|
4137
|
+
rolledBack.push(migration.name);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
return rolledBack;
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Reset all migrations
|
|
4145
|
+
*/
|
|
4146
|
+
async reset() {
|
|
4147
|
+
await this.lock.initialize();
|
|
4148
|
+
return this.lock.withLock(async () => {
|
|
4149
|
+
await this.initialize();
|
|
4150
|
+
const executed = await this.getExecutedMigrations();
|
|
4151
|
+
if (executed.length === 0) {
|
|
4152
|
+
this.options.logger.info("No migrations to reset");
|
|
4153
|
+
return [];
|
|
4154
|
+
}
|
|
4155
|
+
const rolledBack = [];
|
|
4156
|
+
const migrations = await this.loadMigrationsByName(executed.map((m) => m.name).reverse());
|
|
4157
|
+
for (const migration of migrations) {
|
|
4158
|
+
const record = executed.find((e) => e.name === migration.name);
|
|
4159
|
+
await this.runMigration(migration, "down", record.batch);
|
|
4160
|
+
rolledBack.push(migration.name);
|
|
4161
|
+
}
|
|
4162
|
+
return rolledBack;
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Reset and re-run all migrations
|
|
4167
|
+
*/
|
|
4168
|
+
async fresh() {
|
|
4169
|
+
await this.reset();
|
|
4170
|
+
return this.latest();
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Get migration status
|
|
4174
|
+
*/
|
|
4175
|
+
async status() {
|
|
4176
|
+
await this.initialize();
|
|
4177
|
+
const allMigrations = await this.loader.getMigrationFiles();
|
|
4178
|
+
const executed = await this.getExecutedMigrations();
|
|
4179
|
+
const executedMap = new Map(executed.map((e) => [e.name, e]));
|
|
4180
|
+
return allMigrations.map((file) => {
|
|
4181
|
+
const record = executedMap.get(file.name);
|
|
4182
|
+
return {
|
|
4183
|
+
name: file.name,
|
|
4184
|
+
batch: record?.batch ?? null,
|
|
4185
|
+
executedAt: record?.executed_at ?? null,
|
|
4186
|
+
pending: !record
|
|
4187
|
+
};
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* Validate migrations
|
|
4192
|
+
*/
|
|
4193
|
+
async validate() {
|
|
4194
|
+
const errors = [];
|
|
4195
|
+
try {
|
|
4196
|
+
const migrations = await this.loader.loadAll();
|
|
4197
|
+
const executed = await this.getExecutedMigrations();
|
|
4198
|
+
for (const record of executed) {
|
|
4199
|
+
const migration = migrations.find((m) => m.name === record.name);
|
|
4200
|
+
if (!migration) {
|
|
4201
|
+
errors.push(`Migration '${record.name}' exists in database but not in codebase`);
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
if (this.options.validateChecksums) {
|
|
4205
|
+
const checksum = this.calculateChecksum(migration);
|
|
4206
|
+
if (checksum !== record.checksum) {
|
|
4207
|
+
errors.push(
|
|
4208
|
+
`Checksum mismatch for migration '${record.name}'. The migration may have been modified.`
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
} catch (error) {
|
|
4214
|
+
errors.push(`Validation error: ${error.message}`);
|
|
4215
|
+
}
|
|
4216
|
+
return {
|
|
4217
|
+
valid: errors.length === 0,
|
|
4218
|
+
errors
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
/**
|
|
4222
|
+
* Get pending migrations
|
|
4223
|
+
*/
|
|
4224
|
+
async getPendingMigrations() {
|
|
4225
|
+
const allMigrations = await this.loader.loadAll();
|
|
4226
|
+
const executed = await this.getExecutedMigrations();
|
|
4227
|
+
const executedNames = new Set(executed.map((e) => e.name));
|
|
4228
|
+
return allMigrations.filter((m) => !executedNames.has(m.name));
|
|
4229
|
+
}
|
|
4230
|
+
/**
|
|
4231
|
+
* Get executed migrations from database
|
|
4232
|
+
*/
|
|
4233
|
+
async getExecutedMigrations() {
|
|
4234
|
+
const result = await this.adapter.query(
|
|
4235
|
+
`SELECT * FROM ${this.options.tableName} ORDER BY id ASC`
|
|
4236
|
+
);
|
|
4237
|
+
return result.rows;
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* Get the next batch number
|
|
4241
|
+
*/
|
|
4242
|
+
async getNextBatch() {
|
|
4243
|
+
const result = await this.adapter.query(
|
|
4244
|
+
`SELECT MAX(batch) as max_batch FROM ${this.options.tableName}`
|
|
4245
|
+
);
|
|
4246
|
+
return (result.rows[0]?.max_batch ?? 0) + 1;
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Get the last N batches
|
|
4250
|
+
*/
|
|
4251
|
+
async getLastBatches(count2) {
|
|
4252
|
+
const result = await this.adapter.query(
|
|
4253
|
+
`SELECT * FROM ${this.options.tableName} ORDER BY batch DESC, id DESC`
|
|
4254
|
+
);
|
|
4255
|
+
const batches = /* @__PURE__ */ new Map();
|
|
4256
|
+
for (const record of result.rows) {
|
|
4257
|
+
if (!batches.has(record.batch)) {
|
|
4258
|
+
batches.set(record.batch, []);
|
|
4259
|
+
}
|
|
4260
|
+
batches.get(record.batch).push(record);
|
|
4261
|
+
}
|
|
4262
|
+
const batchNumbers = Array.from(batches.keys()).slice(0, count2);
|
|
4263
|
+
return batchNumbers.map((batch) => ({
|
|
4264
|
+
batch,
|
|
4265
|
+
migrations: batches.get(batch)
|
|
4266
|
+
}));
|
|
4267
|
+
}
|
|
4268
|
+
/**
|
|
4269
|
+
* Load migrations by name
|
|
4270
|
+
*/
|
|
4271
|
+
async loadMigrationsByName(names) {
|
|
4272
|
+
const allMigrations = await this.loader.loadAll();
|
|
4273
|
+
const migrationMap = new Map(allMigrations.map((m) => [m.name, m]));
|
|
4274
|
+
return names.map((name) => migrationMap.get(name)).filter((m) => m !== void 0);
|
|
4275
|
+
}
|
|
4276
|
+
/**
|
|
4277
|
+
* Run a single migration
|
|
4278
|
+
*/
|
|
4279
|
+
async runMigration(migration, direction, batch) {
|
|
4280
|
+
const startTime = Date.now();
|
|
4281
|
+
const action = direction === "up" ? "Running" : "Rolling back";
|
|
4282
|
+
this.options.logger.info(`${action}: ${migration.name}`);
|
|
4283
|
+
if (this.options.dryRun) {
|
|
4284
|
+
this.options.logger.info(`DRY RUN: Would ${direction} ${migration.name}`);
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
const transaction = migration.transactional ? await this.adapter.beginTransaction() : null;
|
|
4288
|
+
try {
|
|
4289
|
+
const schema = new SchemaBuilder({
|
|
4290
|
+
dialect: this.options.dialect,
|
|
4291
|
+
adapter: this.adapter
|
|
4292
|
+
});
|
|
4293
|
+
await migration[direction](schema);
|
|
4294
|
+
const executionTime = Date.now() - startTime;
|
|
4295
|
+
if (direction === "up") {
|
|
4296
|
+
const checksum = this.calculateChecksum(migration);
|
|
4297
|
+
await this.adapter.execute(
|
|
4298
|
+
`INSERT INTO ${this.options.tableName}
|
|
4299
|
+
(name, batch, executed_at, execution_time_ms, checksum)
|
|
4300
|
+
VALUES (?, ?, NOW(), ?, ?)`,
|
|
4301
|
+
[migration.name, batch, executionTime, checksum]
|
|
4302
|
+
);
|
|
4303
|
+
} else {
|
|
4304
|
+
await this.adapter.execute(`DELETE FROM ${this.options.tableName} WHERE name = ?`, [
|
|
4305
|
+
migration.name
|
|
4306
|
+
]);
|
|
4307
|
+
}
|
|
4308
|
+
if (transaction) {
|
|
4309
|
+
await transaction.commit();
|
|
4310
|
+
}
|
|
4311
|
+
this.options.logger.info(
|
|
4312
|
+
`${direction === "up" ? "Completed" : "Rolled back"}: ${migration.name} (${executionTime}ms)`
|
|
4313
|
+
);
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
if (transaction) {
|
|
4316
|
+
await transaction.rollback();
|
|
4317
|
+
}
|
|
4318
|
+
throw new Error(
|
|
4319
|
+
`Failed to ${direction} migration '${migration.name}': ${error.message}`
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
/**
|
|
4324
|
+
* Calculate migration checksum
|
|
4325
|
+
*/
|
|
4326
|
+
calculateChecksum(migration) {
|
|
4327
|
+
const content = `${migration.name}:${migration.up.toString()}:${migration.down.toString()}`;
|
|
4328
|
+
return createHash("sha256").update(content).digest("hex");
|
|
4329
|
+
}
|
|
4330
|
+
};
|
|
4331
|
+
|
|
4332
|
+
// src/migrations/ExpandContract.ts
|
|
4333
|
+
var ExpandContractHelper = class {
|
|
4334
|
+
constructor(adapter, schema) {
|
|
4335
|
+
this.adapter = adapter;
|
|
4336
|
+
this.schema = schema;
|
|
4337
|
+
}
|
|
4338
|
+
/**
|
|
4339
|
+
* Rename a column using expand/contract pattern
|
|
4340
|
+
* Expand: Add new column
|
|
4341
|
+
* Migrate: Copy data from old to new
|
|
4342
|
+
* Contract: Drop old column
|
|
4343
|
+
*/
|
|
4344
|
+
async renameColumn(table, oldColumn, newColumn, phase) {
|
|
4345
|
+
switch (phase) {
|
|
4346
|
+
case "expand": {
|
|
4347
|
+
await this.schema.alterTable(table, (t) => {
|
|
4348
|
+
t.addColumn(newColumn, "text");
|
|
4349
|
+
});
|
|
4350
|
+
break;
|
|
4351
|
+
}
|
|
4352
|
+
case "migrate": {
|
|
4353
|
+
await this.adapter.execute(
|
|
4354
|
+
`UPDATE ${table} SET ${newColumn} = ${oldColumn} WHERE ${newColumn} IS NULL`
|
|
4355
|
+
);
|
|
4356
|
+
break;
|
|
4357
|
+
}
|
|
4358
|
+
case "contract": {
|
|
4359
|
+
await this.schema.alterTable(table, (t) => {
|
|
4360
|
+
t.dropColumn(oldColumn);
|
|
4361
|
+
});
|
|
4362
|
+
break;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Change column type using expand/contract pattern
|
|
4368
|
+
*/
|
|
4369
|
+
async changeColumnType(table, column, newType, transform, phase) {
|
|
4370
|
+
const tempColumn = `${column}_new`;
|
|
4371
|
+
switch (phase) {
|
|
4372
|
+
case "expand": {
|
|
4373
|
+
await this.adapter.execute(`ALTER TABLE ${table} ADD COLUMN ${tempColumn} ${newType}`);
|
|
4374
|
+
break;
|
|
4375
|
+
}
|
|
4376
|
+
case "migrate": {
|
|
4377
|
+
await this.adapter.execute(`UPDATE ${table} SET ${tempColumn} = ${transform}`);
|
|
4378
|
+
break;
|
|
4379
|
+
}
|
|
4380
|
+
case "contract": {
|
|
4381
|
+
await this.adapter.execute(`ALTER TABLE ${table} DROP COLUMN ${column}`);
|
|
4382
|
+
await this.adapter.execute(`ALTER TABLE ${table} RENAME COLUMN ${tempColumn} TO ${column}`);
|
|
4383
|
+
break;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Split a table into two tables using expand/contract
|
|
4389
|
+
*/
|
|
4390
|
+
async splitTable(sourceTable, newTable, columnsToMove, foreignKeyColumn, phase) {
|
|
4391
|
+
switch (phase) {
|
|
4392
|
+
case "expand": {
|
|
4393
|
+
await this.schema.createTable(newTable, (t) => {
|
|
4394
|
+
t.increments("id");
|
|
4395
|
+
t.integer(foreignKeyColumn).notNull();
|
|
4396
|
+
});
|
|
4397
|
+
break;
|
|
4398
|
+
}
|
|
4399
|
+
case "migrate": {
|
|
4400
|
+
const cols = columnsToMove.join(", ");
|
|
4401
|
+
await this.adapter.execute(
|
|
4402
|
+
`INSERT INTO ${newTable} (${foreignKeyColumn}, ${cols})
|
|
4403
|
+
SELECT id, ${cols} FROM ${sourceTable}`
|
|
4404
|
+
);
|
|
4405
|
+
break;
|
|
4406
|
+
}
|
|
4407
|
+
case "contract": {
|
|
4408
|
+
for (const col of columnsToMove) {
|
|
4409
|
+
await this.schema.alterTable(sourceTable, (t) => {
|
|
4410
|
+
t.dropColumn(col);
|
|
4411
|
+
});
|
|
4412
|
+
}
|
|
4413
|
+
break;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Merge two tables into one using expand/contract
|
|
4419
|
+
*/
|
|
4420
|
+
async mergeTables(targetTable, sourceTable, columnsToMerge, joinColumn, phase) {
|
|
4421
|
+
switch (phase) {
|
|
4422
|
+
case "expand": {
|
|
4423
|
+
for (const col of columnsToMerge) {
|
|
4424
|
+
await this.schema.alterTable(targetTable, (t) => {
|
|
4425
|
+
t.addColumn(col, "text");
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
break;
|
|
4429
|
+
}
|
|
4430
|
+
case "migrate": {
|
|
4431
|
+
const setClauses = columnsToMerge.map((col) => `${targetTable}.${col} = ${sourceTable}.${col}`).join(", ");
|
|
4432
|
+
await this.adapter.execute(
|
|
4433
|
+
`UPDATE ${targetTable}
|
|
4434
|
+
SET ${setClauses}
|
|
4435
|
+
FROM ${sourceTable}
|
|
4436
|
+
WHERE ${targetTable}.id = ${sourceTable}.${joinColumn}`
|
|
4437
|
+
);
|
|
4438
|
+
break;
|
|
4439
|
+
}
|
|
4440
|
+
case "contract": {
|
|
4441
|
+
await this.schema.dropTable(sourceTable);
|
|
4442
|
+
break;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Add NOT NULL constraint safely
|
|
4448
|
+
*/
|
|
4449
|
+
async addNotNullConstraint(table, column, defaultValue, phase) {
|
|
4450
|
+
switch (phase) {
|
|
4451
|
+
case "expand": {
|
|
4452
|
+
await this.adapter.execute(
|
|
4453
|
+
`UPDATE ${table} SET ${column} = ${defaultValue} WHERE ${column} IS NULL`
|
|
4454
|
+
);
|
|
4455
|
+
break;
|
|
4456
|
+
}
|
|
4457
|
+
case "migrate": {
|
|
4458
|
+
const result = await this.adapter.query(
|
|
4459
|
+
`SELECT COUNT(*) as count FROM ${table} WHERE ${column} IS NULL`
|
|
4460
|
+
);
|
|
4461
|
+
if (result.rows[0]?.count && result.rows[0].count > 0) {
|
|
4462
|
+
throw new Error(
|
|
4463
|
+
`Cannot add NOT NULL: ${result.rows[0].count} rows still have NULL values`
|
|
4464
|
+
);
|
|
4465
|
+
}
|
|
4466
|
+
break;
|
|
4467
|
+
}
|
|
4468
|
+
case "contract": {
|
|
4469
|
+
await this.schema.alterTable(table, (t) => {
|
|
4470
|
+
t.modifyColumn(column, "text", { nullable: false });
|
|
4471
|
+
});
|
|
4472
|
+
break;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
};
|
|
4477
|
+
function createExpandContractHelper(adapter, schema) {
|
|
4478
|
+
return new ExpandContractHelper(adapter, schema);
|
|
4479
|
+
}
|
|
4480
|
+
var SeederLoader = class {
|
|
4481
|
+
constructor(directory) {
|
|
4482
|
+
this.directory = directory;
|
|
4483
|
+
}
|
|
4484
|
+
/**
|
|
4485
|
+
* Load all seeder files from directory
|
|
4486
|
+
*/
|
|
4487
|
+
async loadAll() {
|
|
4488
|
+
const files = await readdir(this.directory);
|
|
4489
|
+
const seederFiles = files.filter((f) => /\.(ts|js|mjs)$/.test(f) && !f.endsWith(".d.ts")).sort().map((f) => ({
|
|
4490
|
+
name: this.getSeederName(f),
|
|
4491
|
+
path: resolve(this.directory, f)
|
|
4492
|
+
}));
|
|
4493
|
+
return seederFiles;
|
|
4494
|
+
}
|
|
4495
|
+
/**
|
|
4496
|
+
* Load a specific seeder by path
|
|
4497
|
+
*/
|
|
4498
|
+
async load(seederPath) {
|
|
4499
|
+
const fileUrl = pathToFileURL(seederPath).href;
|
|
4500
|
+
const module = await import(fileUrl);
|
|
4501
|
+
const seeder = module.default || module;
|
|
4502
|
+
if (!seeder || typeof seeder.run !== "function") {
|
|
4503
|
+
throw new Error(`Invalid seeder: ${seederPath} - must export a run() function`);
|
|
4504
|
+
}
|
|
4505
|
+
return seeder;
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Get seeder name from filename
|
|
4509
|
+
*/
|
|
4510
|
+
getSeederName(filename) {
|
|
4511
|
+
return basename(filename).replace(/\.(ts|js|mjs)$/, "");
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Generate a new seeder filename
|
|
4515
|
+
*/
|
|
4516
|
+
static generateFilename(name) {
|
|
4517
|
+
const snakeName = name.replaceAll(/([a-z])([A-Z])/g, "$1_$2").replaceAll(/[\s-]+/g, "_").toLowerCase();
|
|
4518
|
+
return `${snakeName}_seeder.ts`;
|
|
4519
|
+
}
|
|
4520
|
+
/**
|
|
4521
|
+
* Get seeder template
|
|
4522
|
+
*/
|
|
4523
|
+
static getSeederTemplate(name) {
|
|
4524
|
+
const className = name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
4525
|
+
return `/**
|
|
4526
|
+
* ${className} Seeder
|
|
4527
|
+
*/
|
|
4528
|
+
|
|
4529
|
+
import type { Seeder, DatabaseAdapter } from '@db-bridge/core';
|
|
4530
|
+
|
|
4531
|
+
export default {
|
|
4532
|
+
async run(adapter: DatabaseAdapter): Promise<void> {
|
|
4533
|
+
// Insert seed data
|
|
4534
|
+
// await adapter.execute(\`
|
|
4535
|
+
// INSERT INTO users (name, email) VALUES
|
|
4536
|
+
// ('John Doe', 'john@example.com'),
|
|
4537
|
+
// ('Jane Doe', 'jane@example.com')
|
|
4538
|
+
// \`);
|
|
4539
|
+
},
|
|
4540
|
+
} satisfies Seeder;
|
|
4541
|
+
`;
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
|
|
4545
|
+
// src/seeds/SeederRunner.ts
|
|
4546
|
+
var SeederRunner = class {
|
|
4547
|
+
constructor(adapter, options) {
|
|
4548
|
+
this.adapter = adapter;
|
|
4549
|
+
this.options = options;
|
|
4550
|
+
this.loader = new SeederLoader(options.directory);
|
|
4551
|
+
}
|
|
4552
|
+
loader;
|
|
4553
|
+
options;
|
|
4554
|
+
/**
|
|
4555
|
+
* Run all seeders (or filtered by options)
|
|
4556
|
+
*/
|
|
4557
|
+
async run() {
|
|
4558
|
+
const files = await this.loader.loadAll();
|
|
4559
|
+
const filteredFiles = this.filterSeeders(files);
|
|
4560
|
+
const results = [];
|
|
4561
|
+
for (const file of filteredFiles) {
|
|
4562
|
+
const startTime = Date.now();
|
|
4563
|
+
try {
|
|
4564
|
+
const seeder = await this.loader.load(file.path);
|
|
4565
|
+
await seeder.run(this.adapter);
|
|
4566
|
+
results.push({
|
|
4567
|
+
name: file.name,
|
|
4568
|
+
success: true,
|
|
4569
|
+
duration: Date.now() - startTime
|
|
4570
|
+
});
|
|
4571
|
+
} catch (error) {
|
|
4572
|
+
results.push({
|
|
4573
|
+
name: file.name,
|
|
4574
|
+
success: false,
|
|
4575
|
+
error: error.message,
|
|
4576
|
+
duration: Date.now() - startTime
|
|
4577
|
+
});
|
|
4578
|
+
break;
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
return results;
|
|
4582
|
+
}
|
|
4583
|
+
/**
|
|
4584
|
+
* Run a specific seeder by name
|
|
4585
|
+
*/
|
|
4586
|
+
async runSeeder(name) {
|
|
4587
|
+
const files = await this.loader.loadAll();
|
|
4588
|
+
const file = files.find((f) => f.name === name || f.name === `${name}_seeder`);
|
|
4589
|
+
if (!file) {
|
|
4590
|
+
return {
|
|
4591
|
+
name,
|
|
4592
|
+
success: false,
|
|
4593
|
+
error: `Seeder not found: ${name}`,
|
|
4594
|
+
duration: 0
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
const startTime = Date.now();
|
|
4598
|
+
try {
|
|
4599
|
+
const seeder = await this.loader.load(file.path);
|
|
4600
|
+
await seeder.run(this.adapter);
|
|
4601
|
+
return {
|
|
4602
|
+
name: file.name,
|
|
4603
|
+
success: true,
|
|
4604
|
+
duration: Date.now() - startTime
|
|
4605
|
+
};
|
|
4606
|
+
} catch (error) {
|
|
4607
|
+
return {
|
|
4608
|
+
name: file.name,
|
|
4609
|
+
success: false,
|
|
4610
|
+
error: error.message,
|
|
4611
|
+
duration: Date.now() - startTime
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Filter seeders based on options
|
|
4617
|
+
*/
|
|
4618
|
+
filterSeeders(files) {
|
|
4619
|
+
let filtered = files;
|
|
4620
|
+
if (this.options.only && this.options.only.length > 0) {
|
|
4621
|
+
filtered = filtered.filter(
|
|
4622
|
+
(f) => this.options.only.some((name) => f.name === name || f.name === `${name}_seeder`)
|
|
4623
|
+
);
|
|
4624
|
+
}
|
|
4625
|
+
if (this.options.except && this.options.except.length > 0) {
|
|
4626
|
+
filtered = filtered.filter(
|
|
4627
|
+
(f) => !this.options.except.some((name) => f.name === name || f.name === `${name}_seeder`)
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
4630
|
+
return filtered;
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* List all available seeders
|
|
4634
|
+
*/
|
|
4635
|
+
async list() {
|
|
4636
|
+
return this.loader.loadAll();
|
|
4637
|
+
}
|
|
4638
|
+
};
|
|
2108
4639
|
var PerformanceMonitor = class extends EventEmitter {
|
|
2109
4640
|
traces = /* @__PURE__ */ new Map();
|
|
2110
4641
|
slowQueries = [];
|
|
@@ -4188,13 +6719,13 @@ var SQLDialect = class {
|
|
|
4188
6719
|
const fromClause = components.fromAlias ? `${this.escapeIdentifier(components.from)} AS ${this.escapeIdentifier(components.fromAlias)}` : this.escapeIdentifier(components.from);
|
|
4189
6720
|
parts.push(fromClause);
|
|
4190
6721
|
}
|
|
4191
|
-
for (const
|
|
4192
|
-
parts.push(`${
|
|
4193
|
-
if (
|
|
4194
|
-
parts.push(`AS ${this.escapeIdentifier(
|
|
6722
|
+
for (const join2 of components.joins) {
|
|
6723
|
+
parts.push(`${join2.type} JOIN ${this.escapeIdentifier(join2.table)}`);
|
|
6724
|
+
if (join2.alias) {
|
|
6725
|
+
parts.push(`AS ${this.escapeIdentifier(join2.alias)}`);
|
|
4195
6726
|
}
|
|
4196
|
-
parts.push(`ON ${
|
|
4197
|
-
bindings.push(...
|
|
6727
|
+
parts.push(`ON ${join2.condition}`);
|
|
6728
|
+
bindings.push(...join2.bindings);
|
|
4198
6729
|
}
|
|
4199
6730
|
if (components.where.length > 0) {
|
|
4200
6731
|
parts.push("WHERE");
|
|
@@ -4341,7 +6872,7 @@ var SQLDialect = class {
|
|
|
4341
6872
|
};
|
|
4342
6873
|
|
|
4343
6874
|
// src/dialect/mysql-dialect.ts
|
|
4344
|
-
var
|
|
6875
|
+
var MySQLDialect2 = class extends SQLDialect {
|
|
4345
6876
|
name = "mysql";
|
|
4346
6877
|
config = {
|
|
4347
6878
|
identifierQuote: "`",
|
|
@@ -4453,7 +6984,7 @@ var MySQLDialect = class extends SQLDialect {
|
|
|
4453
6984
|
};
|
|
4454
6985
|
|
|
4455
6986
|
// src/dialect/postgresql-dialect.ts
|
|
4456
|
-
var
|
|
6987
|
+
var PostgreSQLDialect2 = class extends SQLDialect {
|
|
4457
6988
|
name = "postgresql";
|
|
4458
6989
|
config = {
|
|
4459
6990
|
identifierQuote: '"',
|
|
@@ -4607,10 +7138,10 @@ var DialectFactory = class {
|
|
|
4607
7138
|
const normalizedType = this.normalizeType(type);
|
|
4608
7139
|
switch (normalizedType) {
|
|
4609
7140
|
case "mysql": {
|
|
4610
|
-
return new
|
|
7141
|
+
return new MySQLDialect2();
|
|
4611
7142
|
}
|
|
4612
7143
|
case "postgresql": {
|
|
4613
|
-
return new
|
|
7144
|
+
return new PostgreSQLDialect2();
|
|
4614
7145
|
}
|
|
4615
7146
|
default: {
|
|
4616
7147
|
throw new Error(`Unsupported database type: ${type}`);
|
|
@@ -6316,7 +8847,7 @@ function calculateDelay(attempt, baseDelay, maxDelay, multiplier) {
|
|
|
6316
8847
|
return Math.floor(cappedDelay + jitter);
|
|
6317
8848
|
}
|
|
6318
8849
|
function sleep2(ms) {
|
|
6319
|
-
return new Promise((
|
|
8850
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6320
8851
|
}
|
|
6321
8852
|
function createRetryMiddleware(options = {}) {
|
|
6322
8853
|
const {
|
|
@@ -7164,12 +9695,12 @@ var DBBridge2 = class _DBBridge {
|
|
|
7164
9695
|
const dbType = this.config.type;
|
|
7165
9696
|
switch (dbType) {
|
|
7166
9697
|
case "mysql": {
|
|
7167
|
-
this._dialect = new
|
|
9698
|
+
this._dialect = new MySQLDialect2();
|
|
7168
9699
|
break;
|
|
7169
9700
|
}
|
|
7170
9701
|
case "postgresql":
|
|
7171
9702
|
case "postgres": {
|
|
7172
|
-
this._dialect = new
|
|
9703
|
+
this._dialect = new PostgreSQLDialect2();
|
|
7173
9704
|
break;
|
|
7174
9705
|
}
|
|
7175
9706
|
case "redis": {
|
|
@@ -7793,6 +10324,6 @@ var DBBridge3 = class _DBBridge {
|
|
|
7793
10324
|
}
|
|
7794
10325
|
};
|
|
7795
10326
|
|
|
7796
|
-
export { ANALYSIS_DEFAULTS, BaseAdapter, BaseQueryBuilder, BaseTransaction, CACHE_DEFAULTS, CONNECTION_DEFAULTS, CRYPTO_DEFAULTS, CacheAPI, CacheError, CacheKeyGenerator, CacheManager, CacheableQuery, CachedAdapter, CachedDBBridge, DBBridge as Client, ConnectionError, CryptoAlgorithms, CryptoProvider, DBBridge2 as DBBridge, DBBridgeError, DBBridge3 as DBBridgeFactory, DEFAULT_POOL_CONFIG, DEFAULT_TIMEOUTS, DURATION_BUCKETS, DatabaseError, DefaultCacheStrategy, DeleteBuilder, DialectFactory, HEALTH_DEFAULTS, HealthChecker, ISOLATION_LEVELS, InsertBuilder, IsolationLevel, LOGGING_DEFAULTS, MetricsCollector, MiddlewareChain, MigrationRunner, ModularCacheManager, ModularPerformanceMonitor, MySQLDialect, NotImplementedError, POOL_DEFAULTS, POOL_DEFAULTS_LEGACY, PerformanceMonitor, PoolExhaustedError, PostgreSQLDialect, QUERY_DEFAULTS, QueryContext, QueryError, QueryTimeoutError, RETRY_DEFAULTS, SIZE_UNITS, SQLDialect, SelectBuilder, SmartCacheStrategy, TIME_UNITS, TimeoutError, TransactionError, UpdateBuilder, ValidationError, WhereBuilder, avg, cacheKey, chunk, composeMiddleware, compress, count, createAdapter, createCacheInvalidationMiddleware, createCacheKeyPattern, createCacheMiddleware, createCachedAdapter, createCircuitBreakerMiddleware, createDeadlineMiddleware, createLoggingMiddleware, createMetricsMiddleware, createModularQueryBuilder, createQueryState, createRetryMiddleware, createTimeoutMiddleware, crypto, cursorPaginate, decompress, encryptRow, exists, generateCacheKey, generateUUID, getCompressionRatio, isCompressed, isDeleteResult, isInsertResult, isSelectResult, isUpdateResult, max, min, paginate, parseCacheKey, processDataForEncryption, processResultsForDecryption, registerAdapterFactory, retry, sanitizeCacheKey, shouldCompress, sql, sum, validateColumnName, validateConnectionConfig, validateSQL, validateTableName, whereBetweenDates, whereDate, whereDay, whereLastDays, whereMonth, whereToday, whereYear, whereYesterday, withTimeout };
|
|
10327
|
+
export { ANALYSIS_DEFAULTS, AlterTableBuilder, BaseAdapter, BaseQueryBuilder, BaseTransaction, CACHE_DEFAULTS, CONNECTION_DEFAULTS, CRYPTO_DEFAULTS, CacheAPI, CacheError, CacheKeyGenerator, CacheManager, CacheableQuery, CachedAdapter, CachedDBBridge, DBBridge as Client, ColumnBuilder, ConnectionError, CryptoAlgorithms, CryptoProvider, DBBridge2 as DBBridge, DBBridgeError, DBBridge3 as DBBridgeFactory, DEFAULT_POOL_CONFIG, DEFAULT_TIMEOUTS, DURATION_BUCKETS, DatabaseError, DefaultCacheStrategy, DeleteBuilder, DialectFactory, ExpandContractHelper, FileMigrationRunner, ForeignKeyBuilder, ForeignKeyChain, HEALTH_DEFAULTS, HealthChecker, ISOLATION_LEVELS, InsertBuilder, IsolationLevel, LOGGING_DEFAULTS, MetricsCollector, MiddlewareChain, MigrationLoader, MigrationLock, MigrationRunner, ModularCacheManager, ModularPerformanceMonitor, MySQLDialect2 as MySQLDialect, NotImplementedError, POOL_DEFAULTS, POOL_DEFAULTS_LEGACY, PerformanceMonitor, PoolExhaustedError, PostgreSQLDialect2 as PostgreSQLDialect, QUERY_DEFAULTS, QueryContext, QueryError, QueryTimeoutError, RETRY_DEFAULTS, SIZE_UNITS, SQLDialect, SchemaBuilder, MySQLDialect as SchemaMySQLDialect, PostgreSQLDialect as SchemaPostgreSQLDialect, SeederLoader, SeederRunner, SelectBuilder, SmartCacheStrategy, TIME_UNITS, TableBuilder, TimeoutError, TransactionError, UpdateBuilder, ValidationError, WhereBuilder, avg, cacheKey, chunk, composeMiddleware, compress, count, createAdapter, createCacheInvalidationMiddleware, createCacheKeyPattern, createCacheMiddleware, createCachedAdapter, createCircuitBreakerMiddleware, createDeadlineMiddleware, createExpandContractHelper, createLoggingMiddleware, createMetricsMiddleware, createModularQueryBuilder, createQueryState, createRetryMiddleware, createTimeoutMiddleware, crypto, cursorPaginate, decompress, encryptRow, exists, generateCacheKey, generateUUID, getCompressionRatio, isCompressed, isDeleteResult, isInsertResult, isSelectResult, isUpdateResult, max, min, paginate, parseCacheKey, processDataForEncryption, processResultsForDecryption, registerAdapterFactory, retry, sanitizeCacheKey, shouldCompress, sql, sum, validateColumnName, validateConnectionConfig, validateSQL, validateTableName, whereBetweenDates, whereDate, whereDay, whereLastDays, whereMonth, whereToday, whereYear, whereYesterday, withTimeout };
|
|
7797
10328
|
//# sourceMappingURL=index.js.map
|
|
7798
10329
|
//# sourceMappingURL=index.js.map
|