switched to new config and removed old

This commit is contained in:
Boki 2025-06-18 21:03:45 -04:00
parent 6b69bcbcaa
commit 269364fbc8
70 changed files with 889 additions and 2978 deletions

View file

@ -1,118 +0,0 @@
/**
* Admin interfaces configuration using Yup
* PgAdmin, Mongo Express, Redis Insight for database management
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, strWithChoices } = envValidators;
/**
* PgAdmin configuration with validation and defaults
*/
export const pgAdminConfig = cleanEnv(process.env, {
// PgAdmin Server
PGADMIN_HOST: str('localhost', 'PgAdmin host'),
PGADMIN_PORT: port(8080, 'PgAdmin port'),
// Authentication
PGADMIN_DEFAULT_EMAIL: str('admin@tradingbot.local', 'PgAdmin default admin email'),
PGADMIN_DEFAULT_PASSWORD: str('admin123', 'PgAdmin default admin password'),
// Configuration
PGADMIN_SERVER_MODE: bool(false, 'Enable server mode (multi-user)'),
PGADMIN_DISABLE_POSTFIX: bool(true, 'Disable postfix for email'),
PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: bool(true, 'Enhanced cookie protection'),
// Security
PGADMIN_MASTER_PASSWORD_REQUIRED: bool(false, 'Require master password'),
PGADMIN_SESSION_TIMEOUT: str('60', 'Session timeout in minutes'),
});
/**
* Mongo Express configuration with validation and defaults
*/
export const mongoExpressConfig = cleanEnv(process.env, {
// Mongo Express Server
MONGO_EXPRESS_HOST: str('localhost', 'Mongo Express host'),
MONGO_EXPRESS_PORT: port(8081, 'Mongo Express port'),
// MongoDB Connection
MONGO_EXPRESS_MONGODB_SERVER: str('mongodb', 'MongoDB server name/host'),
MONGO_EXPRESS_MONGODB_PORT: port(27017, 'MongoDB port'),
MONGO_EXPRESS_MONGODB_ADMINUSERNAME: str('trading_admin', 'MongoDB admin username'),
MONGO_EXPRESS_MONGODB_ADMINPASSWORD: str('', 'MongoDB admin password'),
// Basic Authentication for Mongo Express
MONGO_EXPRESS_BASICAUTH_USERNAME: str('admin', 'Basic auth username for Mongo Express'),
MONGO_EXPRESS_BASICAUTH_PASSWORD: str('admin123', 'Basic auth password for Mongo Express'),
// Configuration
MONGO_EXPRESS_ENABLE_ADMIN: bool(true, 'Enable admin features'),
MONGO_EXPRESS_OPTIONS_EDITOR_THEME: str('rubyblue', 'Editor theme (rubyblue, 3024-night, etc.)'),
MONGO_EXPRESS_REQUEST_SIZE: str('100kb', 'Maximum request size'),
});
/**
* Redis Insight configuration with validation and defaults
*/
export const redisInsightConfig = cleanEnv(process.env, {
// Redis Insight Server
REDIS_INSIGHT_HOST: str('localhost', 'Redis Insight host'),
REDIS_INSIGHT_PORT: port(8001, 'Redis Insight port'),
// Redis Connection Settings
REDIS_INSIGHT_REDIS_HOSTS: str(
'local:dragonfly:6379',
'Redis hosts in format name:host:port,name:host:port'
),
// Configuration
REDIS_INSIGHT_LOG_LEVEL: strWithChoices(
['error', 'warn', 'info', 'verbose', 'debug'],
'info',
'Redis Insight log level'
),
REDIS_INSIGHT_DISABLE_ANALYTICS: bool(true, 'Disable analytics collection'),
REDIS_INSIGHT_BUILD_TYPE: str('DOCKER', 'Build type identifier'),
});
// Export typed configuration objects
export type PgAdminConfig = typeof pgAdminConfig;
export type MongoExpressConfig = typeof mongoExpressConfig;
export type RedisInsightConfig = typeof redisInsightConfig;
// Export individual config values for convenience
export const {
PGADMIN_HOST,
PGADMIN_PORT,
PGADMIN_DEFAULT_EMAIL,
PGADMIN_DEFAULT_PASSWORD,
PGADMIN_SERVER_MODE,
PGADMIN_DISABLE_POSTFIX,
PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION,
PGADMIN_MASTER_PASSWORD_REQUIRED,
PGADMIN_SESSION_TIMEOUT,
} = pgAdminConfig;
export const {
MONGO_EXPRESS_HOST,
MONGO_EXPRESS_PORT,
MONGO_EXPRESS_MONGODB_SERVER,
MONGO_EXPRESS_MONGODB_PORT,
MONGO_EXPRESS_MONGODB_ADMINUSERNAME,
MONGO_EXPRESS_MONGODB_ADMINPASSWORD,
MONGO_EXPRESS_BASICAUTH_USERNAME,
MONGO_EXPRESS_BASICAUTH_PASSWORD,
MONGO_EXPRESS_ENABLE_ADMIN,
MONGO_EXPRESS_OPTIONS_EDITOR_THEME,
MONGO_EXPRESS_REQUEST_SIZE,
} = mongoExpressConfig;
export const {
REDIS_INSIGHT_HOST,
REDIS_INSIGHT_PORT,
REDIS_INSIGHT_REDIS_HOSTS,
REDIS_INSIGHT_LOG_LEVEL,
REDIS_INSIGHT_DISABLE_ANALYTICS,
REDIS_INSIGHT_BUILD_TYPE,
} = redisInsightConfig;

194
libs/config/src/cli.ts Normal file
View file

@ -0,0 +1,194 @@
#!/usr/bin/env bun
import { parseArgs } from 'util';
import { join } from 'path';
import { ConfigManager } from './config-manager';
import { appConfigSchema } from './schemas';
import {
validateConfig,
formatValidationResult,
checkDeprecations,
checkRequiredEnvVars,
validateCompleteness
} from './utils/validation';
import { redactSecrets } from './utils/secrets';
interface CliOptions {
config?: string;
env?: string;
validate?: boolean;
show?: boolean;
check?: boolean;
json?: boolean;
help?: boolean;
}
const DEPRECATIONS = {
'service.legacyMode': 'Use service.mode instead',
'database.redis': 'Use database.dragonfly instead',
};
const REQUIRED_PATHS = [
'service.name',
'service.port',
'database.postgres.host',
'database.postgres.database',
];
const REQUIRED_ENV_VARS = [
'NODE_ENV',
];
const SECRET_PATHS = [
'database.postgres.password',
'database.mongodb.uri',
'providers.quoteMedia.apiKey',
'providers.interactiveBrokers.clientId',
];
function printUsage() {
console.log(`
Stock Bot Configuration CLI
Usage: bun run config-cli [options]
Options:
--config <path> Path to config directory (default: ./config)
--env <env> Environment to use (development, test, production)
--validate Validate configuration against schema
--show Show current configuration (secrets redacted)
--check Run all configuration checks
--json Output in JSON format
--help Show this help message
Examples:
# Validate configuration
bun run config-cli --validate
# Show configuration for production
bun run config-cli --env production --show
# Run all checks
bun run config-cli --check
# Output configuration as JSON
bun run config-cli --show --json
`);
}
async function main() {
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
config: { type: 'string' },
env: { type: 'string' },
validate: { type: 'boolean' },
show: { type: 'boolean' },
check: { type: 'boolean' },
json: { type: 'boolean' },
help: { type: 'boolean' },
},
}) as { values: CliOptions };
if (values.help) {
printUsage();
process.exit(0);
}
const configPath = values.config || join(process.cwd(), 'config');
const environment = values.env as any;
try {
const manager = new ConfigManager({
configPath,
environment,
});
const config = await manager.initialize(appConfigSchema);
if (values.validate) {
const result = validateConfig(config, appConfigSchema);
if (values.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatValidationResult(result));
}
process.exit(result.valid ? 0 : 1);
}
if (values.show) {
const redacted = redactSecrets(config, SECRET_PATHS);
if (values.json) {
console.log(JSON.stringify(redacted, null, 2));
} else {
console.log('Current Configuration:');
console.log(JSON.stringify(redacted, null, 2));
}
}
if (values.check) {
console.log('Running configuration checks...\n');
// Schema validation
console.log('1. Schema Validation:');
const schemaResult = validateConfig(config, appConfigSchema);
console.log(formatValidationResult(schemaResult));
console.log();
// Environment variables
console.log('2. Required Environment Variables:');
const envResult = checkRequiredEnvVars(REQUIRED_ENV_VARS);
console.log(formatValidationResult(envResult));
console.log();
// Required paths
console.log('3. Required Configuration Paths:');
const pathResult = validateCompleteness(config, REQUIRED_PATHS);
console.log(formatValidationResult(pathResult));
console.log();
// Deprecations
console.log('4. Deprecation Warnings:');
const warnings = checkDeprecations(config, DEPRECATIONS);
if (warnings && warnings.length > 0) {
for (const warning of warnings) {
console.log(` ⚠️ ${warning.path}: ${warning.message}`);
}
} else {
console.log(' ✅ No deprecated options found');
}
console.log();
// Overall result
const allValid = schemaResult.valid && envResult.valid && pathResult.valid;
if (allValid) {
console.log('✅ All configuration checks passed!');
process.exit(0);
} else {
console.log('❌ Some configuration checks failed');
process.exit(1);
}
}
if (!values.validate && !values.show && !values.check) {
console.log('No action specified. Use --help for usage information.');
process.exit(1);
}
} catch (error) {
if (values.json) {
console.error(JSON.stringify({ error: String(error) }));
} else {
console.error('Error:', error);
}
process.exit(1);
}
}
// Run CLI
if (import.meta.main) {
main();
}

