/** * GET /api/servers - Server list with filtering handler */ import type { Env } from '../types'; import { jsonResponse, isValidServer } from '../utils'; import { DEFAULT_ANVIL_REGION_FILTER_SQL, buildFlexibleRegionConditionsAnvil } from '../region-utils'; /** * GET /api/servers - Server list with filtering * Uses anvil_* tables for pricing data */ export async function handleGetServers( request: Request, env: Env, corsHeaders: Record ): Promise { try { const url = new URL(request.url); const minCpu = url.searchParams.get('minCpu'); const minMemory = url.searchParams.get('minMemory'); const region = url.searchParams.get('region'); console.log('[GetServers] Query params:', { minCpu, minMemory, region, }); // Generate cache key from query parameters const cacheKey = `servers:${url.search || 'all'}`; // Check cache first if (env.CACHE) { const cached = await env.CACHE.get(cacheKey); if (cached) { console.log('[GetServers] Cache hit for:', cacheKey); return jsonResponse({ ...JSON.parse(cached), cached: true }, 200, corsHeaders); } } // Build SQL query using anvil_* tables let query = ` SELECT ai.id, 'Anvil' as provider_name, ai.name as instance_id, ai.display_name as instance_name, ai.vcpus as vcpu, CAST(ai.memory_gb * 1024 AS INTEGER) as memory_mb, ai.memory_gb, ai.disk_gb as storage_gb, ai.network_gbps as network_speed_gbps, ai.category as instance_family, CASE WHEN ai.gpu_model IS NOT NULL THEN 1 ELSE 0 END as gpu_count, ai.gpu_model as gpu_type, ap.monthly_price, ar.display_name as region_name, ar.name as region_code FROM anvil_instances ai JOIN anvil_pricing ap ON ap.anvil_instance_id = ai.id JOIN anvil_regions ar ON ap.anvil_region_id = ar.id WHERE ai.active = 1 AND ar.active = 1 AND ${DEFAULT_ANVIL_REGION_FILTER_SQL} `; const params: (string | number)[] = []; if (minCpu) { const parsedCpu = parseInt(minCpu, 10); if (isNaN(parsedCpu)) { return jsonResponse({ error: 'Invalid minCpu parameter' }, 400, corsHeaders); } query += ` AND ai.vcpus >= ?`; params.push(parsedCpu); } if (minMemory) { const parsedMemory = parseInt(minMemory, 10); if (isNaN(parsedMemory)) { return jsonResponse({ error: 'Invalid minMemory parameter' }, 400, corsHeaders); } // minMemory is in GB, anvil_instances stores memory_gb query += ` AND ai.memory_gb >= ?`; params.push(parsedMemory); } if (region) { // Flexible region matching: supports country names, codes, city names const { conditions, params: regionParams } = buildFlexibleRegionConditionsAnvil([region]); query += ` AND (${conditions.join(' OR ')})`; params.push(...regionParams); } query += ` ORDER BY ap.monthly_price ASC LIMIT 100`; const result = await env.DB.prepare(query).bind(...params).all(); if (!result.success) { throw new Error('Database query failed'); } // Add USD currency to each result and validate with type guard const serversWithCurrency = (result.results as unknown[]).map(server => { if (typeof server === 'object' && server !== null) { return { ...server, currency: 'USD' }; } return server; }); const servers = serversWithCurrency.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); const responseData = { servers, count: servers.length, filters: { minCpu, minMemory, region }, }; // Cache successful results (only if we have servers) if (env.CACHE && servers.length > 0) { await env.CACHE.put(cacheKey, JSON.stringify(responseData), { expirationTtl: 300, // 5 minutes }); } return jsonResponse(responseData, 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 ); } }