import { spawn } from "child_process"; import http from "http"; import https from "https"; import { URL } from "url"; import path from "path"; import fs from "fs"; import { prisma } from "@repo/db/client"; const MIGRATIONS_DIR = path.resolve(__dirname, "../../../../packages/db/prisma/migrations"); 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-sync migration."); return; } let applied: Set; 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: { migration_name: string }) => r.migration_name)); } catch { 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) { console.warn(`Migration ${folder} had errors (may already be applied):`, err.message); } } } export function runNetworkSync(sourceUrl: string, apiKey: string): Promise { return new Promise((resolve, reject) => { let targetUrl: URL; try { targetUrl = new URL("/api/database-management/network-backup", sourceUrl); } catch { return reject(new Error(`Invalid source URL: ${sourceUrl}`)); } const client = targetUrl.protocol === "https:" ? https : http; const req = client.get( targetUrl.href, { headers: { "x-network-backup-key": apiKey } }, async (res) => { if (res.statusCode !== 200) { res.resume(); return reject( new Error(`Source returned HTTP ${res.statusCode}: ${res.statusMessage}`) ); } // Drop and recreate the public schema try { await prisma.$executeRawUnsafe(`DROP SCHEMA public CASCADE`); await prisma.$executeRawUnsafe(`CREATE SCHEMA public`); } catch (err: any) { res.destroy(); return reject(new Error(`Failed to reset schema: ${err.message}`)); } 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 exited ${code}: ${stderr}`)); } try { await prisma.$disconnect(); await prisma.$connect(); } catch (_) {} try { await applyMissingMigrations(); } catch (err) { console.error("applyMissingMigrations failed after network sync:", err); } resolve(); }); res.pipe(psql.stdin); } ); req.on("error", (err) => reject(new Error(`Network request failed: ${err.message}`))); req.setTimeout(120_000, () => { req.destroy(); reject(new Error("Network backup request timed out after 120s")); }); }); }