feat: 添加 Vercel Postgres 数据库支持
- 安装 @vercel/postgres 和 pg 依赖 - 创建 PostgresAdapter 适配器,兼容 D1 接口 - 创建 PostgresStorage 类,实现完整的 IStorage 接口 - 添加 Postgres 数据库初始化脚本和 schema - 更 next.config.js 排除 Postgres 模块的客户端打包 - 添加 VERCEL_DEPLOYMENT.md 部署指南 - 支持 Vercel serverless 环境部署 注意事项:观影室功能在 Vercel 上不可用(需要 WebSocket 支持)
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Vercel Postgres (Neon/Postgres) 适配器
|
||||
*
|
||||
* 将 Vercel Postgres API 转换为与 D1 兼容的接口
|
||||
*
|
||||
* 注意:此模块仅在服务端使用,通过 webpack 配置排除客户端打包
|
||||
*/
|
||||
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { DatabaseAdapter, D1PreparedStatement, D1Result } from './d1-adapter';
|
||||
|
||||
/**
|
||||
* Vercel Postgres 适配器
|
||||
*
|
||||
* 使用 @vercel/postgres 包装为 D1 兼容接口
|
||||
*/
|
||||
export class PostgresAdapter implements DatabaseAdapter {
|
||||
private queryParams: { query: string; values: any[] } | null = null;
|
||||
|
||||
prepare(query: string): D1PreparedStatement {
|
||||
return new PostgresPreparedStatement(query);
|
||||
}
|
||||
|
||||
batch(statements: D1PreparedStatement[]): Promise<D1Result[]> {
|
||||
// Postgres 使用事务模拟 batch
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.all(statements.map((stmt) => (stmt as PostgresPreparedStatement).execute()))
|
||||
.then((results) => resolve(results))
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
exec(query: string): void {
|
||||
// Vercel Postgres 不支持直接 exec,需要使用 sql 模板
|
||||
throw new Error('exec() is not supported for Vercel Postgres. Use prepare() instead.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vercel Postgres PreparedStatement 包装器
|
||||
* 将 Vercel Postgres API 转换为 D1 兼容 API
|
||||
*/
|
||||
class PostgresPreparedStatement implements D1PreparedStatement {
|
||||
private params: any[] = [];
|
||||
private paramIndex = 1;
|
||||
|
||||
constructor(private query: string) {}
|
||||
|
||||
bind(...values: any[]): D1PreparedStatement {
|
||||
this.params = values;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SQLite 风格的 ? 占位符替换为 Postgres 风格的 $1, $2, ...
|
||||
*/
|
||||
private convertQuery(query: string): string {
|
||||
let index = 1;
|
||||
return query.replace(/\?/g, () => `$${index++}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SQL 查询中的表名和列名转换为双引号包裹(Postgres 要求)
|
||||
* 注意:需要排除已经有引号的内容
|
||||
*/
|
||||
private quoteIdentifiers(query: string): string {
|
||||
// 这个方法主要用于处理列值,表名在 schema 中已经创建好
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询并返回第一行
|
||||
*/
|
||||
async first<T = any>(colName?: string): Promise<T | null> {
|
||||
try {
|
||||
const convertedQuery = this.convertQuery(this.query);
|
||||
|
||||
// 使用 Vercel Postgres 的 unsafe 方法执行参数化查询
|
||||
const result = await sql.unsafe(convertedQuery, this.params);
|
||||
|
||||
if (!result || result.rows.length === 0) return null;
|
||||
|
||||
const row = result.rows[0];
|
||||
|
||||
if (colName) return row[colName] ?? null;
|
||||
|
||||
return row as T;
|
||||
} catch (err) {
|
||||
console.error('Postgres first() error:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询并返回结果
|
||||
*/
|
||||
async run<T = any>(): Promise<D1Result<T>> {
|
||||
try {
|
||||
const convertedQuery = this.convertQuery(this.query);
|
||||
|
||||
const result = await sql.unsafe(convertedQuery, this.params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
meta: {
|
||||
changes: result.rowCount || 0,
|
||||
last_row_id: null, // Postgres 不直接返回 lastInsertId
|
||||
},
|
||||
results: result.rows,
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('Postgres run() error:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询并返回所有行
|
||||
*/
|
||||
async all<T = any>(): Promise<D1Result<T>> {
|
||||
try {
|
||||
const convertedQuery = this.convertQuery(this.query);
|
||||
|
||||
const result = await sql.unsafe(convertedQuery, this.params);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results: result.rows || [],
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('Postgres all() error:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
results: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部执行方法(用于 batch 操作)
|
||||
*/
|
||||
async execute(): Promise<D1Result> {
|
||||
return this.run();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user