import "server-only"; import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as schema from "./schema"; /** * 数据库客户端(node-postgres 连接池)。 * * ## 为什么用 pg 而不是 neon-http * * 脚手架原本使用 `drizzle-orm/neon-http` + `@neondatabase/serverless`。 * 该驱动走的是 **Neon 的 HTTP 代理端点**,不是 Postgres 的 TCP 协议, * 因此无法连接本地或自建的普通 Postgres。 * * 本项目改用 `pg`(node-postgres),它既能连本地 Postgres, * 也能连 Neon / Supabase / Railway 等任何标准 Postgres—— * 部署到 Neon 时只需保留其 TCP 连接串(`*.neon.tech` 的 5432 端口)。 * * ## 连接池与 Next.js 开发模式 * * Next.js 开发模式下模块会被热重载,若不缓存连接池,每次改动都会新建一批 * 连接,很快耗尽数据库的 max_connections。因此把 pool 挂到 globalThis 上做单例。 */ const connectionString = process.env.DATABASE_URL; if (!connectionString) { throw new Error( "缺少环境变量 DATABASE_URL。请在 .env.local 中配置 Postgres 连接串," + "例如:postgresql://user:password@localhost:5432/blog", ); } // 收窄为 string,供下方闭包使用(模块级已判空,但闭包内 TS 无法自动收窄) const resolvedConnectionString: string = connectionString; /** 在 globalThis 上缓存连接池,避免开发模式热重载导致的连接泄漏 */ const globalForDb = globalThis as unknown as { __postgresPool?: Pool; }; function createPool(): Pool { const pool = new Pool({ connectionString: resolvedConnectionString, // 本地开发(localhost)通常没配证书,强制 SSL 会直接失败; // 托管数据库(Neon / Supabase 等)则要求 SSL。 ssl: shouldUseSsl(resolvedConnectionString) ? { rejectUnauthorized: false } : undefined, // 单实例默认 10 连接足够;Serverless 环境建议调小或接 Neon 的 pooler max: Number(process.env.DATABASE_POOL_MAX ?? 10), idleTimeoutMillis: 30_000, connectionTimeoutMillis: 10_000, // 语句级超时,避免慢查询长期占用连接 statement_timeout: Number(process.env.DATABASE_STATEMENT_TIMEOUT_MS ?? 15_000), }); // 连接池错误不应导致进程崩溃(例如数据库重启期间的瞬时失败) pool.on("error", (error) => { console.error("[db] 空闲连接异常:", error.message); }); return pool; } /** 判断是否需要启用 SSL:本地地址不启用,远端默认启用 */ function shouldUseSsl(url: string): boolean { try { const { hostname, searchParams } = new URL(url); const sslmode = searchParams.get("sslmode"); // 显式声明优先 if (sslmode === "disable") { return false; } if (sslmode === "require" || sslmode === "verify-full") { return true; } // 未显式声明时,按主机推断 const isLocal = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".local"); return !isLocal; } catch { // URL 解析失败时交给 pg 自己报错,这里默认不启用 SSL return false; } } const pool = globalForDb.__postgresPool ?? createPool(); // 生产环境不需要挂载(模块不会热重载),但挂载也无副作用 globalForDb.__postgresPool = pool; export const db = drizzle(pool, { schema }); /** 底层连接池,供健康检查与脚本使用 */ export { pool }; export type DB = typeof db; /** 检查数据库连通性,用于健康检查接口 */ export async function checkDatabaseHealth(): Promise<{ ok: boolean; latencyMs: number; error?: string; }> { const startedAt = Date.now(); try { await pool.query("select 1"); return { ok: true, latencyMs: Date.now() - startedAt }; } catch (error) { return { ok: false, latencyMs: Date.now() - startedAt, error: error instanceof Error ? error.message : String(error), }; } }