/** * Pricing endpoint - Direct D1 query (no rate limiting) * GET /api/pricing → D1 cloud-instances-db * * Supported regions: * - Tokyo, Osaka, Singapore: Linode * - Seoul: Vultr */ import { type PagesFunction } from '@cloudflare/workers-types'; import { Env, createCorsPreflightResponse } from '../_shared/proxy'; interface InstanceRow { instance_id: string; instance_name: string; vcpu: number; memory_mb: number; storage_gb: number | null; transfer_tb: number | null; provider_name: string; region_name: string; region_code: string; monthly_price: number; hourly_price: number | null; monthly_price_krw: number | null; hourly_price_krw: number | null; // GPU fields has_gpu?: number; gpu_count?: number; gpu_type?: string; } const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; export const onRequestGet: PagesFunction = async ({ env }) => { try { // 1. 일반 인스턴스 쿼리 - Linode(Tokyo, Osaka, Singapore) + Vultr(Seoul) const regularSql = ` SELECT it.instance_id, it.instance_name, it.vcpu, it.memory_mb, it.storage_gb, it.transfer_tb, p.name as provider_name, r.region_name, r.region_code, pr.monthly_price, pr.hourly_price, pr.monthly_price_krw, pr.hourly_price_krw, 0 as has_gpu, 0 as gpu_count, NULL as gpu_type FROM instance_types it JOIN providers p ON it.provider_id = p.id JOIN pricing pr ON it.id = pr.instance_type_id JOIN regions r ON pr.region_id = r.id WHERE pr.available = 1 AND it.instance_id NOT LIKE '%-sc1' AND ( (p.name = 'linode' AND ( r.region_name LIKE '%Tokyo%' OR r.region_name LIKE '%Osaka%' OR r.region_code LIKE '%jp-osa%' OR r.region_name LIKE '%Singapore%' )) OR (p.name = 'vultr' AND ( r.region_name LIKE '%Seoul%' OR r.region_code LIKE '%icn%' )) ) ORDER BY pr.monthly_price ASC `; // 2. GPU 인스턴스 쿼리 - Linode(Tokyo) + Vultr(Seoul) const gpuSql = ` SELECT gi.instance_id, gi.instance_name, gi.vcpu, gi.memory_mb, gi.storage_gb, gi.transfer_tb, p.name as provider_name, r.region_name, r.region_code, gp.monthly_price, gp.hourly_price, gp.monthly_price_krw, gp.hourly_price_krw, 1 as has_gpu, gi.gpu_count, gi.gpu_type FROM gpu_instances gi JOIN providers p ON gi.provider_id = p.id JOIN gpu_pricing gp ON gi.id = gp.gpu_instance_id JOIN regions r ON gp.region_id = r.id WHERE gp.available = 1 AND ( (p.name = 'linode' AND ( r.region_name LIKE '%Tokyo%' OR r.region_name LIKE '%Osaka%' OR r.region_code LIKE '%jp-osa%' )) OR (p.name = 'vultr' AND ( r.region_name LIKE '%Seoul%' OR r.region_code LIKE '%icn%' )) ) ORDER BY gp.monthly_price ASC `; const [regularResult, gpuResult] = await Promise.all([ env.DB.prepare(regularSql).all(), env.DB.prepare(gpuSql).all(), ]); // Transform to frontend format const transformRow = (row: InstanceRow) => ({ id: row.instance_id, instance_name: row.instance_name, vcpu: row.vcpu, memory_mb: row.memory_mb, storage_gb: row.storage_gb, transfer_tb: row.transfer_tb, provider: { name: row.provider_name }, region: { region_name: row.region_name, region_code: row.region_code }, pricing: { monthly_price: row.monthly_price, hourly_price: row.hourly_price, monthly_price_krw: row.monthly_price_krw, hourly_price_krw: row.hourly_price_krw, }, has_gpu: row.has_gpu === 1, gpu_count: row.gpu_count || 0, gpu_type: row.gpu_type || null, }); const regularInstances = regularResult.results.map(transformRow); const gpuInstances = gpuResult.results.map(transformRow); const instances = [...regularInstances, ...gpuInstances]; // Count by region const regionCounts: Record = {}; for (const inst of instances) { const name = inst.region.region_name.toLowerCase(); if (name.includes('tokyo')) regionCounts['tokyo'] = (regionCounts['tokyo'] || 0) + 1; else if (name.includes('osaka')) regionCounts['osaka'] = (regionCounts['osaka'] || 0) + 1; else if (name.includes('singapore')) regionCounts['singapore'] = (regionCounts['singapore'] || 0) + 1; else if (name.includes('seoul')) regionCounts['seoul'] = (regionCounts['seoul'] || 0) + 1; } // GPU counts const gpuCounts = { 'gpu-japan': gpuInstances.filter(i => i.region.region_name.toLowerCase().includes('tokyo') || i.region.region_name.toLowerCase().includes('osaka') ).length, 'gpu-korea': gpuInstances.filter(i => i.region.region_name.toLowerCase().includes('seoul') ).length, }; return new Response(JSON.stringify({ success: true, total: instances.length, region_counts: regionCounts, gpu_counts: gpuCounts, instances, }), { headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300', // 5분 캐시 }, }); } catch (error) { console.error('[Pricing] D1 query error:', error); return new Response(JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Database query failed', }), { status: 500, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', }, }); } }; export const onRequestOptions: PagesFunction = async () => { return createCorsPreflightResponse(); };