feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
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);
}