View file

@ -0,0 +1,220 @@
import { join } from 'path';
import { z } from 'zod';
import {
ConfigManagerOptions,
Environment,
ConfigLoader,
DeepPartial,
ConfigSchema
} from './types';
import { ConfigError, ConfigValidationError } from './errors';
import { EnvLoader } from './loaders/env.loader';
import { FileLoader } from './loaders/file.loader';
export class ConfigManager<T = Record<string, unknown>> {
private config: T | null = null;
private loaders: ConfigLoader[];
private environment: Environment;
private schema?: ConfigSchema;
constructor(options: ConfigManagerOptions = {}) {
this.environment = options.environment || this.detectEnvironment();
// Default loaders if none provided
if (options.loaders) {
this.loaders = options.loaders;
} else {
const configPath = options.configPath || join(process.cwd(), 'config');
this.loaders = [
new FileLoader(configPath, this.environment),
new EnvLoader(''), // No prefix for env vars to match our .env file
];
}
}
/**
* Initialize the configuration by loading from all sources
*/
async initialize(schema?: ConfigSchema): Promise<T> {
if (this.config) {
return this.config;
}
this.schema = schema;
// Sort loaders by priority (higher priority last)
const sortedLoaders = [...this.loaders].sort((a, b) => a.priority - b.priority);
// Load configurations from all sources
const configs: Record<string, unknown>[] = [];
for (const loader of sortedLoaders) {
const config = await loader.load();
if (config && Object.keys(config).length > 0) {
configs.push(config);
}
}
// Merge all configurations
const mergedConfig = this.deepMerge(...configs) as T;
// Add environment if not present
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) {
(mergedConfig as any).environment = this.environment;
}
// Validate if schema provided
if (this.schema) {
try {
this.config = this.schema.parse(mergedConfig) as T;
} catch (error) {
if (error instanceof z.ZodError) {
throw new ConfigValidationError(
'Configuration validation failed',
error.errors
);
}
throw error;
}
} else {
this.config = mergedConfig;
}
return this.config;
}
/**
* Get the current configuration
*/
get(): T {
if (!this.config) {
throw new ConfigError('Configuration not initialized. Call initialize() first.');
}
return this.config;
}
/**
* Get a specific configuration value by path
*/
getValue<R = unknown>(path: string): R {
const config = this.get();
const keys = path.split('.');
let value: any = config;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
throw new ConfigError(`Configuration key not found: ${path}`);
}
}
return value as R;
}
/**
* Check if a configuration path exists
*/
has(path: string): boolean {
try {
this.getValue(path);
return true;
} catch {
return false;
}
}
/**
* Update configuration at runtime (useful for testing)
*/
set(updates: DeepPartial<T>): void {
if (!this.config) {
throw new ConfigError('Configuration not initialized. Call initialize() first.');
}
const updated = this.deepMerge(this.config as any, updates as any) as T;
// Re-validate if schema is present
if (this.schema) {
try {
this.config = this.schema.parse(updated) as T;
} catch (error) {
if (error instanceof z.ZodError) {
throw new ConfigValidationError(
'Configuration validation failed after update',
error.errors
);
}
throw error;
}
} else {
this.config = updated;
}
}
/**
* Get the current environment
*/
getEnvironment(): Environment {
return this.environment;
}
/**
* Reset configuration (useful for testing)
*/
reset(): void {
this.config = null;
}
/**
* Validate configuration against a schema
*/
validate<S extends ConfigSchema>(schema: S): z.infer<S> {
const config = this.get();
return schema.parse(config);
}
/**
* Create a typed configuration getter
*/
createTypedGetter<S extends z.ZodSchema>(schema: S): () => z.infer<S> {
return () => this.validate(schema);
}
private detectEnvironment(): Environment {
const env = process.env.NODE_ENV?.toLowerCase();
switch (env) {
case 'production':
case 'prod':
return 'production';
case 'test':
return 'test';
case 'development':
case 'dev':
default:
return 'development';
}
}
private deepMerge(...objects: Record<string, any>[]): Record<string, any> {
const result: Record<string, any> = {};
for (const obj of objects) {
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) {
result[key] = value;
} else if (
typeof value === 'object' &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof RegExp)
) {
result[key] = this.deepMerge(result[key] || {}, value);
} else {
result[key] = value;
}
}
}
return result;
}
}

View file

@ -1,63 +0,0 @@
/**
* Core configuration module for the Stock Bot platform using Yup
*/
import path from 'node:path';
import { config as dotenvConfig } from 'dotenv';
/**
* Represents an error related to configuration validation
*/
export class ConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigurationError';
}
}
/**
* Environment types
*/
export enum Environment {
Development = 'development',
Testing = 'testing',
Staging = 'staging',
Production = 'production',
}
/**
* Loads environment variables from .env files based on the current environment
*/
export function loadEnvVariables(envOverride?: string): void {
const env = envOverride || process.env.NODE_ENV || 'development';
console.log(`Current environment: ${env}`);
// Order of loading:
// 1. .env (base environment variables)
// 2. .env.{environment} (environment-specific variables)
// 3. .env.local (local overrides, not to be committed)
const envFiles = ['.env', `.env.${env}`, '.env.local'];
for (const file of envFiles) {
dotenvConfig({ path: path.resolve(process.cwd(), file) });
}
}
/**
* Gets the current environment from process.env.NODE_ENV
*/
export function getEnvironment(): Environment {
const env = process.env.NODE_ENV?.toLowerCase() || 'development';
switch (env) {
case 'development':
return Environment.Development;
case 'testing':
case 'test': // Handle both 'test' and 'testing' for compatibility
return Environment.Testing;
case 'staging':
return Environment.Staging;
case 'production':
return Environment.Production;
default:
return Environment.Development;
}
}

View file

@ -1,185 +0,0 @@
/**
* Data provider configurations using Yup
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, num, bool, strWithChoices } = envValidators;
export interface ProviderConfig {
name: string;
type: 'rest' | 'websocket';
enabled: boolean;
baseUrl?: string;
apiKey?: string;
apiSecret?: string;
rateLimits?: {
maxRequestsPerMinute?: number;
maxRequestsPerSecond?: number;
maxRequestsPerHour?: number;
};
}
/**
* Data providers configuration with validation and defaults
*/
export const dataProvidersConfig = cleanEnv(process.env, {
// Default Provider
DEFAULT_DATA_PROVIDER: strWithChoices(
['alpaca', 'polygon', 'yahoo', 'iex'],
'alpaca',
'Default data provider'
),
// Alpaca Configuration
ALPACA_API_KEY: str('', 'Alpaca API key'),
ALPACA_API_SECRET: str('', 'Alpaca API secret'),
ALPACA_BASE_URL: str('https://data.alpaca.markets/v1beta1', 'Alpaca base URL'),
ALPACA_RATE_LIMIT: num(200, 'Alpaca rate limit per minute'),
ALPACA_ENABLED: bool(true, 'Enable Alpaca provider'),
// Polygon Configuration
POLYGON_API_KEY: str('', 'Polygon API key'),
POLYGON_BASE_URL: str('https://api.polygon.io', 'Polygon base URL'),
POLYGON_RATE_LIMIT: num(5, 'Polygon rate limit per minute'),
POLYGON_ENABLED: bool(false, 'Enable Polygon provider'),
// Yahoo Finance Configuration
YAHOO_BASE_URL: str('https://query1.finance.yahoo.com', 'Yahoo Finance base URL'),
YAHOO_RATE_LIMIT: num(2000, 'Yahoo Finance rate limit per hour'),
YAHOO_ENABLED: bool(true, 'Enable Yahoo Finance provider'),
// IEX Cloud Configuration
IEX_API_KEY: str('', 'IEX Cloud API key'),
IEX_BASE_URL: str('https://cloud.iexapis.com/stable', 'IEX Cloud base URL'),
IEX_RATE_LIMIT: num(100, 'IEX Cloud rate limit per second'),
IEX_ENABLED: bool(false, 'Enable IEX Cloud provider'),
// Connection Settings
DATA_PROVIDER_TIMEOUT: num(30000, 'Request timeout in milliseconds'),
DATA_PROVIDER_RETRIES: num(3, 'Number of retry attempts'),
DATA_PROVIDER_RETRY_DELAY: num(1000, 'Retry delay in milliseconds'),
// Cache Settings
DATA_CACHE_ENABLED: bool(true, 'Enable data caching'),
DATA_CACHE_TTL: num(300000, 'Cache TTL in milliseconds'),
DATA_CACHE_MAX_SIZE: num(1000, 'Maximum cache entries'),
});
/**
* Helper function to get provider-specific configuration
*/
export function getProviderConfig(providerName: string) {
// make a interface for the provider config
const name = providerName.toUpperCase();
switch (name) {
case 'ALPACA':
return {
name: 'alpaca',
type: 'rest' as const,
enabled: dataProvidersConfig.ALPACA_ENABLED,
baseUrl: dataProvidersConfig.ALPACA_BASE_URL,
apiKey: dataProvidersConfig.ALPACA_API_KEY,
apiSecret: dataProvidersConfig.ALPACA_API_SECRET,
rateLimits: {
maxRequestsPerMinute: dataProvidersConfig.ALPACA_RATE_LIMIT,
},
};
case 'POLYGON':
return {
name: 'polygon',
type: 'rest' as const,
enabled: dataProvidersConfig.POLYGON_ENABLED,
baseUrl: dataProvidersConfig.POLYGON_BASE_URL,
apiKey: dataProvidersConfig.POLYGON_API_KEY,
rateLimits: {
maxRequestsPerMinute: dataProvidersConfig.POLYGON_RATE_LIMIT,
},
};
case 'YAHOO':
return {
name: 'yahoo',
type: 'rest' as const,
enabled: dataProvidersConfig.YAHOO_ENABLED,
baseUrl: dataProvidersConfig.YAHOO_BASE_URL,
rateLimits: {
maxRequestsPerHour: dataProvidersConfig.YAHOO_RATE_LIMIT,
},
};
case 'IEX':
return {
name: 'iex',
type: 'rest' as const,
enabled: dataProvidersConfig.IEX_ENABLED,
baseUrl: dataProvidersConfig.IEX_BASE_URL,
apiKey: dataProvidersConfig.IEX_API_KEY,
rateLimits: {
maxRequestsPerSecond: dataProvidersConfig.IEX_RATE_LIMIT,
},
};
default:
throw new Error(`Unknown provider: ${providerName}`);
}
}
/**
* Get all enabled providers
*/
export function getEnabledProviders() {
const providers = ['alpaca', 'polygon', 'yahoo', 'iex'];
return providers.map(provider => getProviderConfig(provider)).filter(config => config.enabled);
}
/**
* Get the default provider configuration
*/
export function getDefaultProvider() {
return getProviderConfig(dataProvidersConfig.DEFAULT_DATA_PROVIDER);
}
// Export typed configuration object
export type DataProvidersConfig = typeof dataProvidersConfig;
export class DataProviders {
static getProviderConfig(providerName: string): ProviderConfig {
return getProviderConfig(providerName);
}
static getEnabledProviders(): ProviderConfig[] {
return getEnabledProviders();
}
static getDefaultProvider(): ProviderConfig {
return getDefaultProvider();
}
}
// Export individual config values for convenience
export const {
DEFAULT_DATA_PROVIDER,
ALPACA_API_KEY,
ALPACA_API_SECRET,
ALPACA_BASE_URL,
ALPACA_RATE_LIMIT,
ALPACA_ENABLED,
POLYGON_API_KEY,
POLYGON_BASE_URL,
POLYGON_RATE_LIMIT,
POLYGON_ENABLED,
YAHOO_BASE_URL,
YAHOO_RATE_LIMIT,
YAHOO_ENABLED,
IEX_API_KEY,
IEX_BASE_URL,
IEX_RATE_LIMIT,
IEX_ENABLED,
DATA_PROVIDER_TIMEOUT,
DATA_PROVIDER_RETRIES,
DATA_PROVIDER_RETRY_DELAY,
DATA_CACHE_ENABLED,
DATA_CACHE_TTL,
DATA_CACHE_MAX_SIZE,
} = dataProvidersConfig;

