fix(api): skip baseline replay when schema already exists

Dokploy boots crash-looped on CREATE TYPE AdminRole against an existing
Postgres with an empty _goldbank_migrations table. Adopt Prisma CLI history
when present, record the baseline if AdminRole exists, and treat duplicate
object errors as already-applied so the API can start without DB shell access.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-10-27 17:27:52 +03:30
co-authored by Cursor
parent 37d22d2f7d
commit b05b6515f2
3 changed files with 221 additions and 26 deletions
+75 -11
View File
@@ -164102,6 +164102,7 @@ var require_migrate = __commonJS({
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.splitSqlStatements = splitSqlStatements;
exports2.sortMigrations = sortMigrations;
exports2.isAlreadyExistsPgError = isAlreadyExistsPgError;
exports2.runMigrations = runMigrations;
exports2.runOnce = runOnce;
var fs_1 = require("fs");
@@ -164212,6 +164213,50 @@ var require_migrate = __commonJS({
return a.name.localeCompare(b.name);
});
}
function isAlreadyExistsPgError(err) {
const meta = err && typeof err === "object" && "meta" in err ? err.meta : void 0;
const code = String(meta?.code ?? "");
if (code === "42710" || code === "42P07" || code === "42701")
return true;
const msg = [
err instanceof Error ? err.message : String(err),
meta?.message ?? ""
].join(" ");
return /already exists/i.test(msg) || /duplicate_(object|table|column|relation)/i.test(msg);
}
async function typeExists(prisma, typname) {
const rows = await prisma.$queryRawUnsafe(`SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_type t
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
WHERE t.typname = $1 AND n.nspname = current_schema()
) AS "exists"`, typname);
return Boolean(rows[0]?.exists);
}
async function recordApplied(prisma, name) {
await prisma.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)
ON CONFLICT (name) DO NOTHING`, name);
}
async function adoptPrismaMigrationLedger(prisma, appliedNames) {
const table = await prisma.$queryRawUnsafe(`SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema() AND table_name = '_prisma_migrations'
) AS "exists"`);
if (!table[0]?.exists)
return [];
const rows = await prisma.$queryRawUnsafe(`SELECT migration_name FROM "_prisma_migrations"
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL`);
const adopted = [];
for (const row of rows) {
const name = row.migration_name;
if (!name || appliedNames.has(name))
continue;
await recordApplied(prisma, name);
appliedNames.add(name);
adopted.push(name);
}
return adopted;
}
async function runMigrations(prisma) {
const dir = resolveMigrationsDir();
const files = discoverMigrationFiles(dir);
@@ -164226,26 +164271,45 @@ var require_migrate = __commonJS({
)`);
const alreadyApplied = await prisma.$queryRawUnsafe(`SELECT name FROM "${TRACKING_TABLE}"`);
const appliedNames = new Set(alreadyApplied.map((row) => row.name));
const adopted = await adoptPrismaMigrationLedger(prisma, appliedNames);
if (adopted.length > 0) {
console.warn(`[migrate] Adopted ${adopted.length} migration(s) from _prisma_migrations: ${adopted.join(", ")}`);
}
const applied = [];
for (const file2 of files) {
const legacyName = LEGACY_NAME_ALIASES[file2.name];
if (appliedNames.has(file2.name) || legacyName && appliedNames.has(legacyName))
continue;
if (file2.name === BASELINE_MIGRATION && await typeExists(prisma, "AdminRole")) {
await recordApplied(prisma, file2.name);
appliedNames.add(file2.name);
applied.push(file2.name);
console.warn(`[migrate] Baseline ${file2.name} already present in schema (AdminRole); recorded without re-running SQL`);
continue;
}
const statements = splitSqlStatements((0, fs_1.readFileSync)(file2.path, "utf8")).filter((statement) => {
const normalized = statement.replace(/\s+/g, " ").trim().toLowerCase();
return !normalized.startsWith(`drop table "${TRACKING_TABLE.toLowerCase()}"`) && !normalized.startsWith(`drop table ${TRACKING_TABLE.toLowerCase()}`);
});
await prisma.$transaction(
async (tx) => {
for (const statement of statements) {
await tx.$executeRawUnsafe(statement);
}
await tx.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, file2.name);
},
// The baseline migration is hundreds of DDL statements; the 5s default is
// not enough, especially against a remote database.
{ maxWait: 6e4, timeout: 6e5 }
);
try {
await prisma.$transaction(
async (tx) => {
for (const statement of statements) {
await tx.$executeRawUnsafe(statement);
}
await tx.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, file2.name);
},
// The baseline migration is hundreds of DDL statements; the 5s default is
// not enough, especially against a remote database.
{ maxWait: 6e4, timeout: 6e5 }
);
} catch (err) {
if (!isAlreadyExistsPgError(err))
throw err;
await recordApplied(prisma, file2.name);
console.warn(`[migrate] ${file2.name}: objects already exist \u2014 recorded as applied (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
}
appliedNames.add(file2.name);
applied.push(file2.name);
}
return { freshDatabase, applied };