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();
|
||||
}
|
||||
15
ts/core/config/index.ts
Normal file
15
ts/core/config/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Configuration management for Elasticsearch client
|
||||
*
|
||||
* This module provides:
|
||||
* - Fluent configuration builder
|
||||
* - Environment variable support
|
||||
* - File-based configuration
|
||||
* - Secret provider integration
|
||||
* - Configuration validation
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
export * from './configuration-builder.js';
|
||||
232
ts/core/config/types.ts
Normal file
232
ts/core/config/types.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import type { RetryConfig } from '../errors/types.js';
|
||||
import { LogLevel } from '../observability/logger.js';
|
||||
|
||||
/**
|
||||
* Authentication configuration
|
||||
*/
|
||||
export type AuthConfig =
|
||||
| {
|
||||
type: 'basic';
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
type: 'apiKey';
|
||||
apiKey: string;
|
||||
}
|
||||
| {
|
||||
type: 'bearer';
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: 'cloud';
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TLS/SSL configuration
|
||||
*/
|
||||
export interface TLSConfig {
|
||||
/** Reject unauthorized certificates */
|
||||
rejectUnauthorized?: boolean;
|
||||
|
||||
/** CA certificate(s) */
|
||||
ca?: string | string[] | Buffer | Buffer[];
|
||||
|
||||
/** Client certificate */
|
||||
cert?: string | Buffer;
|
||||
|
||||
/** Client private key */
|
||||
key?: string | Buffer;
|
||||
|
||||
/** Passphrase for client key */
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection pool configuration
|
||||
*/
|
||||
export interface ConnectionPoolConfig {
|
||||
/** Maximum number of connections */
|
||||
maxConnections?: number;
|
||||
|
||||
/** Minimum number of idle connections to maintain */
|
||||
minIdleConnections?: number;
|
||||
|
||||
/** Maximum time (ms) a connection can be idle before being closed */
|
||||
maxIdleTime?: number;
|
||||
|
||||
/** Maximum time (ms) to wait for a connection from the pool */
|
||||
acquireTimeout?: number;
|
||||
|
||||
/** Enable connection pool metrics */
|
||||
enableMetrics?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request configuration
|
||||
*/
|
||||
export interface RequestConfig {
|
||||
/** Request timeout in milliseconds */
|
||||
timeout?: number;
|
||||
|
||||
/** Maximum number of retries */
|
||||
maxRetries?: number;
|
||||
|
||||
/** Retry delay strategy */
|
||||
retryDelay?: 'exponential' | 'linear' | 'fixed';
|
||||
|
||||
/** Compression for request bodies */
|
||||
compression?: boolean;
|
||||
|
||||
/** Maximum request body size */
|
||||
maxBodySize?: number;
|
||||
|
||||
/** Request headers to include in all requests */
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovery configuration (node sniffing)
|
||||
*/
|
||||
export interface DiscoveryConfig {
|
||||
/** Enable node discovery/sniffing */
|
||||
enabled?: boolean;
|
||||
|
||||
/** Interval (ms) between discovery attempts */
|
||||
interval?: number;
|
||||
|
||||
/** Whether to sniff on connection failure */
|
||||
sniffOnConnectionFault?: boolean;
|
||||
|
||||
/** Whether to sniff on start */
|
||||
sniffOnStart?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Elasticsearch configuration
|
||||
*/
|
||||
export interface ElasticsearchConfig {
|
||||
/** Elasticsearch node(s) */
|
||||
nodes: string | string[];
|
||||
|
||||
/** Authentication configuration */
|
||||
auth?: AuthConfig;
|
||||
|
||||
/** TLS/SSL configuration */
|
||||
tls?: TLSConfig;
|
||||
|
||||
/** Connection pool configuration */
|
||||
pool?: ConnectionPoolConfig;
|
||||
|
||||
/** Request configuration */
|
||||
request?: RequestConfig;
|
||||
|
||||
/** Node discovery configuration */
|
||||
discovery?: DiscoveryConfig;
|
||||
|
||||
/** Retry configuration */
|
||||
retry?: Partial<RetryConfig>;
|
||||
|
||||
/** Logging configuration */
|
||||
logging?: {
|
||||
level?: LogLevel;
|
||||
enableRequestLogging?: boolean;
|
||||
enableResponseLogging?: boolean;
|
||||
};
|
||||
|
||||
/** Metrics collection */
|
||||
metrics?: {
|
||||
enabled?: boolean;
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
/** Tracing configuration */
|
||||
tracing?: {
|
||||
enabled?: boolean;
|
||||
serviceName?: string;
|
||||
serviceVersion?: string;
|
||||
};
|
||||
|
||||
/** Proxy configuration */
|
||||
proxy?: string;
|
||||
|
||||
/** Custom agent for HTTP requests */
|
||||
agent?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration validation error
|
||||
*/
|
||||
export class ConfigValidationError extends Error {
|
||||
constructor(
|
||||
public readonly field: string,
|
||||
public readonly reason: string
|
||||
) {
|
||||
super(`Configuration validation failed for field "${field}": ${reason}`);
|
||||
this.name = 'ConfigValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secret provider interface for fetching secrets from external sources
|
||||
*/
|
||||
export interface SecretProvider {
|
||||
/**
|
||||
* Get a secret by key
|
||||
*/
|
||||
getSecret(key: string): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Get multiple secrets by keys
|
||||
*/
|
||||
getSecrets(keys: string[]): Promise<Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment variable secret provider
|
||||
*/
|
||||
export class EnvironmentSecretProvider implements SecretProvider {
|
||||
async getSecret(key: string): Promise<string | null> {
|
||||
return process.env[key] || null;
|
||||
}
|
||||
|
||||
async getSecrets(keys: string[]): Promise<Record<string, string>> {
|
||||
const secrets: Record<string, string> = {};
|
||||
for (const key of keys) {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
secrets[key] = value;
|
||||
}
|
||||
}
|
||||
return secrets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory secret provider (for testing)
|
||||
*/
|
||||
export class InMemorySecretProvider implements SecretProvider {
|
||||
constructor(private secrets: Record<string, string> = {}) {}
|
||||
|
||||
async getSecret(key: string): Promise<string | null> {
|
||||
return this.secrets[key] || null;
|
||||
}
|
||||
|
||||
async getSecrets(keys: string[]): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const key of keys) {
|
||||
if (this.secrets[key]) {
|
||||
result[key] = this.secrets[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
setSecret(key: string, value: string): void {
|
||||
this.secrets[key] = value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user