Fix script execution logs being lost

- Add ScriptExecutionError class that preserves captured logs/queries
- IsolatedScriptExecutor: throw ScriptExecutionError with accumulated
  logs instead of plain Error on script failure
- ScriptExecutor (Python): same fix for Python execution errors
- testEndpoint: return captured logs/queries on script errors
- dynamicApiController: correctly extract scriptResult.result instead
  of stuffing entire IsolatedExecutionResult into rows; include logs
  in detailed_response output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 02:52:51 +03:00
parent 3a3c87164d
commit 49f262d8ae
5 changed files with 35 additions and 12 deletions

View File

@@ -3,7 +3,7 @@ import { ApiKeyRequest } from '../middleware/apiKey';
import { mainPool } from '../config/database'; import { mainPool } from '../config/database';
import { sqlExecutor } from '../services/SqlExecutor'; import { sqlExecutor } from '../services/SqlExecutor';
import { scriptExecutor } from '../services/ScriptExecutor'; import { scriptExecutor } from '../services/ScriptExecutor';
import { EndpointParameter, ScriptQuery } from '../types'; import { EndpointParameter, ScriptQuery, ScriptExecutionError } from '../types';
export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) => { export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) => {
const startTime = Date.now(); const startTime = Date.now();
@@ -202,9 +202,11 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
}); });
result = { result = {
rows: scriptResult, rows: scriptResult.result,
rowCount: 0, rowCount: 0,
executionTime: 0, executionTime: 0,
scriptLogs: scriptResult.logs,
scriptQueries: scriptResult.queries,
}; };
} else { } else {
// Execute SQL query // Execute SQL query
@@ -241,6 +243,8 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
data: result.rows, data: result.rows,
rowCount: result.rowCount, rowCount: result.rowCount,
executionTime: result.executionTime, executionTime: result.executionTime,
...(result.scriptLogs && result.scriptLogs.length > 0 ? { logs: result.scriptLogs } : {}),
...(result.scriptQueries && result.scriptQueries.length > 0 ? { queries: result.scriptQueries } : {}),
} }
: result.rows; : result.rows;
@@ -267,9 +271,12 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
} catch (error: any) { } catch (error: any) {
console.error('Dynamic API execution error:', error); console.error('Dynamic API execution error:', error);
const errorResponse = { const isScriptError = error instanceof ScriptExecutionError;
const errorResponse: any = {
success: false, success: false,
error: error.message, error: error.message,
...(isScriptError && error.logs.length > 0 ? { logs: error.logs } : {}),
...(isScriptError && error.queries.length > 0 ? { queries: error.queries } : {}),
}; };
// Log error if needed // Log error if needed

View File

@@ -2,7 +2,7 @@ import { Response } from 'express';
import { AuthRequest } from '../middleware/auth'; import { AuthRequest } from '../middleware/auth';
import { mainPool } from '../config/database'; import { mainPool } from '../config/database';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { ExportedEndpoint, ExportedScriptQuery } from '../types'; import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError } from '../types';
import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto'; import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto';
export const getEndpoints = async (req: AuthRequest, res: Response) => { export const getEndpoints = async (req: AuthRequest, res: Response) => {
@@ -388,13 +388,14 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
return res.status(400).json({ error: 'Invalid execution_type' }); return res.status(400).json({ error: 'Invalid execution_type' });
} }
} catch (error: any) { } catch (error: any) {
const isScriptError = error instanceof ScriptExecutionError;
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: error.message, error: error.message,
detail: error.detail || undefined, detail: error.detail || undefined,
hint: error.hint || undefined, hint: error.hint || undefined,
logs: [], logs: isScriptError ? error.logs : [],
queries: [], queries: isScriptError ? error.queries : [],
}); });
} }
}; };

View File

@@ -1,7 +1,7 @@
import * as vm from 'vm'; import * as vm from 'vm';
import { sqlExecutor } from './SqlExecutor'; import { sqlExecutor } from './SqlExecutor';
import { aqlExecutor } from './AqlExecutor'; import { aqlExecutor } from './AqlExecutor';
import { ScriptQuery, EndpointParameter, LogEntry, QueryExecution, IsolatedExecutionResult } from '../types'; import { ScriptQuery, EndpointParameter, LogEntry, QueryExecution, IsolatedExecutionResult, ScriptExecutionError } from '../types';
import { databasePoolManager } from './DatabasePoolManager'; import { databasePoolManager } from './DatabasePoolManager';
interface IsolatedScriptContext { interface IsolatedScriptContext {
@@ -228,7 +228,9 @@ export class IsolatedScriptExecutor {
} }
timerIds.clear(); timerIds.clear();
throw new Error(`JavaScript execution error: ${error.message}`); // Preserve captured logs and queries in the error
logs.push({ type: 'error', message: error.message, timestamp: Date.now() });
throw new ScriptExecutionError(`JavaScript execution error: ${error.message}`, logs, queries);
} }
} }

View File

@@ -1,7 +1,7 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { sqlExecutor } from './SqlExecutor'; import { sqlExecutor } from './SqlExecutor';
import { aqlExecutor } from './AqlExecutor'; import { aqlExecutor } from './AqlExecutor';
import { ScriptQuery, EndpointParameter, LogEntry, QueryExecution, IsolatedExecutionResult } from '../types'; import { ScriptQuery, EndpointParameter, LogEntry, QueryExecution, IsolatedExecutionResult, ScriptExecutionError } from '../types';
import { databasePoolManager } from './DatabasePoolManager'; import { databasePoolManager } from './DatabasePoolManager';
import { isolatedScriptExecutor } from './IsolatedScriptExecutor'; import { isolatedScriptExecutor } from './IsolatedScriptExecutor';
@@ -258,7 +258,8 @@ print(json.dumps(result))
python.on('close', (exitCode) => { python.on('close', (exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
reject(new Error(`Python execution error: ${errorOutput}`)); logs.push({ type: 'error', message: errorOutput, timestamp: Date.now() });
reject(new ScriptExecutionError(`Python execution error: ${errorOutput}`, logs, queries));
} else { } else {
try { try {
// Последняя строка вывода - результат, остальные - логи // Последняя строка вывода - результат, остальные - логи
@@ -276,7 +277,7 @@ print(json.dumps(result))
const result = JSON.parse(resultLine); const result = JSON.parse(resultLine);
resolve({ result, logs, queries }); resolve({ result, logs, queries });
} catch (error) { } catch (error) {
reject(new Error(`Failed to parse Python output: ${output}`)); reject(new ScriptExecutionError(`Failed to parse Python output: ${output}`, logs, queries));
} }
} }
}); });
@@ -284,7 +285,7 @@ print(json.dumps(result))
// Таймаут 10 минут // Таймаут 10 минут
setTimeout(() => { setTimeout(() => {
python.kill(); python.kill();
reject(new Error('Python script execution timeout (10min)')); reject(new ScriptExecutionError('Python script execution timeout (10min)', logs, queries));
}, 600000); }, 600000);
}); });
} }

View File

@@ -123,6 +123,18 @@ export interface IsolatedExecutionResult {
queries: QueryExecution[]; queries: QueryExecution[];
} }
export class ScriptExecutionError extends Error {
logs: LogEntry[];
queries: QueryExecution[];
constructor(message: string, logs: LogEntry[], queries: QueryExecution[]) {
super(message);
this.name = 'ScriptExecutionError';
this.logs = logs;
this.queries = queries;
}
}
export interface SwaggerEndpoint { export interface SwaggerEndpoint {
tags: string[]; tags: string[];
summary: string; summary: string;