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:
138
src/index.ts
138
src/index.ts
@@ -626,8 +626,12 @@ async function handleGetServers(
|
||||
throw new Error('Database query failed');
|
||||
}
|
||||
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
const servers = result.results as unknown as Server[];
|
||||
// Validate each result with type guard
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -1093,8 +1171,13 @@ async function queryCandidateServers(
|
||||
throw new Error('Failed to query candidate servers');
|
||||
}
|
||||
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
return result.results as unknown as Server[];
|
||||
// Validate each result with type guard
|
||||
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 [];
|
||||
}
|
||||
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
return result.results as unknown as BenchmarkData[];
|
||||
// Validate each result with type guard
|
||||
return (result.results as unknown[]).filter(isValidBenchmarkData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1256,8 +1339,8 @@ async function queryVPSBenchmarks(
|
||||
const providerResult = await db.prepare(providerQuery).bind(providerPattern, providerPattern).all();
|
||||
|
||||
if (providerResult.success && providerResult.results.length > 0) {
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
return providerResult.results as unknown as VPSBenchmark[];
|
||||
// Validate each result with type guard
|
||||
return (providerResult.results as unknown[]).filter(isValidVPSBenchmark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1277,8 +1360,8 @@ async function queryVPSBenchmarks(
|
||||
return [];
|
||||
}
|
||||
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
return result.results as unknown as VPSBenchmark[];
|
||||
// Validate each result with type guard
|
||||
return (result.results as unknown[]).filter(isValidVPSBenchmark);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1325,8 +1408,8 @@ async function queryVPSBenchmarksBatch(
|
||||
return [];
|
||||
}
|
||||
|
||||
// NOTE: Type assertion without validation - should implement proper validation
|
||||
return result.results as unknown as VPSBenchmark[];
|
||||
// Validate each result with type guard
|
||||
return (result.results as unknown[]).filter(isValidVPSBenchmark);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1425,8 +1508,10 @@ async function queryTechSpecs(
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[TechSpecs] Found ${result.results.length} specs for: ${normalizedStack.join(', ')}`);
|
||||
return result.results as unknown as TechSpec[];
|
||||
// Validate each result with type guard
|
||||
const validSpecs = (result.results as unknown[]).filter(isValidTechSpec);
|
||||
console.log(`[TechSpecs] Found ${validSpecs.length} specs for: ${normalizedStack.join(', ')}`);
|
||||
return validSpecs;
|
||||
} catch (error) {
|
||||
console.error('[TechSpecs] Error:', error);
|
||||
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');
|
||||
|
||||
// Create AbortController with 30 second timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
try {
|
||||
const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
@@ -1620,8 +1709,11 @@ The option with the LOWEST TOTAL MONTHLY COST (including bandwidth) should have
|
||||
max_tokens: 2000,
|
||||
temperature: 0.3,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!openaiResponse.ok) {
|
||||
const errorText = await openaiResponse.text();
|
||||
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,
|
||||
};
|
||||
} 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);
|
||||
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');
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('[AI] Parse error:', error);
|
||||
console.error('[AI] Response was:', response);
|
||||
|
||||
Reference in New Issue
Block a user