Files
api_builder/backend/src/services/SqlExecutor.ts
2025-11-29 16:28:02 +03:00

153 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { QueryResult } from '../types';
import { databasePoolManager } from './DatabasePoolManager';
export class SqlExecutor {
private async retryQuery<T>(
fn: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error: any) {
// Retry only on connection-related errors
const isConnectionError =
error.message?.includes('timeout') ||
error.message?.includes('Connection terminated') ||
error.message?.includes('ECONNREFUSED') ||
error.message?.includes('ETIMEDOUT');
if (!isConnectionError || attempt === retries) {
throw error;
}
console.warn(`Query attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
async executeQuery(
databaseId: string,
sqlQuery: string,
params: any[] = []
): Promise<QueryResult> {
const pool = databasePoolManager.getPool(databaseId);
if (!pool) {
throw new Error(`Database with id ${databaseId} not found or not configured`);
}
const startTime = Date.now();
try {
// Security: Prevent multiple statements and dangerous commands
this.validateQuery(sqlQuery);
// Log SQL query and parameters before execution
console.log('\n[SQL DB]', databaseId);
// @ts-ignore - accessing pool options for debugging
const poolOpts = pool.options;
console.log('[SQL Pool Config] host:', poolOpts?.host, 'database:', poolOpts?.database, 'user:', poolOpts?.user);
console.log('[SQL Query]', sqlQuery);
console.log('[SQL Params]', params);
// Execute with retry mechanism
const result = await this.retryQuery(async () => {
// Check if connected to replica
const debugResult = await pool.query(`
SELECT
pg_backend_pid() as pid,
pg_is_in_recovery() as is_replica,
inet_server_addr() as server_ip
`);
const d = debugResult.rows[0];
console.log('[SQL Debug] pid:', d?.pid, 'is_replica:', d?.is_replica, 'server_ip:', d?.server_ip);
// Disable prepared statements by using unique name each time
const queryResult = await pool.query({
text: sqlQuery,
values: params,
name: undefined // This disables prepared statement caching
});
console.log('[SQL Result] rowCount:', queryResult.rowCount, 'rows:', JSON.stringify(queryResult.rows).substring(0, 500));
return queryResult;
}, 3, 500); // 3 попытки с задержкой 500ms
const executionTime = Date.now() - startTime;
return {
rows: result.rows,
rowCount: result.rowCount || 0,
executionTime,
};
} catch (error: any) {
console.error('SQL Execution Error:', error);
throw new Error(`SQL Error: ${error.message}`);
}
}
private validateQuery(sqlQuery: string) {
const normalized = sqlQuery.trim().toLowerCase();
// Prevent multiple statements (basic check)
if (normalized.includes(';') && normalized.indexOf(';') < normalized.length - 1) {
throw new Error('Multiple SQL statements are not allowed');
}
// Prevent dangerous commands (you can extend this list)
const dangerousCommands = ['drop', 'truncate', 'delete from', 'alter', 'create', 'grant', 'revoke'];
const isDangerous = dangerousCommands.some(cmd => normalized.startsWith(cmd));
if (isDangerous && !normalized.startsWith('select')) {
// For safety, you might want to allow only SELECT queries
// Or implement a whitelist/permission system for write operations
console.warn('Potentially dangerous query detected:', sqlQuery);
}
}
async testQuery(databaseId: string, sqlQuery: string): Promise<{ success: boolean; error?: string }> {
try {
await this.executeQuery(databaseId, sqlQuery);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getTableSchema(databaseId: string, tableName: string): Promise<any[]> {
const query = `
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM
information_schema.columns
WHERE
table_name = $1
ORDER BY
ordinal_position;
`;
const result = await this.executeQuery(databaseId, query, [tableName]);
return result.rows;
}
async getAllTables(databaseId: string): Promise<string[]> {
const query = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
`;
const result = await this.executeQuery(databaseId, query);
return result.rows.map(row => row.table_name);
}
}
export const sqlExecutor = new SqlExecutor();