View file

@ -1,56 +0,0 @@
/**
* Database configuration using Yup
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, num, bool } = envValidators;
/**
* Database configuration with validation and defaults
*/
export const databaseConfig = cleanEnv(process.env, {
// PostgreSQL Configuration
DB_HOST: str('localhost', 'Database host'),
DB_PORT: port(5432, 'Database port'),
DB_NAME: str('stockbot', 'Database name'),
DB_USER: str('stockbot', 'Database user'),
DB_PASSWORD: str('', 'Database password'),
// Connection Pool Settings
DB_POOL_MIN: num(2, 'Minimum pool connections'),
DB_POOL_MAX: num(10, 'Maximum pool connections'),
DB_POOL_IDLE_TIMEOUT: num(30000, 'Pool idle timeout in ms'),
// SSL Configuration
DB_SSL: bool(false, 'Enable SSL for database connection'),
DB_SSL_REJECT_UNAUTHORIZED: bool(true, 'Reject unauthorized SSL certificates'),
// Additional Settings
DB_QUERY_TIMEOUT: num(30000, 'Query timeout in ms'),
DB_CONNECTION_TIMEOUT: num(5000, 'Connection timeout in ms'),
DB_STATEMENT_TIMEOUT: num(30000, 'Statement timeout in ms'),
DB_LOCK_TIMEOUT: num(10000, 'Lock timeout in ms'),
DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: num(60000, 'Idle in transaction timeout in ms'),
});
// Export typed configuration object
export type DatabaseConfig = typeof databaseConfig;
// Export individual config values for convenience
export const {
DB_HOST,
DB_PORT,
DB_NAME,
DB_USER,
DB_PASSWORD,
DB_POOL_MIN,
DB_POOL_MAX,
DB_POOL_IDLE_TIMEOUT,
DB_SSL,
DB_SSL_REJECT_UNAUTHORIZED,
DB_QUERY_TIMEOUT,
DB_CONNECTION_TIMEOUT,
DB_STATEMENT_TIMEOUT,
DB_LOCK_TIMEOUT,
DB_IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
} = databaseConfig;

View file

@ -1,81 +0,0 @@
/**
* Dragonfly (Redis replacement) configuration using Yup
* High-performance caching and event streaming
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, num, bool } = envValidators;
/**
* Dragonfly configuration with validation and defaults
*/
export const dragonflyConfig = cleanEnv(process.env, {
// Dragonfly Connection
DRAGONFLY_HOST: str('localhost', 'Dragonfly host'),
DRAGONFLY_PORT: port(6379, 'Dragonfly port'),
DRAGONFLY_PASSWORD: str('', 'Dragonfly password (if auth enabled)'),
DRAGONFLY_USERNAME: str('', 'Dragonfly username (if ACL enabled)'),
// Database Selection
DRAGONFLY_DATABASE: num(0, 'Dragonfly database number (0-15)'),
// Connection Pool Settings
DRAGONFLY_MAX_RETRIES: num(3, 'Maximum retry attempts'),
DRAGONFLY_RETRY_DELAY: num(50, 'Retry delay in ms'),
DRAGONFLY_CONNECT_TIMEOUT: num(10000, 'Connection timeout in ms'),
DRAGONFLY_COMMAND_TIMEOUT: num(5000, 'Command timeout in ms'),
// Pool Configuration
DRAGONFLY_POOL_SIZE: num(10, 'Connection pool size'),
DRAGONFLY_POOL_MIN: num(1, 'Minimum pool connections'),
DRAGONFLY_POOL_MAX: num(20, 'Maximum pool connections'),
// TLS Settings
DRAGONFLY_TLS: bool(false, 'Enable TLS for Dragonfly connection'),
DRAGONFLY_TLS_CERT_FILE: str('', 'Path to TLS certificate file'),
DRAGONFLY_TLS_KEY_FILE: str('', 'Path to TLS key file'),
DRAGONFLY_TLS_CA_FILE: str('', 'Path to TLS CA certificate file'),
DRAGONFLY_TLS_SKIP_VERIFY: bool(false, 'Skip TLS certificate verification'),
// Performance Settings
DRAGONFLY_ENABLE_KEEPALIVE: bool(true, 'Enable TCP keepalive'),
DRAGONFLY_KEEPALIVE_INTERVAL: num(60, 'Keepalive interval in seconds'),
// Clustering (if using cluster mode)
DRAGONFLY_CLUSTER_MODE: bool(false, 'Enable cluster mode'),
DRAGONFLY_CLUSTER_NODES: str('', 'Comma-separated list of cluster nodes (host:port)'),
// Memory and Cache Settings
DRAGONFLY_MAX_MEMORY: str('2gb', 'Maximum memory usage'),
DRAGONFLY_CACHE_MODE: bool(true, 'Enable cache mode'),
});
// Export typed configuration object
export type DragonflyConfig = typeof dragonflyConfig;
// Export individual config values for convenience
export const {
DRAGONFLY_HOST,
DRAGONFLY_PORT,
DRAGONFLY_PASSWORD,
DRAGONFLY_USERNAME,
DRAGONFLY_DATABASE,
DRAGONFLY_MAX_RETRIES,
DRAGONFLY_RETRY_DELAY,
DRAGONFLY_CONNECT_TIMEOUT,
DRAGONFLY_COMMAND_TIMEOUT,
DRAGONFLY_POOL_SIZE,
DRAGONFLY_POOL_MIN,
DRAGONFLY_POOL_MAX,
DRAGONFLY_TLS,
DRAGONFLY_TLS_CERT_FILE,
DRAGONFLY_TLS_KEY_FILE,
DRAGONFLY_TLS_CA_FILE,
DRAGONFLY_TLS_SKIP_VERIFY,
DRAGONFLY_ENABLE_KEEPALIVE,
DRAGONFLY_KEEPALIVE_INTERVAL,
DRAGONFLY_CLUSTER_MODE,
DRAGONFLY_CLUSTER_NODES,
DRAGONFLY_MAX_MEMORY,
DRAGONFLY_CACHE_MODE,
} = dragonflyConfig;

View file

