refactor: code review 기반 품질 개선

- 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>
This commit is contained in:
kappa
2026-01-29 10:58:27 +09:00
parent d9c6f78f38
commit de790988b4
10 changed files with 440 additions and 100 deletions

View File

@@ -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;

View File

@@ -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<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;
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<string>;
export type ValidSortField = keyof typeof SORT_FIELD_MAP;
/**
* Valid sort orders

View File

@@ -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<void> {
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<void> => {
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());
}
},
};

View File

@@ -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<void> {
// 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<Response | void> {
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<Response | void> {
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<void> {
const apiKey = c.req.header('X-API-Key');

View File

@@ -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<Response> {
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);
}
}

View File

@@ -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<Response> {
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}`,
},
}
);
}
@@ -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}`,
},
}
);
} 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
);
}
}

View File

@@ -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<Response> {
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
);
}
}

View File

@@ -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<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',
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.')) {

View File

@@ -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<ProviderSyncResult> {
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<SyncReport> {
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
*

View File

@@ -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
// ============================================================