/** * HTTP Transport for postgres.do * Sends SQL queries over HTTP to the postgres.do API */ import type { Transport, QueryResult, Row, TransactionOptions, HttpTransportConfig, FieldInfo, } from '../types' import { PostgresError, ConnectionError, TimeoutError } from '../types' /** Default configuration values */ const DEFAULTS = { timeout: 30000, // 30 seconds baseUrl: 'https://db.postgres.do', } /** * HTTP Transport implementation * Stateless transport that sends each query as an HTTP request */ export class HttpTransport implements Transport { private readonly config: Required> & HttpTransportConfig private readonly fetchImpl: typeof fetch private connected = false constructor(config: HttpTransportConfig) { this.config = { ...DEFAULTS, ...config, } this.fetchImpl = config.fetch || globalThis.fetch this.connected = true } /** * Execute a SQL query */ async query( sql: string, params?: unknown[] ): Promise> { const response = await this.request>('/query', { sql, params: params || [], }) return this.parseQueryResponse(response) } /** * Execute multiple queries in a transaction */ async transaction( queries: Array<{ sql: string; params?: unknown[] }>, options?: TransactionOptions ): Promise { const response = await this.request('/transaction', { queries, options: options ? this.serializeTransactionOptions(options) : undefined, }) // Return the results of all queries return response.results as T } /** * Close the transport (no-op for HTTP) */ async close(): Promise { this.connected = false } /** * Check if transport is connected */ isConnected(): boolean { return this.connected } /** * Make an HTTP request to the postgres.do API */ private async request( path: string, body: unknown ): Promise { const url = `${this.config.baseUrl}${path}` const headers: Record = { 'Content-Type': 'application/json', ...this.config.headers, } if (this.config.apiKey) { headers['Authorization'] = `Bearer ${this.config.apiKey}` } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), this.config.timeout) try { const response = await this.fetchImpl(url, { method: 'POST', headers, body: JSON.stringify(body), signal: controller.signal, }) clearTimeout(timeoutId) if (!response.ok) { const error = await this.parseErrorResponse(response) throw error } return await response.json() as T } catch (error) { clearTimeout(timeoutId) if (error instanceof Error) { if (error.name === 'AbortError') { throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`) } if (error instanceof PostgresError || error instanceof ConnectionError) { throw error } } throw new ConnectionError( `Failed to connect to postgres.do: ${error instanceof Error ? error.message : String(error)}` ) } } /** * Parse error response from the API */ private async parseErrorResponse(response: Response): Promise { try { const body = await response.json() as ErrorResponse if (body.error?.code) { return new PostgresError(body.error.message || 'Database error', { code: body.error.code, severity: body.error.severity || 'ERROR', detail: body.error.detail, hint: body.error.hint, position: body.error.position, schema: body.error.schema, table: body.error.table, column: body.error.column, constraint: body.error.constraint, }) } return new ConnectionError(body.error?.message || `HTTP ${response.status}: ${response.statusText}`) } catch { return new ConnectionError(`HTTP ${response.status}: ${response.statusText}`) } } /** * Parse query response into QueryResult */ private parseQueryResponse(response: QueryResponse): QueryResult { return { rows: response.rows || [], rowCount: response.rowCount ?? response.rows?.length ?? 0, fields: response.fields || [], command: response.command || 'SELECT', } } /** * Serialize transaction options for the API */ private serializeTransactionOptions(options: TransactionOptions): Record { return { isolationLevel: options.isolationLevel, readOnly: options.readOnly, deferrable: options.deferrable, } } } /** API response types */ interface QueryResponse { rows?: T[] rowCount?: number fields?: FieldInfo[] command?: string } interface TransactionResponse { results: unknown[] } interface ErrorResponse { error?: { message?: string code?: string severity?: string detail?: string hint?: string position?: number schema?: string table?: string column?: string constraint?: string } } /** * Create an HTTP transport instance */ export function createHttpTransport(config: HttpTransportConfig): HttpTransport { return new HttpTransport(config) }