prisma-migrations
Version:
A Node.js library to manage Prisma ORM migrations like other ORMs
829 lines (818 loc) • 27.4 kB
JavaScript
// src/migrations/index.ts
import { readdir, readFile as readFile2, access } from "fs/promises";
import { join } from "path";
// src/logger/index.ts
var LOG_LEVELS = {
silent: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5
};
class Logger {
currentLevel;
constructor(level = "silent") {
this.currentLevel = level;
}
set level(level) {
this.currentLevel = level;
}
shouldLog(level) {
return LOG_LEVELS[level] <= LOG_LEVELS[this.currentLevel];
}
log(level, message) {
if (!this.shouldLog(level))
return;
const timestamp = new Date().toLocaleTimeString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
if (message instanceof Error) {
console.error(prefix, message.message);
if (this.currentLevel === "debug" || this.currentLevel === "trace") {
console.error(message.stack);
}
} else {
console.log(prefix, message);
}
}
trace(message) {
this.log("trace", message);
}
debug(message) {
this.log("debug", message);
}
info(message) {
this.log("info", message);
}
warn(message) {
this.log("warn", message);
}
error(message) {
this.log("error", message);
}
}
var logLevel = process.env.PRISMA_MIGRATIONS_LOG_LEVEL || "silent";
var logger = new Logger(logLevel);
// src/utils/index.ts
import { createHash } from "crypto";
import { readFile } from "fs/promises";
// src/utils/colors.ts
var reset = "\x1B[0m";
var bold = "\x1B[1m";
var red = "\x1B[31m";
var green = "\x1B[32m";
var yellow = "\x1B[33m";
var blue = "\x1B[34m";
var cyan = "\x1B[36m";
var gray = "\x1B[90m";
var colors = {
reset: (str) => `${reset}${str}`,
bold: (str) => `${bold}${str}${reset}`,
red: (str) => `${red}${str}${reset}`,
green: (str) => `${green}${str}${reset}`,
yellow: (str) => `${yellow}${str}${reset}`,
blue: (str) => `${blue}${str}${reset}`,
cyan: (str) => `${cyan}${str}${reset}`,
gray: (str) => `${gray}${str}${reset}`
};
// src/utils/index.ts
async function generateChecksum(filePath) {
const content = await readFile(filePath, "utf-8");
return createHash("sha256").update(content).digest("hex");
}
// src/errors/index.ts
class MigrationError extends Error {
suggestions;
helpCommand;
constructor(message, suggestions = [], helpCommand) {
super(message);
this.suggestions = suggestions;
this.helpCommand = helpCommand;
this.name = "MigrationError";
}
format() {
let output = `
${colors.bold(colors.red(this.message))}
`;
if (this.suggestions.length > 0) {
output += `
${colors.cyan("Suggestions:")}
`;
this.suggestions.forEach((suggestion) => {
output += ` ${colors.gray("•")} ${suggestion}
`;
});
}
if (this.helpCommand) {
output += `
${colors.yellow("Need help?")} Run: ${colors.cyan(this.helpCommand)}
`;
}
return output;
}
}
function createMigrationNotFoundError(migrationId) {
return new MigrationError(`Migration file not found for ${migrationId}`, [
"Run 'prisma-migrations status' to see all migrations",
"Check if the migration directory exists in prisma/migrations",
"The migration file may have been deleted or moved"
]);
}
function createInvalidMigrationError(migrationId, reason) {
return new MigrationError(`Migration ${migrationId} is invalid: ${reason}`, [
"Check that migration.sql exists in the migration directory",
"Ensure the file contains both '-- Migration: Up' and '-- Migration: Down' markers",
"Verify the SQL syntax is correct"
]);
}
function createChecksumMismatchError(migrationId) {
return new MigrationError(`Migration ${migrationId} has been modified after being applied`, [
"The migration file content has changed since it was applied",
"Never modify migrations that have been applied to production",
"Create a new migration instead to make schema changes",
"If this is a development environment, you can run 'prisma-migrations fresh --force'"
]);
}
function createTransactionFailedError(migrationId, error) {
return new MigrationError(`Transaction failed for migration ${migrationId}: ${error.message}`, [
"The migration was rolled back automatically",
"No changes were applied to the database",
"Check the SQL syntax and fix the migration",
"Review the error message above for details"
]);
}
// src/migrations/locking.ts
class MigrationLockError extends Error {
isTimeout;
constructor(message, isTimeout = false) {
super(message);
this.name = "MigrationLockError";
this.isTimeout = isTimeout;
}
}
class MigrationLock {
prisma;
lockId;
lockAcquired = false;
databaseType = "unknown";
constructor(prisma) {
this.prisma = prisma;
this.lockId = 1836213295;
}
async detectDatabaseType() {
if (this.databaseType !== "unknown") {
return;
}
try {
await this.prisma.$queryRaw`
SELECT version() as version
`;
this.databaseType = "postgresql";
logger.debug("Detected PostgreSQL database");
return;
} catch {}
try {
await this.prisma.$queryRaw`
SELECT VERSION() as version
`;
this.databaseType = "mysql";
logger.debug("Detected MySQL database");
return;
} catch {}
this.databaseType = "sqlite";
logger.debug("Detected SQLite database");
}
async acquire(timeoutMs = 30000) {
await this.detectDatabaseType();
const startTime = Date.now();
logger.debug(`Attempting to acquire migration lock (database: ${this.databaseType})...`);
while (Date.now() - startTime < timeoutMs) {
const acquired = await this.tryAcquire();
if (acquired) {
this.lockAcquired = true;
logger.debug("Migration lock acquired successfully");
return;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
logger.debug("Lock not available, retrying...");
}
throw new MigrationLockError(`Failed to acquire migration lock after ${timeoutMs}ms. Another migration may be in progress.`, true);
}
async tryAcquire() {
switch (this.databaseType) {
case "postgresql":
return await this.acquirePostgresLock();
case "mysql":
return await this.acquireMySQLLock();
case "sqlite":
return await this.acquireSQLiteLock();
default:
logger.warn("Unknown database type, skipping lock acquisition (unsafe!)");
return true;
}
}
async acquirePostgresLock() {
try {
const result = await this.prisma.$queryRaw`
SELECT pg_try_advisory_lock(${this.lockId}) as locked
`;
return result[0]?.locked ?? false;
} catch (error) {
logger.error(`Failed to acquire PostgreSQL advisory lock: ${error}`);
return false;
}
}
async acquireMySQLLock() {
try {
const result = await this.prisma.$queryRaw`
SELECT GET_LOCK('prisma_migrations_lock', 0) as locked
`;
return result[0]?.locked === 1;
} catch (error) {
logger.error(`Failed to acquire MySQL lock: ${error}`);
return false;
}
}
async acquireSQLiteLock() {
try {
await this.prisma.$executeRaw`
CREATE TABLE IF NOT EXISTS _prisma_migrations_lock (
id INTEGER PRIMARY KEY CHECK (id = 1),
locked_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`;
await this.prisma.$executeRaw`
INSERT INTO _prisma_migrations_lock (id) VALUES (1)
`;
return true;
} catch (error) {
return false;
}
}
async release() {
if (!this.lockAcquired) {
logger.warn("Attempted to release lock that was not acquired");
return;
}
logger.debug("Releasing migration lock...");
switch (this.databaseType) {
case "postgresql":
await this.releasePostgresLock();
break;
case "mysql":
await this.releaseMySQLLock();
break;
case "sqlite":
await this.releaseSQLiteLock();
break;
default:
logger.warn("Unknown database type, skipping lock release");
}
this.lockAcquired = false;
logger.debug("Migration lock released");
}
async releasePostgresLock() {
try {
await this.prisma.$queryRaw`
SELECT pg_advisory_unlock(${this.lockId})
`;
} catch (error) {
logger.error(`Failed to release PostgreSQL advisory lock: ${error}`);
}
}
async releaseMySQLLock() {
try {
await this.prisma.$queryRaw`
SELECT RELEASE_LOCK('prisma_migrations_lock')
`;
} catch (error) {
logger.error(`Failed to release MySQL lock: ${error}`);
}
}
async releaseSQLiteLock() {
try {
await this.prisma.$executeRaw`
DELETE FROM _prisma_migrations_lock WHERE id = 1
`;
} catch (error) {
logger.error(`Failed to release SQLite lock: ${error}`);
}
}
async tryLock(fn) {
await this.detectDatabaseType();
const acquired = await this.tryAcquire();
if (!acquired) {
logger.debug("Lock not available, skipping");
return { acquired: false };
}
this.lockAcquired = true;
logger.debug("Lock acquired");
try {
const result = await fn();
return { acquired: true, result };
} finally {
await this.release();
}
}
async withLock(fn, timeoutMs) {
await this.acquire(timeoutMs);
try {
return await fn();
} finally {
await this.release();
}
}
async isLocked() {
await this.detectDatabaseType();
switch (this.databaseType) {
case "postgresql":
return await this.isPostgresLocked();
case "mysql":
return await this.isMySQLLocked();
case "sqlite":
return await this.isSQLiteLocked();
default:
return false;
}
}
async isPostgresLocked() {
try {
const result = await this.prisma.$queryRaw`
SELECT EXISTS(
SELECT 1 FROM pg_locks
WHERE locktype = 'advisory'
AND objid = ${this.lockId}
) as locked
`;
return result[0]?.locked ?? false;
} catch (error) {
logger.error(`Failed to check PostgreSQL lock status: ${error}`);
return false;
}
}
async isMySQLLocked() {
try {
const result = await this.prisma.$queryRaw`
SELECT IS_USED_LOCK('prisma_migrations_lock') as locked
`;
const lockStatus = result[0]?.locked;
return lockStatus !== null && lockStatus !== 0;
} catch (error) {
logger.error(`Failed to check MySQL lock status: ${error}`);
return false;
}
}
async isSQLiteLocked() {
try {
const result = await this.prisma.$queryRaw`
SELECT COUNT(*) as count FROM _prisma_migrations_lock WHERE id = 1
`;
return (result[0]?.count ?? 0) > 0;
} catch {
return false;
}
}
async forceRelease() {
await this.detectDatabaseType();
logger.warn("Force releasing migration lock...");
switch (this.databaseType) {
case "postgresql":
await this.forceReleasePostgres();
break;
case "mysql":
await this.forceReleaseMySQL();
break;
case "sqlite":
await this.forceReleaseSQLite();
break;
default:
logger.warn("Unknown database type, cannot force release lock");
}
logger.info("Migration lock force released");
}
async forceReleasePostgres() {
try {
await this.prisma.$queryRaw`
SELECT pg_advisory_unlock_all()
`;
} catch (error) {
logger.error(`Failed to force release PostgreSQL lock: ${error}`);
throw error;
}
}
async forceReleaseMySQL() {
try {
await this.prisma.$queryRaw`
SELECT RELEASE_LOCK('prisma_migrations_lock')
`;
} catch (error) {
logger.error(`Failed to force release MySQL lock: ${error}`);
throw error;
}
}
async forceReleaseSQLite() {
try {
await this.prisma.$executeRaw`
DELETE FROM _prisma_migrations_lock WHERE id = 1
`;
} catch (error) {
logger.error(`Failed to force release SQLite lock: ${error}`);
throw error;
}
}
}
// src/migrations/index.ts
function parseSqlMigration(sql) {
const upMarker = "-- Migration: Up";
const downMarker = "-- Migration: Down";
const upIndex = sql.indexOf(upMarker);
const downIndex = sql.indexOf(downMarker);
if (upIndex === -1 || downIndex === -1) {
throw new Error(`Invalid migration format. Migration file must contain both "${upMarker}" and "${downMarker}" markers.`);
}
if (upIndex >= downIndex) {
throw new Error(`Invalid migration format. "${upMarker}" must come before "${downMarker}".`);
}
const upSql = sql.substring(upIndex + upMarker.length, downIndex).trim();
const downSql = sql.substring(downIndex + downMarker.length).trim();
return { up: upSql, down: downSql };
}
function splitSqlStatements(sql) {
return sql.split(";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0);
}
async function loadSqlMigration(migrationPath) {
const sql = await readFile2(migrationPath, "utf-8");
const parsed = parseSqlMigration(sql);
return {
up: async (prisma) => {
if (!parsed.up) {
throw new Error(`No up migration found in ${migrationPath}`);
}
const statements = splitSqlStatements(parsed.up);
for (const statement of statements) {
await prisma.$executeRawUnsafe(statement);
}
},
down: async (prisma) => {
if (!parsed.down) {
logger.warn(`No down migration available for: ${migrationPath}`);
return;
}
const statements = splitSqlStatements(parsed.down);
for (const statement of statements) {
await prisma.$executeRawUnsafe(statement);
}
}
};
}
class Migrations {
prisma;
migrationsDir;
lock;
disableLocking;
skipChecksumValidation;
lockTimeout;
constructor(prisma, options) {
this.prisma = prisma;
this.migrationsDir = options?.migrationsDir || "./prisma/migrations";
this.disableLocking = options?.disableLocking ?? false;
this.skipChecksumValidation = options?.skipChecksumValidation ?? false;
this.lockTimeout = options?.lockTimeout ?? 30000;
this.lock = this.disableLocking ? null : new MigrationLock(prisma);
}
async validateMigrationFile(migration) {
try {
const mod = await loadSqlMigration(migration.path);
return typeof mod.up === "function" && typeof mod.down === "function";
} catch {
return false;
}
}
async validateAppliedMigrationChecksums() {
const shouldSkipValidation = this.skipChecksumValidation;
if (shouldSkipValidation) {
logger.debug("Skipping checksum validation (disabled)");
return;
}
logger.debug("Validating checksums for applied migrations...");
const appliedMigrations = await this.prisma.$queryRaw`
SELECT id, checksum, migration_name
FROM _prisma_migrations
WHERE finished_at IS NOT NULL
ORDER BY finished_at ASC
`;
const allMigrations = await this.getAllMigrations();
await appliedMigrations.reduce(async (prev, applied) => {
await prev;
const migrationFile = allMigrations.find((m) => m.id === applied.id);
if (!migrationFile) {
logger.warn(`Applied migration ${applied.id}_${applied.migration_name} not found in migrations directory`);
return;
}
const currentChecksum = await generateChecksum(migrationFile.path);
if (currentChecksum !== applied.checksum) {
throw createChecksumMismatchError(`${applied.id}_${applied.migration_name}`);
}
}, Promise.resolve());
logger.debug(`Validated ${appliedMigrations.length} applied migration checksums`);
}
async dryRun(steps) {
const pending = await this.pending();
return steps ? pending.slice(0, steps) : pending;
}
getMigrationsDir() {
logger.debug(`Using migrations dir: ${this.migrationsDir}`);
return this.migrationsDir;
}
async up(steps) {
const shouldUseLocking = !this.disableLocking;
if (shouldUseLocking) {
return await this.lock.withLock(() => this.runUpMigrations(steps), this.lockTimeout);
}
return await this.runUpMigrations(steps);
}
async runUpMigrations(steps) {
await this.validateAppliedMigrationChecksums();
const applied = await this.getApplied();
logger.debug(`Found ${applied.length} applied migrations`);
const all = await this.getAllMigrations();
logger.debug(`Found ${all.length} total migrations`);
const pending = all.filter((m) => !applied.includes(m.id));
logger.debug(`Found ${pending.length} pending migrations`);
let toRun = pending;
if (steps)
toRun = pending.slice(0, steps);
logger.debug(`Will run ${toRun.length} migrations`);
await toRun.reduce(async (prev, migration) => {
await prev;
await this.runMigrationInTransaction(migration, "up");
}, Promise.resolve());
return toRun.length;
}
async runMigrationInTransaction(migration, direction) {
const migrationName = `${migration.id}_${migration.name}`;
const isUpMigration = direction === "up";
logger.info(`Running ${migrationName}...`);
try {
const isValid = await this.validateMigrationFile(migration);
const isInvalidMigration = !isValid;
if (isInvalidMigration) {
throw createInvalidMigrationError(migrationName, "missing up or down function");
}
const mod = await loadSqlMigration(migration.path);
const migrationFn = isUpMigration ? mod.up : mod.down;
const isMissingMigrationFunction = typeof migrationFn !== "function";
if (isMissingMigrationFunction) {
throw new Error(`Migration ${migrationName} does not export a '${direction}' function`);
}
await this.prisma.$executeRaw`BEGIN`;
try {
await migrationFn(this.prisma);
if (isUpMigration) {
const checksum = await generateChecksum(migration.path);
await this.prisma.$executeRaw`INSERT INTO _prisma_migrations (id, checksum, finished_at, migration_name, logs, started_at, applied_steps_count) VALUES (${migration.id}, ${checksum}, NOW(), ${migration.name}, NULL, NOW(), 1)`;
} else {
await this.prisma.$executeRaw`DELETE FROM _prisma_migrations WHERE id = ${migration.id}`;
}
await this.prisma.$executeRaw`COMMIT`;
const action = isUpMigration ? "Applied" : "Rolled back";
logger.info(`✓ ${action} ${migrationName}`);
} catch (error) {
await this.prisma.$executeRaw`ROLLBACK`;
const migrationError = error instanceof Error ? error : new Error(String(error));
throw createTransactionFailedError(migrationName, migrationError);
}
} catch (error) {
const action = isUpMigration ? "apply" : "rollback";
logger.error(`Failed to ${action} migration ${migrationName}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(errorMessage);
throw error;
}
}
async down(steps = 1) {
const shouldUseLocking = !this.disableLocking;
if (shouldUseLocking) {
return await this.lock.withLock(() => this.runDownMigrations(steps), this.lockTimeout);
}
return await this.runDownMigrations(steps);
}
async runDownMigrations(steps) {
const applied = await this.getApplied();
logger.debug(`Found ${applied.length} applied migrations`);
const toRollback = applied.slice(-steps);
logger.debug(`Will rollback ${toRollback.length} migrations`);
const reversed = toRollback.toReversed();
await reversed.reduce(async (prev, id) => {
await prev;
const migration = await this.findMigration(id);
const migrationNotFound = !migration;
if (migrationNotFound) {
throw createMigrationNotFoundError(id);
}
await this.runMigrationInTransaction(migration, "down");
}, Promise.resolve());
return toRollback.length;
}
async status() {
const applied = await this.getApplied();
const all = await this.getAllMigrations();
logger.info(`
Migration Status:
`);
for (const migration of all) {
const status = applied.includes(migration.id) ? "[x]" : "[ ]";
logger.info(`${status} ${migration.id}_${migration.name}`);
}
}
async pending() {
const applied = await this.getApplied();
const all = await this.getAllMigrations();
return all.filter((m) => !applied.includes(m.id));
}
async applied() {
const appliedIds = await this.getApplied();
const all = await this.getAllMigrations();
return all.filter((m) => appliedIds.includes(m.id));
}
async latest() {
const applied = await this.applied();
return applied.length > 0 ? applied[applied.length - 1] : null;
}
async reset() {
const shouldUseLocking = !this.disableLocking;
if (shouldUseLocking) {
return await this.lock.withLock(() => this.runResetMigrations(), this.lockTimeout);
}
return await this.runResetMigrations();
}
async runResetMigrations() {
const applied = await this.applied();
logger.debug(`Found ${applied.length} applied migrations to reset`);
const count = applied.length;
const reversed = applied.toReversed();
await reversed.reduce(async (prev, migration) => {
await prev;
await this.runMigrationInTransaction(migration, "down");
}, Promise.resolve());
return count;
}
async fresh() {
await this.reset();
return await this.up();
}
async refresh() {
const down = await this.reset();
const up = await this.up();
return { down, up };
}
async upTo(migrationId) {
const shouldUseLocking = !this.disableLocking;
if (shouldUseLocking) {
return await this.lock.withLock(() => this.runUpToMigration(migrationId), this.lockTimeout);
}
return await this.runUpToMigration(migrationId);
}
async runUpToMigration(migrationId) {
await this.validateAppliedMigrationChecksums();
const applied = await this.getApplied();
logger.debug(`Found ${applied.length} applied migrations`);
const all = await this.getAllMigrations();
logger.debug(`Found ${all.length} total migrations`);
const pending = all.filter((m) => !applied.includes(m.id));
logger.debug(`Found ${pending.length} pending migrations`);
const targetIndex = pending.findIndex((m) => m.id === migrationId);
const migrationNotFound = targetIndex === -1;
if (migrationNotFound) {
throw new Error(`Migration ${migrationId} not found in pending migrations`);
}
const toRun = pending.slice(0, targetIndex + 1);
logger.debug(`Will run ${toRun.length} migrations up to ${migrationId}`);
await toRun.reduce(async (prev, migration) => {
await prev;
await this.runMigrationInTransaction(migration, "up");
}, Promise.resolve());
return toRun.length;
}
async downTo(migrationId) {
const shouldUseLocking = !this.disableLocking;
if (shouldUseLocking) {
return await this.lock.withLock(() => this.runDownToMigration(migrationId), this.lockTimeout);
}
return await this.runDownToMigration(migrationId);
}
async runDownToMigration(migrationId) {
const appliedIds = await this.getApplied();
logger.debug(`Found ${appliedIds.length} applied migrations`);
const targetIndex = appliedIds.indexOf(migrationId);
const migrationNotFound = targetIndex === -1;
if (migrationNotFound) {
throw new Error(`Migration ${migrationId} not found in applied migrations`);
}
const toRollback = appliedIds.slice(targetIndex + 1);
logger.debug(`Will rollback ${toRollback.length} migrations down to ${migrationId}`);
const reversed = toRollback.toReversed();
await reversed.reduce(async (prev, id) => {
await prev;
const migration = await this.findMigration(id);
const migrationNotFound2 = !migration;
if (migrationNotFound2) {
throw createMigrationNotFoundError(id);
}
await this.runMigrationInTransaction(migration, "down");
}, Promise.resolve());
return toRollback.length;
}
async upIfNotLocked(steps) {
const lockingDisabled = this.disableLocking;
if (lockingDisabled) {
const count = await this.runUpMigrations(steps);
return { ran: true, count };
}
const result = await this.lock.tryLock(() => this.runUpMigrations(steps));
if (!result.acquired) {
logger.info("Another instance is running migrations, skipping");
return {
ran: false,
count: 0,
reason: "Another instance is running migrations"
};
}
return { ran: true, count: result.result ?? 0 };
}
async checkLockStatus() {
const hasLock = !this.lock;
if (hasLock) {
logger.warn("Locking is disabled");
return false;
}
return await this.lock.isLocked();
}
async releaseLock() {
const hasNoLock = !this.lock;
if (hasNoLock) {
logger.warn("Locking is disabled, no lock to release");
return;
}
await this.lock.forceRelease();
}
async getApplied() {
const result = await this.prisma.$queryRaw`
SELECT id FROM _prisma_migrations WHERE finished_at IS NOT NULL ORDER BY finished_at ASC
`;
return result.map((r) => r.id);
}
async detectMigrationFile(migrationsDir, dirName) {
const sqlPath = join(migrationsDir, dirName, "migration.sql");
const hasSqlFile = await access(sqlPath).then(() => true).catch(() => false);
if (!hasSqlFile) {
throw new Error(`No migration.sql file found in ${dirName}`);
}
return sqlPath;
}
isValidMigrationDir(name) {
return /^(\d+)_(.+)$/.test(name);
}
parseMigrationName(name) {
const match = name.match(/^(\d+)_(.+)$/);
if (!match)
throw new Error(`Invalid migration name: ${name}`);
const [, id, migrationName] = match;
return { id, name: migrationName };
}
filterValidDirectories(entries) {
return entries.filter((entry) => {
const isDirectory = entry.isDirectory();
const isValid = isDirectory && this.isValidMigrationDir(entry.name);
logger.debug(`Entry ${entry.name}: directory=${isDirectory}, valid=${isValid}`);
return isValid;
});
}
async mapToMigrationFiles(migrationsDir, validDirs) {
return await Promise.all(validDirs.map(async (entry) => {
const { id, name } = this.parseMigrationName(entry.name);
const path = await this.detectMigrationFile(migrationsDir, entry.name);
logger.debug(`Mapped migration: ${id}_${name} at ${path}`);
return { id, name, path };
}));
}
async getAllMigrations() {
const migrationsDir = this.getMigrationsDir();
logger.debug(`Reading migrations from: ${migrationsDir}`);
const entries = await readdir(migrationsDir, { withFileTypes: true });
logger.debug(`Found ${entries.length} entries in migrations directory`);
const validDirs = this.filterValidDirectories(entries);
const migrations = await this.mapToMigrationFiles(migrationsDir, validDirs);
const sorted = migrations.sort((a, b) => a.id.localeCompare(b.id));
logger.debug(`Loaded ${sorted.length} valid migrations`);
return sorted;
}
async findMigration(id) {
const all = await this.getAllMigrations();
return all.find((m) => m.id === id) || null;
}
}
export {
Migrations
};
//# debugId=33696B5256DFB18264756E2164756E21