diff --git a/src/app.ts b/src/app.ts index 0201e98..703fee3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,7 @@ import { Hono } from 'hono'; import type { Context } from 'hono'; -import type { Env } from './types'; +import type { Env, HonoVariables } from './types'; import { CORS, HTTP_STATUS } from './constants'; import { createLogger } from './utils/logger'; import { @@ -21,20 +21,14 @@ import { handleSync } from './routes/sync'; const logger = createLogger('[App]'); -// Context variables type -type Variables = { - requestId: string; - authenticated?: boolean; -}; - // Create Hono app with type-safe bindings -const app = new Hono<{ Bindings: Env; Variables: Variables }>(); +const app = new Hono<{ Bindings: Env; Variables: HonoVariables }>(); /** * Get CORS origin for request * Reused from original index.ts logic */ -function getCorsOrigin(c: Context<{ Bindings: Env; Variables: Variables }>): string { +function getCorsOrigin(c: Context<{ Bindings: Env; Variables: HonoVariables }>): string { const origin = c.req.header('Origin'); const env = c.env; diff --git a/src/constants.ts b/src/constants.ts index 810288b..d9eb8f5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -129,22 +129,32 @@ export const TABLES = { // ============================================================ /** - * Valid sort fields for instance queries + * 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 VALID_SORT_FIELDS = [ - 'price', - 'hourly_price', - 'monthly_price', - 'vcpu', - 'memory_mb', - 'memory_gb', - 'storage_gb', - 'instance_name', - 'provider', - 'region', -] as const; +export const SORT_FIELD_MAP: Record = { + 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; -export type ValidSortField = typeof VALID_SORT_FIELDS[number]; +/** + * Valid sort fields for instance queries (derived from SORT_FIELD_MAP) + */ +export const VALID_SORT_FIELDS = Object.keys(SORT_FIELD_MAP) as ReadonlyArray; + +export type ValidSortField = keyof typeof SORT_FIELD_MAP; /** * Valid sort orders diff --git a/src/index.ts b/src/index.ts index aa937ea..a8c7f27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export default { * * Cron Schedules: * - 0 0 * * * : Daily full sync at 00:00 UTC - * - 0 star-slash-6 * * * : Pricing update every 6 hours + * - 0 star/6 * * * : Pricing update every 6 hours */ async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { const logger = createLogger('[Cron]', env); @@ -47,17 +47,18 @@ export default { for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { - logger.info('Starting sync attempt', { + logger.info('Starting full sync attempt', { attempt_number: attempt, max_retries: MAX_RETRIES }); const report = await orchestrator.syncAll(['linode', 'vultr', 'aws']); - logger.info('Daily sync complete', { + logger.info('Daily full sync complete', { attempt_number: attempt, total_regions: report.summary.total_regions, total_instances: report.summary.total_instances, + total_pricing: report.summary.total_pricing, successful_providers: report.summary.successful_providers, failed_providers: report.summary.failed_providers, duration_ms: report.total_duration_ms @@ -111,5 +112,77 @@ export default { ctx.waitUntil(executeSyncWithRetry()); } + // Pricing update every 6 hours + else if (cron === '0 */6 * * *') { + const MAX_RETRIES = 3; + + const executePricingSyncWithRetry = async (): Promise => { + const orchestrator = new SyncOrchestrator(env.DB, env); + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + logger.info('Starting pricing sync attempt', { + attempt_number: attempt, + max_retries: MAX_RETRIES + }); + + const report = await orchestrator.syncAllPricingOnly(['linode', 'vultr', 'aws']); + + logger.info('Pricing sync complete', { + attempt_number: attempt, + total_pricing: report.summary.total_pricing, + successful_providers: report.summary.successful_providers, + failed_providers: report.summary.failed_providers, + duration_ms: report.total_duration_ms + }); + + // Alert on partial failures + if (report.summary.failed_providers > 0) { + const failedProviders = report.providers + .filter(p => !p.success) + .map(p => p.provider); + + logger.warn('Some providers failed during pricing sync', { + failed_count: report.summary.failed_providers, + failed_providers: failedProviders, + errors: report.providers + .filter(p => !p.success) + .map(p => ({ provider: p.provider, error: p.error })) + }); + } + + // Success - exit retry loop + return; + + } catch (error) { + const willRetry = attempt < MAX_RETRIES; + const retryDelayMs = willRetry ? Math.min(Math.pow(2, attempt - 1) * 1000, 10000) : 0; + + logger.error('Pricing sync attempt failed', { + attempt_number: attempt, + max_retries: MAX_RETRIES, + will_retry: willRetry, + retry_delay_ms: retryDelayMs, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + + if (willRetry) { + // Wait before retry with exponential backoff + await new Promise(resolve => setTimeout(resolve, retryDelayMs)); + } else { + // Final failure - re-throw to make cron failure visible + logger.error('Pricing sync failed after all retries', { + total_attempts: MAX_RETRIES, + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + } + } + }; + + ctx.waitUntil(executePricingSyncWithRetry()); + } }, }; diff --git a/src/middleware/hono-adapters.ts b/src/middleware/hono-adapters.ts index 0dcde33..16e788f 100644 --- a/src/middleware/hono-adapters.ts +++ b/src/middleware/hono-adapters.ts @@ -5,7 +5,7 @@ */ import type { Context, Next } from 'hono'; -import type { Env } from '../types'; +import type { Env, HonoVariables } from '../types'; import { authenticateRequest, verifyApiKey, @@ -16,18 +16,12 @@ import { createLogger } from '../utils/logger'; const logger = createLogger('[Middleware]'); -// Context variables type -type Variables = { - requestId: string; - authenticated?: boolean; -}; - /** * Request ID middleware * Adds unique request ID to context for tracing */ export async function requestIdMiddleware( - c: Context<{ Bindings: Env; Variables: Variables }>, + c: Context<{ Bindings: Env; Variables: HonoVariables }>, next: Next ): Promise { // Use CF-Ray if available, otherwise generate UUID @@ -47,7 +41,7 @@ export async function requestIdMiddleware( * Validates X-API-Key header using existing auth logic */ export async function authMiddleware( - c: Context<{ Bindings: Env; Variables: Variables }>, + c: Context<{ Bindings: Env; Variables: HonoVariables }>, next: Next ): Promise { const request = c.req.raw; @@ -71,7 +65,7 @@ export async function authMiddleware( * Applies rate limits based on endpoint using existing rate limit logic */ export async function rateLimitMiddleware( - c: Context<{ Bindings: Env; Variables: Variables }>, + c: Context<{ Bindings: Env; Variables: HonoVariables }>, next: Next ): Promise { const request = c.req.raw; @@ -97,7 +91,7 @@ export async function rateLimitMiddleware( * Checks if API key is provided and valid, stores result in context */ export async function optionalAuthMiddleware( - c: Context<{ Bindings: Env; Variables: Variables }>, + c: Context<{ Bindings: Env; Variables: HonoVariables }>, next: Next ): Promise { const apiKey = c.req.header('X-API-Key'); diff --git a/src/routes/health.ts b/src/routes/health.ts index ea642d0..b89e6bf 100644 --- a/src/routes/health.ts +++ b/src/routes/health.ts @@ -4,18 +4,12 @@ */ import type { Context } from 'hono'; -import { Env } from '../types'; +import { Env, HonoVariables } from '../types'; import { HTTP_STATUS } from '../constants'; import { createLogger } from '../utils/logger'; const logger = createLogger('[Health]'); -// Context variables type -type Variables = { - requestId: string; - authenticated?: boolean; -}; - /** * Component health status */ @@ -169,7 +163,7 @@ function sanitizeError(error: string): string { * @param c - Hono context */ export async function handleHealth( - c: Context<{ Bindings: Env; Variables: Variables }> + c: Context<{ Bindings: Env; Variables: HonoVariables }> ): Promise { const timestamp = new Date().toISOString(); const authenticated = c.get('authenticated') ?? false; @@ -186,7 +180,7 @@ export async function handleHealth( status: 'unhealthy', timestamp, }; - return Response.json(publicResponse, { status: HTTP_STATUS.SERVICE_UNAVAILABLE }); + return c.json(publicResponse, HTTP_STATUS.SERVICE_UNAVAILABLE); } // Detailed response: full information with sanitized errors @@ -208,7 +202,7 @@ export async function handleHealth( }, }; - return Response.json(detailedResponse, { status: HTTP_STATUS.SERVICE_UNAVAILABLE }); + return c.json(detailedResponse, HTTP_STATUS.SERVICE_UNAVAILABLE); } // Get all providers with aggregated counts in a single query @@ -296,7 +290,7 @@ export async function handleHealth( status: overallStatus, timestamp, }; - return Response.json(publicResponse, { status: statusCode }); + return c.json(publicResponse, statusCode); } // Detailed response: full information @@ -319,7 +313,7 @@ export async function handleHealth( }, }; - return Response.json(detailedResponse, { status: statusCode }); + return c.json(detailedResponse, statusCode); } catch (error) { logger.error('Health check failed', { error }); @@ -329,7 +323,7 @@ export async function handleHealth( status: 'unhealthy', timestamp, }; - return Response.json(publicResponse, { status: HTTP_STATUS.SERVICE_UNAVAILABLE }); + return c.json(publicResponse, HTTP_STATUS.SERVICE_UNAVAILABLE); } // Detailed response: sanitized error information @@ -352,6 +346,6 @@ export async function handleHealth( }, }; - return Response.json(detailedResponse, { status: HTTP_STATUS.SERVICE_UNAVAILABLE }); + return c.json(detailedResponse, HTTP_STATUS.SERVICE_UNAVAILABLE); } } diff --git a/src/routes/instances.ts b/src/routes/instances.ts index 585d024..a309df8 100644 --- a/src/routes/instances.ts +++ b/src/routes/instances.ts @@ -6,16 +6,10 @@ */ import type { Context } from 'hono'; -import type { Env, InstanceQueryParams } from '../types'; +import type { Env, HonoVariables, InstanceQueryParams } from '../types'; import { QueryService } from '../services/query'; import { getGlobalCacheService } from '../services/cache'; import { logger } from '../utils/logger'; - -// Context variables type -type Variables = { - requestId: string; - authenticated?: boolean; -}; import { SUPPORTED_PROVIDERS, type SupportedProvider, @@ -325,7 +319,7 @@ function parseQueryParams(url: URL): { * GET /instances?provider=linode&min_vcpu=2&max_price=20&sort_by=price&order=asc&limit=50 */ export async function handleInstances( - c: Context<{ Bindings: Env; Variables: Variables }> + c: Context<{ Bindings: Env; Variables: HonoVariables }> ): Promise { const startTime = Date.now(); @@ -339,12 +333,12 @@ export async function handleInstances( // Handle validation errors if (parseResult.error) { logger.error('[Instances] Validation error', parseResult.error); - return Response.json( + return c.json( { success: false, error: parseResult.error, }, - { status: HTTP_STATUS.BAD_REQUEST } + HTTP_STATUS.BAD_REQUEST ); } @@ -383,7 +377,7 @@ export async function handleInstances( age: cached.cache_age_seconds, }); - return Response.json( + return c.json( { success: true, data: { @@ -396,11 +390,9 @@ export async function handleInstances( }, }, }, + HTTP_STATUS.OK, { - status: HTTP_STATUS.OK, - headers: { - 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, - }, + 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, } ); } @@ -463,23 +455,21 @@ export async function handleInstances( error instanceof Error ? { message: error.message } : { error: String(error) }); } - return Response.json( + return c.json( { success: true, data: responseData, }, + HTTP_STATUS.OK, { - status: HTTP_STATUS.OK, - headers: { - 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, - }, + 'Cache-Control': `public, max-age=${CACHE_TTL.INSTANCES}`, } ); } catch (error) { logger.error('[Instances] Unexpected error', { error }); - return Response.json( + return c.json( { success: false, error: { @@ -488,7 +478,7 @@ export async function handleInstances( request_id: crypto.randomUUID(), }, }, - { status: HTTP_STATUS.INTERNAL_ERROR } + HTTP_STATUS.INTERNAL_ERROR ); } } diff --git a/src/routes/sync.ts b/src/routes/sync.ts index 1e4c3ac..cf2da73 100644 --- a/src/routes/sync.ts +++ b/src/routes/sync.ts @@ -6,18 +6,12 @@ */ import type { Context } from 'hono'; -import type { Env } from '../types'; +import type { Env, HonoVariables } from '../types'; import { SyncOrchestrator } from '../services/sync'; import { logger } from '../utils/logger'; import { SUPPORTED_PROVIDERS, HTTP_STATUS } from '../constants'; import { parseJsonBody, validateProviders, createErrorResponse } from '../utils/validation'; -// Context variables type -type Variables = { - requestId: string; - authenticated?: boolean; -}; - /** * Request body interface for sync endpoint */ @@ -41,7 +35,7 @@ interface SyncRequestBody { * } */ export async function handleSync( - c: Context<{ Bindings: Env; Variables: Variables }> + c: Context<{ Bindings: Env; Variables: HonoVariables }> ): Promise { const startTime = Date.now(); const startedAt = new Date().toISOString(); @@ -54,9 +48,9 @@ export async function handleSync( if (contentLength) { const bodySize = parseInt(contentLength, 10); if (isNaN(bodySize) || bodySize > 10240) { // 10KB limit for sync - return Response.json( + return c.json( { success: false, error: { code: 'PAYLOAD_TOO_LARGE', message: 'Request body too large' } }, - { status: 413 } + 413 ); } } @@ -111,7 +105,7 @@ export async function handleSync( summary: syncReport.summary }); - return Response.json( + return c.json( { success: syncReport.success, data: { @@ -119,7 +113,7 @@ export async function handleSync( ...syncReport } }, - { status: HTTP_STATUS.OK } + HTTP_STATUS.OK ); } catch (error) { @@ -128,7 +122,7 @@ export async function handleSync( const completedAt = new Date().toISOString(); const totalDuration = Date.now() - startTime; - return Response.json( + return c.json( { success: false, error: { @@ -142,7 +136,7 @@ export async function handleSync( } } }, - { status: HTTP_STATUS.INTERNAL_ERROR } + HTTP_STATUS.INTERNAL_ERROR ); } } diff --git a/src/services/query.ts b/src/services/query.ts index d65af71..13cc7ad 100644 --- a/src/services/query.ts +++ b/src/services/query.ts @@ -4,6 +4,7 @@ */ import { createLogger } from '../utils/logger'; +import { SORT_FIELD_MAP } from '../constants'; import type { Env, InstanceQueryParams, @@ -299,19 +300,8 @@ export class QueryService { // Validate sort order at service level (defense in depth) const validatedSortOrder = sortOrder?.toLowerCase() === 'desc' ? 'DESC' : 'ASC'; - // Map sort fields to actual column names - const sortFieldMap: Record = { - 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', - name: 'it.instance_name', - instance_name: 'it.instance_name', - }; - - const sortColumn = sortFieldMap[sortBy] ?? 'pr.hourly_price'; + // Map sort fields to actual column names (imported from constants.ts) + const sortColumn = SORT_FIELD_MAP[sortBy] ?? 'pr.hourly_price'; // Handle NULL values in pricing columns (NULL values go last) if (sortColumn.startsWith('pr.')) { diff --git a/src/services/sync.ts b/src/services/sync.ts index 1efa684..6ae1271 100644 --- a/src/services/sync.ts +++ b/src/services/sync.ts @@ -438,6 +438,296 @@ export class SyncOrchestrator { } } + /** + * Synchronize pricing data only (no regions/instances update) + * + * Lightweight sync operation that only updates pricing data from provider APIs. + * Skips region and instance type synchronization. + * + * @param provider - Provider name (linode, vultr, aws) + * @returns Sync result with pricing statistics + */ + async syncPricingOnly(provider: string): Promise { + const startTime = Date.now(); + let stage = SyncStage.INIT; + + this.logger.info('Starting pricing-only sync for provider', { provider }); + + try { + // Stage 1: Initialize - Fetch provider record + stage = SyncStage.INIT; + const providerRecord = await this.repos.providers.findByName(provider); + if (!providerRecord) { + throw new Error(`Provider not found in database: ${provider}`); + } + + // Update provider status to syncing + await this.repos.providers.updateSyncStatus(provider, 'syncing'); + this.logger.info(`${provider} → ${stage} (pricing only)`); + + // Stage 2: Initialize connector and authenticate + const connector = await this.createConnector(provider, providerRecord.id); + await withTimeout(connector.authenticate(), 10000, `${provider} authentication`); + this.logger.info(`${provider} → initialized (pricing only)`); + + // Fetch existing instance and region IDs from database + const batchQueries = [ + this.repos.db.prepare('SELECT id, region_code FROM regions WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM instance_types WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM gpu_instances WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM g8_instances WHERE provider_id = ?').bind(providerRecord.id), + this.repos.db.prepare('SELECT id, instance_id FROM vpu_instances WHERE provider_id = ?').bind(providerRecord.id) + ]; + + const [dbRegionsResult, dbInstancesResult, dbGpuResult, dbG8Result, dbVpuResult] = await this.repos.db.batch(batchQueries); + + if (!dbRegionsResult.success || !dbInstancesResult.success) { + throw new Error('Failed to fetch regions/instances for pricing'); + } + + // Validate and extract region IDs + if (!Array.isArray(dbRegionsResult.results)) { + throw new Error('Unexpected database result format for regions'); + } + const regionIds = dbRegionsResult.results.map((r: any) => { + if (typeof r?.id !== 'number') { + throw new Error('Invalid region id in database result'); + } + return r.id; + }); + + // Validate and extract instance type data + if (!Array.isArray(dbInstancesResult.results)) { + throw new Error('Unexpected database result format for instances'); + } + const dbInstancesData = dbInstancesResult.results.map((i: any) => { + if (typeof i?.id !== 'number' || typeof i?.instance_id !== 'string') { + throw new Error('Invalid instance data in database result'); + } + return { id: i.id, instance_id: i.instance_id }; + }); + const instanceTypeIds = dbInstancesData.map(i => i.id); + + // Create instance mapping + const dbInstanceMap = new Map( + dbInstancesData.map(i => [i.id, { instance_id: i.instance_id }]) + ); + + // Create specialized instance mappings + if (!Array.isArray(dbGpuResult.results)) { + throw new Error('Unexpected database result format for GPU instances'); + } + const dbGpuMap = new Map( + dbGpuResult.results.map((i: any) => { + if (typeof i?.id !== 'number' || typeof i?.instance_id !== 'string') { + throw new Error('Invalid GPU instance data in database result'); + } + return [i.id, { instance_id: i.instance_id }]; + }) + ); + + if (!Array.isArray(dbG8Result.results)) { + throw new Error('Unexpected database result format for G8 instances'); + } + const dbG8Map = new Map( + dbG8Result.results.map((i: any) => { + if (typeof i?.id !== 'number' || typeof i?.instance_id !== 'string') { + throw new Error('Invalid G8 instance data in database result'); + } + return [i.id, { instance_id: i.instance_id }]; + }) + ); + + if (!Array.isArray(dbVpuResult.results)) { + throw new Error('Unexpected database result format for VPU instances'); + } + const dbVpuMap = new Map( + dbVpuResult.results.map((i: any) => { + if (typeof i?.id !== 'number' || typeof i?.instance_id !== 'string') { + throw new Error('Invalid VPU instance data in database result'); + } + return [i.id, { instance_id: i.instance_id }]; + }) + ); + + // Get pricing data + stage = SyncStage.PERSIST; + const pricingResult = await withTimeout( + connector.getPricing( + instanceTypeIds, + regionIds, + dbInstanceMap, + dbGpuMap, + dbG8Map, + dbVpuMap + ), + 180000, + `${provider} fetch pricing` + ); + + // Handle both return types + let pricingCount = 0; + if (typeof pricingResult === 'number') { + pricingCount = pricingResult; + } else if (pricingResult.length > 0) { + pricingCount = await this.repos.pricing.upsertMany(pricingResult); + } + + this.logger.info(`${provider} → pricing updated`, { pricing: pricingCount }); + + // Stage: Sync Anvil Pricing (if applicable) + stage = SyncStage.SYNC_ANVIL_PRICING; + let anvilPricingCount = 0; + try { + anvilPricingCount = await this.syncAnvilPricing(provider); + if (anvilPricingCount > 0) { + this.logger.info(`${provider} → ${stage}`, { anvil_pricing: anvilPricingCount }); + } + } catch (anvilError) { + this.logger.error('Anvil pricing sync failed', { + provider, + error: anvilError instanceof Error ? anvilError.message : String(anvilError) + }); + } + + // Sync Anvil Transfer Pricing + let anvilTransferPricingCount = 0; + try { + anvilTransferPricingCount = await this.syncAnvilTransferPricing(provider); + if (anvilTransferPricingCount > 0) { + this.logger.info(`${provider} → SYNC_ANVIL_TRANSFER_PRICING`, { anvil_transfer_pricing: anvilTransferPricingCount }); + } + } catch (transferError) { + this.logger.error('Anvil transfer pricing sync failed', { + provider, + error: transferError instanceof Error ? transferError.message : String(transferError) + }); + } + + // Complete - Update provider status + stage = SyncStage.COMPLETE; + await this.repos.providers.updateSyncStatus(provider, 'success'); + + const duration = Date.now() - startTime; + this.logger.info(`${provider} → ${stage} (pricing only)`, { duration_ms: duration }); + + return { + provider, + success: true, + regions_synced: 0, + instances_synced: 0, + pricing_synced: pricingCount, + duration_ms: duration, + }; + + } catch (error) { + const duration = Date.now() - startTime; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + + this.logger.error(`${provider} pricing sync failed at ${stage}`, { + error: error instanceof Error ? error.message : String(error), + stage + }); + + // Update provider status to error + try { + await this.repos.providers.updateSyncStatus(provider, 'error', errorMessage); + } catch (statusError) { + this.logger.error('Failed to update provider status', { + error: statusError instanceof Error ? statusError.message : String(statusError) + }); + } + + return { + provider, + success: false, + regions_synced: 0, + instances_synced: 0, + pricing_synced: 0, + duration_ms: duration, + error: errorMessage, + error_details: { + stage, + message: errorMessage, + }, + }; + } + } + + /** + * Synchronize pricing data only for all providers + * + * Lightweight sync operation that only updates pricing data. + * Skips region and instance type synchronization. + * + * @param providers - Array of provider names to sync (defaults to all supported providers) + * @returns Complete sync report with pricing statistics + */ + async syncAllPricingOnly(providers: string[] = [...SUPPORTED_PROVIDERS]): Promise { + const startedAt = new Date().toISOString(); + const startTime = Date.now(); + + this.logger.info('Starting pricing-only sync for providers', { providers: providers.join(', ') }); + + const providerResults: ProviderSyncResult[] = []; + + for (const provider of providers) { + try { + const result = await this.syncPricingOnly(provider); + providerResults.push(result); + + this.logger.info('Provider pricing sync completed', { + provider, + success: result.success, + elapsed_ms: Date.now() - startTime + }); + } catch (error) { + providerResults.push({ + provider, + success: false, + regions_synced: 0, + instances_synced: 0, + pricing_synced: 0, + duration_ms: 0, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + const completedAt = new Date().toISOString(); + const totalDuration = Date.now() - startTime; + + const successful = providerResults.filter(r => r.success); + const failed = providerResults.filter(r => !r.success); + + const summary = { + total_providers: providers.length, + successful_providers: successful.length, + failed_providers: failed.length, + total_regions: 0, + total_instances: 0, + total_pricing: providerResults.reduce((sum, r) => sum + r.pricing_synced, 0), + }; + + const report: SyncReport = { + success: failed.length === 0, + started_at: startedAt, + completed_at: completedAt, + total_duration_ms: totalDuration, + providers: providerResults, + summary, + }; + + this.logger.info('Pricing sync complete', { + total: summary.total_providers, + success: summary.successful_providers, + failed: summary.failed_providers, + duration_ms: totalDuration, + }); + + return report; + } + /** * Synchronize all providers * diff --git a/src/types.ts b/src/types.ts index 19c4177..99a02e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -416,6 +416,17 @@ export interface Env { ENVIRONMENT?: string; } +/** + * Hono context variables + * Shared across all request contexts + */ +export interface HonoVariables { + /** Unique request ID for tracing */ + requestId: string; + /** Authentication status (set by auth middleware) */ + authenticated?: boolean; +} + // ============================================================ // Synchronization Types // ============================================================