Files
DentalManagement09/apps/Backend/src/services/autoImportService.ts
T
Summit Dental CareandClaude Sonnet 4.6 7deba90db3 fix: use prisma db push after backup import to sync schema reliably
applyMissingMigrations ran the entire migration SQL as one transaction,
which rolled back all changes when any statement failed (e.g. adding a
NOT NULL column to a non-empty table). Replaced with prisma db push
which compares the schema and applies only what's needed. Also added
support for importing .sql files (not just .zip) in auto-import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:05:18 -04:00

114 lines
3.7 KiB
TypeScript

import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import { prisma } from "@repo/db/client";
const LOCAL_BACKUP_DIR = path.resolve(process.cwd(), "backups");
const SCHEMA_PATH = path.resolve(__dirname, "../../../../packages/db/prisma/schema.prisma");
function runDbPush(): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn("npx", ["prisma", "db", "push", "--config", path.resolve(__dirname, "../../../../packages/db/prisma/prisma.config.ts"), "--schema", SCHEMA_PATH, "--accept-data-loss"], {
cwd: process.cwd(),
env: process.env,
});
let stderr = "";
proc.stdout.on("data", (d) => console.log(`[db-push] ${d.toString().trim()}`));
proc.stderr.on("data", (d) => {
const line = d.toString().trim();
stderr += line + "\n";
console.log(`[db-push] ${line}`);
});
proc.on("error", (err) => reject(new Error(`Failed to run prisma db push: ${err.message}`)));
proc.on("close", (code) => {
if (code !== 0) return reject(new Error(`prisma db push failed (exit ${code}): ${stderr}`));
console.log("[db-push] Schema synced successfully");
resolve();
});
});
}
function getLatestBackupFile(): string | null {
if (!fs.existsSync(LOCAL_BACKUP_DIR)) return null;
const files = fs.readdirSync(LOCAL_BACKUP_DIR)
.filter((f) => f.endsWith(".zip") || f.endsWith(".sql"))
.map((f) => ({ name: f, mtime: fs.statSync(path.join(LOCAL_BACKUP_DIR, f)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime);
if (files.length === 0) return null;
return path.join(LOCAL_BACKUP_DIR, files[0].name);
}
export async function importLatestBackup(): Promise<void> {
const backupFile = getLatestBackupFile();
if (!backupFile) {
throw new Error("No backup files found in backups folder");
}
console.log(`[auto-import] Importing ${path.basename(backupFile)}...`);
try {
await prisma.$executeRawUnsafe(`DROP SCHEMA public CASCADE`);
await prisma.$executeRawUnsafe(`CREATE SCHEMA public`);
} catch (err: any) {
throw new Error(`Failed to reset schema: ${err.message}`);
}
return new Promise((resolve, reject) => {
const psql = spawn(
"psql",
[
"-h", process.env.DB_HOST || "localhost",
"-U", process.env.DB_USER || "postgres",
process.env.DB_NAME || "dental_db",
],
{ env: { ...process.env, PGPASSWORD: process.env.DB_PASSWORD } }
);
let stderr = "";
psql.stderr.on("data", (d) => (stderr += d.toString()));
psql.on("error", (err) => {
reject(new Error(`Failed to start psql: ${err.message}`));
});
psql.on("close", async (code) => {
if (code !== 0) {
return reject(new Error(`psql restore failed (exit ${code}): ${stderr}`));
}
try {
await prisma.$disconnect();
await prisma.$connect();
} catch (_) {}
try {
await runDbPush();
} catch (err) {
console.error("[auto-import] db push failed after import:", err);
}
console.log(`[auto-import] Successfully imported ${path.basename(backupFile)}`);
resolve();
});
if (backupFile.endsWith(".zip")) {
const unzip = spawn("unzip", ["-p", backupFile, "*.sql"]);
unzip.stderr.on("data", (d) => console.warn(`[auto-import] unzip: ${d.toString().trim()}`));
unzip.on("error", (err) => {
reject(new Error(`Failed to extract backup zip: ${err.message}`));
});
unzip.stdout.pipe(psql.stdin);
} else {
const sqlStream = fs.createReadStream(backupFile);
sqlStream.on("error", (err) => {
reject(new Error(`Failed to read backup file: ${err.message}`));
});
sqlStream.pipe(psql.stdin);
}
});
}