prisma-migrations
Version:
A Node.js library to manage Prisma ORM migrations like other ORMs
1,524 lines (1,504 loc) • 46.6 kB
JavaScript
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