diff --git a/package.json b/package.json index 2edc473..db7b82c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "type-check": "turbo type-check", "clerk:keys": "bash scripts/setup-clerk-keys.sh", "db:generate": "turbo db:generate", - "db:deploy": "sh -c 'if [ -n \"$(find packages/database/prisma/migrations -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)\" ]; then npm run db:migrate:deploy --workspace @rentaldrivego/database; else npm run db:push --workspace @rentaldrivego/database; fi'", + "db:deploy": "node packages/database/scripts/db-deploy.cjs", "db:push": "turbo db:push", "db:migrate": "turbo db:migrate", "db:seed": "turbo db:seed", diff --git a/packages/database/scripts/db-deploy.cjs b/packages/database/scripts/db-deploy.cjs new file mode 100644 index 0000000..0cbe331 --- /dev/null +++ b/packages/database/scripts/db-deploy.cjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +const { spawnSync } = require('node:child_process') +const { readdirSync } = require('node:fs') +const path = require('node:path') +const { PrismaClient } = require('../generated') + +const repoRoot = path.resolve(__dirname, '../../..') +const prismaBin = path.join(repoRoot, 'node_modules', '.bin', 'prisma') +const schemaPath = path.join(repoRoot, 'packages/database/prisma/schema.prisma') +const migrationsDir = path.join(repoRoot, 'packages/database/prisma/migrations') + +function run(command, args) { + const result = spawnSync(command, args, { + stdio: 'inherit', + cwd: repoRoot, + env: process.env, + }) + + if (result.status !== 0) { + process.exit(result.status ?? 1) + } +} + +async function main() { + const prisma = new PrismaClient() + + try { + const [state] = await prisma.$queryRawUnsafe(` + SELECT + to_regclass('public.companies') IS NOT NULL AS "hasCompanies", + to_regclass('public.employees') IS NOT NULL AS "hasEmployees", + to_regclass('public._prisma_migrations') IS NOT NULL AS "hasMigrationsTable" + `) + + const failedMigrations = state.hasMigrationsTable + ? await prisma.$queryRawUnsafe(` + SELECT migration_name + FROM "_prisma_migrations" + WHERE finished_at IS NULL + AND rolled_back_at IS NULL + ORDER BY started_at + `) + : [] + + const hasBaseSchema = state.hasCompanies || state.hasEmployees + + if (!hasBaseSchema) { + if (failedMigrations.length > 0) { + console.log('[db:deploy] Fresh database with failed migration records detected; marking them rolled back before bootstrap') + for (const migration of failedMigrations) { + run(prismaBin, [ + 'migrate', + 'resolve', + '--schema', + schemaPath, + '--rolled-back', + migration.migration_name, + ]) + } + } + + console.log('[db:deploy] Fresh database detected; bootstrapping schema with prisma db push') + run(prismaBin, ['db', 'push', '--schema', schemaPath]) + + const migrationNames = readdirSync(migrationsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort() + + if (migrationNames.length > 0) { + console.log('[db:deploy] Marking existing migrations as applied for the bootstrapped schema') + for (const migrationName of migrationNames) { + run(prismaBin, [ + 'migrate', + 'resolve', + '--schema', + schemaPath, + '--applied', + migrationName, + ]) + } + } + + return + } + + if (failedMigrations.length > 0) { + console.error('[db:deploy] Found failed migrations in a non-empty database. Resolve them manually before deploying again.') + for (const migration of failedMigrations) { + console.error(`- ${migration.migration_name}`) + } + process.exit(1) + } + + console.log('[db:deploy] Existing database detected; applying Prisma migrations') + run(prismaBin, ['migrate', 'deploy', '--schema', schemaPath]) + } finally { + await prisma.$disconnect() + } +} + +main().catch((error) => { + console.error('[db:deploy] Failed:', error) + process.exit(1) +})