refactor: 타입 안전성 및 안정성 개선

- DB 결과 타입 검증용 type guard 함수 추가 (isValidServer, isValidVPSBenchmark, isValidTechSpec, isValidBenchmarkData, isValidAIRecommendation)
- 모든 DB 쿼리 결과에 타입 가드 적용하여 런타임 검증
- AI 응답 파싱에 구조 검증 추가
- OpenAI API 호출에 30초 타임아웃 추가 (AbortController)
- 타임아웃 에러 처리 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 14:28:09 +09:00
parent f87ce77595
commit 502bbd271e

View File

@@ -626,8 +626,12 @@ async function handleGetServers(
throw new Error('Database query failed'); throw new Error('Database query failed');
} }
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
const servers = result.results as unknown as Server[]; const servers = (result.results as unknown[]).filter(isValidServer);
const invalidCount = result.results.length - servers.length;
if (invalidCount > 0) {
console.warn(`[GetServers] Filtered out ${invalidCount} invalid server records`);
}
console.log('[GetServers] Found servers:', servers.length); console.log('[GetServers] Found servers:', servers.length);
@@ -819,6 +823,80 @@ async function handleRecommend(
} }
} }
/**
* Type guard to validate Server object structure
*/
function isValidServer(obj: unknown): obj is Server {
if (!obj || typeof obj !== 'object') return false;
const s = obj as Record<string, unknown>;
return (
typeof s.id === 'number' &&
typeof s.provider_name === 'string' &&
typeof s.instance_id === 'string' &&
typeof s.vcpu === 'number' &&
typeof s.memory_mb === 'number' &&
typeof s.monthly_price === 'number'
);
}
/**
* Type guard to validate VPSBenchmark object structure
*/
function isValidVPSBenchmark(obj: unknown): obj is VPSBenchmark {
if (!obj || typeof obj !== 'object') return false;
const v = obj as Record<string, unknown>;
return (
typeof v.id === 'number' &&
typeof v.provider_name === 'string' &&
typeof v.vcpu === 'number' &&
typeof v.geekbench_single === 'number'
);
}
/**
* Type guard to validate TechSpec object structure
*/
function isValidTechSpec(obj: unknown): obj is TechSpec {
if (!obj || typeof obj !== 'object') return false;
const t = obj as Record<string, unknown>;
return (
typeof t.id === 'number' &&
typeof t.name === 'string' &&
typeof t.vcpu_per_users === 'number' &&
typeof t.min_memory_mb === 'number'
);
}
/**
* Type guard to validate BenchmarkData object structure
*/
function isValidBenchmarkData(obj: unknown): obj is BenchmarkData {
if (!obj || typeof obj !== 'object') return false;
const b = obj as Record<string, unknown>;
return (
typeof b.id === 'number' &&
typeof b.processor_name === 'string' &&
typeof b.benchmark_name === 'string' &&
typeof b.score === 'number'
);
}
/**
* Type guard to validate AI recommendation structure
*/
function isValidAIRecommendation(obj: unknown): obj is AIRecommendationResponse['recommendations'][0] {
if (!obj || typeof obj !== 'object') return false;
const r = obj as Record<string, unknown>;
return (
(typeof r.server_id === 'number' || typeof r.server_id === 'string') &&
typeof r.score === 'number' &&
r.analysis !== null &&
typeof r.analysis === 'object' &&
r.estimated_capacity !== null &&
typeof r.estimated_capacity === 'object'
);
}
/** /**
* Validate recommendation request * Validate recommendation request
*/ */
@@ -1093,8 +1171,13 @@ async function queryCandidateServers(
throw new Error('Failed to query candidate servers'); throw new Error('Failed to query candidate servers');
} }
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
return result.results as unknown as Server[]; const validServers = (result.results as unknown[]).filter(isValidServer);
const invalidCount = result.results.length - validServers.length;
if (invalidCount > 0) {
console.warn(`[Candidates] Filtered out ${invalidCount} invalid server records`);
}
return validServers;
} }
/** /**
@@ -1174,8 +1257,8 @@ async function queryBenchmarkData(
return []; return [];
} }
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
return result.results as unknown as BenchmarkData[]; return (result.results as unknown[]).filter(isValidBenchmarkData);
} }
/** /**
@@ -1256,8 +1339,8 @@ async function queryVPSBenchmarks(
const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all(); const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all();
if (providerResult.success && providerResult.results.length > 0) { if (providerResult.success && providerResult.results.length > 0) {
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
return providerResult.results as unknown as VPSBenchmark[]; return (providerResult.results as unknown[]).filter(isValidVPSBenchmark);
} }
} }
@@ -1277,8 +1360,8 @@ async function queryVPSBenchmarks(
return []; return [];
} }
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
return result.results as unknown as VPSBenchmark[]; return (result.results as unknown[]).filter(isValidVPSBenchmark);
} }
/** /**
@@ -1325,8 +1408,8 @@ async function queryVPSBenchmarksBatch(
return []; return [];
} }
// NOTE: Type assertion without validation - should implement proper validation // Validate each result with type guard
return result.results as unknown as VPSBenchmark[]; return (result.results as unknown[]).filter(isValidVPSBenchmark);
} }
/** /**
@@ -1425,8 +1508,10 @@ async function queryTechSpecs(
return []; return [];
} }
console.log(`[TechSpecs] Found ${result.results.length} specs for: ${normalizedStack.join(', ')}`); // Validate each result with type guard
return result.results as unknown as TechSpec[]; const validSpecs = (result.results as unknown[]).filter(isValidTechSpec);
console.log(`[TechSpecs] Found ${validSpecs.length} specs for: ${normalizedStack.join(', ')}`);
return validSpecs;
} catch (error) { } catch (error) {
console.error('[TechSpecs] Error:', error); console.error('[TechSpecs] Error:', error);
return []; return [];
@@ -1604,6 +1689,10 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have
console.log('[AI] Sending request to OpenAI GPT-4o-mini'); console.log('[AI] Sending request to OpenAI GPT-4o-mini');
// Create AbortController with 30 second timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try { try {
const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', { const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST', method: 'POST',
@@ -1620,8 +1709,11 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have
max_tokens: 2000, max_tokens: 2000,
temperature: 0.3, temperature: 0.3,
}), }),
signal: controller.signal,
}); });
clearTimeout(timeoutId);
if (!openaiResponse.ok) { if (!openaiResponse.ok) {
const errorText = await openaiResponse.text(); const errorText = await openaiResponse.text();
const sanitized = errorText.slice(0, 100).replace(/sk-[a-zA-Z0-9-_]+/g, 'sk-***'); const sanitized = errorText.slice(0, 100).replace(/sk-[a-zA-Z0-9-_]+/g, 'sk-***');
@@ -1716,6 +1808,12 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have
infrastructure_tips: aiResult.infrastructure_tips, infrastructure_tips: aiResult.infrastructure_tips,
}; };
} catch (error) { } catch (error) {
clearTimeout(timeoutId);
// Handle timeout specifically
if (error instanceof Error && error.name === 'AbortError') {
console.error('[AI] Request timed out after 30 seconds');
throw new Error('AI request timed out - please try again');
}
console.error('[AI] Error:', error); console.error('[AI] Error:', error);
throw new Error(`AI processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(`AI processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
@@ -1757,7 +1855,17 @@ function parseAIResponse(response: any): AIRecommendationResponse {
throw new Error('Invalid recommendations structure'); throw new Error('Invalid recommendations structure');
} }
return parsed as AIRecommendationResponse; // Validate each recommendation with type guard
const validRecommendations = parsed.recommendations.filter(isValidAIRecommendation);
if (validRecommendations.length === 0 && parsed.recommendations.length > 0) {
console.warn('[AI] All recommendations failed validation, raw:', JSON.stringify(parsed.recommendations[0]).slice(0, 200));
throw new Error('AI recommendations failed validation');
}
return {
recommendations: validRecommendations,
infrastructure_tips: Array.isArray(parsed.infrastructure_tips) ? parsed.infrastructure_tips : [],
} as AIRecommendationResponse;
} catch (error) { } catch (error) {
console.error('[AI] Parse error:', error); console.error('[AI] Parse error:', error);
console.error('[AI] Response was:', response); console.error('[AI] Response was:', response);