@ -1,165 +0,0 @@
/**
* Environment validation utilities using Yup
*/
import { existsSync } from 'fs';
import { join } from 'path';
import { config } from 'dotenv';
import * as yup from 'yup';
// Function to find and load environment variables
function loadEnvFiles() {
const cwd = process.cwd();
const possiblePaths = [
// Current working directory
join(cwd, '.env'),
join(cwd, '.env.local'),
// Root of the workspace (common pattern)
join(cwd, '../../.env'),
join(cwd, '../../../.env'),
// Config library directory
join(__dirname, '../.env'),
join(__dirname, '../../.env'),
join(__dirname, '../../../.env'),
];
// Try to load each possible .env file
for (const envPath of possiblePaths) {
if (existsSync(envPath)) {
console.log(`📄 Loading environment from: ${envPath}`);
config({ path: envPath });
break; // Use the first .env file found
}
}
// Also try to load environment-specific files
const environment = process.env.NODE_ENV || 'development';
const envSpecificPaths = [
join(cwd, `.env.${environment}`),
join(cwd, `.env.${environment}.local`),
];
for (const envPath of envSpecificPaths) {
if (existsSync(envPath)) {
console.log(`📄 Loading ${environment} environment from: ${envPath}`);
config({ path: envPath, override: false }); // Don't override existing vars
}
}
}
// Load environment variables
loadEnvFiles();
/**
* Creates a Yup schema for environment variable validation
*/
export function createEnvSchema(shape: Record<string, any>) {
return yup.object(shape);
}
/**
* Validates environment variables against a Yup schema
*/
export function validateEnv(schema: yup.ObjectSchema<any>, env = process.env): any {
try {
const result = schema.validateSync(env, { abortEarly: false });
return result;
} catch (error) {
if (error instanceof yup.ValidationError) {
console.error('❌ Invalid environment variables:');
error.inner.forEach(err => {
console.error(` ${err.path}: ${err.message}`);
});
}
throw new Error('Environment validation failed');
}
}
/**
* Manually load environment variables from a specific path
*/
export function loadEnv(path?: string) {
if (path) {
console.log(`📄 Manually loading environment from: ${path}`);
config({ path });
} else {
loadEnvFiles();
}
}
/**
* Helper functions for common validation patterns
*/
export const envValidators = {
// String with default
str: (defaultValue?: string, description?: string) => yup.string().default(defaultValue || ''),
// String with choices (enum)
strWithChoices: (choices: string[], defaultValue?: string, description?: string) =>
yup
.string()
.oneOf(choices)
.default(defaultValue || choices[0]),
// Required string
requiredStr: (description?: string) => yup.string().required('Required'),
// Port number
port: (defaultValue?: number, description?: string) =>
yup
.number()
.integer()
.min(1)
.max(65535)
.transform((val, originalVal) => {
if (typeof originalVal === 'string') {
return parseInt(originalVal, 10);
}
return val;
})
.default(defaultValue || 3000),
// Number with default
num: (defaultValue?: number, description?: string) =>
yup
.number()
.transform((val, originalVal) => {
if (typeof originalVal === 'string') {
return parseFloat(originalVal);
}
return val;
})
.default(defaultValue || 0),
// Boolean with default
bool: (defaultValue?: boolean, description?: string) =>
yup
.boolean()
.transform((val, originalVal) => {
if (typeof originalVal === 'string') {
return originalVal === 'true' || originalVal === '1';
}
return val;
})
.default(defaultValue || false),
// URL validation
url: (defaultValue?: string, description?: string) =>
yup
.string()
.url()
.default(defaultValue || 'http://localhost'),
// Email validation
email: (description?: string) => yup.string().email(),
};
/**
* Legacy compatibility - creates a cleanEnv-like function
*/
export function cleanEnv(
env: Record<string, string | undefined>,
validators: Record<string, any>
): any {
const schema = createEnvSchema(validators);
return validateEnv(schema, env);
}

20
libs/config/src/errors.ts Normal file
View file

@ -0,0 +1,20 @@
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}
export class ConfigValidationError extends ConfigError {
constructor(message: string, public errors: unknown) {
super(message);
this.name = 'ConfigValidationError';
}
}
export class ConfigLoaderError extends ConfigError {
constructor(message: string, public loader: string) {
super(`${loader}: ${message}`);
this.name = 'ConfigLoaderError';
}
}

View file

@ -1,20 +1,127 @@
/**
* @stock-bot/config
*
* Configuration management library for Stock Bot platform using Yup
*/
// Export all schemas
export * from './schemas';
// Re-export everything from all modules
export * from './env-utils';
export * from './core';
export * from './admin-interfaces';
export * from './database';
export * from './dragonfly';
export * from './postgres';
export * from './questdb';
export * from './mongodb';
export * from './logging';
export * from './loki';
export * from './monitoring';
export * from './data-providers';
export * from './risk';
// Export types
export * from './types';
// Export errors
export * from './errors';
// Export loaders
export { EnvLoader } from './loaders/env.loader';
export { FileLoader } from './loaders/file.loader';
// Export ConfigManager
export { ConfigManager } from './config-manager';
// Export utilities
export * from './utils/secrets';
export * from './utils/validation';
// Import necessary types for singleton
import { ConfigManager } from './config-manager';
import { AppConfig, appConfigSchema } from './schemas';
import { FileLoader } from './loaders/file.loader';
import { EnvLoader } from './loaders/env.loader';
// Create singleton instance
let configInstance: ConfigManager<AppConfig> | null = null;
/**
* Initialize the global configuration
*/
export async function initializeConfig(
configPath?: string
): Promise<AppConfig> {
if (!configInstance) {
configInstance = new ConfigManager<AppConfig>({
configPath,
});
}
return configInstance.initialize(appConfigSchema);
}
/**
* Initialize configuration for a service in a monorepo
* Automatically loads configs from:
* 1. Root config directory (../../config)
* 2. Service-specific config directory (./config)
* 3. Environment variables
*/
export async function initializeServiceConfig(): Promise<AppConfig> {
if (!configInstance) {
const environment = process.env.NODE_ENV || 'development';
configInstance = new ConfigManager<AppConfig>({
loaders: [
new FileLoader('../../config', environment), // Root config
new FileLoader('./config', environment), // Service config
new EnvLoader(''), // Environment variables
]
});
}
return configInstance.initialize(appConfigSchema);
}
/**
* Get the current configuration
*/
export function getConfig(): AppConfig {
if (!configInstance) {
throw new Error('Configuration not initialized. Call initializeConfig() first.');
}
return configInstance.get();
}
/**
* Get configuration manager instance
*/
export function getConfigManager(): ConfigManager<AppConfig> {
if (!configInstance) {
throw new Error('Configuration not initialized. Call initializeConfig() first.');
}
return configInstance;
}
/**
* Reset configuration (useful for testing)
*/
export function resetConfig(): void {
if (configInstance) {
configInstance.reset();
configInstance = null;
}
}
// Export convenience functions for common configs
export function getDatabaseConfig() {
return getConfig().database;
}
export function getServiceConfig() {
return getConfig().service;
}
export function getLoggingConfig() {
return getConfig().logging;
}
export function getProviderConfig(provider: string) {
const providers = getConfig().providers;
if (!providers || !(provider in providers)) {
throw new Error(`Provider configuration not found: ${provider}`);
}
return (providers as any)[provider];
}
// Export environment helpers
export function isDevelopment(): boolean {
return getConfig().environment === 'development';
}
export function isProduction(): boolean {
return getConfig().environment === 'production';
}
export function isTest(): boolean {
return getConfig().environment === 'test';
}

View file

@ -0,0 +1,127 @@
import { ConfigLoader } from '../types';
import { ConfigLoaderError } from '../errors';
export interface EnvLoaderOptions {
convertCase?: boolean;
parseJson?: boolean;
parseValues?: boolean;
nestedDelimiter?: string;
}
export class EnvLoader implements ConfigLoader {
readonly priority = 100; // Highest priority
constructor(
private prefix = '',
private options: EnvLoaderOptions = {}
) {
this.options = {
convertCase: false,
parseJson: true,
parseValues: true,
nestedDelimiter: '_',
...options
};
}
async load(): Promise<Record<string, unknown>> {
try {
const config: Record<string, unknown> = {};
const envVars = process.env;
for (const [key, value] of Object.entries(envVars)) {
if (this.prefix && !key.startsWith(this.prefix)) {
continue;
}
const configKey = this.prefix
? key.slice(this.prefix.length)
: key;
if (!this.options.convertCase && !this.options.nestedDelimiter) {
// Simple case - just keep the key as is
config[configKey] = this.parseValue(value || '');
} else {
// Handle nested structure or case conversion
this.setConfigValue(config, configKey, value || '');
}
}
return config;
} catch (error) {
throw new ConfigLoaderError(
`Failed to load environment variables: ${error}`,
'EnvLoader'
);
}
}
private setConfigValue(config: Record<string, any>, key: string, value: string): void {
const parsedValue = this.parseValue(value);
if (this.options.nestedDelimiter && key.includes(this.options.nestedDelimiter)) {
// Handle nested delimiter (e.g., APP__NAME -> { APP: { NAME: value } })
const parts = key.split(this.options.nestedDelimiter);
this.setNestedValue(config, parts, parsedValue);
} else if (this.options.convertCase) {
// Convert to camelCase
const camelKey = this.toCamelCase(key);
config[camelKey] = parsedValue;
} else {
// Convert to nested structure based on underscores
const path = key.toLowerCase().split('_');
this.setNestedValue(config, path, parsedValue);
}
}
private setNestedValue(obj: Record<string, any>, path: string[], value: unknown): void {
const lastKey = path.pop()!;
const target = path.reduce((acc, key) => {
if (!acc[key]) {
acc[key] = {};
}
return acc[key];
}, obj);
target[lastKey] = value;
}
private toCamelCase(str: string): string {
return str
.toLowerCase()
.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
}
private parseValue(value: string): unknown {
if (!this.options.parseValues && !this.options.parseJson) {
return value;
}
// Try to parse as JSON first if enabled
if (this.options.parseJson) {
try {
return JSON.parse(value);
} catch {
// Not JSON, continue with other parsing
}
}
if (!this.options.parseValues) {
return value;
}
// Handle booleans
if (value.toLowerCase() === 'true') return true;
if (value.toLowerCase() === 'false') return false;
// Handle numbers
const num = Number(value);
if (!isNaN(num) && value !== '') return num;
// Handle null/undefined
if (value.toLowerCase() === 'null') return null;
if (value.toLowerCase() === 'undefined') return undefined;
// Return as string
return value;
}
}

