import { readFileSync } from 'fs'; import type { ElasticsearchConfig, AuthConfig, SecretProvider, } from './types.js'; import { ConfigValidationError, EnvironmentSecretProvider, } from './types.js'; import { LogLevel } from '../observability/logger.js'; /** * Configuration builder for fluent Elasticsearch configuration * * @example * ```typescript * const config = new ConfigurationBuilder() * .nodes(['http://localhost:9200', 'http://localhost:9201']) * .auth({ type: 'basic', username: 'elastic', password: 'changeme' }) * .timeout(30000) * .retries(3) * .build(); * ``` */ export class ConfigurationBuilder { private config: Partial = {}; private secretProvider?: SecretProvider; /** * Set Elasticsearch node(s) */ nodes(nodes: string | string[]): this { this.config.nodes = nodes; return this; } /** * Set authentication configuration */ auth(auth: AuthConfig): this { this.config.auth = auth; return this; } /** * Set basic authentication */ basicAuth(username: string, password: string): this { this.config.auth = { type: 'basic', username, password }; return this; } /** * Set API key authentication */ apiKeyAuth(apiKey: string): this { this.config.auth = { type: 'apiKey', apiKey }; return this; } /** * Set bearer token authentication */ bearerAuth(token: string): this { this.config.auth = { type: 'bearer', token }; return this; } /** * Set cloud ID authentication */ cloudAuth(id: string, options?: { username?: string; password?: string; apiKey?: string }): this { this.config.auth = { type: 'cloud', id, ...options, }; return this; } /** * Set request timeout */ timeout(timeoutMs: number): this { if (!this.config.request) { this.config.request = {}; } this.config.request.timeout = timeoutMs; return this; } /** * Set maximum retries */ retries(maxRetries: number): this { if (!this.config.request) { this.config.request = {}; } this.config.request.maxRetries = maxRetries; return this; } /** * Enable compression */ compression(enabled: boolean = true): this { if (!this.config.request) { this.config.request = {}; } this.config.request.compression = enabled; return this; } /** * Set connection pool size */ poolSize(max: number, min?: number): this { if (!this.config.pool) { this.config.pool = {}; } this.config.pool.maxConnections = max; if (min !== undefined) { this.config.pool.minIdleConnections = min; } return this; } /** * Enable node discovery/sniffing */ discovery(enabled: boolean = true, options?: { interval?: number }): this { this.config.discovery = { enabled, ...options, }; return this; } /** * Set log level */ logLevel(level: LogLevel): this { if (!this.config.logging) { this.config.logging = {}; } this.config.logging.level = level; return this; } /** * Enable request/response logging */ enableRequestLogging(enabled: boolean = true): this { if (!this.config.logging) { this.config.logging = {}; } this.config.logging.enableRequestLogging = enabled; this.config.logging.enableResponseLogging = enabled; return this; } /** * Enable metrics collection */ enableMetrics(enabled: boolean = true, prefix?: string): this { this.config.metrics = { enabled, ...(prefix && { prefix }), }; return this; } /** * Enable tracing */ enableTracing(enabled: boolean = true, options?: { serviceName?: string; serviceVersion?: string }): this { this.config.tracing = { enabled, ...options, }; return this; } /** * Set proxy URL */ proxy(proxyUrl: string): this { this.config.proxy = proxyUrl; return this; } /** * Load configuration from environment variables * * Supported environment variables: * - ELASTICSEARCH_URL or ELASTICSEARCH_NODES (comma-separated) * - ELASTICSEARCH_USERNAME * - ELASTICSEARCH_PASSWORD * - ELASTICSEARCH_API_KEY * - ELASTICSEARCH_CLOUD_ID * - ELASTICSEARCH_TIMEOUT * - ELASTICSEARCH_MAX_RETRIES * - ELASTICSEARCH_LOG_LEVEL * - ELASTICSEARCH_PROXY */ fromEnv(): this { // Nodes const url = process.env.ELASTICSEARCH_URL; const nodes = process.env.ELASTICSEARCH_NODES; if (url) { this.config.nodes = url; } else if (nodes) { this.config.nodes = nodes.split(',').map((n) => n.trim()); } // Authentication const apiKey = process.env.ELASTICSEARCH_API_KEY; const username = process.env.ELASTICSEARCH_USERNAME; const password = process.env.ELASTICSEARCH_PASSWORD; const cloudId = process.env.ELASTICSEARCH_CLOUD_ID; if (apiKey) { this.apiKeyAuth(apiKey); } else if (cloudId) { this.cloudAuth(cloudId, { username, password, apiKey }); } else if (username && password) { this.basicAuth(username, password); } // Request settings const timeout = process.env.ELASTICSEARCH_TIMEOUT; if (timeout) { this.timeout(parseInt(timeout, 10)); } const maxRetries = process.env.ELASTICSEARCH_MAX_RETRIES; if (maxRetries) { this.retries(parseInt(maxRetries, 10)); } // Logging const logLevel = process.env.ELASTICSEARCH_LOG_LEVEL as LogLevel; if (logLevel && Object.values(LogLevel).includes(logLevel)) { this.logLevel(logLevel); } // Proxy const proxy = process.env.ELASTICSEARCH_PROXY; if (proxy) { this.proxy(proxy); } return this; } /** * Load configuration from JSON file */ fromFile(filePath: string): this { try { const fileContent = readFileSync(filePath, 'utf-8'); const fileConfig = JSON.parse(fileContent) as Partial; this.fromObject(fileConfig); } catch (error) { throw new ConfigValidationError( 'file', `Failed to load configuration from file: ${(error as Error).message}` ); } return this; } /** * Load configuration from object */ fromObject(configObject: Partial): this { // Merge the object into current config this.config = { ...this.config, ...configObject, }; return this; } /** * Set secret provider for fetching credentials */ withSecrets(provider: SecretProvider): this { this.secretProvider = provider; return this; } /** * Resolve secrets using the configured secret provider */ private async resolveSecrets(): Promise { if (!this.secretProvider) { this.secretProvider = new EnvironmentSecretProvider(); } // Resolve authentication secrets if (this.config.auth) { switch (this.config.auth.type) { case 'basic': { const usernameSecret = await this.secretProvider.getSecret('ELASTICSEARCH_USERNAME'); const passwordSecret = await this.secretProvider.getSecret('ELASTICSEARCH_PASSWORD'); if (usernameSecret) this.config.auth.username = usernameSecret; if (passwordSecret) this.config.auth.password = passwordSecret; break; } case 'apiKey': { const apiKeySecret = await this.secretProvider.getSecret('ELASTICSEARCH_API_KEY'); if (apiKeySecret) this.config.auth.apiKey = apiKeySecret; break; } case 'bearer': { const tokenSecret = await this.secretProvider.getSecret('ELASTICSEARCH_BEARER_TOKEN'); if (tokenSecret) this.config.auth.token = tokenSecret; break; } } } } /** * Validate the configuration */ private validate(config: Partial): ElasticsearchConfig { // Required fields if (!config.nodes) { throw new ConfigValidationError('nodes', 'Elasticsearch node(s) must be specified'); } // Normalize nodes to array const nodes = Array.isArray(config.nodes) ? config.nodes : [config.nodes]; if (nodes.length === 0) { throw new ConfigValidationError('nodes', 'At least one Elasticsearch node must be specified'); } // Validate node URLs for (const node of nodes) { try { new URL(node); } catch { throw new ConfigValidationError( 'nodes', `Invalid node URL: ${node}. Must be a valid HTTP(S) URL` ); } } // Validate timeout if (config.request?.timeout !== undefined && config.request.timeout <= 0) { throw new ConfigValidationError('request.timeout', 'Timeout must be a positive number'); } // Validate retries if (config.request?.maxRetries !== undefined && config.request.maxRetries < 0) { throw new ConfigValidationError('request.maxRetries', 'Max retries cannot be negative'); } // Validate pool size if (config.pool?.maxConnections !== undefined && config.pool.maxConnections <= 0) { throw new ConfigValidationError('pool.maxConnections', 'Max connections must be positive'); } if ( config.pool?.minIdleConnections !== undefined && config.pool?.maxConnections !== undefined && config.pool.minIdleConnections > config.pool.maxConnections ) { throw new ConfigValidationError( 'pool.minIdleConnections', 'Min idle connections cannot exceed max connections' ); } return { nodes, ...config, } as ElasticsearchConfig; } /** * Build and validate the configuration */ async buildAsync(): Promise { await this.resolveSecrets(); return this.validate(this.config); } /** * Build and validate the configuration (synchronous) */ build(): ElasticsearchConfig { return this.validate(this.config); } } /** * Create a new configuration builder */ export function createConfig(): ConfigurationBuilder { return new ConfigurationBuilder(); }