Files
BYW-VoiceHub/app/drizzle/db.ts
T

199 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {drizzle} from 'drizzle-orm/postgres-js';
import {and, asc, count, desc, eq, exists, gt, gte, lt, lte, ne, or, sql} from 'drizzle-orm';
import postgres from 'postgres';
import * as schema from './schema.ts';
import {config} from 'dotenv';
import path from 'path';
import {fileURLToPath} from 'url';
// 加载环境变量(优先使用工作目录的 .env,确保构建后运行时能正确加载)
config({ path: path.resolve(process.cwd(), '.env') });
// 检查环境变量
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable is not set');
}
// 创建PostgreSQL连接
const connectionString = process.env.DATABASE_URL;
// 检测数据库类型
const getDatabaseHostname = (value: string) => {
try {
return new URL(value).hostname.toLowerCase().replace(/\.$/, '');
} catch {
return '';
}
};
const isDomainOrSubdomain = (hostname: string, domain: string) => {
return hostname === domain || hostname.endsWith(`.${domain}`);
};
const databaseHostname = getDatabaseHostname(connectionString);
const isNeonDatabase =
isDomainOrSubdomain(databaseHostname, 'neon.tech') ||
isDomainOrSubdomain(databaseHostname, 'neon.database.com');
// Serverless 部署(每个函数实例独立建池,必须压低单实例连接数,防止实例数×池大小超过数据库 max_connections)
const isServerlessDatabase = !!process.env.VERCEL || !!process.env.NETLIFY;
// 根据数据库类型选择配置
const getDatabaseConfig = () => {
if (isNeonDatabase) {
// Neon Database Serverless 优化配置
return {
max: 1, // Serverless 环境下每个实例保持最小连接数,利用 Neon 自身的连接池
idle_timeout: 0, // 立即释放空闲连接,适应 Serverless 的快速冻结特性
connect_timeout: 10, // Neon 连接速度快,减少超时时间
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: 'require', // Neon 默认需要 SSL
prepare: false, // 禁用预处理语句以提高兼容性
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
} else {
// 标准 PostgreSQL 数据库配置
return {
// Serverless 下单实例池压到 2:1 vCPU 数据库同时只能执行一条查询,大池只会耗尽 max_connections
max: isServerlessDatabase ? 2 : process.env.NODE_ENV === 'production' ? 10 : 5,
idle_timeout: 20, // 增加空闲超时时间
// 连接失败快速抛出,避免 30 秒挂起拖垮函数实例并引发重试风暴
connect_timeout: isServerlessDatabase ? 10 : 30,
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false,
prepare: false, // 禁用预处理语句以提高兼容性
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
}
};
const client = postgres(connectionString, getDatabaseConfig());
// 创建Drizzle数据库实例
export const db = drizzle(client, { schema });
// 导出连接客户端(用于手动查询或关闭连接)
export { client };
// 导出schema以便在其他地方使用
export * from './schema.ts';
// 导出drizzle-orm函数
export {eq, ne, and, gt, gte, lt, lte, count, exists, desc, asc, or, sql};
// 数据库连接测试函数
export async function testConnection() {
try {
await client`SELECT 1`;
console.log('✅ Database connection successful');
return true;
} catch (error) {
console.error('❌ Database connection failed:', error);
return false;
}
}
// 获取数据库连接状态
export function getConnectionStatus() {
const isConnected = !client.ended;
return {
isConnected,
connected: isConnected,
status: isConnected ? 'connected' : 'disconnected',
maxConnections: client.options.max,
idleTimeout: client.options.idle_timeout,
connectTimeout: client.options.connect_timeout
};
}
// 连接管理 - 根据数据库类型自适应
let idleTimer: NodeJS.Timeout | null = null;
// Neon 数据库使用更短的空闲时间以支持自动启停,普通 PostgreSQL 使用更长的空闲时间
const IDLE_TIMEOUT = isNeonDatabase ? 5 * 60 * 1000 : 10 * 60 * 1000; // Neon: 5分钟,PostgreSQL: 10分钟
// 重置空闲计时器
function resetIdleTimer() {
if (idleTimer) {
clearTimeout(idleTimer);
}
// 只在生产环境启用自动断开
if (process.env.NODE_ENV === 'production') {
idleTimer = setTimeout(async () => {
try {
if (!client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 Auto-closing idle ${dbType} database connections${isNeonDatabase ? ' for Serverless optimization' : ''}`);
await client.end({ timeout: isNeonDatabase ? 5 : 10 });
}
} catch (error) {
console.error('❌ Error during auto-close:', error);
}
}, IDLE_TIMEOUT);
}
}
// 包装数据库操作以支持自动启停
export function withAutoReconnect<T extends any[], R>(
operation: (...args: T) => Promise<R>
) {
return async (...args: T): Promise<R> => {
resetIdleTimer();
try {
return await operation(...args);
} catch (error: any) {
// 如果连接已关闭,记录信息
if (error?.code === 'CONNECTION_ENDED' || client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 ${dbType} database connection ended${isNeonDatabase ? ', Neon will auto-reconnect on next query' : ', will reconnect on next query'}`);
}
throw error;
}
};
}
// 优雅关闭数据库连接
export async function closeConnection() {
try {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
if (!client.ended) {
await client.end({ timeout: 10 });
console.log('✅ Database connection closed gracefully');
}
} catch (error) {
console.error('❌ Error closing database connection:', error);
}
}
// 设置优雅关闭处理
if (typeof process !== 'undefined') {
const gracefulShutdown = async () => {
console.log('🔄 Shutting down database connections...');
await closeConnection();
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('beforeExit', gracefulShutdown);
}