View file

@ -0,0 +1,72 @@
import { readFile } from 'fs/promises';
import { join } from 'path';
import { ConfigLoader } from '../types';
import { ConfigLoaderError } from '../errors';
export class FileLoader implements ConfigLoader {
readonly priority = 50; // Medium priority
constructor(
private configPath: string,
private environment: string
) {}
async load(): Promise<Record<string, unknown>> {
try {
const configs: Record<string, unknown>[] = [];
// Load default config
const defaultConfig = await this.loadFile('default.json');
if (defaultConfig) {
configs.push(defaultConfig);
}
// Load environment-specific config
const envConfig = await this.loadFile(`${this.environment}.json`);
if (envConfig) {
configs.push(envConfig);
}
// Merge configs (later configs override earlier ones)
return this.deepMerge(...configs);
} catch (error) {
throw new ConfigLoaderError(
`Failed to load configuration files: ${error}`,
'FileLoader'
);
}
}
private async loadFile(filename: string): Promise<Record<string, unknown> | null> {
const filepath = join(this.configPath, filename);
try {
const content = await readFile(filepath, 'utf-8');
return JSON.parse(content);
} catch (error: any) {
// File not found is not an error (configs are optional)
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
}
private deepMerge(...objects: Record<string, any>[]): Record<string, any> {
const result: Record<string, any> = {};
for (const obj of objects) {
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) {
result[key] = value;
} else if (typeof value === 'object' && !Array.isArray(value)) {
result[key] = this.deepMerge(result[key] || {}, value);
} else {
result[key] = value;
}
}
}
return result;
}
}

View file

@ -1,74 +0,0 @@
/**
* Logging configuration using Yup
* Application logging settings without Loki (Loki config is in monitoring.ts)
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, bool, num, strWithChoices } = envValidators;
/**
* Logging configuration with validation and defaults
*/
export const loggingConfig = cleanEnv(process.env, {
// Basic Logging Settings
LOG_LEVEL: strWithChoices(['debug', 'info', 'warn', 'error'], 'info', 'Logging level'),
LOG_FORMAT: strWithChoices(['json', 'simple', 'combined'], 'json', 'Log output format'),
LOG_CONSOLE: bool(true, 'Enable console logging'),
LOG_FILE: bool(false, 'Enable file logging'),
// File Logging Settings
LOG_FILE_PATH: str('logs', 'Log file directory path'),
LOG_FILE_MAX_SIZE: str('20m', 'Maximum log file size'),
LOG_FILE_MAX_FILES: num(14, 'Maximum number of log files to keep'),
LOG_FILE_DATE_PATTERN: str('YYYY-MM-DD', 'Log file date pattern'),
// Error Logging
LOG_ERROR_FILE: bool(true, 'Enable separate error log file'),
LOG_ERROR_STACK: bool(true, 'Include stack traces in error logs'),
// Performance Logging
LOG_PERFORMANCE: bool(false, 'Enable performance logging'),
LOG_SQL_QUERIES: bool(false, 'Log SQL queries'),
LOG_HTTP_REQUESTS: bool(true, 'Log HTTP requests'),
// Structured Logging
LOG_STRUCTURED: bool(true, 'Use structured logging format'),
LOG_TIMESTAMP: bool(true, 'Include timestamps in logs'),
LOG_CALLER_INFO: bool(false, 'Include caller information in logs'),
// Log Filtering
LOG_SILENT_MODULES: str('', 'Comma-separated list of modules to silence'),
LOG_VERBOSE_MODULES: str('', 'Comma-separated list of modules for verbose logging'),
// Application Context
LOG_SERVICE_NAME: str('stock-bot', 'Service name for log context'),
LOG_SERVICE_VERSION: str('1.0.0', 'Service version for log context'),
LOG_ENVIRONMENT: str('development', 'Environment for log context'),
});
// Export typed configuration object
export type LoggingConfig = typeof loggingConfig;
// Export individual config values for convenience
export const {
LOG_LEVEL,
LOG_FORMAT,
LOG_CONSOLE,
LOG_FILE,
LOG_FILE_PATH,
LOG_FILE_MAX_SIZE,
LOG_FILE_MAX_FILES,
LOG_FILE_DATE_PATTERN,
LOG_ERROR_FILE,
LOG_ERROR_STACK,
LOG_PERFORMANCE,
LOG_SQL_QUERIES,
LOG_HTTP_REQUESTS,
LOG_STRUCTURED,
LOG_TIMESTAMP,
LOG_CALLER_INFO,
LOG_SILENT_MODULES,
LOG_VERBOSE_MODULES,
LOG_SERVICE_NAME,
LOG_SERVICE_VERSION,
LOG_ENVIRONMENT,
} = loggingConfig;

View file

@ -1,63 +0,0 @@
/**
* Loki log aggregation configuration using Yup
* Centralized logging configuration for the Stock Bot platform
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, num } = envValidators;
/**
* Loki configuration with validation and defaults
*/
export const lokiConfig = cleanEnv(process.env, {
// Loki Server
LOKI_HOST: str('localhost', 'Loki host'),
LOKI_PORT: port(3100, 'Loki port'),
LOKI_URL: str('', 'Complete Loki URL (overrides host/port)'),
// Authentication
LOKI_USERNAME: str('', 'Loki username (if auth enabled)'),
LOKI_PASSWORD: str('', 'Loki password (if auth enabled)'),
LOKI_TENANT_ID: str('', 'Loki tenant ID (for multi-tenancy)'),
// Push Configuration
LOKI_PUSH_TIMEOUT: num(10000, 'Push timeout in ms'),
LOKI_BATCH_SIZE: num(1024, 'Batch size for log entries'),
LOKI_BATCH_WAIT: num(5, 'Batch wait time in ms'),
// Retention Settings
LOKI_RETENTION_PERIOD: str('30d', 'Log retention period'),
LOKI_MAX_CHUNK_AGE: str('1h', 'Maximum chunk age'),
// TLS Settings
LOKI_TLS_ENABLED: bool(false, 'Enable TLS for Loki'),
LOKI_TLS_INSECURE: bool(false, 'Skip TLS verification'),
// Log Labels
LOKI_DEFAULT_LABELS: str('', 'Default labels for all log entries (JSON format)'),
LOKI_SERVICE_LABEL: str('stock-bot', 'Service label for log entries'),
LOKI_ENVIRONMENT_LABEL: str('development', 'Environment label for log entries'),
});
// Export typed configuration object
export type LokiConfig = typeof lokiConfig;
// Export individual config values for convenience
export const {
LOKI_HOST,
LOKI_PORT,
LOKI_URL,
LOKI_USERNAME,
LOKI_PASSWORD,
LOKI_TENANT_ID,
LOKI_PUSH_TIMEOUT,
LOKI_BATCH_SIZE,
LOKI_BATCH_WAIT,
LOKI_RETENTION_PERIOD,
LOKI_MAX_CHUNK_AGE,
LOKI_TLS_ENABLED,
LOKI_TLS_INSECURE,
LOKI_DEFAULT_LABELS,
LOKI_SERVICE_LABEL,
LOKI_ENVIRONMENT_LABEL,
} = lokiConfig;

View file

@ -1,77 +0,0 @@
/**
* MongoDB configuration using Yup
* Document storage for sentiment data, raw documents, and unstructured data
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, num, strWithChoices } = envValidators;
/**
* MongoDB configuration with validation and defaults
*/
export const mongodbConfig = cleanEnv(process.env, {
// MongoDB Connection
MONGODB_HOST: str('localhost', 'MongoDB host'),
MONGODB_PORT: port(27017, 'MongoDB port'),
MONGODB_DATABASE: str('stock', 'MongoDB database name'),
// Authentication
MONGODB_USERNAME: str('trading_admin', 'MongoDB username'),
MONGODB_PASSWORD: str('', 'MongoDB password'),
MONGODB_AUTH_SOURCE: str('admin', 'MongoDB authentication database'),
// Connection URI (alternative to individual settings)
MONGODB_URI: str('', 'Complete MongoDB connection URI (overrides individual settings)'),
// Connection Pool Settings
MONGODB_MAX_POOL_SIZE: num(10, 'Maximum connection pool size'),
MONGODB_MIN_POOL_SIZE: num(0, 'Minimum connection pool size'),
MONGODB_MAX_IDLE_TIME: num(30000, 'Maximum idle time for connections in ms'),
// Timeouts
MONGODB_CONNECT_TIMEOUT: num(10000, 'Connection timeout in ms'),
MONGODB_SOCKET_TIMEOUT: num(30000, 'Socket timeout in ms'),
MONGODB_SERVER_SELECTION_TIMEOUT: num(5000, 'Server selection timeout in ms'),
// SSL/TLS Settings
MONGODB_TLS: bool(false, 'Enable TLS for MongoDB connection'),
MONGODB_TLS_INSECURE: bool(false, 'Allow invalid certificates in TLS mode'),
MONGODB_TLS_CA_FILE: str('', 'Path to TLS CA certificate file'),
// Additional Settings
MONGODB_RETRY_WRITES: bool(true, 'Enable retryable writes'),
MONGODB_JOURNAL: bool(true, 'Enable write concern journal'),
MONGODB_READ_PREFERENCE: strWithChoices(
['primary', 'primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'],
'primary',
'MongoDB read preference'
),
MONGODB_WRITE_CONCERN: str('majority', 'Write concern level'),
});
// Export typed configuration object
export type MongoDbConfig = typeof mongodbConfig;
// Export individual config values for convenience
export const {
MONGODB_HOST,
MONGODB_PORT,
MONGODB_DATABASE,
MONGODB_USERNAME,
MONGODB_PASSWORD,
MONGODB_AUTH_SOURCE,
MONGODB_URI,
MONGODB_MAX_POOL_SIZE,
MONGODB_MIN_POOL_SIZE,
MONGODB_MAX_IDLE_TIME,
MONGODB_CONNECT_TIMEOUT,
MONGODB_SOCKET_TIMEOUT,
MONGODB_SERVER_SELECTION_TIMEOUT,
MONGODB_TLS,
MONGODB_TLS_INSECURE,
MONGODB_TLS_CA_FILE,
MONGODB_RETRY_WRITES,
MONGODB_JOURNAL,
MONGODB_READ_PREFERENCE,
MONGODB_WRITE_CONCERN,
} = mongodbConfig;

