From 2e370f9821a6d215d4a8019bb20d56d00665ccbd Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Fri, 6 Feb 2026 09:54:13 -0800 Subject: [PATCH] fix(dev): reset legacy dev.db and apply migrations --- backend/package.json | 2 +- backend/scripts/predev-migrate.cjs | 107 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/predev-migrate.cjs diff --git a/backend/package.json b/backend/package.json index f963037..2a469ec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "predev": "node -e \"process.env.DATABASE_URL=process.env.DATABASE_URL||'file:./prisma/dev.db'; require('child_process').execSync('npx prisma migrate deploy', { stdio: 'inherit' });\"", + "predev": "node scripts/predev-migrate.cjs", "dev": "nodemon src/index.ts", "test": "vitest run", "test:watch": "vitest", diff --git a/backend/scripts/predev-migrate.cjs b/backend/scripts/predev-migrate.cjs new file mode 100644 index 0000000..6d691e1 --- /dev/null +++ b/backend/scripts/predev-migrate.cjs @@ -0,0 +1,107 @@ +/* eslint-disable no-console */ +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const backendRoot = path.resolve(__dirname, ".."); + +const resolveDatabaseUrl = (rawUrl) => { + const defaultDbPath = path.resolve(backendRoot, "prisma/dev.db"); + + if (!rawUrl || String(rawUrl).trim().length === 0) { + return `file:${defaultDbPath}`; + } + + if (!String(rawUrl).startsWith("file:")) { + return String(rawUrl); + } + + const filePath = String(rawUrl).replace(/^file:/, ""); + const prismaDir = path.resolve(backendRoot, "prisma"); + const normalizedRelative = filePath.replace(/^\.\/?/, ""); + const hasLeadingPrismaDir = + normalizedRelative === "prisma" || normalizedRelative.startsWith("prisma/"); + + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.resolve(hasLeadingPrismaDir ? backendRoot : prismaDir, normalizedRelative); + + return `file:${absolutePath}`; +}; + +const databaseUrl = resolveDatabaseUrl(process.env.DATABASE_URL); +process.env.DATABASE_URL = databaseUrl; + +const nodeEnv = process.env.NODE_ENV || "development"; + +const run = (cmd) => { + execSync(cmd, { + cwd: backendRoot, + stdio: "inherit", + env: { ...process.env, DATABASE_URL: databaseUrl }, + }); +}; + +const getDbFilePath = () => { + if (!databaseUrl.startsWith("file:")) return null; + return databaseUrl.replace(/^file:/, ""); +}; + +const isNonEmptyLegacyDbWithoutMigrations = () => { + const dbPath = getDbFilePath(); + if (!dbPath) return false; + if (!fs.existsSync(dbPath)) return false; + + // Only attempt this heuristic for SQLite file DBs. + const Database = require("better-sqlite3"); + const db = new Database(dbPath, { readonly: true }); + try { + const hasMigrations = + db + .prepare( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='_prisma_migrations' LIMIT 1", + ) + .get() !== undefined; + + const nonEmptyRow = db + .prepare("SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") + .get(); + const nonEmpty = Number(nonEmptyRow?.cnt || 0) > 0; + + return nonEmpty && !hasMigrations; + } finally { + db.close(); + } +}; + +const backupDbIfPresent = () => { + const dbPath = getDbFilePath(); + if (!dbPath) return null; + if (!fs.existsSync(dbPath)) return null; + + const dir = path.dirname(dbPath); + const base = path.basename(dbPath, path.extname(dbPath)); + const stamp = new Date().toISOString().replace(/[:.]/g, "-"); + const backupPath = path.join(dir, `${base}.${stamp}.backup`); + + fs.copyFileSync(dbPath, backupPath); + return backupPath; +}; + +const isNonProd = nodeEnv !== "production"; +const isFileDb = databaseUrl.startsWith("file:"); + +if (isNonProd && isFileDb && isNonEmptyLegacyDbWithoutMigrations()) { + const backupPath = backupDbIfPresent(); + console.warn( + `[predev] Prisma migrations cannot be deployed because the database was created without migrations.\n` + + ` DATABASE_URL=${databaseUrl}\n` + + (backupPath ? ` Backup: ${backupPath}\n` : "") + + ` Resetting local SQLite database to apply migrations.`, + ); + + run("npx prisma migrate reset --force --skip-seed"); + process.exit(0); +} + +run("npx prisma migrate deploy");