Initial commit: Cloud Instances API

Multi-cloud VM instance database with Cloudflare Workers
- Linode, Vultr, AWS connector integration
- D1 database with regions, instances, pricing
- Query API with filtering, caching, pagination
- Cron-based auto-sync (daily + 6-hourly)
- Health monitoring endpoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-21 20:17:07 +09:00
commit 95043049b4
32 changed files with 10151 additions and 0 deletions

413
src/routes/instances.ts Normal file
View File

@@ -0,0 +1,413 @@
/**
* Instances Route Handler
*
* Endpoint for querying instance types with filtering, sorting, and pagination.
* Integrates with cache service for performance optimization.
*/
import type { Env } from '../types';
/**
* Parsed and validated query parameters
*/
interface ParsedQueryParams {
provider?: string;
region?: string;
min_vcpu?: number;
max_vcpu?: number;
min_memory_gb?: number;
max_memory_gb?: number;
max_price?: number;
instance_family?: string;
has_gpu?: boolean;
sort_by?: string;
order?: 'asc' | 'desc';
limit: number;
offset: number;
}
/**
* Supported cloud providers
*/
const SUPPORTED_PROVIDERS = ['linode', 'vultr', 'aws'] as const;
type SupportedProvider = typeof SUPPORTED_PROVIDERS[number];
/**
* Valid sort fields
*/
const VALID_SORT_FIELDS = [
'price',
'hourly_price',
'monthly_price',
'vcpu',
'memory_mb',
'memory_gb',
'storage_gb',
'instance_name',
'provider',
'region'
] as const;
/**
* Valid instance families
*/
const VALID_FAMILIES = ['general', 'compute', 'memory', 'storage', 'gpu'] as const;
/**
* Default query parameters
*/
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 100;
const DEFAULT_OFFSET = 0;
/**
* Validate provider name
*/
function isSupportedProvider(provider: string): provider is SupportedProvider {
return SUPPORTED_PROVIDERS.includes(provider as SupportedProvider);
}
/**
* Validate sort field
*/
function isValidSortField(field: string): boolean {
return VALID_SORT_FIELDS.includes(field as typeof VALID_SORT_FIELDS[number]);
}
/**
* Validate instance family
*/
function isValidFamily(family: string): boolean {
return VALID_FAMILIES.includes(family as typeof VALID_FAMILIES[number]);
}
/**
* Parse and validate query parameters
*/
function parseQueryParams(url: URL): {
params?: ParsedQueryParams;
error?: { code: string; message: string; parameter?: string };
} {
const searchParams = url.searchParams;
const params: ParsedQueryParams = {
limit: DEFAULT_LIMIT,
offset: DEFAULT_OFFSET,
};
// Provider validation
const provider = searchParams.get('provider');
if (provider !== null) {
if (!isSupportedProvider(provider)) {
return {
error: {
code: 'INVALID_PARAMETER',
message: `Invalid provider: ${provider}. Supported providers: ${SUPPORTED_PROVIDERS.join(', ')}`,
parameter: 'provider',
},
};
}
params.provider = provider;
}
// Region (no validation, passed as-is)
const region = searchParams.get('region');
if (region !== null) {
params.region = region;
}
// Numeric parameter validation helper
function parsePositiveNumber(
name: string,
value: string | null
): number | undefined | { error: any } {
if (value === null) return undefined;
const parsed = Number(value);
if (isNaN(parsed) || parsed < 0) {
return {
error: {
code: 'INVALID_PARAMETER',
message: `Invalid value for ${name}: must be a positive number`,
parameter: name,
},
};
}
return parsed;
}
// Parse min_vcpu
const minVcpuResult = parsePositiveNumber('min_vcpu', searchParams.get('min_vcpu'));
if (minVcpuResult && typeof minVcpuResult === 'object' && 'error' in minVcpuResult) {
return minVcpuResult;
}
if (typeof minVcpuResult === 'number') {
params.min_vcpu = minVcpuResult;
}
// Parse max_vcpu
const maxVcpuResult = parsePositiveNumber('max_vcpu', searchParams.get('max_vcpu'));
if (maxVcpuResult && typeof maxVcpuResult === 'object' && 'error' in maxVcpuResult) {
return maxVcpuResult;
}
if (typeof maxVcpuResult === 'number') {
params.max_vcpu = maxVcpuResult;
}
// Parse min_memory_gb
const minMemoryResult = parsePositiveNumber('min_memory_gb', searchParams.get('min_memory_gb'));
if (minMemoryResult && typeof minMemoryResult === 'object' && 'error' in minMemoryResult) {
return minMemoryResult;
}
if (typeof minMemoryResult === 'number') {
params.min_memory_gb = minMemoryResult;
}
// Parse max_memory_gb
const maxMemoryResult = parsePositiveNumber('max_memory_gb', searchParams.get('max_memory_gb'));
if (maxMemoryResult && typeof maxMemoryResult === 'object' && 'error' in maxMemoryResult) {
return maxMemoryResult;
}
if (typeof maxMemoryResult === 'number') {
params.max_memory_gb = maxMemoryResult;
}
// Parse max_price
const maxPriceResult = parsePositiveNumber('max_price', searchParams.get('max_price'));
if (maxPriceResult && typeof maxPriceResult === 'object' && 'error' in maxPriceResult) {
return maxPriceResult;
}
if (typeof maxPriceResult === 'number') {
params.max_price = maxPriceResult;
}
// Instance family validation
const family = searchParams.get('instance_family');
if (family !== null) {
if (!isValidFamily(family)) {
return {
error: {
code: 'INVALID_PARAMETER',
message: `Invalid instance_family: ${family}. Valid values: ${VALID_FAMILIES.join(', ')}`,
parameter: 'instance_family',
},
};
}
params.instance_family = family;
}
// GPU filter (boolean)
const hasGpu = searchParams.get('has_gpu');
if (hasGpu !== null) {
if (hasGpu !== 'true' && hasGpu !== 'false') {
return {
error: {
code: 'INVALID_PARAMETER',
message: 'Invalid value for has_gpu: must be "true" or "false"',
parameter: 'has_gpu',
},
};
}
params.has_gpu = hasGpu === 'true';
}
// Sort by validation
const sortBy = searchParams.get('sort_by');
if (sortBy !== null) {
if (!isValidSortField(sortBy)) {
return {
error: {
code: 'INVALID_PARAMETER',
message: `Invalid sort_by: ${sortBy}. Valid values: ${VALID_SORT_FIELDS.join(', ')}`,
parameter: 'sort_by',
},
};
}
params.sort_by = sortBy;
}
// Sort order validation
const order = searchParams.get('order');
if (order !== null) {
if (order !== 'asc' && order !== 'desc') {
return {
error: {
code: 'INVALID_PARAMETER',
message: 'Invalid order: must be "asc" or "desc"',
parameter: 'order',
},
};
}
params.order = order;
}
// Limit validation
const limitStr = searchParams.get('limit');
if (limitStr !== null) {
const limit = Number(limitStr);
if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) {
return {
error: {
code: 'INVALID_PARAMETER',
message: `Invalid limit: must be between 1 and ${MAX_LIMIT}`,
parameter: 'limit',
},
};
}
params.limit = limit;
}
// Offset validation
const offsetStr = searchParams.get('offset');
if (offsetStr !== null) {
const offset = Number(offsetStr);
if (isNaN(offset) || offset < 0) {
return {
error: {
code: 'INVALID_PARAMETER',
message: 'Invalid offset: must be a non-negative number',
parameter: 'offset',
},
};
}
params.offset = offset;
}
return { params };
}
/**
* Generate cache key from query parameters
* TODO: Replace with cacheService.generateKey(params) when cache service is implemented
*/
function generateCacheKey(params: ParsedQueryParams): string {
const parts: string[] = ['instances'];
if (params.provider) parts.push(`provider:${params.provider}`);
if (params.region) parts.push(`region:${params.region}`);
if (params.min_vcpu !== undefined) parts.push(`min_vcpu:${params.min_vcpu}`);
if (params.max_vcpu !== undefined) parts.push(`max_vcpu:${params.max_vcpu}`);
if (params.min_memory_gb !== undefined) parts.push(`min_memory:${params.min_memory_gb}`);
if (params.max_memory_gb !== undefined) parts.push(`max_memory:${params.max_memory_gb}`);
if (params.max_price !== undefined) parts.push(`max_price:${params.max_price}`);
if (params.instance_family) parts.push(`family:${params.instance_family}`);
if (params.has_gpu !== undefined) parts.push(`gpu:${params.has_gpu}`);
if (params.sort_by) parts.push(`sort:${params.sort_by}`);
if (params.order) parts.push(`order:${params.order}`);
parts.push(`limit:${params.limit}`);
parts.push(`offset:${params.offset}`);
return parts.join('|');
}
/**
* Handle GET /instances endpoint
*
* @param request - HTTP request object
* @param env - Cloudflare Worker environment bindings
* @returns JSON response with instance query results
*
* @example
* GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50
*/
export async function handleInstances(
request: Request,
_env: Env
): Promise<Response> {
const startTime = Date.now();
console.log('[Instances] Request received', { url: request.url });
try {
// Parse URL and query parameters
const url = new URL(request.url);
const parseResult = parseQueryParams(url);
// Handle validation errors
if (parseResult.error) {
console.error('[Instances] Validation error', parseResult.error);
return Response.json(
{
success: false,
error: parseResult.error,
},
{ status: 400 }
);
}
const params = parseResult.params!;
console.log('[Instances] Query params validated', params);
// Generate cache key
const cacheKey = generateCacheKey(params);
console.log('[Instances] Cache key generated', { cacheKey });
// TODO: Implement cache check
// const cacheService = new CacheService(env);
// const cached = await cacheService.get(cacheKey);
// if (cached) {
// console.log('[Instances] Cache hit', { cacheKey, age: cached.cache_age_seconds });
// return Response.json({
// success: true,
// data: {
// ...cached.data,
// metadata: {
// cached: true,
// cache_age_seconds: cached.cache_age_seconds,
// },
// },
// });
// }
console.log('[Instances] Cache miss (or cache service not implemented)');
// TODO: Implement database query
// const queryService = new QueryService(env.DB);
// const result = await queryService.queryInstances(params);
// Placeholder response until query service is implemented
const queryTime = Date.now() - startTime;
const placeholderResponse = {
success: true,
data: {
instances: [],
pagination: {
total: 0,
limit: params.limit,
offset: params.offset,
has_more: false,
},
metadata: {
cached: false,
last_sync: new Date().toISOString(),
query_time_ms: queryTime,
},
},
};
console.log('[Instances] TODO: Implement query service');
console.log('[Instances] Placeholder response generated', {
queryTime,
cacheKey,
});
// TODO: Implement cache storage
// await cacheService.set(cacheKey, result);
return Response.json(placeholderResponse, { status: 200 });
} catch (error) {
console.error('[Instances] Unexpected error', { error });
return Response.json(
{
success: false,
error: {
code: 'QUERY_FAILED',
message: 'Instance query failed',
details: error instanceof Error ? error.message : 'Unknown error',
},
},
{ status: 500 }
);
}
}