View file

@ -1,92 +0,0 @@
/**
* Monitoring configuration using Yup
* Prometheus metrics, Grafana visualization, and Loki logging
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, num, strWithChoices } = envValidators;
/**
* Prometheus configuration with validation and defaults
*/
export const prometheusConfig = cleanEnv(process.env, {
// Prometheus Server
PROMETHEUS_HOST: str('localhost', 'Prometheus host'),
PROMETHEUS_PORT: port(9090, 'Prometheus port'),
PROMETHEUS_URL: str('', 'Complete Prometheus URL (overrides host/port)'),
// Authentication
PROMETHEUS_USERNAME: str('', 'Prometheus username (if auth enabled)'),
PROMETHEUS_PASSWORD: str('', 'Prometheus password (if auth enabled)'),
// Metrics Collection
PROMETHEUS_SCRAPE_INTERVAL: str('15s', 'Default scrape interval'),
PROMETHEUS_EVALUATION_INTERVAL: str('15s', 'Rule evaluation interval'),
PROMETHEUS_RETENTION_TIME: str('15d', 'Data retention time'),
// TLS Settings
PROMETHEUS_TLS_ENABLED: bool(false, 'Enable TLS for Prometheus'),
PROMETHEUS_TLS_INSECURE: bool(false, 'Skip TLS verification'),
});
/**
* Grafana configuration with validation and defaults
*/
export const grafanaConfig = cleanEnv(process.env, {
// Grafana Server
GRAFANA_HOST: str('localhost', 'Grafana host'),
GRAFANA_PORT: port(3000, 'Grafana port'),
GRAFANA_URL: str('', 'Complete Grafana URL (overrides host/port)'),
// Authentication
GRAFANA_ADMIN_USER: str('admin', 'Grafana admin username'),
GRAFANA_ADMIN_PASSWORD: str('admin', 'Grafana admin password'),
// Security Settings
GRAFANA_ALLOW_SIGN_UP: bool(false, 'Allow user sign up'),
GRAFANA_SECRET_KEY: str('', 'Grafana secret key for encryption'),
// Database Settings
GRAFANA_DATABASE_TYPE: strWithChoices(
['mysql', 'postgres', 'sqlite3'],
'sqlite3',
'Grafana database type'
),
GRAFANA_DATABASE_URL: str('', 'Grafana database URL'),
// Feature Flags
GRAFANA_DISABLE_GRAVATAR: bool(true, 'Disable Gravatar avatars'),
GRAFANA_ENABLE_GZIP: bool(true, 'Enable gzip compression'),
});
// Export typed configuration objects
export type PrometheusConfig = typeof prometheusConfig;
export type GrafanaConfig = typeof grafanaConfig;
// Export individual config values for convenience
export const {
PROMETHEUS_HOST,
PROMETHEUS_PORT,
PROMETHEUS_URL,
PROMETHEUS_USERNAME,
PROMETHEUS_PASSWORD,
PROMETHEUS_SCRAPE_INTERVAL,
PROMETHEUS_EVALUATION_INTERVAL,
PROMETHEUS_RETENTION_TIME,
PROMETHEUS_TLS_ENABLED,
PROMETHEUS_TLS_INSECURE,
} = prometheusConfig;
export const {
GRAFANA_HOST,
GRAFANA_PORT,
GRAFANA_URL,
GRAFANA_ADMIN_USER,
GRAFANA_ADMIN_PASSWORD,
GRAFANA_ALLOW_SIGN_UP,
GRAFANA_SECRET_KEY,
GRAFANA_DATABASE_TYPE,
GRAFANA_DATABASE_URL,
GRAFANA_DISABLE_GRAVATAR,
GRAFANA_ENABLE_GZIP,
} = grafanaConfig;

View file

@ -1,56 +0,0 @@
/**
* PostgreSQL configuration using Yup
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, num } = envValidators;
/**
* PostgreSQL configuration with validation and defaults
*/
export const postgresConfig = cleanEnv(process.env, {
// PostgreSQL Connection Settings
POSTGRES_HOST: str('localhost', 'PostgreSQL host'),
POSTGRES_PORT: port(5432, 'PostgreSQL port'),
POSTGRES_DATABASE: str('stockbot', 'PostgreSQL database name'),
POSTGRES_USERNAME: str('stockbot', 'PostgreSQL username'),
POSTGRES_PASSWORD: str('', 'PostgreSQL password'),
// Connection Pool Settings
POSTGRES_POOL_MIN: num(2, 'Minimum pool connections'),
POSTGRES_POOL_MAX: num(10, 'Maximum pool connections'),
POSTGRES_POOL_IDLE_TIMEOUT: num(30000, 'Pool idle timeout in ms'),
// SSL Configuration
POSTGRES_SSL: bool(false, 'Enable SSL for PostgreSQL connection'),
POSTGRES_SSL_REJECT_UNAUTHORIZED: bool(true, 'Reject unauthorized SSL certificates'),
// Additional Settings
POSTGRES_QUERY_TIMEOUT: num(30000, 'Query timeout in ms'),
POSTGRES_CONNECTION_TIMEOUT: num(5000, 'Connection timeout in ms'),
POSTGRES_STATEMENT_TIMEOUT: num(30000, 'Statement timeout in ms'),
POSTGRES_LOCK_TIMEOUT: num(10000, 'Lock timeout in ms'),
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: num(60000, 'Idle in transaction timeout in ms'),
});
// Export typed configuration object
export type PostgresConfig = typeof postgresConfig;
// Export individual config values for convenience
export const {
POSTGRES_HOST,
POSTGRES_PORT,
POSTGRES_DATABASE,
POSTGRES_USERNAME,
POSTGRES_PASSWORD,
POSTGRES_POOL_MIN,
POSTGRES_POOL_MAX,
POSTGRES_POOL_IDLE_TIMEOUT,
POSTGRES_SSL,
POSTGRES_SSL_REJECT_UNAUTHORIZED,
POSTGRES_QUERY_TIMEOUT,
POSTGRES_CONNECTION_TIMEOUT,
POSTGRES_STATEMENT_TIMEOUT,
POSTGRES_LOCK_TIMEOUT,
POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
} = postgresConfig;

View file

@ -1,55 +0,0 @@
/**
* QuestDB configuration using Yup
* Time-series database for OHLCV data, indicators, and performance metrics
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, port, bool, num } = envValidators;
/**
* QuestDB configuration with validation and defaults
*/
export const questdbConfig = cleanEnv(process.env, {
// QuestDB Connection
QUESTDB_HOST: str('localhost', 'QuestDB host'),
QUESTDB_HTTP_PORT: port(9000, 'QuestDB HTTP port (web console)'),
QUESTDB_PG_PORT: port(8812, 'QuestDB PostgreSQL wire protocol port'),
QUESTDB_INFLUX_PORT: port(9009, 'QuestDB InfluxDB line protocol port'),
// Authentication (if enabled)
QUESTDB_USER: str('', 'QuestDB username (if auth enabled)'),
QUESTDB_PASSWORD: str('', 'QuestDB password (if auth enabled)'),
// Connection Settings
QUESTDB_CONNECTION_TIMEOUT: num(5000, 'Connection timeout in ms'),
QUESTDB_REQUEST_TIMEOUT: num(30000, 'Request timeout in ms'),
QUESTDB_RETRY_ATTEMPTS: num(3, 'Number of retry attempts'),
// TLS Settings
QUESTDB_TLS_ENABLED: bool(false, 'Enable TLS for QuestDB connection'),
QUESTDB_TLS_VERIFY_SERVER_CERT: bool(true, 'Verify server certificate'),
// Database Settings
QUESTDB_DEFAULT_DATABASE: str('qdb', 'Default database name'),
QUESTDB_TELEMETRY_ENABLED: bool(false, 'Enable telemetry'),
});
// Export typed configuration object
export type QuestDbConfig = typeof questdbConfig;
// Export individual config values for convenience
export const {
QUESTDB_HOST,
QUESTDB_HTTP_PORT,
QUESTDB_PG_PORT,
QUESTDB_INFLUX_PORT,
QUESTDB_USER,
QUESTDB_PASSWORD,
QUESTDB_CONNECTION_TIMEOUT,
QUESTDB_REQUEST_TIMEOUT,
QUESTDB_RETRY_ATTEMPTS,
QUESTDB_TLS_ENABLED,
QUESTDB_TLS_VERIFY_SERVER_CERT,
QUESTDB_DEFAULT_DATABASE,
QUESTDB_TELEMETRY_ENABLED,
} = questdbConfig;

