/** * Connection URL parsing for postgres.do * * Parses postgres:// URLs into configuration objects */ import type { PostgresConfig } from './types' import { ConnectionError } from './types' /** * Parsed connection information */ export interface ConnectionInfo { /** The https base URL for postgres.do */ url: string /** Database name */ database?: string /** API key for authentication */ apiKey?: string /** SSL mode */ ssl?: PostgresConfig['ssl'] /** Additional parameters from URL query string */ params: Record } /** * Parse a postgres:// connection URL * * @param connectionUrl - The postgres:// or postgresql:// URL * @returns Parsed connection information * * @example * ```typescript * // Basic URL * parseConnectionUrl('postgres://db.postgres.do/mydb') * // => { url: 'https://db.postgres.do/mydb', database: 'mydb', params: {} } * * // With API key in password * parseConnectionUrl('postgres://user:apikey123@db.postgres.do/mydb') * // => { url: 'https://db.postgres.do/mydb', apiKey: 'apikey123', params: {} } * * // With query parameters * parseConnectionUrl('postgres://db.postgres.do/mydb?sslmode=require') * // => { url: 'https://db.postgres.do/mydb', ssl: 'require', params: { sslmode: 'require' } } * ``` */ export function parseConnectionUrl(connectionUrl: string): ConnectionInfo { let parsed: URL try { parsed = new URL(connectionUrl) } catch (error) { throw new ConnectionError( `Invalid connection URL: ${error instanceof Error ? error.message : String(error)}` ) } // Validate protocol if (!parsed.protocol.startsWith('postgres')) { throw new ConnectionError( `Invalid protocol "${parsed.protocol}": must be postgres:// or postgresql://` ) } // Validate host is present if (!parsed.host) { throw new ConnectionError('Invalid connection URL: missing host') } // Extract components const info: ConnectionInfo = { url: `https://${parsed.host}${parsed.pathname}`, params: {}, } // Database name from pathname const pathname = parsed.pathname if (pathname && pathname !== '/') { info.database = pathname.slice(1) // Remove leading / } // API key from password field if (parsed.password) { info.apiKey = decodeURIComponent(parsed.password) } // API key from query parameter (takes precedence) const apiKeyParam = parsed.searchParams.get('apiKey') || parsed.searchParams.get('apikey') if (apiKeyParam) { info.apiKey = apiKeyParam } // SSL mode const sslMode = parsed.searchParams.get('sslmode') || parsed.searchParams.get('ssl') if (sslMode) { info.ssl = sslMode as PostgresConfig['ssl'] } // Collect all query parameters for (const [key, value] of parsed.searchParams) { info.params[key] = value } return info } /** * Convert ConnectionInfo to PostgresConfig */ export function connectionInfoToConfig(info: ConnectionInfo): Partial { const config: Partial = { url: info.url, } if (info.apiKey) { config.apiKey = info.apiKey } if (info.ssl) { config.ssl = info.ssl } // Handle transport parameter const transport = info.params['transport'] if (transport === 'ws' || transport === 'websocket') { config.transport = 'ws' } // Handle timeout parameters const connectTimeout = info.params['connect_timeout'] || info.params['connectTimeout'] if (connectTimeout) { config.connectTimeout = parseInt(connectTimeout, 10) * 1000 // Convert to ms } const queryTimeout = info.params['statement_timeout'] || info.params['queryTimeout'] if (queryTimeout) { config.queryTimeout = parseInt(queryTimeout, 10) * 1000 // Convert to ms } return config } /** * Build a connection URL from config options */ export function buildConnectionUrl(config: PostgresConfig): string { const baseUrl = config.url || 'https://db.postgres.do' const url = new URL(baseUrl.replace('https://', 'postgres://').replace('http://', 'postgres://')) if (config.apiKey) { url.password = config.apiKey } if (config.ssl) { url.searchParams.set('sslmode', typeof config.ssl === 'boolean' ? 'require' : config.ssl) } if (config.transport === 'ws') { url.searchParams.set('transport', 'ws') } return url.toString() } /** * Validate a connection URL without parsing */ export function isValidConnectionUrl(connectionUrl: string): boolean { try { const url = new URL(connectionUrl) return url.protocol.startsWith('postgres') } catch { return false } }