- HonoVariables 타입 중앙화 (types.ts로 추출, 5개 파일 중복 제거) - 6시간 pricing update cron 핸들러 추가 (syncPricingOnly 메서드) - Response.json() → c.json() 패턴 통일 (Hono 표준) - SORT_FIELD_MAP 중앙화 (constants.ts, 12개 필드 지원) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
291 lines
8.2 KiB
TypeScript
291 lines
8.2 KiB
TypeScript
/**
|
||
* Cloud Server API - Constants
|
||
*
|
||
* Centralized constants for the cloud server API.
|
||
* All magic numbers and repeated constants should be defined here.
|
||
*/
|
||
|
||
// ============================================================
|
||
// Provider Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Supported cloud providers
|
||
*/
|
||
export const SUPPORTED_PROVIDERS = ['linode', 'vultr', 'aws'] as const;
|
||
export type SupportedProvider = typeof SUPPORTED_PROVIDERS[number];
|
||
|
||
// ============================================================
|
||
// Cache Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Cache TTL values in seconds
|
||
*/
|
||
export const CACHE_TTL = {
|
||
/** Cache TTL for instance queries (5 minutes) */
|
||
INSTANCES: 300,
|
||
/** Cache TTL for health checks (30 seconds) */
|
||
HEALTH: 30,
|
||
/** Cache TTL for pricing data (1 hour) */
|
||
PRICING: 3600,
|
||
/** Default cache TTL (5 minutes) */
|
||
DEFAULT: 300,
|
||
} as const;
|
||
|
||
/**
|
||
* Cache TTL values in milliseconds
|
||
*/
|
||
export const CACHE_TTL_MS = {
|
||
/** Cache TTL for instance queries (5 minutes) */
|
||
INSTANCES: 5 * 60 * 1000,
|
||
/** Cache TTL for health checks (30 seconds) */
|
||
HEALTH: 30 * 1000,
|
||
/** Cache TTL for pricing data (1 hour) */
|
||
PRICING: 60 * 60 * 1000,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Rate Limiting Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Rate limiting defaults
|
||
*/
|
||
export const RATE_LIMIT_DEFAULTS = {
|
||
/** Time window in milliseconds (1 minute) */
|
||
WINDOW_MS: 60 * 1000,
|
||
/** Maximum requests per window for /instances endpoint */
|
||
MAX_REQUESTS_INSTANCES: 100,
|
||
/** Maximum requests per window for /sync endpoint */
|
||
MAX_REQUESTS_SYNC: 10,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Pagination Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Pagination defaults
|
||
*/
|
||
export const PAGINATION = {
|
||
/** Default page number (1-indexed) */
|
||
DEFAULT_PAGE: 1,
|
||
/** Default number of results per page */
|
||
DEFAULT_LIMIT: 50,
|
||
/** Maximum number of results per page */
|
||
MAX_LIMIT: 100,
|
||
/** Default offset for pagination */
|
||
DEFAULT_OFFSET: 0,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// HTTP Status Codes
|
||
// ============================================================
|
||
|
||
/**
|
||
* HTTP status codes used throughout the API
|
||
*/
|
||
export const HTTP_STATUS = {
|
||
/** 200 - OK */
|
||
OK: 200,
|
||
/** 201 - Created */
|
||
CREATED: 201,
|
||
/** 204 - No Content */
|
||
NO_CONTENT: 204,
|
||
/** 400 - Bad Request */
|
||
BAD_REQUEST: 400,
|
||
/** 401 - Unauthorized */
|
||
UNAUTHORIZED: 401,
|
||
/** 404 - Not Found */
|
||
NOT_FOUND: 404,
|
||
/** 413 - Payload Too Large */
|
||
PAYLOAD_TOO_LARGE: 413,
|
||
/** 429 - Too Many Requests */
|
||
TOO_MANY_REQUESTS: 429,
|
||
/** 500 - Internal Server Error */
|
||
INTERNAL_ERROR: 500,
|
||
/** 503 - Service Unavailable */
|
||
SERVICE_UNAVAILABLE: 503,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Database Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Database table names
|
||
*/
|
||
export const TABLES = {
|
||
PROVIDERS: 'providers',
|
||
REGIONS: 'regions',
|
||
INSTANCE_TYPES: 'instance_types',
|
||
PRICING: 'pricing',
|
||
PRICE_HISTORY: 'price_history',
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Query Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Mapping of user-facing sort field names to database column names
|
||
*
|
||
* This is the single source of truth for sort field validation and mapping.
|
||
* Query aliases: it=instance_types, pr=pricing, p=providers, r=regions
|
||
*/
|
||
export const SORT_FIELD_MAP: Record<string, string> = {
|
||
price: 'pr.hourly_price',
|
||
hourly_price: 'pr.hourly_price',
|
||
monthly_price: 'pr.monthly_price',
|
||
vcpu: 'it.vcpu',
|
||
memory: 'it.memory_mb',
|
||
memory_mb: 'it.memory_mb',
|
||
memory_gb: 'it.memory_mb', // Note: memory_gb is converted to memory_mb at query level
|
||
storage_gb: 'it.storage_gb',
|
||
name: 'it.instance_name',
|
||
instance_name: 'it.instance_name',
|
||
provider: 'p.name',
|
||
region: 'r.region_code',
|
||
} as const;
|
||
|
||
/**
|
||
* Valid sort fields for instance queries (derived from SORT_FIELD_MAP)
|
||
*/
|
||
export const VALID_SORT_FIELDS = Object.keys(SORT_FIELD_MAP) as ReadonlyArray<string>;
|
||
|
||
export type ValidSortField = keyof typeof SORT_FIELD_MAP;
|
||
|
||
/**
|
||
* Valid sort orders
|
||
*/
|
||
export const SORT_ORDERS = ['asc', 'desc'] as const;
|
||
export type SortOrder = typeof SORT_ORDERS[number];
|
||
|
||
/**
|
||
* Valid instance families
|
||
*/
|
||
export const INSTANCE_FAMILIES = ['general', 'compute', 'memory', 'storage', 'gpu'] as const;
|
||
export type InstanceFamily = typeof INSTANCE_FAMILIES[number];
|
||
|
||
// ============================================================
|
||
// CORS Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* CORS configuration
|
||
*
|
||
* Security: Explicit allowed origins only. No wildcard fallback.
|
||
* Development origins are separated and only used in development environment.
|
||
*/
|
||
export const CORS = {
|
||
/** Default CORS origin - explicit production origin */
|
||
DEFAULT_ORIGIN: 'https://anvil.it.com',
|
||
/** Allowed production origins for CORS */
|
||
ALLOWED_ORIGINS: [
|
||
'https://anvil.it.com',
|
||
'https://cloud.anvil.it.com',
|
||
'https://hosting.anvil.it.com',
|
||
] as string[],
|
||
/** Development origins - only used when ENVIRONMENT === 'development' */
|
||
DEVELOPMENT_ORIGINS: [
|
||
'http://localhost:3000',
|
||
'http://127.0.0.1:3000',
|
||
] as string[],
|
||
/** Max age for CORS preflight cache (24 hours) */
|
||
MAX_AGE: '86400',
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Timeout Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Timeout values in milliseconds
|
||
*/
|
||
export const TIMEOUTS = {
|
||
/** Request timeout for AWS API calls (15 seconds) */
|
||
AWS_REQUEST: 15000,
|
||
/** Default API request timeout (30 seconds) */
|
||
DEFAULT_REQUEST: 30000,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Validation Constants
|
||
// ============================================================
|
||
|
||
/**
|
||
* Validation rules
|
||
*/
|
||
export const VALIDATION = {
|
||
/** Minimum memory in MB */
|
||
MIN_MEMORY_MB: 1,
|
||
/** Minimum vCPU count */
|
||
MIN_VCPU: 1,
|
||
/** Minimum price in USD */
|
||
MIN_PRICE: 0,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// Request Security Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Request security limits
|
||
*/
|
||
export const REQUEST_LIMITS = {
|
||
/** Maximum request body size in bytes (10KB) */
|
||
MAX_BODY_SIZE: 10 * 1024,
|
||
} as const;
|
||
|
||
// ============================================================
|
||
// USD Retail Pricing Configuration
|
||
// ============================================================
|
||
|
||
/**
|
||
* Default USD retail pricing configuration
|
||
*
|
||
* These defaults are used to calculate retail prices from wholesale prices.
|
||
* Calculation formula:
|
||
* Retail = Wholesale × Margin (1.1) × VAT (1.1)
|
||
* Retail = Wholesale × 1.21
|
||
*/
|
||
export const USD_RETAIL_DEFAULTS = {
|
||
/** Margin multiplier (10% margin) */
|
||
MARGIN_MULTIPLIER: 1.1,
|
||
/** VAT multiplier (10% VAT) */
|
||
VAT_MULTIPLIER: 1.1,
|
||
/** Total multiplier (margin × VAT) */
|
||
TOTAL_MULTIPLIER: 1.21,
|
||
} as const;
|
||
|
||
/**
|
||
* Calculate USD retail hourly price from wholesale price
|
||
* Applies margin and VAT
|
||
*
|
||
* @param wholesale - Wholesale hourly price in USD
|
||
* @returns Retail price in USD, rounded to 4 decimal places
|
||
*
|
||
* @example
|
||
* calculateRetailHourly(0.0075) // Returns 0.0091 (with defaults)
|
||
* calculateRetailHourly(0.144) // Returns 0.1742 (with defaults)
|
||
*/
|
||
export function calculateRetailHourly(wholesale: number): number {
|
||
return Math.round(wholesale * USD_RETAIL_DEFAULTS.TOTAL_MULTIPLIER * 10000) / 10000;
|
||
}
|
||
|
||
/**
|
||
* Calculate USD retail monthly price from wholesale price
|
||
* Applies margin and VAT
|
||
*
|
||
* @param wholesale - Wholesale monthly price in USD
|
||
* @returns Retail price in USD, rounded to nearest $1
|
||
*
|
||
* @example
|
||
* calculateRetailMonthly(5) // Returns 6 (with defaults)
|
||
* calculateRetailMonthly(96) // Returns 116 (with defaults)
|
||
*/
|
||
export function calculateRetailMonthly(wholesale: number): number {
|
||
return Math.round(wholesale * USD_RETAIL_DEFAULTS.TOTAL_MULTIPLIER);
|
||
}
|