UNPKG

prisma-migrations

Version:

A Node.js library to manage Prisma ORM migrations like other ORMs

1,524 lines (1,504 loc) 46.6 kB
#!/usr/bin/env node var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/cli.ts import { Command } from "commander"; // src/config.ts import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { pathToFileURL } from "url"; var ConfigManager = class { constructor(configPath) { this.configPromise = this.loadConfig(configPath); this.config = this.getDefaultConfig(); } getDefaultConfig() { return { migrationsDir: "./migrations", schemaPath: "./prisma/schema.prisma", tableName: "_prisma_migrations", createTable: true, migrationFormat: "ts", extension: ".ts" }; } async loadConfig(configPath) { const defaultConfig = { migrationsDir: "./migrations", schemaPath: "./prisma/schema.prisma", tableName: "_prisma_migrations", createTable: true, migrationFormat: "ts", extension: ".ts" }; const packageJsonPath = join(process.cwd(), "package.json"); if (existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); if (packageJson.prismaMigrations) { return { ...defaultConfig, ...packageJson.prismaMigrations }; } } catch { } } const configFile = configPath || this.findConfigFile(); if (configFile && existsSync(configFile)) { try { if (configFile.endsWith(".ts")) { console.warn( "TypeScript config files require tsx. Please use .mjs config files or ensure tsx is available." ); } else if (configFile.endsWith(".mjs") || configFile.endsWith(".js")) { const configModule = await import(pathToFileURL(configFile).href); const config = configModule.default || configModule; return { ...defaultConfig, ...config }; } else { const configContent = readFileSync(configFile, "utf-8"); const config = JSON.parse(configContent); return { ...defaultConfig, ...config }; } } catch (error) { console.warn(`Failed to load config file ${configFile}:`, error); } } const prismaDir = join(process.cwd(), "prisma"); if (existsSync(prismaDir)) { const schemaPath = join(prismaDir, "schema.prisma"); if (existsSync(schemaPath)) { defaultConfig.schemaPath = schemaPath; } } return defaultConfig; } findConfigFile() { const possibleFiles = [ join(process.cwd(), "prisma-migrations.config.js"), join(process.cwd(), "prisma-migrations.config.ts"), join(process.cwd(), "prisma-migrations.config.mjs"), join(process.cwd(), "prisma-migrations.config.json") ]; for (const file of possibleFiles) { if (existsSync(file)) { return file; } } return null; } getConfig() { return this.config; } async getConfigAsync() { this.config = await this.configPromise; return this.config; } updateConfig(updates) { this.config = { ...this.config, ...updates }; } getDatabaseUrl() { if (this.config.databaseUrl) { return this.config.databaseUrl; } const envUrl = process.env.DATABASE_URL; if (envUrl) { return envUrl; } if (existsSync(this.config.schemaPath)) { try { const schema = readFileSync(this.config.schemaPath, "utf-8"); const urlMatch = schema.match(/url\s*=\s*env\("([^"]+)"\)/); if (urlMatch) { const envVar = urlMatch[1]; const envValue = process.env[envVar]; if (envValue) { return envValue; } } } catch { } } throw new Error( "Database URL not found. Please set DATABASE_URL environment variable or configure it in your config file." ); } }; // src/file-manager.ts import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, unlinkSync } from "fs"; import { join as join2 } from "path"; var FileManager = class { constructor(migrationsDir, config) { this.migrationsDir = migrationsDir; this.config = config; this.ensureDirectoryExists(); } ensureDirectoryExists() { if (!existsSync2(this.migrationsDir)) { mkdirSync(this.migrationsDir, { recursive: true }); } } createMigrationFile(name, template) { const timestamp = this.generateTimestamp(); const format = this.config.migrationFormat || "ts"; const extension = this.config.extension || `.${format}`; const filename = `${timestamp}_${name}${extension}`; const filePath = join2(this.migrationsDir, filename); let content; if (format === "sql") { const defaultTemplate = { up: `-- Migration: ${name} -- Created at: ${(/* @__PURE__ */ new Date()).toISOString()} -- Add your migration SQL here `, down: `-- Rollback for: ${name} -- Created at: ${(/* @__PURE__ */ new Date()).toISOString()} -- Add your rollback SQL here ` }; content = this.formatSqlMigrationContent( template || defaultTemplate ); } else { content = this.formatJsMigrationContent( name, format, template ); } writeFileSync(filePath, content, "utf-8"); return { path: filePath, content, timestamp, name, type: format }; } readMigrationFiles() { const files = readdirSync(this.migrationsDir).filter( (file) => file.endsWith(".sql") || file.endsWith(".js") || file.endsWith(".ts") ).sort(); return files.map((file) => { const filePath = join2(this.migrationsDir, file); const content = readFileSync2(filePath, "utf-8"); const match = file.match(/^(\d+)_(.+)\.(sql|js|ts)$/); if (!match) { throw new Error(`Invalid migration file format: ${file}`); } const [, timestamp, name, type] = match; return { path: filePath, content, timestamp, name, type }; }); } getMigrationFile(timestamp) { const files = this.readMigrationFiles(); return files.find((file) => file.timestamp === timestamp) || null; } parseMigrationContent(migrationFile) { if (migrationFile.type === "sql") { return this.parseSqlMigrationContent(migrationFile.content); } else { return this.parseJsMigrationContent(migrationFile); } } parseSqlMigrationContent(content) { const upMatch = content.match(/-- UP\s*\n([\s\S]*?)(?=-- DOWN|$)/); const downMatch = content.match(/-- DOWN\s*\n([\s\S]*?)$/); return { up: upMatch ? upMatch[1].trim() : content.trim(), down: downMatch ? downMatch[1].trim() : "" }; } parseJsMigrationContent(_migrationFile) { return { up: "", down: "" }; } formatSqlMigrationContent(template) { return `-- UP ${template.up} -- DOWN ${template.down} `; } formatJsMigrationContent(name, format, _template) { return this.generatePrismaMigrationTemplate(name, format); } generatePrismaMigrationTemplate(name, format) { const isTypeScript = format === "ts"; if (isTypeScript) { return `import { PrismaClient } from '@prisma/client'; /** * Migration: ${name} * Created at: ${(/* @__PURE__ */ new Date()).toISOString()} */ export async function up(prisma: PrismaClient): Promise<void> { // Add your migration logic here // Example - Raw SQL: // await prisma.$executeRaw\` // CREATE TABLE users ( // id SERIAL PRIMARY KEY, // email VARCHAR(255) UNIQUE NOT NULL, // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, // updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP // ) // \`; // Example - Using Prisma operations: // await prisma.user.createMany({ // data: [ // { email: 'admin@example.com' }, // { email: 'user@example.com' } // ] // }); } export async function down(prisma: PrismaClient): Promise<void> { // Add your rollback logic here // Example: // await prisma.$executeRaw\`DROP TABLE IF EXISTS users\`; } `; } else { return `// @ts-check /** * Migration: ${name} * Created at: ${(/* @__PURE__ */ new Date()).toISOString()} */ /** * @param {import('@prisma/client').PrismaClient} prisma */ exports.up = async function(prisma) { // Add your migration logic here // Example - Raw SQL: // await prisma.$executeRaw\` // CREATE TABLE users ( // id SERIAL PRIMARY KEY, // email VARCHAR(255) UNIQUE NOT NULL, // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, // updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP // ) // \`; // Example - Using Prisma operations: // await prisma.user.createMany({ // data: [ // { email: 'admin@example.com' }, // { email: 'user@example.com' } // ] // }); }; /** * @param {import('@prisma/client').PrismaClient} prisma */ exports.down = async function(prisma) { // Add your rollback logic here // Example: // await prisma.$executeRaw\`DROP TABLE IF EXISTS users\`; }; `; } } generateTimestamp() { const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); return `${year}${month}${day}${hours}${minutes}${seconds}`; } getLatestMigration() { const files = this.readMigrationFiles(); return files.length > 0 ? files[files.length - 1] : null; } getMigrationByName(name) { const files = this.readMigrationFiles(); return files.find((file) => file.name === name) || null; } deleteMigrationFile(timestamp) { const file = this.getMigrationFile(timestamp); if (file && existsSync2(file.path)) { unlinkSync(file.path); return true; } return false; } }; // src/database-adapter.ts import { PrismaClient } from "@prisma/client"; import { resolve } from "path"; var DatabaseAdapter = class { constructor(databaseUrl, tableName = "_prisma_migrations") { this.isPrismaTable = null; this.prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } }); this.tableName = tableName; } async connect() { await this.prisma.$connect(); } async disconnect() { await this.prisma.$disconnect(); } async detectPrismaTable() { if (this.isPrismaTable !== null) { return this.isPrismaTable; } try { const columnExists = await this.prisma.$queryRawUnsafe(` SELECT COUNT(*) as count FROM information_schema.columns WHERE table_name = '${this.tableName}' AND column_name = 'migration_name' `); this.isPrismaTable = columnExists[0].count > 0; return this.isPrismaTable; } catch { this.isPrismaTable = false; return false; } } async ensureMigrationsTable() { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { return; } const createTableQuery = ` CREATE TABLE IF NOT EXISTS ${this.tableName} ( id VARCHAR(255) PRIMARY KEY, name VARCHAR(255) NOT NULL, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, checksum VARCHAR(255) ) `; await this.prisma.$executeRawUnsafe(createTableQuery); } async getAppliedMigrations() { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { const results = await this.prisma.$queryRawUnsafe(` SELECT id, migration_name, started_at as appliedAt FROM ${this.tableName} ORDER BY started_at ASC `); return results.map((row) => ({ id: row.id, name: row.migration_name, filename: `${row.id}_${row.migration_name}.sql`, timestamp: new Date(row.id.substring(0, 8)), applied: true, appliedAt: row.appliedAt })); } else { const results = await this.prisma.$queryRawUnsafe(` SELECT id, name, applied_at as appliedAt FROM ${this.tableName} ORDER BY applied_at ASC `); return results.map((row) => ({ id: row.id, name: row.name, filename: `${row.id}_${row.name}.sql`, timestamp: new Date(row.id.substring(0, 8)), applied: true, appliedAt: row.appliedAt })); } } async isMigrationApplied(migrationId) { const result = await this.prisma.$queryRawUnsafe( ` SELECT COUNT(*) as count FROM ${this.tableName} WHERE id = ? `, migrationId ); return result[0].count > 0; } async recordMigration(migrationId, name) { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { return; } else { await this.prisma.$executeRawUnsafe( ` INSERT INTO ${this.tableName} (id, name) VALUES (?, ?) `, migrationId, name ); } } async removeMigration(migrationId) { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { return; } else { await this.prisma.$executeRawUnsafe( ` DELETE FROM ${this.tableName} WHERE id = ? `, migrationId ); } } async executeMigration(sql) { const statements = sql.split(";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0); for (const statement of statements) { await this.prisma.$executeRawUnsafe(statement); } } async executeMigrationFile(migrationFile, direction) { if (migrationFile.type === "sql") { throw new Error("Use executeMigration() for SQL files"); } const migration = await this.loadMigrationModule(migrationFile.path); if (direction === "up") { await migration.up(this.prisma); } else { await migration.down(this.prisma); } } async loadMigrationModule(filePath) { try { const resolvedPath = resolve(filePath); delete __require.cache[resolvedPath]; if (filePath.endsWith(".ts")) { try { __require.resolve("tsx"); } catch { throw new Error( "tsx is required to run TypeScript migrations. Install it with: npm install tsx" ); } const module = await import(resolvedPath); if (!module.up || !module.down) { throw new Error( `TypeScript migration ${filePath} must export both 'up' and 'down' functions` ); } return { up: module.up, down: module.down }; } else { const module = await import(resolvedPath); const up = module.up || module.default?.up || module.exports?.up; const down = module.down || module.default?.down || module.exports?.down; if (!up || !down) { throw new Error( `JavaScript migration ${filePath} must export both 'up' and 'down' functions` ); } return { up, down }; } } catch (error) { if (error instanceof Error && (error.message.includes("tsx is required") || error.message.includes("must export"))) { throw error; } const fileType = filePath.endsWith(".ts") ? "TypeScript" : "JavaScript"; const suggestion = filePath.endsWith(".ts") ? "Make sure tsx is installed and the file exports 'up' and 'down' functions." : "Make sure the file exports 'up' and 'down' functions."; throw new Error( `Failed to load ${fileType} migration ${filePath}. ${suggestion} Error: ${error instanceof Error ? error.message : String(error)}` ); } } async executeInTransaction(callback) { await this.prisma.$transaction(async (_tx) => { await callback(); }); } async testConnection() { try { await this.prisma.$queryRaw`SELECT 1`; return true; } catch { return false; } } async getLastMigration() { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { const result = await this.prisma.$queryRawUnsafe(` SELECT id, migration_name, started_at as appliedAt FROM ${this.tableName} ORDER BY started_at DESC LIMIT 1 `); if (result.length === 0) { return null; } const row = result[0]; return { id: row.id, name: row.migration_name, filename: `${row.id}_${row.migration_name}.sql`, timestamp: new Date(row.id.substring(0, 8)), applied: true, appliedAt: row.appliedAt }; } else { const result = await this.prisma.$queryRawUnsafe(` SELECT id, name, applied_at as appliedAt FROM ${this.tableName} ORDER BY applied_at DESC LIMIT 1 `); if (result.length === 0) { return null; } const row = result[0]; return { id: row.id, name: row.name, filename: `${row.id}_${row.name}.sql`, timestamp: new Date(row.id.substring(0, 8)), applied: true, appliedAt: row.appliedAt }; } } async getMigrationStatus(migrationId) { const isPrismaTable = await this.detectPrismaTable(); if (isPrismaTable) { const result = await this.prisma.$queryRawUnsafe( ` SELECT id, migration_name, started_at as appliedAt FROM ${this.tableName} WHERE id = ? `, migrationId ); if (result.length === 0) { return null; } const row = result[0]; return { id: row.id, name: row.migration_name, status: "applied", appliedAt: row.appliedAt }; } else { const result = await this.prisma.$queryRawUnsafe( ` SELECT id, name, applied_at as appliedAt FROM ${this.tableName} WHERE id = ? `, migrationId ); if (result.length === 0) { return null; } const row = result[0]; return { id: row.id, name: row.name, status: "applied", appliedAt: row.appliedAt }; } } async clearMigrations() { await this.prisma.$executeRawUnsafe(`DELETE FROM ${this.tableName}`); } async dropMigrationsTable() { await this.prisma.$executeRawUnsafe( `DROP TABLE IF EXISTS ${this.tableName}` ); } getDatabaseProvider() { return "postgresql"; } }; // src/version-manager.ts import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs"; import { join as join3 } from "path"; // src/commit-manager.ts import { execSync } from "child_process"; var CommitManager = class { constructor(gitDir = process.cwd()) { this.gitDir = gitDir; } /** * Get current commit hash */ getCurrentCommit() { try { return this.execGitCommand("git rev-parse HEAD").trim(); } catch (error) { throw new Error( `Failed to get current commit: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get current short commit hash */ getCurrentShortCommit() { try { return this.execGitCommand("git rev-parse --short HEAD").trim(); } catch (error) { throw new Error( `Failed to get current short commit: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get current branch name */ getCurrentBranch() { try { return this.execGitCommand("git rev-parse --abbrev-ref HEAD").trim(); } catch (error) { throw new Error( `Failed to get current branch: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get detailed commit information */ getCommitInfo(commitHash) { const commit = commitHash || "HEAD"; try { const hash = this.execGitCommand(`git rev-parse ${commit}`).trim(); const shortHash = this.execGitCommand( `git rev-parse --short ${commit}` ).trim(); const message = this.execGitCommand( `git log -1 --pretty=format:"%s" ${commit}` ).trim(); const author = this.execGitCommand( `git log -1 --pretty=format:"%an <%ae>" ${commit}` ).trim(); const dateStr = this.execGitCommand( `git log -1 --pretty=format:"%ai" ${commit}` ).trim(); const date = new Date(dateStr); let branch; try { branch = this.execGitCommand( `git branch --contains ${hash} | grep -v "detached" | head -1` ).trim().replace(/^\*?\s*/, ""); } catch { } return { hash, shortHash, message, author, date, branch }; } catch (error) { throw new Error( `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Check if the working directory is clean (no uncommitted changes) */ isWorkingDirectoryClean() { try { const status = this.execGitCommand("git status --porcelain").trim(); return status.length === 0; } catch { return false; } } /** * Get commits between two references */ getCommitsBetween(from, to) { try { const range = `${from}..${to}`; const hashes = this.execGitCommand(`git rev-list ${range}`).trim().split("\n").filter(Boolean); return hashes.map((hash) => this.getCommitInfo(hash)); } catch (error) { throw new Error( `Failed to get commits between ${from} and ${to}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Check if a commit exists */ commitExists(commitHash) { try { this.execGitCommand(`git rev-parse --verify ${commitHash}`); return true; } catch { return false; } } /** * Get tags for a specific commit */ getTagsForCommit(commitHash) { try { const tags = this.execGitCommand( `git tag --points-at ${commitHash}` ).trim(); return tags ? tags.split("\n").filter(Boolean) : []; } catch { return []; } } /** * Find the most recent tag reachable from a commit */ getLatestTag(commitHash) { try { const commit = commitHash || "HEAD"; const tag = this.execGitCommand( `git describe --tags --abbrev=0 ${commit}` ).trim(); return tag || null; } catch { return null; } } /** * Check if Git repository exists */ isGitRepository() { try { this.execGitCommand("git rev-parse --git-dir"); return true; } catch { return false; } } /** * Generate a version string based on git state */ generateVersionFromGit() { try { const latestTag = this.getLatestTag(); const shortCommit = this.getCurrentShortCommit(); const branch = this.getCurrentBranch(); const isClean = this.isWorkingDirectoryClean(); if (latestTag) { const tagCommit = this.execGitCommand( `git rev-parse ${latestTag}` ).trim(); const currentCommit = this.getCurrentCommit(); if (tagCommit === currentCommit && isClean) { return latestTag; } const commitsSinceTag = this.execGitCommand( `git rev-list --count ${latestTag}..HEAD` ).trim(); const suffix2 = isClean ? "" : "-dirty"; return `${latestTag}-${commitsSinceTag}-g${shortCommit}${suffix2}`; } const cleanBranch = branch.replace(/[^a-zA-Z0-9.-]/g, "-"); const suffix = isClean ? "" : "-dirty"; return `${cleanBranch}-${shortCommit}${suffix}`; } catch (error) { throw new Error( `Failed to generate version from git: ${error instanceof Error ? error.message : String(error)}` ); } } execGitCommand(command) { return execSync(command, { cwd: this.gitDir, encoding: "utf8", stdio: "pipe" }); } }; // src/version-manager.ts var VersionManager = class { constructor(migrationsDir) { this.manifestPath = join3(migrationsDir, "migration-manifest.json"); this.manifest = this.loadManifest(); this.commitManager = new CommitManager(); } loadManifest() { if (existsSync3(this.manifestPath)) { try { const content = readFileSync3(this.manifestPath, "utf-8"); const manifest = JSON.parse(content); manifest.lastUpdated = new Date(manifest.lastUpdated); manifest.versions = manifest.versions.map((v) => ({ ...v, createdAt: new Date(v.createdAt) })); return manifest; } catch { console.warn("Failed to load migration manifest, creating new one"); } } return { versions: [], lastUpdated: /* @__PURE__ */ new Date() }; } saveManifest() { writeFileSync2(this.manifestPath, JSON.stringify(this.manifest, null, 2)); } /** * Register a version with its associated migrations */ registerVersion(version, migrations, description, commit) { const existingIndex = this.manifest.versions.findIndex( (v) => v.version === version ); const versionMapping = { version, commit, migrations, description, createdAt: /* @__PURE__ */ new Date() }; if (existingIndex >= 0) { this.manifest.versions[existingIndex] = versionMapping; } else { this.manifest.versions.push(versionMapping); } this.manifest.versions.sort( (a, b) => this.compareVersions(a.version, b.version) ); this.manifest.lastUpdated = /* @__PURE__ */ new Date(); this.saveManifest(); } /** * Get all migrations that need to be applied/rolled back between versions or commits */ getMigrationsBetween(from, to, isCommit = false) { let fromMigrations = /* @__PURE__ */ new Set(); let toMigrations = /* @__PURE__ */ new Set(); if (isCommit) { fromMigrations = from ? new Set(this.getCommitMigrations(from)) : /* @__PURE__ */ new Set(); toMigrations = new Set(this.getCommitMigrations(to)); } else { const fromVersionData = from ? this.getVersionData(from) : null; const toVersionData = this.getVersionData(to); if (!toVersionData) { throw new Error(`Version ${to} not found in manifest`); } fromMigrations = new Set(fromVersionData?.migrations || []); toMigrations = new Set(toVersionData.migrations); } const migrationsToRun = Array.from(toMigrations).filter( (m) => !fromMigrations.has(m) ); const migrationsToRollback = Array.from(fromMigrations).filter( (m) => !toMigrations.has(m) ); return { migrationsToRun, migrationsToRollback }; } /** * Get migrations for a specific commit by finding the closest version */ getCommitMigrations(commit) { const info = this.commitManager.getCommitInfo(commit); const version = info.branch ? this.commitManager.getLatestTag(commit) : null; if (!version) { throw new Error(`No version tag found for commit ${commit}`); } const versionData = this.getVersionData(version); if (!versionData) { throw new Error(`Version data not found for tag ${version}`); } return versionData.migrations; } /** * Get version data by version string */ getVersionData(version) { return this.manifest.versions.find((v) => v.version === version) || null; } /** * Get all registered versions */ getAllVersions() { return [...this.manifest.versions]; } /** * Get current version from manifest */ getCurrentVersion() { return this.manifest.currentVersion; } /** * Set current version */ setCurrentVersion(version) { this.manifest.currentVersion = version; this.manifest.lastUpdated = /* @__PURE__ */ new Date(); this.saveManifest(); } /** * Get the latest version based on semantic versioning */ getLatestVersion() { if (this.manifest.versions.length === 0) return void 0; return this.manifest.versions[this.manifest.versions.length - 1].version; } /** * Compare two semantic versions */ compareVersions(a, b) { const parseVersion = (version) => { const parts = version.split(".").map(Number); return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 }; }; const versionA = parseVersion(a); const versionB = parseVersion(b); if (versionA.major !== versionB.major) { return versionA.major - versionB.major; } if (versionA.minor !== versionB.minor) { return versionA.minor - versionB.minor; } return versionA.patch - versionB.patch; } /** * Validate that all migrations for a version exist */ validateVersionMigrations(version, existingMigrations) { const versionData = this.getVersionData(version); if (!versionData) return false; const existingSet = new Set(existingMigrations); return versionData.migrations.every( (migration) => existingSet.has(migration) ); } /** * Generate a deployment plan between versions */ generateDeploymentPlan(fromVersion, toVersion) { const { migrationsToRun, migrationsToRollback } = this.getMigrationsBetween( fromVersion, toVersion ); const plan = []; migrationsToRollback.reverse().forEach((migration, index) => { plan.push({ action: "rollback", migration, order: index + 1 }); }); migrationsToRun.forEach((migration, index) => { plan.push({ action: "run", migration, order: migrationsToRollback.length + index + 1 }); }); const summary = `Deployment plan from ${fromVersion || "initial"} to ${toVersion}: - ${migrationsToRollback.length} migration(s) to rollback - ${migrationsToRun.length} migration(s) to run - Total steps: ${plan.length}`; return { plan, summary }; } }; // src/migration-manager.ts var MigrationManager = class { constructor(configPath) { this.config = new ConfigManager(configPath); const config = this.config.getConfig(); const { migrationsDir, tableName } = config; this.fileManager = new FileManager(migrationsDir, config); this.versionManager = new VersionManager(migrationsDir); const databaseUrl = this.config.getDatabaseUrl(); this.dbAdapter = new DatabaseAdapter(databaseUrl, tableName); } async ensureConfigLoaded() { const config = await this.config.getConfigAsync(); const { migrationsDir, tableName } = config; this.fileManager = new FileManager(migrationsDir, config); this.versionManager = new VersionManager(migrationsDir); const databaseUrl = this.config.getDatabaseUrl(); this.dbAdapter = new DatabaseAdapter(databaseUrl, tableName); } async initialize() { await this.dbAdapter.connect(); await this.dbAdapter.ensureMigrationsTable(); } async destroy() { await this.dbAdapter.disconnect(); } async createMigration(options) { await this.ensureConfigLoaded(); const { name, template } = options; if (!name || name.trim().length === 0) { throw new Error("Migration name cannot be empty"); } const existingMigration = this.fileManager.getMigrationByName(name); if (existingMigration) { throw new Error(`Migration with name '${name}' already exists`); } return this.fileManager.createMigrationFile(name, template); } async runMigrations(options = {}) { const { to, steps, dryRun = false, force = false } = options; try { await this.initialize(); let migrationsToRun = []; if (to) { const targetMigration = this.fileManager.getMigrationFile(to) || this.fileManager.getMigrationByName(to); if (!targetMigration) { throw new Error(`Migration '${to}' not found`); } migrationsToRun = this.getMigrationsUpTo(targetMigration.timestamp); } else if (steps) { migrationsToRun = this.getMigrationsToRun(steps); } else { migrationsToRun = this.getAllPendingMigrations(); } if (dryRun) { return { success: true, migrations: migrationsToRun.map((file) => ({ id: file.timestamp, name: file.name, filename: file.path, timestamp: /* @__PURE__ */ new Date(), applied: false })) }; } const appliedMigrations = []; for (const migrationFile of migrationsToRun) { const isApplied = await this.dbAdapter.isMigrationApplied( migrationFile.timestamp ); if (isApplied && !force) { continue; } await this.dbAdapter.executeInTransaction(async () => { if (migrationFile.type === "sql") { const { up } = this.fileManager.parseMigrationContent(migrationFile); await this.dbAdapter.executeMigration(up); } else { await this.dbAdapter.executeMigrationFile(migrationFile, "up"); } await this.dbAdapter.recordMigration( migrationFile.timestamp, migrationFile.name ); }); appliedMigrations.push({ id: migrationFile.timestamp, name: migrationFile.name, filename: migrationFile.path, timestamp: /* @__PURE__ */ new Date(), applied: true, appliedAt: /* @__PURE__ */ new Date() }); } return { success: true, migrations: appliedMigrations }; } catch (error) { return { success: false, migrations: [], error: error instanceof Error ? error.message : String(error) }; } finally { await this.destroy(); } } async rollbackMigrations(options = {}) { const { to, steps, dryRun = false, force = false } = options; try { await this.initialize(); const appliedMigrations = await this.dbAdapter.getAppliedMigrations(); let migrationsToRollback = []; if (to) { const targetMigration = this.fileManager.getMigrationFile(to) || this.fileManager.getMigrationByName(to); if (!targetMigration) { throw new Error(`Migration '${to}' not found`); } migrationsToRollback = this.getMigrationsToRollbackTo( appliedMigrations, targetMigration.timestamp ); } else if (steps) { migrationsToRollback = appliedMigrations.slice(-steps).reverse(); } else { const lastMigration = await this.dbAdapter.getLastMigration(); if (lastMigration) { migrationsToRollback = [lastMigration]; } } if (dryRun) { return { success: true, migrations: migrationsToRollback }; } const rolledBackMigrations = []; for (const migration of migrationsToRollback) { const migrationFile = this.fileManager.getMigrationFile(migration.id); if (!migrationFile) { throw new Error(`Migration file not found for ${migration.id}`); } await this.dbAdapter.executeInTransaction(async () => { if (migrationFile.type === "sql") { const { down } = this.fileManager.parseMigrationContent(migrationFile); if (!down || down.trim().length === 0) { if (!force) { throw new Error( `No rollback SQL found for migration ${migration.name}` ); } return; } await this.dbAdapter.executeMigration(down); } else { try { await this.dbAdapter.executeMigrationFile(migrationFile, "down"); } catch (error) { if (!force) { throw new Error( `Failed to rollback migration ${migration.name}: ${error instanceof Error ? error.message : String(error)}` ); } return; } } await this.dbAdapter.removeMigration(migration.id); }); rolledBackMigrations.push({ ...migration, applied: false }); } return { success: true, migrations: rolledBackMigrations }; } catch (error) { return { success: false, migrations: [], error: error instanceof Error ? error.message : String(error) }; } finally { await this.destroy(); } } async getMigrationState() { await this.initialize(); const allMigrations = this.fileManager.readMigrationFiles(); const appliedMigrations = await this.dbAdapter.getAppliedMigrations(); const appliedIds = new Set(appliedMigrations.map((m) => m.id)); const pendingMigrations = allMigrations.filter( (m) => !appliedIds.has(m.timestamp) ); await this.destroy(); return { current: appliedMigrations.map((m) => m.id), pending: pendingMigrations.map((m) => m.timestamp), applied: appliedMigrations }; } async getMigrationStatus() { await this.initialize(); const allMigrations = this.fileManager.readMigrationFiles(); const appliedMigrations = await this.dbAdapter.getAppliedMigrations(); const appliedMap = new Map(appliedMigrations.map((m) => [m.id, m])); const statuses = allMigrations.map((file) => { const applied = appliedMap.get(file.timestamp); return { id: file.timestamp, name: file.name, status: applied ? "applied" : "pending", appliedAt: applied?.appliedAt }; }); await this.destroy(); return statuses; } async testConnection() { try { await this.initialize(); const result = await this.dbAdapter.testConnection(); await this.destroy(); return result; } catch { return false; } } getMigrationsUpTo(targetTimestamp) { const allMigrations = this.fileManager.readMigrationFiles(); return allMigrations.filter((m) => m.timestamp <= targetTimestamp); } getMigrationsToRun(steps) { const allMigrations = this.fileManager.readMigrationFiles(); return allMigrations.slice(0, steps); } getAllPendingMigrations() { return this.fileManager.readMigrationFiles(); } getMigrationsToRollbackTo(appliedMigrations, targetTimestamp) { const targetIndex = appliedMigrations.findIndex( (m) => m.id === targetTimestamp ); if (targetIndex === -1) { return []; } return appliedMigrations.slice(targetIndex + 1).reverse(); } // Version-based migration management methods /** * Register a version with its associated migrations */ registerVersion(version, migrations, description, commit) { this.versionManager.registerVersion( version, migrations, description, commit ); } /** * Deploy to a specific version */ async deployToVersion(options) { const { fromVersion, toVersion, dryRun = false, force = false } = options; try { await this.initialize(); const currentVersion = fromVersion || this.versionManager.getCurrentVersion(); const { migrationsToRun, migrationsToRollback } = this.versionManager.getMigrationsBetween(currentVersion, toVersion); if (dryRun) { const plan = this.versionManager.generateDeploymentPlan( currentVersion, toVersion ); console.log(plan.summary); return { success: true, fromVersion: currentVersion, toVersion, migrationsRun: [], migrationsRolledBack: [] }; } const migrationsRun = []; const migrationsRolledBack = []; for (const migrationId of migrationsToRollback.reverse()) { const migrationFile = this.fileManager.getMigrationFile(migrationId); if (!migrationFile) { throw new Error(`Migration file not found for ${migrationId}`); } await this.dbAdapter.executeInTransaction(async () => { if (migrationFile.type === "sql") { const { down } = this.fileManager.parseMigrationContent(migrationFile); if (!down || down.trim().length === 0) { if (!force) { throw new Error( `No rollback SQL found for migration ${migrationFile.name}` ); } return; } await this.dbAdapter.executeMigration(down); } else { await this.dbAdapter.executeMigrationFile(migrationFile, "down"); } await this.dbAdapter.removeMigration(migrationId); }); migrationsRolledBack.push({ id: migrationId, name: migrationFile.name, filename: migrationFile.path, timestamp: /* @__PURE__ */ new Date(), applied: false }); } for (const migrationId of migrationsToRun) { const migrationFile = this.fileManager.getMigrationFile(migrationId); if (!migrationFile) { throw new Error(`Migration file not found for ${migrationId}`); } const isApplied = await this.dbAdapter.isMigrationApplied(migrationId); if (isApplied && !force) { continue; } await this.dbAdapter.executeInTransaction(async () => { if (migrationFile.type === "sql") { const { up } = this.fileManager.parseMigrationContent(migrationFile); await this.dbAdapter.executeMigration(up); } else { await this.dbAdapter.executeMigrationFile(migrationFile, "up"); } await this.dbAdapter.recordMigration(migrationId, migrationFile.name); }); migrationsRun.push({ id: migrationId, name: migrationFile.name, filename: migrationFile.path, timestamp: /* @__PURE__ */ new Date(), applied: true, appliedAt: /* @__PURE__ */ new Date() }); } this.versionManager.setCurrentVersion(toVersion); return { success: true, fromVersion: currentVersion, toVersion, migrationsRun, migrationsRolledBack }; } catch (error) { return { success: false, fromVersion, toVersion, migrationsRun: [], migrationsRolledBack: [], error: error instanceof Error ? error.message : String(error) }; } finally { await this.destroy(); } } /** * Get deployment plan between versions */ getDeploymentPlan(fromVersion, toVersion) { return this.versionManager.generateDeploymentPlan(fromVersion, toVersion); } /** * Get all registered versions */ getAllVersions() { return this.versionManager.getAllVersions(); } /** * Get current version */ getCurrentVersion() { return this.versionManager.getCurrentVersion(); } /** * Set current version */ setCurrentVersion(version) { this.versionManager.setCurrentVersion(version); } /** * Validate version migrations */ validateVersionMigrations(version) { const allMigrations = this.fileManager.readMigrationFiles(); const migrationIds = allMigrations.map((m) => m.timestamp); return this.versionManager.validateVersionMigrations(version, migrationIds); } }; // src/cli.ts var program = new Command(); function getManager() { return new MigrationManager(); } program.version("1.0.0").description("Prisma Migrations CLI"); program.command("create <name>").description("Create a new migration").action(async (name) => { try { const manager = getManager(); await manager.createMigration({ name }); console.log(`Migration '${name}' created successfully.`); } catch (error) { console.error( `Error creating migration: ${error instanceof Error ? error.message : String(error)}` ); } }); program.command("up").description("Run all pending migrations").option("-t, --to <timestamp>", "Run up to a specific migration").option( "-s, --steps <number>", "Run a specific number of migrations", parseInt ).option("-d, --dry-run", "Preview migrations without applying").action(async ({ to, steps, dryRun }) => { try { const manager = getManager(); const result = await manager.runMigrations({ to, steps, dryRun }); console.log(`Migrations applied successfully: ${result.success}`); } catch (error) { console.error( `Error running migrations: ${error instanceof Error ? error.message : String(error)}` ); } }); program.command("down").description("Rollback migrations").option("-t, --to <timestamp>", "Rollback to a specific migration").option( "-s, --steps <number>", "Rollback a specific number of migrations", parseInt ).option("-d, --dry-run", "Preview rollback without applying").action(async ({ to, steps, dryRun }) => { try { const manager = getManager(); const result = await manager.rollbackMigrations({ to, steps, dryRun }); console.log(`Migrations rolled back successfully: ${result.success}`); } catch (error) { console.error( `Error rolling back migrations: ${error instanceof Error ? error.message : String(error)}` ); } }); program.command("status").description("Get migration status").action(async () => { try { const manager = getManager(); const status = await manager.getMigrationStatus(); status.forEach(({ name, status: status2, appliedAt }) => { console.log( `${name} [${status2}] - ${appliedAt ? appliedAt : "Pending"}` ); }); } catch (error) { console.error( `Error retrieving status: ${error instanceof Error ? error.message : String(error)}` ); } }); program.command("test").description("Test database connection").action(async () => { try { const manager = getManager(); const result = await manager.testConnection(); console.log(`Database connection successful: ${result}`); } catch (error) { console.error( `Error testing connection: ${error instanceof Error ? error.message : String(error)}` ); } }); program.parse(process.argv); //# sourceMappingURL=cli.js.map