/** * Migration Progress API Client * * Client library for interacting with the postgres.do migration progress API. * Provides programmatic access to migration status, version distribution, * and operation management across Durable Objects. * * @module migrations/progress-api */ /** * Migration status summary from API */ export interface MigrationStatusSummary { /** Total number of DOs being tracked */ totalDOs: number /** Distribution of DOs by schema version */ versionDistribution: Record /** Current target version (latest) */ currentVersion: number /** Number of failed migrations */ failedMigrations: number /** Number of DOs currently migrating */ runningMigrations: number /** Migration velocity (DOs per hour) */ migrationVelocity: number /** Estimated time to completion in milliseconds */ estimatedTimeRemainingMs: number | null /** Overall completion percentage */ percentComplete: number /** Timestamp of last update */ lastUpdated: string } /** * DO migration detail for list views */ export interface DOMigrationDetail { /** Durable Object ID */ doId: string /** Current schema version */ version: number /** Migration status */ status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' /** Last migration attempt timestamp */ lastAttempt?: string /** Error message if failed */ error?: string /** Execution time in milliseconds */ executionTimeMs?: number } /** * Active migration operation */ export interface MigrationOperationSummary { /** Migration operation ID */ migrationId: string /** Target schema version */ targetVersion: number /** Operation status */ status: 'pending' | 'running' | 'paused' | 'completed' | 'failed' /** Total DOs in this operation */ totalDOs: number /** Completed DOs */ completedDOs: number /** Failed DOs */ failedDOs: number /** When operation started */ startedAt: string /** When operation completed (if applicable) */ completedAt?: string /** Who initiated the operation */ initiatedBy?: string /** Operation description */ description?: string } /** * Version distribution entry */ export interface VersionDistributionEntry { /** Schema version */ version: number /** Number of DOs at this version */ count: number /** Percentage of total */ percentage: number } /** * Pagination options for list requests */ export interface PaginationOptions { /** Number of results to return */ limit?: number /** Number of results to skip */ offset?: number } /** * Alert configuration for migration monitoring */ export interface AlertConfig { /** Alert on failure rate exceeding threshold */ failureRateThreshold?: number /** Alert on velocity drop below threshold (DOs/hour) */ velocityThreshold?: number /** Alert on DOs stuck at old version for longer than threshold (ms) */ stuckThresholdMs?: number /** Webhook URL to send alerts */ webhookUrl?: string } /** * Configuration for the Migration Progress API client */ export interface MigrationProgressApiConfig { /** Base API URL */ apiUrl: string /** API key for authentication */ apiKey?: string /** Custom fetch implementation */ fetch?: typeof fetch /** Request timeout in milliseconds */ timeout?: number } /** * API Error class */ export class MigrationProgressApiError extends Error { constructor( message: string, public readonly status: number, public readonly response?: string ) { super(message) this.name = 'MigrationProgressApiError' } } /** * Migration Progress API Client * * Provides programmatic access to migration status and progress tracking * across Durable Objects. * * @example * ```typescript * import { MigrationProgressApi } from 'postgres.do/migrations' * * const api = new MigrationProgressApi({ * apiUrl: 'https://api.postgres.do', * apiKey: process.env.POSTGRES_DO_API_KEY, * }) * * // Get overall status * const status = await api.getStatus() * console.log(`${status.percentComplete}% complete`) * * // Get version distribution * const distribution = await api.getVersionDistribution() * for (const entry of distribution) { * console.log(`v${entry.version}: ${entry.count} DOs (${entry.percentage}%)`) * } * * // Get failed migrations * const { dos, total } = await api.getFailedMigrations() * console.log(`${total} failed migrations`) * ``` */ export class MigrationProgressApi { private apiUrl: string private apiKey?: string private fetchFn: typeof fetch private timeout: number constructor(config: MigrationProgressApiConfig) { this.apiUrl = config.apiUrl.replace(/\/$/, '') // Remove trailing slash if (config.apiKey !== undefined) { this.apiKey = config.apiKey } this.fetchFn = config.fetch ?? fetch this.timeout = config.timeout ?? 30000 } /** * Make an authenticated API request */ private async request( method: string, path: string, body?: unknown ): Promise { const url = `${this.apiUrl}${path}` const headers: Record = { 'Content-Type': 'application/json', } if (this.apiKey) { headers['Authorization'] = `Bearer ${this.apiKey}` } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.timeout) try { const requestInit: RequestInit = { method, headers, signal: controller.signal, } if (body) { requestInit.body = JSON.stringify(body) } const response = await this.fetchFn(url, requestInit) if (!response.ok) { const error = await response.text() throw new MigrationProgressApiError( `API request failed: ${response.status} ${response.statusText}`, response.status, error ) } return response.json() as Promise } finally { clearTimeout(timeoutId) } } /** * Get overall migration status summary * * @returns Migration status including version distribution, velocity, and ETA * * @example * ```typescript * const status = await api.getStatus() * console.log(`Total DOs: ${status.totalDOs}`) * console.log(`Current version: v${status.currentVersion}`) * console.log(`Failed: ${status.failedMigrations}`) * console.log(`Completion: ${status.percentComplete}%`) * ``` */ async getStatus(): Promise { return this.request('GET', '/api/migrations/status') } /** * Get version distribution as a sorted list * * @returns Array of version entries sorted by version descending * * @example * ```typescript * const distribution = await api.getVersionDistribution() * for (const entry of distribution) { * console.log(`v${entry.version}: ${entry.count} (${entry.percentage}%)`) * } * ``` */ async getVersionDistribution(): Promise { const status = await this.getStatus() const total = status.totalDOs return Object.entries(status.versionDistribution) .map(([version, count]) => ({ version: parseInt(version, 10), count, percentage: total > 0 ? Math.round((count / total) * 100 * 10) / 10 : 0, })) .sort((a, b) => b.version - a.version) } /** * Get DOs at a specific schema version * * @param version Schema version to query * @param options Pagination options * @returns Paginated list of DO migration details * * @example * ```typescript * const { dos, total } = await api.getDOsByVersion(10) * console.log(`${total} DOs at version 10`) * for (const d of dos) { * console.log(` ${d.doId}: ${d.status}`) * } * ``` */ async getDOsByVersion( version: number, options?: PaginationOptions ): Promise<{ dos: DOMigrationDetail[]; total: number }> { const params = new URLSearchParams() params.set('version', version.toString()) if (options?.limit) params.set('limit', options.limit.toString()) if (options?.offset) params.set('offset', options.offset.toString()) return this.request<{ dos: DOMigrationDetail[]; total: number }>( 'GET', `/api/migrations/dos?${params.toString()}` ) } /** * Get failed migrations * * @param options Pagination options * @returns Paginated list of failed DO migrations * * @example * ```typescript * const { dos, total } = await api.getFailedMigrations({ limit: 50 }) * console.log(`${total} failed migrations:`) * for (const d of dos) { * console.log(` ${d.doId}: ${d.error}`) * } * ``` */ async getFailedMigrations( options?: PaginationOptions ): Promise<{ dos: DOMigrationDetail[]; total: number }> { const params = new URLSearchParams() if (options?.limit) params.set('limit', options.limit.toString()) if (options?.offset) params.set('offset', options.offset.toString()) return this.request<{ dos: DOMigrationDetail[]; total: number }>( 'GET', `/api/migrations/failed?${params.toString()}` ) } /** * List all migration operations * * @returns Array of migration operation summaries * * @example * ```typescript * const operations = await api.listOperations() * for (const op of operations) { * console.log(`${op.migrationId}: ${op.status}`) * } * ``` */ async listOperations(): Promise { return this.request('GET', '/api/migrations/operations') } /** * Get details of a specific migration operation * * @param migrationId Migration operation ID * @returns Operation summary with progress details * * @example * ```typescript * const op = await api.getOperation('migration-2024-01') * console.log(`Progress: ${op.completedDOs}/${op.totalDOs}`) * ``` */ async getOperation(migrationId: string): Promise { return this.request( 'GET', `/api/migrations/operations/${encodeURIComponent(migrationId)}` ) } /** * Retry failed migrations for an operation * * @param migrationId Migration operation ID * @returns Number of migrations queued for retry * * @example * ```typescript * const { retried } = await api.retryFailed('migration-2024-01') * console.log(`Queued ${retried} migrations for retry`) * ``` */ async retryFailed(migrationId: string): Promise<{ retried: number }> { return this.request<{ retried: number }>( 'POST', `/api/migrations/operations/${encodeURIComponent(migrationId)}/retry` ) } /** * Pause a running migration operation * * @param migrationId Migration operation ID * * @example * ```typescript * await api.pauseOperation('migration-2024-01') * console.log('Migration paused') * ``` */ async pauseOperation(migrationId: string): Promise { await this.request<{ success: boolean }>( 'POST', `/api/migrations/operations/${encodeURIComponent(migrationId)}/pause` ) } /** * Resume a paused migration operation * * @param migrationId Migration operation ID * * @example * ```typescript * await api.resumeOperation('migration-2024-01') * console.log('Migration resumed') * ``` */ async resumeOperation(migrationId: string): Promise { await this.request<{ success: boolean }>( 'POST', `/api/migrations/operations/${encodeURIComponent(migrationId)}/resume` ) } /** * Cancel a migration operation * * Note: This does not rollback completed migrations. * * @param migrationId Migration operation ID * * @example * ```typescript * await api.cancelOperation('migration-2024-01') * console.log('Migration cancelled') * ``` */ async cancelOperation(migrationId: string): Promise { await this.request<{ success: boolean }>( 'POST', `/api/migrations/operations/${encodeURIComponent(migrationId)}/cancel` ) } /** * Configure alerts for migration monitoring * * @param config Alert configuration * * @example * ```typescript * await api.configureAlerts({ * failureRateThreshold: 0.05, // 5% * velocityThreshold: 100, // 100 DOs/hour * webhookUrl: 'https://hooks.slack.com/...', * }) * ``` */ async configureAlerts(config: AlertConfig): Promise { await this.request<{ success: boolean }>('PUT', '/api/migrations/alerts', config) } /** * Get current alert configuration * * @returns Current alert configuration */ async getAlertConfig(): Promise { return this.request('GET', '/api/migrations/alerts') } } /** * Create a Migration Progress API client * * @param config API configuration * @returns Configured API client * * @example * ```typescript * const api = createMigrationProgressApi({ * apiUrl: 'https://api.postgres.do', * apiKey: process.env.POSTGRES_DO_API_KEY, * }) * ``` */ export function createMigrationProgressApi( config: MigrationProgressApiConfig ): MigrationProgressApi { return new MigrationProgressApi(config) }