// app/api/postgrest-client.ts // import { AsyncLocalStorage } from 'async_hooks'; import { apiRequest, type QueryParams } from './axios-client'; import { handleApiError } from './error-handler'; /** * 请求上下文接口 */ // interface RequestContext { // jwt?: string; // } /** * 创建异步本地存储用于存储请求上下文 */ // const requestContext = new AsyncLocalStorage(); /** * 在指定的上下文中运行函数 * @param context 上下文对象 * @param fn 要执行的函数 * @returns 函数执行结果 */ // export function runWithContext( // context: RequestContext, // fn: () => T | Promise // ): T | Promise { // return requestContext.run(context, fn); // } /** * 获取当前上下文中的 JWT * @returns JWT token 或 undefined */ // function getContextJWT(): string | undefined { // const context = requestContext.getStore(); // return context?.jwt; // } /** * PostgresREST 特定的查询参数接口 */ export interface PostgrestParams { // 查询参数 select?: string; // 排序参数 order?: string; // 分页参数 limit?: number; offset?: number; // 过滤参数 filter?: Record; // 指定 PostgreSQL schema schema?: string; // 支持OR条件查询 or?: Array> | string; // 其他参数 [key: string]: unknown; // 自定义头部参数 headers?: Record; // JWT Token(自动添加到 Authorization 头部) token?: string; } /** * 将编码后的URL解码为可读格式 * @param url 编码后的URL * @returns 解码后的可读URL */ function decodeUrlForDisplay(url: string): string { try { // 首先解码整个URL const decodedUrl = decodeURIComponent(url); // 如果URL中包含@符号作为前缀,则移除它 if (decodedUrl.startsWith('@')) { return decodedUrl.substring(1); } return decodedUrl; } catch (error) { // 如果解码失败,返回原始URL console.error('URL解码失败:', error); return url; } } /** * 合并 JWT Token 到请求头 * @param existingHeaders 已有的请求头 * @param explicitToken 显式传入的 JWT Token(可选) * @returns 合并后的请求头 */ function mergeAuthHeaders( existingHeaders: Record = {}, explicitToken?: string ): Record { const headers = { ...existingHeaders }; // 如果已经有 Authorization 头部(不区分大小写),不覆盖 const hasAuth = Object.keys(headers).some( key => key.toLowerCase() === 'authorization' ); if (hasAuth) { console.log('🔑 [mergeAuthHeaders] 已存在 Authorization 头,不覆盖'); return headers; } // 优先使用显式传入的 token,否则尝试从客户端 localStorage 获取 const token = explicitToken || (typeof window !== 'undefined' ? localStorage.getItem('access_token') : undefined); // 如果有有效的 token(显式传入或从客户端获取),添加到 Authorization 头部 if (token && token !== 'undefined') { // console.log('🔑 [mergeAuthHeaders] 添加 Authorization 头,token 来源:', explicitToken ? 'explicitToken' : 'localStorage'); // console.log('🔑 [mergeAuthHeaders] Token 前10位:', token.substring(0, 10)); headers['Authorization'] = `Bearer ${token}`; } else { console.warn('⚠️ [mergeAuthHeaders] 没有可用的 token!explicitToken:', explicitToken, 'window:', typeof window); } return headers; } /** * 打印 PostgREST 查询日志 * @param endpoint 端点 * @param params 参数 * @param method HTTP 方法 * @param data 请求体数据(用于POST/PATCH请求) */ function logPostgrestQuery(endpoint: string, params?: QueryParams, method: string = 'GET', data?: Record): void { if (process.env.NODE_ENV !== 'production') { // const baseUrl = 'http://172.16.0.119:9000/admin'; // const baseUrl = 'http://172.18.0.100:3000'; const baseUrl = 'http://nas.7bm.co:3000'; // 确保 endpoint 格式正确 const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; let fullUrl = `${baseUrl}/${normalizedEndpoint}`; // 如果有查询参数,将其添加到URL中 if (params && Object.keys(params).length > 0) { const queryString = Object.entries(params) .filter(([, value]) => value !== undefined) .map(([key, value]) => { // 处理特殊的PostgREST操作符(如eq, gt, lt等) if (typeof value === 'string' && value.includes('.')) { return `${key}=${encodeURIComponent(value)}`; } return `${key}=${encodeURIComponent(String(value))}`; }) .join('&'); if (queryString) { fullUrl += `?${queryString}`; } } // console.log('\n📦 PostgREST 查询日志 ========================'); // console.log(`📦 HTTP 方法: ${method}`); // console.log(`📦 完整 URL: ${decodeUrlForDisplay(fullUrl)}`); // 打印请求体数据(仅适用于POST/PATCH/PUT请求) // if (['POST', 'PATCH', 'PUT'].includes(method) && data) { // console.log('📦 请求体数据:'); // console.log(JSON.stringify(data, null, 2)); // } // console.log('📦 PostgREST 查询日志 ========================\n'); } } /** * 将通用查询参数转换为 PostgresREST 支持的格式 * @param params 查询参数 * @returns 转换后的 PostgresREST 参数 */ export function transformParams(params: PostgrestParams): QueryParams { const result: QueryParams = {}; // 处理 select 参数 if (params.select) { result.select = params.select; } // 处理 order 参数 if (params.order) { result.order = params.order; } // 处理 limit 和 offset 参数 if (params.limit !== undefined) { result.limit = params.limit; } if (params.offset !== undefined) { result.offset = params.offset; } // 处理 schema 参数 if (params.schema) { result.schema = params.schema; } // 处理或条件 (OR) - 两种格式支持 if (params.or) { // 如果是字符串格式 (例如 "name.ilike.*keyword*,code.ilike.*keyword*") if (typeof params.or === 'string') { result.or = params.or; } // 如果是数组格式, 转换为 PostgREST 的 or=(condition1,condition2) 格式 else if (Array.isArray(params.or)) { const orConditions: string[] = []; params.or.forEach(condition => { const entries = Object.entries(condition); if (entries.length === 1) { const [field, operator] = entries[0]; orConditions.push(`${field}.${operator}`); } }); if (orConditions.length > 0) { result.or = `(${orConditions.join(',')})`; } } } // 处理过滤条件 - PostgresREST 格式 if (params.filter) { Object.entries(params.filter).forEach(([key, value]) => { // 如果值不为 undefined,则添加到查询参数中 if (value !== undefined) { // 支持 PostgreSQL 的比较操作符 (eq, gt, lt, gte, lte, like, ilike 等) result[key] = value as string | number | boolean; } }); } // 处理其他额外参数 Object.entries(params).forEach(([key, value]) => { // 跳过已处理的特殊参数(包括 headers 和 token) if (!['select', 'order', 'limit', 'offset', 'filter', 'schema', 'or', 'headers', 'token'].includes(key) && value !== undefined) { result[key] = value as string | number | boolean; } }); return result; } /** * 发送 GET 请求到 PostgresREST 接口 * @param endpoint 端点 * @param params 查询参数(可包含 token 和 headers) * @returns 响应数据 */ export async function postgrestGet(endpoint: string, params?: PostgrestParams): Promise<{data: T; headers?: Record; error?: never} | {data?: never; error: string; status?: number}> { try { const queryParams = params ? transformParams(params) : {}; // 确保端点没有前导斜杠,因为API_BASE_URL已经包含了路径前缀 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 打印查询信息 logPostgrestQuery(apiEndpoint, queryParams, 'GET'); // 合并 JWT Token 到请求头 const headers = mergeAuthHeaders(params?.headers, params?.token); const response = await apiRequest( apiEndpoint, { method: 'GET', headers: headers }, queryParams ); if (response.error) { // 🔑 检测令牌过期错误 const isTokenExpired = response.error.includes('令牌已过期') || response.error.includes('令牌') || response.error.includes('token') || response.error.includes('expired') || response.error.includes('认证') || response.error.includes('未授权'); if (isTokenExpired && typeof window !== 'undefined') { console.error('🔑 [PostgREST Client - GET] 检测到令牌过期,清除会话并重定向到登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); sessionStorage.clear(); window.location.href = '/login?expired=true'; } throw new Error(response.error); } // 返回数据和响应头 return { data: response.data as T, headers: response.headers }; } catch (error) { const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; } } /** * 处理 PostgreSQL 特定错误 * @param error 错误对象或消息 * @param responseText 原始响应文本 * @returns 格式化的错误信息 */ function handlePostgresError(error: unknown, responseText?: string): { message: string, status?: number } { let errorMessage = error instanceof Error ? error.message : String(error); let statusCode: number | undefined = undefined; // 如果有原始响应文本,尝试从中提取更详细的错误信息 if (responseText) { try { const errorData = JSON.parse(responseText); // 处理 PostgreSQL 错误码 if (errorData.code) { switch (errorData.code) { case '23505': // 唯一约束冲突 errorMessage = '记录已存在,无法创建重复数据'; if (errorData.details) { // 尝试提取冲突的字段名 const match = errorData.details.match(/Key \((.+?)\)=/); if (match && match[1]) { const field = match[1]; errorMessage = `${field} 已存在,请使用不同的值`; } } statusCode = 409; // Conflict break; case '23503': // 外键约束失败 errorMessage = '引用的记录不存在'; if (errorData.details) { // 尝试提取外键字段 const match = errorData.details.match(/Key \((.+?)\)=/); if (match && match[1]) { const field = match[1]; errorMessage = `所引用的 ${field} 不存在或无效`; } } statusCode = 400; // Bad Request break; case '42P01': // 表不存在 errorMessage = '所请求的数据表不存在'; statusCode = 404; // Not Found break; case '42703': // 列不存在 errorMessage = '所引用的字段不存在'; if (errorData.details) { const match = errorData.details.match(/column "(.+?)"/); if (match && match[1]) { const column = match[1]; errorMessage = `字段 "${column}" 不存在`; } } statusCode = 400; // Bad Request break; default: // 使用 PostgreSQL 提供的错误消息 if (errorData.message) { errorMessage = errorData.message; } break; } } // 处理 HTTP 状态码 if (errorData.status) { statusCode = errorData.status; } } catch (e) { console.error('解析错误响应失败:', e); // 如果解析失败,尝试直接使用响应文本 if (responseText && responseText.length < 200) { errorMessage = responseText; } } } return { message: errorMessage, status: statusCode }; } /** * 发送 POST 请求到 PostgresREST 接口 * @param endpoint 端点(表名) * @param data 请求体数据 * @param token JWT Token(可选) * @returns 响应数据 */ export async function postgrestPost>(endpoint: string, data: D, token?: string): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 预处理数据,确保所有字段类型符合 PostgreSQL 要求 const processedData = preprocessData(data as Record); // 打印查询信息 logPostgrestQuery(apiEndpoint, undefined, 'POST', processedData); // 确保数据是合法的JSON对象 const requestBody = JSON.stringify(processedData); // console.log(`准备发送 PostgreSQL 插入请求到: ${apiEndpoint}`); // console.log(`请求体: ${requestBody}`); // 合并 JWT Token 到请求头 const headers = mergeAuthHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json', 'Prefer': 'return=representation' }, token); try { const response = await apiRequest( apiEndpoint, { method: 'POST', body: requestBody, headers: headers } ); if (response.error) { console.error(`POST请求失败: ${response.error}`); // 🔑 检测令牌过期错误 const isTokenExpired = response.error.includes('令牌已过期') || response.error.includes('令牌') || response.error.includes('token') || response.error.includes('expired') || response.error.includes('认证') || response.error.includes('未授权'); if (isTokenExpired && typeof window !== 'undefined') { console.error('🔑 [PostgREST Client] 检测到令牌过期,清除会话并重定向到登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); sessionStorage.clear(); window.location.href = '/login?expired=true'; } throw new Error(response.error); } // console.log(`POST请求成功,响应: `, response.data); return { data: response.data as T }; } catch (error) { // 捕获并处理 API 请求错误 let errorText = ''; // 如果错误对象中包含响应文本,尝试提取更详细的错误信息 if (error instanceof Error && 'responseText' in error) { errorText = (error as {responseText: string}).responseText; } // 处理 PostgreSQL 特定错误 const pgError = handlePostgresError(error, errorText); console.error(`POST请求错误: ${pgError.message}`); return { error: pgError.message, status: pgError.status || 500 }; } } catch (error) { console.error(`POST请求处理错误: ${error instanceof Error ? error.message : String(error)}`); const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; } } /** * 预处理数据,确保类型与 PostgreSQL 兼容 * @param data 原始数据 * @returns 处理后的数据 */ function preprocessData(data: Record): Record { const processed: Record = {}; for (const [key, value] of Object.entries(data)) { // 确保 null 值被正确处理 if (value === null) { processed[key] = null; continue; } // 处理字符串可能被错误解析为数字的情况 if (typeof value === 'string' && /^\d+$/.test(value) && key !== 'code' && !key.endsWith('_id')) { // 对于可能需要保持字符串格式的值,我们不进行数字转换 processed[key] = value; } // 将字符串 'true'/'false' 转为布尔值 else if (typeof value === 'string' && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { processed[key] = value.toLowerCase() === 'true'; } // 尝试转换 '0'/'1' 为 boolean (如果字段名暗示是布尔值) else if (typeof value === 'string' && (value === '0' || value === '1') && (key.startsWith('is_') || key.startsWith('has_') || key.endsWith('_enabled'))) { processed[key] = value === '1'; } // 对于ID字段确保使用正确的类型 else if ((key === 'id' || key.endsWith('_id') || key === 'pid') && value !== undefined) { try { const numValue = Number(value); if (!isNaN(numValue)) { processed[key] = numValue; } else { processed[key] = value; } } catch { processed[key] = value; } } // 其他值保持不变 else { processed[key] = value; } } return processed; } /** * 发送 PUT 请求到 PostgresREST 接口 * @param endpoint 端点 * @param data 请求体数据 * @param filters 过滤条件 * @param token JWT Token(可选) * @returns 响应数据 */ export async function postgrestPut( endpoint: string, data: D, filters?: Record, token?: string ): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 构建完整的URL,包含过滤条件 const fullEndpoint = apiEndpoint; const queryParams: QueryParams = {}; if (filters) { // 将过滤条件转换为PostgREST格式的查询参数 Object.entries(filters).forEach(([key, value]) => { queryParams[key] = `eq.${value}`; }); } // 打印查询信息 logPostgrestQuery(fullEndpoint, queryParams, 'PATCH', data as unknown as Record); // 合并 JWT Token 到请求头 const headers = mergeAuthHeaders({ 'Prefer': 'return=representation' }, token); const response = await apiRequest( fullEndpoint, { method: 'PATCH', body: JSON.stringify(data), headers: headers }, queryParams ); if (response.error) { // 🔑 检测令牌过期错误 const isTokenExpired = response.error.includes('令牌已过期') || response.error.includes('令牌') || response.error.includes('token') || response.error.includes('expired') || response.error.includes('认证') || response.error.includes('未授权'); if (isTokenExpired && typeof window !== 'undefined') { console.error('🔑 [PostgREST Client - PATCH] 检测到令牌过期,清除会话并重定向到登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); sessionStorage.clear(); window.location.href = '/login?expired=true'; } throw new Error(response.error); } if (!response.data) { throw new Error('更新成功但未返回数据'); } return { data: response.data }; } catch (error) { const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; } } /** * 发送 DELETE 请求到 PostgresREST 接口 * @param endpoint 端点 * @param params 查询参数,用于指定要删除的记录(可包含 token 和 headers) * @returns 响应数据 */ export async function postgrestDelete(endpoint: string, params?: PostgrestParams): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 转换查询参数 const queryParams = params ? transformParams(params) : {}; // 合并 JWT Token 到请求头 const headers = mergeAuthHeaders({ 'Prefer': 'return=representation', // 默认请求返回被删除的记录 ...(params?.headers || {}) }, params?.token); // 打印查询信息 logPostgrestQuery(apiEndpoint, queryParams, 'DELETE'); const response = await apiRequest( apiEndpoint, { method: 'DELETE', headers }, queryParams ); if (response.error) { // 🔑 检测令牌过期错误 const isTokenExpired = response.error.includes('令牌已过期') || response.error.includes('令牌') || response.error.includes('token') || response.error.includes('expired') || response.error.includes('认证') || response.error.includes('未授权'); if (isTokenExpired && typeof window !== 'undefined') { console.error('🔑 [PostgREST Client - DELETE] 检测到令牌过期,清除会话并重定向到登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); sessionStorage.clear(); window.location.href = '/login?expired=true'; } throw new Error(response.error); } return { data: response.data as T }; } catch (error) { const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; } }