177 lines
6.4 KiB
TypeScript
177 lines
6.4 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') });
|
||
|
||
// 检查环境变量
|
||
// 注意:在 CI/Nuxt prerender 构建阶段,可能没有 DATABASE_URL
|
||
const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:dummy@localhost:5432/dummy';
|
||
if (!process.env.DATABASE_URL && !process.env.npm_lifecycle_script?.includes('build')) {
|
||
console.warn('Warning: DATABASE_URL environment variable is not set. Using dummy connection string for build/prerender.');
|
||
}
|
||
|
||
// 检测数据库类型
|
||
const isNeonDatabase = connectionString.includes('neon.tech') || connectionString.includes('neon.database.com');
|
||
|
||
// 检测 Serverless 部署环境
|
||
const isServerlessEnv = process.env.NETLIFY === 'true' || !!process.env.VERCEL;
|
||
|
||
// 根据数据库类型和部署环境选择配置
|
||
const getDatabaseConfig = () => {
|
||
if (isNeonDatabase || isServerlessEnv) {
|
||
// Neon Database / Serverless 环境 (Netlify/Vercel) 优化配置
|
||
return {
|
||
max: 1, // Serverless 环境下每个实例保持最小连接数,利用平台自身的连接池
|
||
idle_timeout: 1, // 尽快释放空闲连接,适应 Serverless 的快速冻结特性
|
||
connect_timeout: 10, // 减少连接超时时间
|
||
max_lifetime: 3600, // 连接最大生命周期(1小时)
|
||
ssl: isNeonDatabase ? 'require' : (connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false),
|
||
prepare: false, // 禁用预处理语句以提高兼容性
|
||
keep_alive: false, // 禁用 TCP keep-alive,避免 Serverless 环境下连接残留
|
||
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 数据库配置(本地开发 / 常规 Node Server)
|
||
return {
|
||
max: 10, // 普通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);
|
||
} |