#!/usr/bin/env node import { execSync } from 'child_process' import fs from 'fs' import path from 'path' import { config } from 'dotenv' // 加载环境变量 config({ path: path.resolve(process.cwd(), '.env') }) // 颜色输出函数 const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m' } function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`) } function logSuccess(message) { log(`✅ ${message}`, 'green') } function logWarning(message) { log(`⚠️ ${message}`, 'yellow') } function logError(message) { log(`❌ ${message}`, 'red') } // 检查环境变量 if (!process.env.DATABASE_URL) { logError('DATABASE_URL 环境变量未设置') process.exit(1) } // 安全执行命令 function safeExec(command, options = {}) { try { execSync(command, { stdio: 'inherit', ...options }) return true } catch (error) { logError(`命令执行失败: ${command}`) logError(error.message) return false } } // 检查文件是否存在 function fileExists(filePath) { try { return fs.existsSync(filePath) } catch { return false } } // 处理数据冲突的函数 async function handleDataConflicts() { try { // 在Vercel环境中,我们主要关注schema同步而不是数据冲突处理 log('检查数据库连接...', 'cyan') // 简单的连接测试 if (!safeExec('cd .. && pnpm exec drizzle-kit check --config=drizzle.config.ts', { stdio: 'pipe' })) { logWarning('数据库schema检查失败,将尝试强制同步') } logSuccess('数据冲突检查完成') } catch (error) { logWarning(`数据冲突处理警告: ${error.message}`) } } async function safeMigrate() { log('🔄 开始安全数据库迁移流程...', 'bright') try { // 获取项目根目录路径 const projectRoot = path.resolve(process.cwd(), '..') const drizzleConfigPath = path.join(projectRoot, 'drizzle.config.ts') const schemaPath = path.join(projectRoot, 'app/drizzle/schema.ts') const migrationsPath = path.join(projectRoot, 'app/drizzle/migrations') // 1. 确保drizzle配置存在 if (!fileExists(drizzleConfigPath)) { throw new Error(`drizzle.config.ts 配置文件不存在: ${drizzleConfigPath}`) } // 2. 检查schema文件 if (!fileExists(schemaPath)) { throw new Error(`app/drizzle/schema.ts 文件不存在: ${schemaPath}`) } // 3. 创建迁移目录(如果不存在) if (!fileExists(migrationsPath)) { log('创建迁移目录...', 'cyan') fs.mkdirSync(migrationsPath, { recursive: true }) } // 自动执行 generate 命令并处理交互 async function runGenerateWithAutoConfirm(env) { return new Promise((resolve) => { const { spawn } = require('child_process') const child = spawn('pnpm', ['run', 'db:generate'], { env, shell: true }) // 监听输出,透传给用户,并自动响应提示 child.stdout.on('data', (data) => { process.stdout.write(data) const str = data.toString() // 如果检测到交互提示(通常包含问号或选项),自动发送回车 if (str.includes('?') || str.includes('renamed') || str.includes('created')) { try { // 发送回车以选择默认选项(通常是 Created) child.stdin.write('\n') } catch (e) { // 忽略写入错误 } } }) child.stderr.on('data', (data) => { process.stderr.write(data) }) child.on('close', (code) => { resolve(code === 0) }) child.on('error', () => { resolve(false) }) }) } // 4. 生成迁移文件(如果需要) log('生成数据库迁移文件...', 'cyan') // 设置非交互式环境变量 const nonInteractiveEnv = { ...process.env, DRIZZLE_KIT_FORCE: 'true', CI: 'true', NODE_ENV: 'production' } // 使用新的自动确认函数 const generateSuccess = await runGenerateWithAutoConfirm(nonInteractiveEnv) if (!generateSuccess) { logWarning('迁移文件生成可能遇到问题,尝试继续...') } else { logSuccess('迁移文件生成完成') } // 5. 预处理数据冲突 log('🔍 检查并处理数据冲突...', 'cyan') await handleDataConflicts() // 6. 检查是否为全新部署(数据库为空) log('📋 检查数据库状态...', 'cyan') // 设置非交互式环境变量 const env = { ...process.env, DRIZZLE_KIT_FORCE: 'true', CI: 'true', NODE_ENV: 'production' } // 检查数据库是否有任何表(基于 introspect 输出更稳健的判断) let isEmptyDatabase = false try { const checkResult = execSync( 'cd .. && pnpm exec drizzle-kit introspect --config=drizzle.config.ts', { stdio: 'pipe', env, encoding: 'utf8' } ) // 优先解析 " tables" 提示,其次检查是否包含 columns 索引等摘要 const tablesMatch = checkResult.match(/(\d+)\s+tables/i) const hasTablesCount = tablesMatch && Number(tablesMatch[1]) > 0 const listsTables = /\bcolumns\b|\bindexes\b|\bfks\b/i.test(checkResult) // 数据库为空当且仅当:统计为0且没有任何表摘要 isEmptyDatabase = !(hasTablesCount || listsTables) } catch (error) { // introspect 失败时不轻易判定为空库,改为保守:视为非空库,走 push 路径 logWarning('数据库状态检查失败,按非空库处理以避免重复建表') isEmptyDatabase = false } if (isEmptyDatabase) { log('🆕 检测到全新部署,执行标准迁移...', 'cyan') // 对于全新部署,直接使用migrate避免交互式提示 if (!safeExec('cd .. && pnpm run db:migrate', { env })) { throw new Error('数据库迁移失败') } logSuccess('全新数据库迁移成功') } else { log('🔄 检测到现有数据库,执行schema同步...', 'cyan') // 对于现有数据库,使用push进行增量更新 if (safeExec('cd .. && pnpm exec drizzle-kit push --force --config=drizzle.config.ts', { env })) { logSuccess('数据库schema同步成功') } else { logWarning('schema同步失败,尝试标准迁移...') // 7. 执行迁移(作为后备) if (!safeExec('cd .. && pnpm run db:migrate', { env })) { throw new Error('数据库迁移完全失败') } logSuccess('数据库迁移成功') } } // 8. 验证迁移结果 log('✅ 数据库迁移流程完成!', 'green') } catch (error) { logError(`迁移失败: ${error.message}`) logError('请检查数据库连接和迁移文件') process.exit(1) } } // 运行迁移 safeMigrate().catch((error) => { logError(`未预期的错误: ${error.message}`) process.exit(1) })