136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
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);
|
||
console.log('[SQL Query]', sqlQuery);
|
||
console.log('[SQL Query HEX first 100 chars]', Buffer.from(sqlQuery.substring(0, 100)).toString('hex'));
|
||
console.log('[SQL Params]', params);
|
||
|
||
// Execute with retry mechanism
|
||
const result = await this.retryQuery(async () => {
|
||
const queryResult = await pool.query(sqlQuery, params);
|
||
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();
|