View file

@ -1,80 +0,0 @@
/**
* Risk management configuration using Yup
*/
import { cleanEnv, envValidators } from './env-utils';
const { str, num, bool, strWithChoices } = envValidators;
/**
* Risk configuration with validation and defaults
*/
export const riskConfig = cleanEnv(process.env, {
// Position Sizing
RISK_MAX_POSITION_SIZE: num(0.1, 'Maximum position size as percentage of portfolio'),
RISK_MAX_PORTFOLIO_EXPOSURE: num(0.8, 'Maximum portfolio exposure percentage'),
RISK_MAX_SINGLE_ASSET_EXPOSURE: num(0.2, 'Maximum exposure to single asset'),
RISK_MAX_SECTOR_EXPOSURE: num(0.3, 'Maximum exposure to single sector'),
// Stop Loss and Take Profit
RISK_DEFAULT_STOP_LOSS: num(0.05, 'Default stop loss percentage'),
RISK_DEFAULT_TAKE_PROFIT: num(0.15, 'Default take profit percentage'),
RISK_TRAILING_STOP_ENABLED: bool(true, 'Enable trailing stop losses'),
RISK_TRAILING_STOP_DISTANCE: num(0.03, 'Trailing stop distance percentage'),
// Risk Limits
RISK_MAX_DAILY_LOSS: num(0.05, 'Maximum daily loss percentage'),
RISK_MAX_WEEKLY_LOSS: num(0.1, 'Maximum weekly loss percentage'),
RISK_MAX_MONTHLY_LOSS: num(0.2, 'Maximum monthly loss percentage'),
// Volatility Controls
RISK_MAX_VOLATILITY_THRESHOLD: num(0.4, 'Maximum volatility threshold'),
RISK_VOLATILITY_LOOKBACK_DAYS: num(20, 'Volatility calculation lookback period'),
// Correlation Controls
RISK_MAX_CORRELATION_THRESHOLD: num(0.7, 'Maximum correlation between positions'),
RISK_CORRELATION_LOOKBACK_DAYS: num(60, 'Correlation calculation lookback period'),
// Leverage Controls
RISK_MAX_LEVERAGE: num(2.0, 'Maximum leverage allowed'),
RISK_MARGIN_CALL_THRESHOLD: num(0.3, 'Margin call threshold'),
// Circuit Breakers
RISK_CIRCUIT_BREAKER_ENABLED: bool(true, 'Enable circuit breakers'),
RISK_CIRCUIT_BREAKER_LOSS_THRESHOLD: num(0.1, 'Circuit breaker loss threshold'),
RISK_CIRCUIT_BREAKER_COOLDOWN_MINUTES: num(60, 'Circuit breaker cooldown period'),
// Risk Model
RISK_MODEL_TYPE: strWithChoices(['var', 'cvar', 'expected_shortfall'], 'var', 'Risk model type'),
RISK_CONFIDENCE_LEVEL: num(0.95, 'Risk model confidence level'),
RISK_TIME_HORIZON_DAYS: num(1, 'Risk time horizon in days'),
});
// Export typed configuration object
export type RiskConfig = typeof riskConfig;
// Export individual config values for convenience
export const {
RISK_MAX_POSITION_SIZE,
RISK_MAX_PORTFOLIO_EXPOSURE,
RISK_MAX_SINGLE_ASSET_EXPOSURE,
RISK_MAX_SECTOR_EXPOSURE,
RISK_DEFAULT_STOP_LOSS,
RISK_DEFAULT_TAKE_PROFIT,
RISK_TRAILING_STOP_ENABLED,
RISK_TRAILING_STOP_DISTANCE,
RISK_MAX_DAILY_LOSS,
RISK_MAX_WEEKLY_LOSS,
RISK_MAX_MONTHLY_LOSS,
RISK_MAX_VOLATILITY_THRESHOLD,
RISK_VOLATILITY_LOOKBACK_DAYS,
RISK_MAX_CORRELATION_THRESHOLD,
RISK_CORRELATION_LOOKBACK_DAYS,
RISK_MAX_LEVERAGE,
RISK_MARGIN_CALL_THRESHOLD,
RISK_CIRCUIT_BREAKER_ENABLED,
RISK_CIRCUIT_BREAKER_LOSS_THRESHOLD,
RISK_CIRCUIT_BREAKER_COOLDOWN_MINUTES,
RISK_MODEL_TYPE,
RISK_CONFIDENCE_LEVEL,
RISK_TIME_HORIZON_DAYS,
} = riskConfig;

View file

@ -0,0 +1,10 @@
import { z } from 'zod';
export const environmentSchema = z.enum(['development', 'test', 'production']);
export const baseConfigSchema = z.object({
environment: environmentSchema.optional(),
name: z.string().optional(),
version: z.string().optional(),
debug: z.boolean().default(false),
});

View file

@ -0,0 +1,60 @@
import { z } from 'zod';
// PostgreSQL configuration
export const postgresConfigSchema = z.object({
host: z.string().default('localhost'),
port: z.number().default(5432),
database: z.string(),
user: z.string(),
password: z.string(),
ssl: z.boolean().default(false),
poolSize: z.number().min(1).max(100).default(10),
connectionTimeout: z.number().default(30000),
idleTimeout: z.number().default(10000),
});
// QuestDB configuration
export const questdbConfigSchema = z.object({
host: z.string().default('localhost'),
ilpPort: z.number().default(9009),
httpPort: z.number().default(9000),
pgPort: z.number().default(8812),
database: z.string().default('questdb'),
user: z.string().default('admin'),
password: z.string().default('quest'),
bufferSize: z.number().default(65536),
flushInterval: z.number().default(1000),
});
// MongoDB configuration
export const mongodbConfigSchema = z.object({
uri: z.string().url().optional(),
host: z.string().default('localhost'),
port: z.number().default(27017),
database: z.string(),
user: z.string().optional(),
password: z.string().optional(),
authSource: z.string().default('admin'),
replicaSet: z.string().optional(),
poolSize: z.number().min(1).max(100).default(10),
});
// Dragonfly/Redis configuration
export const dragonflyConfigSchema = z.object({
host: z.string().default('localhost'),
port: z.number().default(6379),
password: z.string().optional(),
db: z.number().min(0).max(15).default(0),
keyPrefix: z.string().optional(),
ttl: z.number().optional(),
maxRetries: z.number().default(3),
retryDelay: z.number().default(100),
});
// Combined database configuration
export const databaseConfigSchema = z.object({
postgres: postgresConfigSchema,
questdb: questdbConfigSchema,
mongodb: mongodbConfigSchema,
dragonfly: dragonflyConfigSchema,
});

View file

@ -0,0 +1,23 @@
export * from './base.schema';
export * from './database.schema';
export * from './service.schema';
export * from './provider.schema';
import { z } from 'zod';
import { baseConfigSchema, environmentSchema } from './base.schema';
import { databaseConfigSchema } from './database.schema';
import { serviceConfigSchema, loggingConfigSchema, queueConfigSchema, httpConfigSchema } from './service.schema';
import { providerConfigSchema } from './provider.schema';
// Complete application configuration schema
export const appConfigSchema = baseConfigSchema.extend({
environment: environmentSchema.default('development'),
service: serviceConfigSchema,
logging: loggingConfigSchema,
database: databaseConfigSchema,
queue: queueConfigSchema.optional(),
http: httpConfigSchema.optional(),
providers: providerConfigSchema.optional(),
});
export type AppConfig = z.infer<typeof appConfigSchema>;

View file

@ -0,0 +1,65 @@
import { z } from 'zod';
// Base provider configuration
export const baseProviderConfigSchema = z.object({
name: z.string(),
enabled: z.boolean().default(true),
priority: z.number().default(0),
rateLimit: z.object({
maxRequests: z.number().default(100),
windowMs: z.number().default(60000),
}).optional(),
timeout: z.number().default(30000),
retries: z.number().default(3),
});
// EOD Historical Data provider
export const eodProviderConfigSchema = baseProviderConfigSchema.extend({
apiKey: z.string(),
baseUrl: z.string().default('https://eodhistoricaldata.com/api'),
tier: z.enum(['free', 'fundamentals', 'all-in-one']).default('free'),
});
// Interactive Brokers provider
export const ibProviderConfigSchema = baseProviderConfigSchema.extend({
gateway: z.object({
host: z.string().default('localhost'),
port: z.number().default(5000),
clientId: z.number().default(1),
}),
account: z.string().optional(),
marketDataType: z.enum(['live', 'delayed', 'frozen']).default('delayed'),
});
// QuoteMedia provider
export const qmProviderConfigSchema = baseProviderConfigSchema.extend({
username: z.string(),
password: z.string(),
baseUrl: z.string().default('https://app.quotemedia.com/quotetools'),
webmasterId: z.string(),
});
// Yahoo Finance provider
export const yahooProviderConfigSchema = baseProviderConfigSchema.extend({
baseUrl: z.string().default('https://query1.finance.yahoo.com'),
cookieJar: z.boolean().default(true),
crumb: z.string().optional(),
});
// Combined provider configuration
export const providerConfigSchema = z.object({
eod: eodProviderConfigSchema.optional(),
ib: ibProviderConfigSchema.optional(),
qm: qmProviderConfigSchema.optional(),
yahoo: yahooProviderConfigSchema.optional(),
});
// Dynamic provider configuration type
export type ProviderName = 'eod' | 'ib' | 'qm' | 'yahoo';
export const providerSchemas = {
eod: eodProviderConfigSchema,
ib: ibProviderConfigSchema,
qm: qmProviderConfigSchema,
yahoo: yahooProviderConfigSchema,
} as const;

