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');
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user