fix: apply missing migrations directly after DB restore

Replace the prisma migrate deploy subprocess with a custom
applyMissingMigrations() that reads each migration SQL file and runs it
via prisma.$executeRawUnsafe. This avoids the Prisma CLI aborting when
the restored _prisma_migrations table contains orphaned entries (migration
names that no longer have a corresponding file in the migrations folder).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-05-17 22:36:35 -04:00
parent 5508a90d28
commit b148c0de30

View File

@@ -8,6 +8,53 @@ import { prisma } from "@repo/db/client";
import { storage } from "../storage";
import { backupDatabaseToPath } from "../services/databaseBackupService";
const MIGRATIONS_DIR = path.resolve(__dirname, "../../../../packages/db/prisma/migrations");
// Applies migration SQL files that are missing from the database.
// Reads each folder in the migrations directory in sorted order, checks
// whether the migration is already recorded as successfully applied in
// _prisma_migrations, and runs the SQL if not. Safe to call after any
// restore because it uses IF NOT EXISTS semantics or checks first.
async function applyMissingMigrations() {
let folders: string[];
try {
folders = fs.readdirSync(MIGRATIONS_DIR)
.filter((name) => fs.statSync(path.join(MIGRATIONS_DIR, name)).isDirectory())
.sort();
} catch {
console.warn("Could not read migrations directory, skipping post-restore migration.");
return;
}
// Fetch the set of successfully-applied migration names from the DB.
let applied: Set<string>;
try {
const rows = await prisma.$queryRaw<{ migration_name: string }[]>`
SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL
`;
applied = new Set(rows.map((r) => r.migration_name));
} catch {
// _prisma_migrations may not exist in very old backups; proceed anyway.
applied = new Set();
}
for (const folder of folders) {
if (applied.has(folder)) continue;
const sqlFile = path.join(MIGRATIONS_DIR, folder, "migration.sql");
if (!fs.existsSync(sqlFile)) continue;
const sql = fs.readFileSync(sqlFile, "utf8");
try {
await prisma.$executeRawUnsafe(sql);
console.log(`Applied migration: ${folder}`);
} catch (err: any) {
// Log but continue — some statements may already exist (e.g. after a
// partial restore) and that is acceptable.
console.warn(`Migration ${folder} had errors (may already be applied):`, err.message);
}
}
}
const restoreUpload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 500 * 1024 * 1024 }, // 500 MB
@@ -394,32 +441,17 @@ router.post("/restore", restoreUpload.single("file"), async (req: Request, res:
await prisma.$connect();
} catch (_) {}
// Apply any migrations the backup may be missing so the schema matches
// the current codebase. Uses prisma migrate deploy which is safe to run
// repeatedly — it skips already-applied migrations.
const migrate = spawn(
"npx",
["prisma", "migrate", "deploy", "--schema", path.resolve(__dirname, "../../../../packages/db/prisma/schema.prisma")],
{
env: { ...process.env, DATABASE_URL: process.env.DATABASE_URL || "" },
cwd: path.resolve(__dirname, "../../../../packages/db"),
}
);
let migrateOut = "";
migrate.stdout.on("data", (d) => (migrateOut += d.toString()));
migrate.stderr.on("data", (d) => (migrateOut += d.toString()));
migrate.on("close", (migrateCode) => {
if (migrateCode !== 0) {
console.error("prisma migrate deploy failed after restore:", migrateOut);
} else {
console.log("prisma migrate deploy completed after restore:", migrateOut.trim());
}
res.json({ success: true });
});
migrate.on("error", (err) => {
console.error("Failed to run prisma migrate deploy:", err.message);
res.json({ success: true }); // still report success — data is restored
});
// Apply any migrations the backup may be missing. We run each migration
// SQL file directly instead of using `prisma migrate deploy` because the
// restored _prisma_migrations table may contain orphaned entries (migration
// names that have no matching file) which cause Prisma CLI to abort.
try {
await applyMissingMigrations();
} catch (err) {
console.error("applyMissingMigrations failed after restore:", err);
}
res.json({ success: true });
});
if (isZip && tmpZipPath) {