View file

@ -0,0 +1,63 @@
import { z } from 'zod';
// Common service configuration
export const serviceConfigSchema = z.object({
name: z.string(),
port: z.number().min(1).max(65535),
host: z.string().default('0.0.0.0'),
healthCheckPath: z.string().default('/health'),
metricsPath: z.string().default('/metrics'),
shutdownTimeout: z.number().default(30000),
cors: z.object({
enabled: z.boolean().default(true),
origin: z.union([z.string(), z.array(z.string())]).default('*'),
credentials: z.boolean().default(true),
}).default({}),
});
// Logging configuration
export const loggingConfigSchema = z.object({
level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
format: z.enum(['json', 'pretty']).default('json'),
loki: z.object({
enabled: z.boolean().default(false),
host: z.string().default('localhost'),
port: z.number().default(3100),
labels: z.record(z.string()).default({}),
}).optional(),
});
// Queue configuration
export const queueConfigSchema = z.object({
redis: z.object({
host: z.string().default('localhost'),
port: z.number().default(6379),
password: z.string().optional(),
db: z.number().default(1),
}),
defaultJobOptions: z.object({
attempts: z.number().default(3),
backoff: z.object({
type: z.enum(['exponential', 'fixed']).default('exponential'),
delay: z.number().default(1000),
}).default({}),
removeOnComplete: z.boolean().default(true),
removeOnFail: z.boolean().default(false),
}).default({}),
});
// HTTP client configuration
export const httpConfigSchema = z.object({
timeout: z.number().default(30000),
retries: z.number().default(3),
retryDelay: z.number().default(1000),
userAgent: z.string().optional(),
proxy: z.object({
enabled: z.boolean().default(false),
url: z.string().url().optional(),
auth: z.object({
username: z.string(),
password: z.string(),
}).optional(),
}).optional(),
});

View file

@ -0,0 +1,28 @@
import { z } from 'zod';
export type Environment = 'development' | 'test' | 'production';
export interface ConfigLoader {
load(): Promise<Record<string, unknown>>;
readonly priority: number;
}
export interface ConfigManagerOptions {
environment?: Environment;
configPath?: string;
loaders?: ConfigLoader[];
}
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export type ConfigSchema = z.ZodSchema<any>;
export interface ProviderConfig {
name: string;
enabled: boolean;
[key: string]: unknown;
}

View file

@ -0,0 +1,182 @@
import { z } from 'zod';
/**
* Secret value wrapper to prevent accidental logging
*/
export class SecretValue<T = string> {
private readonly value: T;
private readonly masked: string;
constructor(value: T, mask: string = '***') {
this.value = value;
this.masked = mask;
}
/**
* Get the actual secret value
* @param reason - Required reason for accessing the secret
*/
reveal(reason: string): T {
if (!reason) {
throw new Error('Reason required for revealing secret value');
}
return this.value;
}
/**
* Get masked representation
*/
toString(): string {
return this.masked;
}
/**
* Prevent JSON serialization of actual value
*/
toJSON(): string {
return this.masked;
}
/**
* Check if value matches without revealing it
*/
equals(other: T): boolean {
return this.value === other;
}
/**
* Transform the secret value
*/
map<R>(fn: (value: T) => R, reason: string): SecretValue<R> {
return new SecretValue(fn(this.reveal(reason)));
}
}
/**
* Zod schema for secret values
*/
export const secretSchema = <T extends z.ZodTypeAny>(schema: T) => {
return z.custom<SecretValue<z.infer<T>>>(
(val) => val instanceof SecretValue,
{
message: 'Expected SecretValue instance',
}
);
};
/**
* Transform string to SecretValue in Zod schema
*/
export const secretStringSchema = z
.string()
.transform((val) => new SecretValue(val));
/**
* Create a secret value
*/
export function secret<T = string>(value: T, mask?: string): SecretValue<T> {
return new SecretValue(value, mask);
}
/**
* Check if a value is a secret
*/
export function isSecret(value: unknown): value is SecretValue {
return value instanceof SecretValue;
}
/**
* Redact secrets from an object
*/
export function redactSecrets<T extends Record<string, any>>(
obj: T,
secretPaths: string[] = []
): T {
const result = { ...obj };
// Redact known secret paths
for (const path of secretPaths) {
const keys = path.split('.');
let current: any = result;
for (let i = 0; i < keys.length - 1; i++) {
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
current = current[keys[i]];
} else {
break;
}
}
const lastKey = keys[keys.length - 1];
if (current && lastKey in current) {
current[lastKey] = '***REDACTED***';
}
}
// Recursively redact SecretValue instances
function redactSecretValues(obj: any): any {
if (obj === null || obj === undefined) {
return obj;
}
if (isSecret(obj)) {
return obj.toString();
}
if (Array.isArray(obj)) {
return obj.map(redactSecretValues);
}
if (typeof obj === 'object') {
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = redactSecretValues(value);
}
return result;
}
return obj;
}
return redactSecretValues(result);
}
/**
* Environment variable names that should be treated as secrets
*/
export const COMMON_SECRET_PATTERNS = [
/password/i,
/secret/i,
/key/i,
/token/i,
/credential/i,
/private/i,
/auth/i,
/api[-_]?key/i,
];
/**
* Check if an environment variable name indicates a secret
*/
export function isSecretEnvVar(name: string): boolean {
return COMMON_SECRET_PATTERNS.some(pattern => pattern.test(name));
}
/**
* Wrap environment variables that look like secrets
*/
export function wrapSecretEnvVars(
env: Record<string, string | undefined>
): Record<string, string | SecretValue | undefined> {
const result: Record<string, string | SecretValue | undefined> = {};
for (const [key, value] of Object.entries(env)) {
if (value !== undefined && isSecretEnvVar(key)) {
result[key] = new SecretValue(value, `***${key}***`);
} else {
result[key] = value;
}
}
return result;
}

View file

@ -0,0 +1,193 @@
import { z } from 'zod';
import { ConfigValidationError } from '../errors';
export interface ValidationResult {
valid: boolean;
errors?: Array<{
path: string;
message: string;
expected?: string;
received?: string;
}>;
warnings?: Array<{
path: string;
message: string;
}>;
}
/**
* Validate configuration against a schema
*/
export function validateConfig<T>(
config: unknown,
schema: z.ZodSchema<T>
): ValidationResult {
try {
schema.parse(config);
return { valid: true };
} catch (error) {
if (error instanceof z.ZodError) {
const errors = error.errors.map(err => ({
path: err.path.join('.'),
message: err.message,
expected: 'expected' in err ? String(err.expected) : undefined,
received: 'received' in err ? String(err.received) : undefined,
}));
return { valid: false, errors };
}
throw error;
}
}
/**
* Check for deprecated configuration options
*/
export function checkDeprecations(
config: Record<string, any>,
deprecations: Record<string, string>
): ValidationResult['warnings'] {
const warnings: ValidationResult['warnings'] = [];
function checkObject(obj: any, path: string[] = []): void {
for (const [key, value] of Object.entries(obj)) {
const currentPath = [...path, key];
const pathStr = currentPath.join('.');
if (pathStr in deprecations) {
warnings?.push({
path: pathStr,
message: deprecations[pathStr],
});
}
if (value && typeof value === 'object' && !Array.isArray(value)) {
checkObject(value, currentPath);
}
}
}
checkObject(config);
return warnings;
}
/**
* Check for required environment variables
*/
export function checkRequiredEnvVars(
required: string[]
): ValidationResult {
const errors: ValidationResult['errors'] = [];
for (const envVar of required) {
if (!process.env[envVar]) {
errors.push({
path: `env.${envVar}`,
message: `Required environment variable ${envVar} is not set`,
});
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
/**
* Validate configuration completeness
*/
export function validateCompleteness(
config: Record<string, any>,
required: string[]
): ValidationResult {
const errors: ValidationResult['errors'] = [];
for (const path of required) {
const keys = path.split('.');
let current: any = config;
let found = true;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
found = false;
break;
}
}
if (!found || current === undefined || current === null) {
errors.push({
path,
message: `Required configuration value is missing`,
});
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
/**
* Format validation result for display
*/
export function formatValidationResult(result: ValidationResult): string {
const lines: string[] = [];
if (result.valid) {
lines.push('✅ Configuration is valid');
} else {
lines.push('❌ Configuration validation failed');
}
if (result.errors && result.errors.length > 0) {
lines.push('\nErrors:');
for (const error of result.errors) {
lines.push(` - ${error.path}: ${error.message}`);
if (error.expected && error.received) {
lines.push(` Expected: ${error.expected}, Received: ${error.received}`);
}
}
}
if (result.warnings && result.warnings.length > 0) {
lines.push('\nWarnings:');
for (const warning of result.warnings) {
lines.push(` - ${warning.path}: ${warning.message}`);
}
}
return lines.join('\n');
}
/**
* Create a strict schema that doesn't allow extra properties
*/
export function createStrictSchema<T extends z.ZodRawShape>(
shape: T
): z.ZodObject<T, 'strict'> {
return z.object(shape).strict();
}
/**
* Merge multiple schemas
*/
export function mergeSchemas<T extends z.ZodSchema[]>(
...schemas: T
): z.ZodIntersection<T[0], T[1]> {
if (schemas.length < 2) {
throw new Error('At least two schemas required for merge');
}
let result = schemas[0].and(schemas[1]);
for (let i = 2; i < schemas.length; i++) {
result = result.and(schemas[i]) as any;
}
return result as any;
}