refactor: modularize codebase and add DB workload multiplier

- Split monolithic index.ts (2370 lines) into modular structure:
  - src/handlers/ for route handlers
  - src/utils.ts for shared utilities
  - src/config.ts for configuration
  - src/types.ts for TypeScript definitions

- Add DB workload multiplier for smarter database resource calculation:
  - Heavy (analytics, logs): 0.3x multiplier
  - Medium-heavy (e-commerce, transactional): 0.5x
  - Medium (API, SaaS): 0.7x
  - Light (blog, portfolio): 1.0x

- Fix tech_specs with realistic vcpu_per_users values (150+ technologies)
- Fix "blog" matching "log" regex bug
- Update documentation to reflect new architecture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 17:46:16 +09:00
parent 0bb7296600
commit b682abc45d
9 changed files with 2729 additions and 2403 deletions

20
src/handlers/health.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Health check endpoint handler
*/
import { jsonResponse } from '../utils';
/**
* Health check endpoint
*/
export function handleHealth(corsHeaders: Record<string, string>): Response {
return jsonResponse(
{
status: 'ok',
timestamp: new Date().toISOString(),
service: 'server-recommend',
},
200,
corsHeaders
);
}

1240
src/handlers/recommend.ts Normal file

File diff suppressed because it is too large Load Diff

139
src/handlers/servers.ts Normal file
View File

@@ -0,0 +1,139 @@
/**
* GET /api/servers - Server list with filtering handler
*/
import type { Env } from '../types';
import { jsonResponse, isValidServer } from '../utils';
/**
* GET /api/servers - Server list with filtering
*/
export async function handleGetServers(
request: Request,
env: Env,
corsHeaders: Record<string, string>
): Promise<Response> {
try {
const url = new URL(request.url);
const provider = url.searchParams.get('provider');
const minCpu = url.searchParams.get('minCpu');
const minMemory = url.searchParams.get('minMemory');
const region = url.searchParams.get('region');
console.log('[GetServers] Query params:', {
provider,
minCpu,
minMemory,
region,
});
// Build SQL query dynamically
let query = `
SELECT
it.id,
p.display_name as provider_name,
it.instance_id,
it.instance_name,
it.vcpu,
it.memory_mb,
ROUND(it.memory_mb / 1024.0, 1) as memory_gb,
it.storage_gb,
it.network_speed_gbps,
it.instance_family,
it.gpu_count,
it.gpu_type,
MIN(pr.monthly_price) as monthly_price,
MIN(r.region_name) as region_name,
MIN(r.region_code) as region_code
FROM instance_types it
JOIN providers p ON it.provider_id = p.id
JOIN pricing pr ON pr.instance_type_id = it.id
JOIN regions r ON pr.region_id = r.id
WHERE p.id IN (1, 2) -- Linode, Vultr only
AND (
-- Korea (Seoul)
r.region_code IN ('icn', 'ap-northeast-2') OR
LOWER(r.region_name) LIKE '%seoul%' OR
-- Japan (Tokyo, Osaka)
r.region_code IN ('nrt', 'itm', 'ap-northeast-1', 'ap-northeast-3') OR
LOWER(r.region_code) LIKE '%tyo%' OR
LOWER(r.region_code) LIKE '%osa%' OR
LOWER(r.region_name) LIKE '%tokyo%' OR
LOWER(r.region_name) LIKE '%osaka%' OR
-- Singapore
r.region_code IN ('sgp', 'ap-southeast-1') OR
LOWER(r.region_code) LIKE '%sin%' OR
LOWER(r.region_code) LIKE '%sgp%' OR
LOWER(r.region_name) LIKE '%singapore%'
)
`;
const params: (string | number)[] = [];
if (provider) {
query += ` AND p.name = ?`;
params.push(provider);
}
if (minCpu) {
const parsedCpu = parseInt(minCpu, 10);
if (isNaN(parsedCpu)) {
return jsonResponse({ error: 'Invalid minCpu parameter' }, 400, corsHeaders);
}
query += ` AND it.vcpu >= ?`;
params.push(parsedCpu);
}
if (minMemory) {
const parsedMemory = parseInt(minMemory, 10);
if (isNaN(parsedMemory)) {
return jsonResponse({ error: 'Invalid minMemory parameter' }, 400, corsHeaders);
}
query += ` AND it.memory_mb >= ?`;
params.push(parsedMemory * 1024);
}
if (region) {
query += ` AND r.region_code = ?`;
params.push(region);
}
query += ` GROUP BY it.id ORDER BY MIN(pr.monthly_price) ASC LIMIT 100`;
const result = await env.DB.prepare(query).bind(...params).all();
if (!result.success) {
throw new Error('Database query failed');
}
// 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);
return jsonResponse(
{
servers,
count: servers.length,
filters: { provider, minCpu, minMemory, region },
},
200,
corsHeaders
);
} catch (error) {
console.error('[GetServers] Error:', error);
const requestId = crypto.randomUUID();
return jsonResponse(
{
error: 'Failed to retrieve servers',
request_id: requestId,
},
500,
corsHeaders
);
}
}