174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import {drizzle} from 'drizzle-orm/postgres-js';
|
|
import {and, asc, count, desc, eq, exists, gt, gte, lt, lte, 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 isNeonDatabase = connectionString.includes('neon.tech') || connectionString.includes('neon.database.com');
|
|
|
|
// 根据数据库类型选择配置
|
|
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 {
|
|
max: process.env.NODE_ENV === 'production' ? 10 : 5, // 普通PostgreSQL可以支持更多连接
|
|
idle_timeout: 20, // 增加空闲超时时间
|
|
connect_timeout: 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, 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() {
|
|
return {
|
|
isConnected: !client.ended,
|
|
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);
|
|
} |