BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
407
ts/core/config/configuration-builder.ts
Normal file
407
ts/core/config/configuration-builder.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
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<ElasticsearchConfig> = {};
|
||||
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<ElasticsearchConfig>;
|
||||
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<ElasticsearchConfig>): 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<void> {
|
||||
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>): 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<ElasticsearchConfig> {
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user