switched to new config and removed old
This commit is contained in:
parent
6b69bcbcaa
commit
269364fbc8
70 changed files with 889 additions and 2978 deletions
|
|
@ -1,53 +1,50 @@
|
|||
# Base environment variables for Stock Bot
|
||||
|
||||
# Environment
|
||||
NODE_ENV=development
|
||||
|
||||
# Service Configuration
|
||||
STOCKBOT_SERVICE_NAME=stock-bot-service
|
||||
STOCKBOT_SERVICE_PORT=3000
|
||||
|
||||
# Database Configuration
|
||||
STOCKBOT_DATABASE_POSTGRES_HOST=localhost
|
||||
STOCKBOT_DATABASE_POSTGRES_PORT=5432
|
||||
STOCKBOT_DATABASE_POSTGRES_DATABASE=stockbot
|
||||
STOCKBOT_DATABASE_POSTGRES_USER=postgres
|
||||
STOCKBOT_DATABASE_POSTGRES_PASSWORD=postgres
|
||||
|
||||
STOCKBOT_DATABASE_QUESTDB_HOST=localhost
|
||||
STOCKBOT_DATABASE_QUESTDB_ILP_PORT=9009
|
||||
STOCKBOT_DATABASE_QUESTDB_HTTP_PORT=9000
|
||||
|
||||
STOCKBOT_DATABASE_MONGODB_HOST=localhost
|
||||
STOCKBOT_DATABASE_MONGODB_PORT=27017
|
||||
STOCKBOT_DATABASE_MONGODB_DATABASE=stockbot
|
||||
|
||||
STOCKBOT_DATABASE_DRAGONFLY_HOST=localhost
|
||||
STOCKBOT_DATABASE_DRAGONFLY_PORT=6379
|
||||
|
||||
# Provider Configuration
|
||||
STOCKBOT_PROVIDERS_EOD_API_KEY=your_eod_api_key
|
||||
STOCKBOT_PROVIDERS_EOD_ENABLED=true
|
||||
|
||||
STOCKBOT_PROVIDERS_IB_ENABLED=false
|
||||
STOCKBOT_PROVIDERS_IB_GATEWAY_HOST=localhost
|
||||
STOCKBOT_PROVIDERS_IB_GATEWAY_PORT=5000
|
||||
STOCKBOT_PROVIDERS_IB_ACCOUNT=your_account_id
|
||||
|
||||
STOCKBOT_PROVIDERS_QM_ENABLED=false
|
||||
STOCKBOT_PROVIDERS_QM_USERNAME=your_username
|
||||
STOCKBOT_PROVIDERS_QM_PASSWORD=your_password
|
||||
STOCKBOT_PROVIDERS_QM_WEBMASTER_ID=your_webmaster_id
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=debug
|
||||
STOCKBOT_LOGGING_LEVEL=info
|
||||
STOCKBOT_LOGGING_LOKI_ENABLED=false
|
||||
STOCKBOT_LOGGING_LOKI_HOST=localhost
|
||||
STOCKBOT_LOGGING_LOKI_PORT=3100
|
||||
|
||||
# Database configuration
|
||||
DRAGONFLY_HOST=localhost
|
||||
DRAGONFLY_PORT=6379
|
||||
DRAGONFLY_PASSWORD=
|
||||
DRAGONFLY_MAX_RETRIES_PER_REQUEST=3
|
||||
|
||||
TIMESCALE_HOST=localhost
|
||||
TIMESCALE_PORT=5432
|
||||
TIMESCALE_DB=stockbot
|
||||
TIMESCALE_USER=postgres
|
||||
TIMESCALE_PASSWORD=postgres
|
||||
|
||||
# Data providers
|
||||
DEFAULT_DATA_PROVIDER=alpaca
|
||||
ALPACA_API_KEY=your_alpaca_key_here
|
||||
ALPACA_API_SECRET=your_alpaca_secret_here
|
||||
POLYGON_API_KEY=your_polygon_key_here
|
||||
|
||||
# Risk parameters
|
||||
RISK_MAX_DRAWDOWN=0.05
|
||||
RISK_MAX_POSITION_SIZE=0.1
|
||||
RISK_MAX_LEVERAGE=1.5
|
||||
RISK_STOP_LOSS_DEFAULT=0.02
|
||||
RISK_TAKE_PROFIT_DEFAULT=0.05
|
||||
|
||||
# Market Data Gateway
|
||||
SERVICE_PORT=4000
|
||||
WEBSOCKET_ENABLED=true
|
||||
WEBSOCKET_PATH=/ws/market-data
|
||||
WEBSOCKET_HEARTBEAT_INTERVAL=30000
|
||||
THROTTLING_MAX_REQUESTS=300
|
||||
THROTTLING_MAX_CONNECTIONS=5
|
||||
CACHING_ENABLED=true
|
||||
CACHING_TTL_SECONDS=60
|
||||
|
||||
# Risk Guardian
|
||||
RISK_CHECKS_PRE_TRADE=true
|
||||
RISK_CHECKS_PORTFOLIO=true
|
||||
RISK_CHECKS_LEVERAGE=true
|
||||
RISK_CHECKS_CONCENTRATION=true
|
||||
ALERTING_ENABLED=true
|
||||
ALERTING_CRITICAL_THRESHOLD=0.8
|
||||
ALERTING_WARNING_THRESHOLD=0.6
|
||||
WATCHDOG_ENABLED=true
|
||||
WATCHDOG_CHECK_INTERVAL=60
|
||||
# HTTP Proxy (optional)
|
||||
STOCKBOT_HTTP_PROXY_ENABLED=false
|
||||
STOCKBOT_HTTP_PROXY_URL=http://proxy.example.com:8080
|
||||
STOCKBOT_HTTP_PROXY_AUTH_USERNAME=username
|
||||
STOCKBOT_HTTP_PROXY_AUTH_PASSWORD=password
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Production environment variables for Stock Bot
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Database configuration (use environment-specific values)
|
||||
DRAGONFLY_HOST=dragonfly.production
|
||||
DRAGONFLY_PORT=6379
|
||||
DRAGONFLY_MAX_RETRIES_PER_REQUEST=5
|
||||
|
||||
TIMESCALE_HOST=timescale.production
|
||||
TIMESCALE_PORT=5432
|
||||
TIMESCALE_DB=stockbot_prod
|
||||
|
||||
# Risk parameters (more conservative for production)
|
||||
RISK_MAX_DRAWDOWN=0.03
|
||||
RISK_MAX_POSITION_SIZE=0.05
|
||||
RISK_MAX_LEVERAGE=1.0
|
||||
|
||||
# Service settings
|
||||
WEBSOCKET_HEARTBEAT_INTERVAL=15000
|
||||
THROTTLING_MAX_REQUESTS=500
|
||||
THROTTLING_MAX_CONNECTIONS=20
|
||||
CACHING_ENABLED=true
|
||||
CACHING_TTL_SECONDS=30
|
||||
|
|
@ -1,103 +1,243 @@
|
|||
# @stock-bot/config
|
||||
|
||||
A configuration management library for the Stock Bot trading platform.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides a centralized way to manage configurations across all Stock Bot microservices and components. It includes:
|
||||
|
||||
- Environment-based configuration loading
|
||||
- Strong TypeScript typing and validation using Zod
|
||||
- Default configurations for services
|
||||
- Environment variable parsing helpers
|
||||
- Service-specific configuration modules
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { databaseConfig, dataProviderConfigs, riskConfig } from '@stock-bot/config';
|
||||
|
||||
// Access database configuration
|
||||
const dragonflyHost = databaseConfig.dragonfly.host;
|
||||
|
||||
// Access data provider configuration
|
||||
const alpacaApiKey = dataProviderConfigs.providers.find(p => p.name === 'alpaca')?.apiKey;
|
||||
|
||||
// Access risk configuration
|
||||
const maxPositionSize = riskConfig.maxPositionSize;
|
||||
```
|
||||
|
||||
### Service-Specific Configuration
|
||||
|
||||
```typescript
|
||||
import { marketDataGatewayConfig, riskGuardianConfig } from '@stock-bot/config';
|
||||
|
||||
// Access Market Data Gateway configuration
|
||||
const websocketPath = marketDataGatewayConfig.websocket.path;
|
||||
|
||||
// Access Risk Guardian configuration
|
||||
const preTradeValidation = riskGuardianConfig.riskChecks.preTradeValidation;
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The library automatically loads environment variables from `.env` files. You can create environment-specific files:
|
||||
|
||||
- `.env` - Base environment variables
|
||||
- `.env.development` - Development-specific variables
|
||||
- `.env.production` - Production-specific variables
|
||||
- `.env.local` - Local overrides (not to be committed to git)
|
||||
|
||||
## Configuration Modules
|
||||
|
||||
### Core Configuration
|
||||
|
||||
- `Environment` - Enum for different environments
|
||||
- `loadEnvVariables()` - Load environment variables from .env files
|
||||
- `getEnvironment()` - Get the current environment
|
||||
- `validateConfig()` - Validate configuration with Zod schema
|
||||
|
||||
### Database Configuration
|
||||
|
||||
- `databaseConfig` - Database connection settings (Dragonfly, QuestDB, MongoDB, PostgreSQL)
|
||||
|
||||
### Data Provider Configuration
|
||||
|
||||
- `dataProviderConfigs` - Settings for market data providers
|
||||
|
||||
### Risk Configuration
|
||||
|
||||
- `riskConfig` - Risk management parameters (max drawdown, position size, etc.)
|
||||
|
||||
### Service-Specific Configuration
|
||||
|
||||
- `marketDataGatewayConfig` - Configs for the Market Data Gateway service
|
||||
- `riskGuardianConfig` - Configs for the Risk Guardian service
|
||||
|
||||
## Extending
|
||||
|
||||
To add a new service configuration:
|
||||
|
||||
1. Create a new file in `src/services/`
|
||||
2. Define a Zod schema for validation
|
||||
3. Create loading and default configuration functions
|
||||
4. Export from `src/services/index.ts`
|
||||
5. The new configuration will be automatically available from the main package
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Run tests
|
||||
bun test
|
||||
|
||||
# Type check
|
||||
bun run type-check
|
||||
|
||||
# Lint
|
||||
bun run lint
|
||||
```
|
||||
# @stock-bot/config
|
||||
|
||||
A robust, type-safe configuration library for the Stock Bot application. Built with Zod for validation and supports multiple configuration sources with proper precedence.
|
||||
|
||||
## Features
|
||||
|
||||
- **Type-safe configuration** with Zod schemas
|
||||
- **Multiple configuration sources**: JSON files and environment variables
|
||||
- **Environment-specific overrides** (development, test, production)
|
||||
- **Dynamic provider configurations**
|
||||
- **No circular dependencies** - designed to be used by all other libraries
|
||||
- **Clear error messages** with validation
|
||||
- **Runtime configuration updates** (useful for testing)
|
||||
- **Singleton pattern** for global configuration access
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @stock-bot/config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { initializeConfig, getConfig } from '@stock-bot/config';
|
||||
|
||||
// Initialize configuration (call once at app startup)
|
||||
await initializeConfig();
|
||||
|
||||
// Get configuration
|
||||
const config = getConfig();
|
||||
console.log(config.database.postgres.host);
|
||||
|
||||
// Use convenience functions
|
||||
import { getDatabaseConfig, isProduction } from '@stock-bot/config';
|
||||
|
||||
const dbConfig = getDatabaseConfig();
|
||||
if (isProduction()) {
|
||||
// Production-specific logic
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```typescript
|
||||
import { ConfigManager } from '@stock-bot/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define your schema
|
||||
const myConfigSchema = z.object({
|
||||
app: z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
features: z.object({
|
||||
enableBeta: z.boolean().default(false),
|
||||
}),
|
||||
});
|
||||
|
||||
// Create config manager
|
||||
const configManager = new ConfigManager({
|
||||
configPath: './my-config',
|
||||
});
|
||||
|
||||
// Initialize with schema
|
||||
const config = await configManager.initialize(myConfigSchema);
|
||||
```
|
||||
|
||||
### Provider-Specific Configuration
|
||||
|
||||
```typescript
|
||||
import { getProviderConfig } from '@stock-bot/config';
|
||||
|
||||
// Get provider configuration
|
||||
const eodConfig = getProviderConfig('eod');
|
||||
console.log(eodConfig.apiKey);
|
||||
|
||||
// Check if provider is enabled
|
||||
if (eodConfig.enabled) {
|
||||
// Use EOD provider
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Environment variables are loaded with the `STOCKBOT_` prefix and follow a naming convention:
|
||||
|
||||
```bash
|
||||
# Database configuration
|
||||
STOCKBOT_DATABASE_POSTGRES_HOST=localhost
|
||||
STOCKBOT_DATABASE_POSTGRES_PORT=5432
|
||||
|
||||
# Provider configuration
|
||||
STOCKBOT_PROVIDERS_EOD_API_KEY=your_api_key
|
||||
STOCKBOT_PROVIDERS_EOD_ENABLED=true
|
||||
|
||||
# Service configuration
|
||||
STOCKBOT_SERVICE_PORT=3000
|
||||
STOCKBOT_LOGGING_LEVEL=debug
|
||||
```
|
||||
|
||||
### Configuration Precedence
|
||||
|
||||
Configuration is loaded in the following order (later sources override earlier ones):
|
||||
|
||||
1. `config/default.json` - Base configuration
|
||||
2. `config/{environment}.json` - Environment-specific overrides
|
||||
3. Environment variables - Highest priority
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```typescript
|
||||
import { getConfigManager } from '@stock-bot/config';
|
||||
|
||||
const manager = getConfigManager();
|
||||
|
||||
// Get specific value by path
|
||||
const port = manager.getValue<number>('service.port');
|
||||
|
||||
// Check if configuration exists
|
||||
if (manager.has('providers.ib')) {
|
||||
// IB provider is configured
|
||||
}
|
||||
|
||||
// Update configuration at runtime (useful for testing)
|
||||
manager.set({
|
||||
logging: {
|
||||
level: 'debug'
|
||||
}
|
||||
});
|
||||
|
||||
// Create typed getter
|
||||
const getQueueConfig = manager.createTypedGetter(queueConfigSchema);
|
||||
const queueConfig = getQueueConfig();
|
||||
```
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
The library provides pre-defined schemas for common configurations:
|
||||
|
||||
### Base Configuration
|
||||
- `environment` - Current environment (development/test/production)
|
||||
- `name` - Application name
|
||||
- `version` - Application version
|
||||
- `debug` - Debug mode flag
|
||||
|
||||
### Service Configuration
|
||||
- `name` - Service name
|
||||
- `port` - Service port
|
||||
- `host` - Service host
|
||||
- `healthCheckPath` - Health check endpoint
|
||||
- `cors` - CORS configuration
|
||||
|
||||
### Database Configuration
|
||||
- `postgres` - PostgreSQL configuration
|
||||
- `questdb` - QuestDB configuration
|
||||
- `mongodb` - MongoDB configuration
|
||||
- `dragonfly` - Dragonfly/Redis configuration
|
||||
|
||||
### Provider Configuration
|
||||
- `eod` - EOD Historical Data provider
|
||||
- `ib` - Interactive Brokers provider
|
||||
- `qm` - QuoteMedia provider
|
||||
- `yahoo` - Yahoo Finance provider
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { resetConfig, initializeConfig } from '@stock-bot/config';
|
||||
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
test('custom config', async () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.STOCKBOT_SERVICE_PORT = '4000';
|
||||
|
||||
await initializeConfig();
|
||||
const config = getConfig();
|
||||
|
||||
expect(config.service.port).toBe(4000);
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Loaders
|
||||
|
||||
You can create custom configuration loaders:
|
||||
|
||||
```typescript
|
||||
import { ConfigLoader } from '@stock-bot/config';
|
||||
|
||||
class ApiConfigLoader implements ConfigLoader {
|
||||
readonly priority = 75; // Between file (50) and env (100)
|
||||
|
||||
async load(): Promise<Record<string, unknown>> {
|
||||
// Fetch configuration from API
|
||||
const response = await fetch('https://api.example.com/config');
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// Use custom loader
|
||||
const configManager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader('./config', 'production'),
|
||||
new ApiConfigLoader(),
|
||||
new EnvLoader('STOCKBOT_'),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library provides specific error types:
|
||||
|
||||
```typescript
|
||||
import { ConfigError, ConfigValidationError } from '@stock-bot/config';
|
||||
|
||||
try {
|
||||
await initializeConfig();
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigValidationError) {
|
||||
console.error('Validation failed:', error.errors);
|
||||
} else if (error instanceof ConfigError) {
|
||||
console.error('Configuration error:', error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Initialize once**: Call `initializeConfig()` once at application startup
|
||||
2. **Use schemas**: Always define and validate configurations with Zod schemas
|
||||
3. **Environment variables**: Use the `STOCKBOT_` prefix for all env vars
|
||||
4. **Type safety**: Leverage TypeScript types from the schemas
|
||||
5. **Testing**: Reset configuration between tests with `resetConfig()`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
# Stock Bot Configuration Library Usage Guide
|
||||
|
||||
This guide shows how to use the Zod-based configuration system in the Stock Bot platform.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { databaseConfig, loggingConfig, riskConfig, dataProvidersConfig } from '@stock-bot/config';
|
||||
|
||||
// Access individual values
|
||||
console.log(`Database: ${databaseConfig.POSTGRES_HOST}:${databaseConfig.POSTGRES_PORT}`);
|
||||
console.log(`Log level: ${loggingConfig.LOG_LEVEL}`);
|
||||
console.log(`Max position size: ${riskConfig.RISK_MAX_POSITION_SIZE}`);
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration is driven by environment variables. You can set them in:
|
||||
- `.env` files
|
||||
- System environment variables
|
||||
- Docker environment variables
|
||||
|
||||
### Database Configuration
|
||||
```bash
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=stockbot
|
||||
DB_USER=stockbot
|
||||
DB_PASSWORD=your_password
|
||||
DB_SSL=false
|
||||
DB_POOL_MAX=10
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
```bash
|
||||
LOG_LEVEL=info
|
||||
LOG_CONSOLE=true
|
||||
LOKI_HOST=localhost
|
||||
LOKI_PORT=3100
|
||||
LOKI_LABELS=service=market-data-gateway,version=1.0.0
|
||||
```
|
||||
|
||||
### Risk Management Configuration
|
||||
```bash
|
||||
RISK_MAX_POSITION_SIZE=0.1
|
||||
RISK_DEFAULT_STOP_LOSS=0.05
|
||||
RISK_DEFAULT_TAKE_PROFIT=0.15
|
||||
RISK_CIRCUIT_BREAKER_ENABLED=true
|
||||
```
|
||||
|
||||
### Data Provider Configuration
|
||||
```bash
|
||||
DEFAULT_DATA_PROVIDER=alpaca
|
||||
ALPACA_API_KEY=your_api_key
|
||||
ALPACA_API_SECRET=your_api_secret
|
||||
ALPACA_ENABLED=true
|
||||
POLYGON_ENABLED=false
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Type Safety
|
||||
All configurations are fully typed:
|
||||
|
||||
```typescript
|
||||
import type { DatabaseConfig, LoggingConfig, RiskConfig } from '@stock-bot/config';
|
||||
|
||||
function setupDatabase(config: DatabaseConfig) {
|
||||
// TypeScript knows all the available properties
|
||||
return {
|
||||
host: config.POSTGRES_HOST,
|
||||
port: config.POSTGRES_PORT, // number
|
||||
ssl: config.POSTGRES_SSL, // boolean
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Detection
|
||||
```typescript
|
||||
import { getEnvironment, Environment } from '@stock-bot/config';
|
||||
|
||||
const env = getEnvironment();
|
||||
if (env === Environment.Production) {
|
||||
// Production-specific logic
|
||||
}
|
||||
```
|
||||
|
||||
### Data Provider Helpers
|
||||
```typescript
|
||||
import { getProviderConfig, getEnabledProviders, getDefaultProvider } from '@stock-bot/config';
|
||||
|
||||
// Get specific provider
|
||||
const alpaca = getProviderConfig('alpaca');
|
||||
|
||||
// Get all enabled providers
|
||||
const providers = getEnabledProviders();
|
||||
|
||||
// Get default provider
|
||||
const defaultProvider = getDefaultProvider();
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The library consists of these modules:
|
||||
|
||||
- **core.ts** - Core utilities and environment detection
|
||||
- **database.ts** - Database connection settings
|
||||
- **logging.ts** - Logging and Loki configuration
|
||||
- **risk.ts** - Risk management parameters
|
||||
- **data-providers.ts** - Data provider settings
|
||||
|
||||
## Benefits of This Approach
|
||||
|
||||
1. **Zero Configuration Schema** - No complex schema definitions needed
|
||||
2. **Automatic Type Inference** - TypeScript types are generated automatically
|
||||
3. **Environment Variable Validation** - Invalid values are caught at startup
|
||||
4. **Great Developer Experience** - IntelliSense works perfectly
|
||||
5. **Production Ready** - Used by many large-scale applications
|
||||
|
||||
## Migration from Previous System
|
||||
|
||||
If you're migrating from the old Valibot-based system:
|
||||
|
||||
```typescript
|
||||
// Old way
|
||||
const config = createConfigLoader('database', databaseSchema, defaultConfig)();
|
||||
|
||||
// New way
|
||||
import { databaseConfig } from '@stock-bot/config';
|
||||
// That's it! No schema needed, no validation needed, no complex setup.
|
||||
```
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
[test]
|
||||
# Configure path mapping for tests
|
||||
preload = ["./test/setup.ts"]
|
||||
|
||||
# Test configuration
|
||||
timeout = 5000
|
||||
|
||||
# Set test environment
|
||||
env = { NODE_ENV = "test" }
|
||||
|
||||
[bun]
|
||||
# Enable TypeScript paths resolution
|
||||
paths = {
|
||||
"@/*" = ["./src/*"]
|
||||
}
|
||||
90
libs/config/config/default.json
Normal file
90
libs/config/config/default.json
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"name": "stock-bot",
|
||||
"version": "1.0.0",
|
||||
"debug": false,
|
||||
"service": {
|
||||
"name": "stock-bot-service",
|
||||
"port": 3000,
|
||||
"host": "0.0.0.0",
|
||||
"healthCheckPath": "/health",
|
||||
"metricsPath": "/metrics",
|
||||
"shutdownTimeout": 30000,
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"origin": "*",
|
||||
"credentials": true
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"format": "json",
|
||||
"loki": {
|
||||
"enabled": false,
|
||||
"host": "localhost",
|
||||
"port": 3100,
|
||||
"labels": {}
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"postgres": {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"database": "stockbot",
|
||||
"user": "postgres",
|
||||
"password": "postgres",
|
||||
"ssl": false,
|
||||
"poolSize": 10,
|
||||
"connectionTimeout": 30000,
|
||||
"idleTimeout": 10000
|
||||
},
|
||||
"questdb": {
|
||||
"host": "localhost",
|
||||
"ilpPort": 9009,
|
||||
"httpPort": 9000,
|
||||
"pgPort": 8812,
|
||||
"database": "questdb",
|
||||
"user": "admin",
|
||||
"password": "quest",
|
||||
"bufferSize": 65536,
|
||||
"flushInterval": 1000
|
||||
},
|
||||
"mongodb": {
|
||||
"host": "localhost",
|
||||
"port": 27017,
|
||||
"database": "stockbot",
|
||||
"authSource": "admin",
|
||||
"poolSize": 10
|
||||
},
|
||||
"dragonfly": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"db": 0,
|
||||
"maxRetries": 3,
|
||||
"retryDelay": 100
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"db": 1
|
||||
},
|
||||
"defaultJobOptions": {
|
||||
"attempts": 3,
|
||||
"backoff": {
|
||||
"type": "exponential",
|
||||
"delay": 1000
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": false
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"retryDelay": 1000,
|
||||
"proxy": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
48
libs/config/config/development.json
Normal file
48
libs/config/config/development.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"debug": true,
|
||||
"logging": {
|
||||
"level": "debug",
|
||||
"format": "pretty"
|
||||
},
|
||||
"providers": {
|
||||
"eod": {
|
||||
"name": "eod-historical-data",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"apiKey": "demo",
|
||||
"tier": "free",
|
||||
"rateLimit": {
|
||||
"maxRequests": 20,
|
||||
"windowMs": 60000
|
||||
}
|
||||
},
|
||||
"yahoo": {
|
||||
"name": "yahoo-finance",
|
||||
"enabled": true,
|
||||
"priority": 2,
|
||||
"rateLimit": {
|
||||
"maxRequests": 100,
|
||||
"windowMs": 60000
|
||||
}
|
||||
},
|
||||
"ib": {
|
||||
"name": "interactive-brokers",
|
||||
"enabled": false,
|
||||
"priority": 0,
|
||||
"gateway": {
|
||||
"host": "localhost",
|
||||
"port": 5000,
|
||||
"clientId": 1
|
||||
},
|
||||
"marketDataType": "delayed"
|
||||
},
|
||||
"qm": {
|
||||
"name": "quotemedia",
|
||||
"enabled": false,
|
||||
"priority": 3,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"webmasterId": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
32
libs/config/config/production.json
Normal file
32
libs/config/config/production.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"debug": false,
|
||||
"logging": {
|
||||
"level": "warn",
|
||||
"format": "json",
|
||||
"loki": {
|
||||
"enabled": true,
|
||||
"labels": {
|
||||
"app": "stock-bot",
|
||||
"env": "production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"database": {
|
||||
"postgres": {
|
||||
"ssl": true,
|
||||
"poolSize": 20
|
||||
},
|
||||
"questdb": {
|
||||
"bufferSize": 131072,
|
||||
"flushInterval": 500
|
||||
},
|
||||
"mongodb": {
|
||||
"poolSize": 20
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"timeout": 60000,
|
||||
"retries": 5,
|
||||
"retryDelay": 2000
|
||||
}
|
||||
}
|
||||
42
libs/config/config/test.json
Normal file
42
libs/config/config/test.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"debug": true,
|
||||
"logging": {
|
||||
"level": "error",
|
||||
"format": "json"
|
||||
},
|
||||
"service": {
|
||||
"port": 0,
|
||||
"shutdownTimeout": 5000
|
||||
},
|
||||
"database": {
|
||||
"postgres": {
|
||||
"database": "stockbot_test",
|
||||
"poolSize": 5
|
||||
},
|
||||
"questdb": {
|
||||
"database": "questdb_test"
|
||||
},
|
||||
"mongodb": {
|
||||
"database": "stockbot_test",
|
||||
"poolSize": 5
|
||||
},
|
||||
"dragonfly": {
|
||||
"db": 15,
|
||||
"keyPrefix": "test:"
|
||||
}
|
||||
},
|
||||
"queue": {
|
||||
"redis": {
|
||||
"db": 15
|
||||
},
|
||||
"defaultJobOptions": {
|
||||
"attempts": 1,
|
||||
"removeOnComplete": false,
|
||||
"removeOnFail": false
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"timeout": 5000,
|
||||
"retries": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,36 @@
|
|||
{
|
||||
"name": "@stock-bot/config",
|
||||
"version": "1.0.0",
|
||||
"description": "Configuration management library for Stock Bot platform",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "bun test",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"bun-types": "^1.2.15"
|
||||
},
|
||||
"keywords": [
|
||||
"configuration",
|
||||
"settings",
|
||||
"env",
|
||||
"stock-bot"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
]
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist",
|
||||
"cli": "bun run src/cli.ts",
|
||||
"validate": "bun run src/cli.ts --validate",
|
||||
"check": "bun run src/cli.ts --check"
|
||||
},
|
||||
"bin": {
|
||||
"config-cli": "./dist/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.0.0",
|
||||
"@types/node": "^20.10.5",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
@echo off
|
||||
echo Building @stock-bot/config library...
|
||||
|
||||
cd /d g:\repos\stock-bot
|
||||
echo Installing dependencies...
|
||||
bun install
|
||||
|
||||
echo Running type check...
|
||||
cd /d g:\repos\stock-bot\libs\config
|
||||
bun run type-check
|
||||
|
||||
echo Running tests...
|
||||
bun test
|
||||
|
||||
echo Setting up example configuration...
|
||||
copy .env.example .env
|
||||
|
||||
echo Running example to display configuration...
|
||||
bun run src/example.ts
|
||||
|
||||
echo.
|
||||
echo Configuration library setup complete!
|
||||
echo.
|
||||
echo You can now import @stock-bot/config in your services.
|
||||
|
|
@ -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
194
libs/config/src/cli.ts
Normal 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();
|
||||
}
|
||||
220
libs/config/src/config-manager.ts
Normal file
220
libs/config/src/config-manager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
20
libs/config/src/errors.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
127
libs/config/src/loaders/env.loader.ts
Normal file
127
libs/config/src/loaders/env.loader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
72
libs/config/src/loaders/file.loader.ts
Normal file
72
libs/config/src/loaders/file.loader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
10
libs/config/src/schemas/base.schema.ts
Normal file
10
libs/config/src/schemas/base.schema.ts
Normal 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),
|
||||
});
|
||||
60
libs/config/src/schemas/database.schema.ts
Normal file
60
libs/config/src/schemas/database.schema.ts
Normal 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,
|
||||
});
|
||||
23
libs/config/src/schemas/index.ts
Normal file
23
libs/config/src/schemas/index.ts
Normal 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>;
|
||||
65
libs/config/src/schemas/provider.schema.ts
Normal file
65
libs/config/src/schemas/provider.schema.ts
Normal 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;
|
||||
63
libs/config/src/schemas/service.schema.ts
Normal file
63
libs/config/src/schemas/service.schema.ts
Normal 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(),
|
||||
});
|
||||
28
libs/config/src/types/index.ts
Normal file
28
libs/config/src/types/index.ts
Normal 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;
|
||||
}
|
||||
182
libs/config/src/utils/secrets.ts
Normal file
182
libs/config/src/utils/secrets.ts
Normal 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;
|
||||
}
|
||||
193
libs/config/src/utils/validation.ts
Normal file
193
libs/config/src/utils/validation.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import {
|
||||
databaseConfig,
|
||||
questdbConfig,
|
||||
mongodbConfig,
|
||||
dragonflyConfig,
|
||||
prometheusConfig,
|
||||
grafanaConfig,
|
||||
lokiConfig,
|
||||
loggingConfig
|
||||
} from './dist/index';
|
||||
|
||||
// Set test environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PORT = '3001';
|
||||
|
||||
// Database configs
|
||||
process.env.DB_HOST = 'localhost';
|
||||
process.env.DB_PORT = '5432';
|
||||
process.env.DB_NAME = 'test_db';
|
||||
process.env.DB_USER = 'test_user';
|
||||
process.env.DB_PASSWORD = 'test_pass';
|
||||
|
||||
// QuestDB configs
|
||||
process.env.QUESTDB_HOST = 'localhost';
|
||||
process.env.QUESTDB_HTTP_PORT = '9000';
|
||||
process.env.QUESTDB_PG_PORT = '8812';
|
||||
|
||||
// MongoDB configs
|
||||
process.env.MONGODB_HOST = 'localhost';
|
||||
process.env.MONGODB_PORT = '27017';
|
||||
process.env.MONGODB_DATABASE = 'test_db';
|
||||
|
||||
// Dragonfly configs
|
||||
process.env.DRAGONFLY_HOST = 'localhost';
|
||||
process.env.DRAGONFLY_PORT = '6379';
|
||||
|
||||
// Monitoring configs
|
||||
process.env.PROMETHEUS_HOST = 'localhost';
|
||||
process.env.PROMETHEUS_PORT = '9090';
|
||||
process.env.GRAFANA_HOST = 'localhost';
|
||||
process.env.GRAFANA_PORT = '3000';
|
||||
|
||||
// Loki configs
|
||||
process.env.LOKI_HOST = 'localhost';
|
||||
process.env.LOKI_PORT = '3100';
|
||||
|
||||
// Logging configs
|
||||
process.env.LOG_LEVEL = 'info';
|
||||
process.env.LOG_FORMAT = 'json';
|
||||
|
||||
console.log('🔍 Testing configuration modules...\n');
|
||||
|
||||
const configs = [
|
||||
{ name: 'Database', config: databaseConfig },
|
||||
{ name: 'QuestDB', config: questdbConfig },
|
||||
{ name: 'MongoDB', config: mongodbConfig },
|
||||
{ name: 'Dragonfly', config: dragonflyConfig },
|
||||
{ name: 'Prometheus', config: prometheusConfig },
|
||||
{ name: 'Grafana', config: grafanaConfig },
|
||||
{ name: 'Loki', config: lokiConfig },
|
||||
{ name: 'Logging', config: loggingConfig },
|
||||
];
|
||||
|
||||
let successful = 0;
|
||||
|
||||
for (const { name, config } of configs) {
|
||||
try {
|
||||
if (config && typeof config === 'object' && Object.keys(config).length > 0) {
|
||||
console.log(`✅ ${name}: Loaded successfully`);
|
||||
successful++;
|
||||
} else {
|
||||
console.log(`❌ ${name}: Invalid config object`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Test Summary: ${successful}/${configs.length} modules loaded successfully`);
|
||||
|
||||
if (successful === configs.length) {
|
||||
console.log('🎉 All configuration modules working correctly!');
|
||||
} else {
|
||||
console.log('⚠️ Some configuration modules have issues.');
|
||||
}
|
||||
215
libs/config/test/config-manager.test.ts
Normal file
215
libs/config/test/config-manager.test.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { ConfigLoader } from '../src/types';
|
||||
import { ConfigValidationError } from '../src/errors';
|
||||
|
||||
// Mock loader for testing
|
||||
class MockLoader implements ConfigLoader {
|
||||
priority = 0;
|
||||
|
||||
constructor(
|
||||
private data: Record<string, unknown>,
|
||||
public override priority: number = 0
|
||||
) {}
|
||||
|
||||
async load(): Promise<Record<string, unknown>> {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Test schema
|
||||
const testSchema = z.object({
|
||||
app: z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
port: z.number().positive(),
|
||||
}),
|
||||
database: z.object({
|
||||
host: z.string(),
|
||||
port: z.number(),
|
||||
}),
|
||||
environment: z.enum(['development', 'test', 'production']),
|
||||
});
|
||||
|
||||
type TestConfig = z.infer<typeof testSchema>;
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager<TestConfig>;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ConfigManager<TestConfig>({
|
||||
loaders: [
|
||||
new MockLoader({
|
||||
app: {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
},
|
||||
}),
|
||||
],
|
||||
environment: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
test('should initialize configuration', async () => {
|
||||
const config = await manager.initialize(testSchema);
|
||||
|
||||
expect(config.app.name).toBe('test-app');
|
||||
expect(config.app.version).toBe('1.0.0');
|
||||
expect(config.environment).toBe('test');
|
||||
});
|
||||
|
||||
test('should merge multiple loaders by priority', async () => {
|
||||
manager = new ConfigManager<TestConfig>({
|
||||
loaders: [
|
||||
new MockLoader({ app: { name: 'base', port: 3000 } }, 0),
|
||||
new MockLoader({ app: { name: 'override', version: '2.0.0' } }, 10),
|
||||
new MockLoader({ database: { host: 'prod-db' } }, 5),
|
||||
],
|
||||
environment: 'test',
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
expect(config.app.name).toBe('override');
|
||||
expect(config.app.version).toBe('2.0.0');
|
||||
expect(config.app.port).toBe(3000);
|
||||
expect(config.database.host).toBe('prod-db');
|
||||
});
|
||||
|
||||
test('should validate configuration with schema', async () => {
|
||||
manager = new ConfigManager<TestConfig>({
|
||||
loaders: [
|
||||
new MockLoader({
|
||||
app: {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
port: 'invalid', // Should be number
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await expect(manager.initialize(testSchema)).rejects.toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
test('should get configuration value by path', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
|
||||
expect(manager.getValue('app.name')).toBe('test-app');
|
||||
expect(manager.getValue<number>('database.port')).toBe(5432);
|
||||
});
|
||||
|
||||
test('should check if configuration path exists', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
|
||||
expect(manager.has('app.name')).toBe(true);
|
||||
expect(manager.has('app.nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
test('should update configuration at runtime', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
|
||||
manager.set({
|
||||
app: {
|
||||
name: 'updated-app',
|
||||
},
|
||||
});
|
||||
|
||||
const config = manager.get();
|
||||
expect(config.app.name).toBe('updated-app');
|
||||
expect(config.app.version).toBe('1.0.0'); // Should preserve other values
|
||||
});
|
||||
|
||||
test('should validate updates against schema', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
|
||||
expect(() => {
|
||||
manager.set({
|
||||
app: {
|
||||
port: 'invalid' as any,
|
||||
},
|
||||
});
|
||||
}).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
test('should reset configuration', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
manager.reset();
|
||||
|
||||
expect(() => manager.get()).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should create typed getter', async () => {
|
||||
await manager.initialize(testSchema);
|
||||
|
||||
const appSchema = z.object({
|
||||
app: z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const getAppConfig = manager.createTypedGetter(appSchema);
|
||||
const appConfig = getAppConfig();
|
||||
|
||||
expect(appConfig.app.name).toBe('test-app');
|
||||
});
|
||||
|
||||
test('should detect environment correctly', () => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
const prodManager = new ConfigManager({ loaders: [] });
|
||||
expect(prodManager.getEnvironment()).toBe('production');
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const testManager = new ConfigManager({ loaders: [] });
|
||||
expect(testManager.getEnvironment()).toBe('test');
|
||||
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
test('should handle deep merge correctly', async () => {
|
||||
manager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({
|
||||
app: {
|
||||
settings: {
|
||||
feature1: true,
|
||||
feature2: false,
|
||||
nested: {
|
||||
value: 'base',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 0),
|
||||
new MockLoader({
|
||||
app: {
|
||||
settings: {
|
||||
feature2: true,
|
||||
feature3: true,
|
||||
nested: {
|
||||
value: 'override',
|
||||
extra: 'new',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 10),
|
||||
],
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
expect(config.app.settings.feature1).toBe(true);
|
||||
expect(config.app.settings.feature2).toBe(true);
|
||||
expect(config.app.settings.feature3).toBe(true);
|
||||
expect(config.app.settings.nested.value).toBe('override');
|
||||
expect(config.app.settings.nested.extra).toBe('new');
|
||||
});
|
||||
});
|
||||
213
libs/config/test/index.test.ts
Normal file
213
libs/config/test/index.test.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
initializeConfig,
|
||||
getConfig,
|
||||
getConfigManager,
|
||||
resetConfig,
|
||||
getDatabaseConfig,
|
||||
getServiceConfig,
|
||||
getLoggingConfig,
|
||||
getProviderConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
} from '../src';
|
||||
|
||||
describe('Config Module', () => {
|
||||
const testConfigDir = join(process.cwd(), 'test-config-module');
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
mkdirSync(testConfigDir, { recursive: true });
|
||||
|
||||
// Create test configuration files
|
||||
const config = {
|
||||
app: {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
},
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
pgPort: 8812,
|
||||
},
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost:27017',
|
||||
database: 'testdb',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
enabled: true,
|
||||
rateLimit: 5,
|
||||
},
|
||||
quoteMedia: {
|
||||
enabled: false,
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
realtime: true,
|
||||
backtesting: true,
|
||||
},
|
||||
environment: 'test',
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testConfigDir, 'default.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
rmSync(testConfigDir, { recursive: true, force: true });
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test('should initialize configuration', async () => {
|
||||
const config = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config.app.name).toBe('test-app');
|
||||
expect(config.service.port).toBe(3000);
|
||||
expect(config.environment).toBe('test');
|
||||
});
|
||||
|
||||
test('should get configuration after initialization', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const config = getConfig();
|
||||
|
||||
expect(config.app.name).toBe('test-app');
|
||||
expect(config.database.postgres.host).toBe('localhost');
|
||||
});
|
||||
|
||||
test('should throw if getting config before initialization', () => {
|
||||
expect(() => getConfig()).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should get config manager instance', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const manager = getConfigManager();
|
||||
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.get().app.name).toBe('test-app');
|
||||
});
|
||||
|
||||
test('should get database configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const dbConfig = getDatabaseConfig();
|
||||
|
||||
expect(dbConfig.postgres.host).toBe('localhost');
|
||||
expect(dbConfig.questdb.httpPort).toBe(9000);
|
||||
expect(dbConfig.mongodb.database).toBe('testdb');
|
||||
});
|
||||
|
||||
test('should get service configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const serviceConfig = getServiceConfig();
|
||||
|
||||
expect(serviceConfig.name).toBe('test-service');
|
||||
expect(serviceConfig.port).toBe(3000);
|
||||
});
|
||||
|
||||
test('should get logging configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const loggingConfig = getLoggingConfig();
|
||||
|
||||
expect(loggingConfig.level).toBe('info');
|
||||
expect(loggingConfig.format).toBe('json');
|
||||
});
|
||||
|
||||
test('should get provider configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
const yahooConfig = getProviderConfig('yahoo');
|
||||
expect(yahooConfig.enabled).toBe(true);
|
||||
expect(yahooConfig.rateLimit).toBe(5);
|
||||
|
||||
const qmConfig = getProviderConfig('quoteMedia');
|
||||
expect(qmConfig.enabled).toBe(false);
|
||||
expect(qmConfig.apiKey).toBe('test-key');
|
||||
});
|
||||
|
||||
test('should throw for non-existent provider', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow(
|
||||
'Provider configuration not found: nonexistent'
|
||||
);
|
||||
});
|
||||
|
||||
test('should check environment correctly', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
expect(isTest()).toBe(true);
|
||||
expect(isDevelopment()).toBe(false);
|
||||
expect(isProduction()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle environment overrides', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.STOCKBOT_APP__NAME = 'env-override-app';
|
||||
process.env.STOCKBOT_DATABASE__POSTGRES__HOST = 'prod-db';
|
||||
|
||||
const prodConfig = {
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'prod-host',
|
||||
port: 5432,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testConfigDir, 'production.json'),
|
||||
JSON.stringify(prodConfig, null, 2)
|
||||
);
|
||||
|
||||
const config = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config.environment).toBe('production');
|
||||
expect(config.app.name).toBe('env-override-app');
|
||||
expect(config.database.postgres.host).toBe('prod-db');
|
||||
expect(isProduction()).toBe(true);
|
||||
});
|
||||
|
||||
test('should reset configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
expect(() => getConfig()).not.toThrow();
|
||||
|
||||
resetConfig();
|
||||
expect(() => getConfig()).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should maintain singleton instance', async () => {
|
||||
const config1 = await initializeConfig(testConfigDir);
|
||||
const config2 = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config1).toBe(config2);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,445 +0,0 @@
|
|||
/**
|
||||
* Integration Tests for Config Library
|
||||
*
|
||||
* Tests the entire configuration system including module interactions,
|
||||
* environment loading, validation across modules, and type exports.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { clearEnvVars, getMinimalTestEnv, setTestEnv } from '../test/setup';
|
||||
|
||||
describe('Config Library Integration', () => {
|
||||
beforeEach(() => {
|
||||
// Clear module cache for clean state
|
||||
// Note: Bun handles module caching differently than Jest
|
||||
});
|
||||
|
||||
describe('Complete Configuration Loading', () => {
|
||||
test('should load all configuration modules successfully', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
// Import all modules
|
||||
const [
|
||||
{ Environment, getEnvironment },
|
||||
{ postgresConfig },
|
||||
{ questdbConfig },
|
||||
{ mongodbConfig },
|
||||
{ loggingConfig },
|
||||
{ riskConfig },
|
||||
] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
|
||||
// Verify all configs are loaded
|
||||
expect(Environment).toBeDefined();
|
||||
expect(getEnvironment).toBeDefined();
|
||||
expect(postgresConfig).toBeDefined();
|
||||
expect(questdbConfig).toBeDefined();
|
||||
expect(mongodbConfig).toBeDefined();
|
||||
expect(loggingConfig).toBeDefined();
|
||||
expect(riskConfig).toBeDefined();
|
||||
// Verify core utilities
|
||||
expect(getEnvironment()).toBe(Environment.Testing); // Should be Testing due to NODE_ENV=test in setup
|
||||
expect(postgresConfig.POSTGRES_HOST).toBe('localhost');
|
||||
expect(questdbConfig.QUESTDB_HOST).toBe('localhost');
|
||||
expect(mongodbConfig.MONGODB_HOST).toBe('localhost'); // fix: use correct property
|
||||
expect(loggingConfig.LOG_LEVEL).toBeDefined();
|
||||
expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1);
|
||||
});
|
||||
test('should handle missing required environment variables gracefully', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'test',
|
||||
// Missing required variables
|
||||
});
|
||||
|
||||
// Should be able to load core utilities
|
||||
const { Environment, getEnvironment } = await import('../src/core');
|
||||
expect(Environment).toBeDefined();
|
||||
expect(getEnvironment()).toBe(Environment.Testing);
|
||||
// Should fail to load modules requiring specific vars (if they have required vars)
|
||||
// Note: Most modules have defaults, so they might not throw
|
||||
try {
|
||||
const { postgresConfig } = await import('../src/postgres');
|
||||
expect(postgresConfig).toBeDefined();
|
||||
expect(postgresConfig.POSTGRES_HOST).toBe('localhost'); // default value
|
||||
} catch (error) {
|
||||
// If it throws, that's also acceptable behavior
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
test('should maintain consistency across environment detection', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'production',
|
||||
...getMinimalTestEnv(),
|
||||
});
|
||||
const [
|
||||
{ Environment, getEnvironment },
|
||||
{ postgresConfig },
|
||||
{ questdbConfig },
|
||||
{ mongodbConfig },
|
||||
{ loggingConfig },
|
||||
] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
]);
|
||||
// Note: Due to module caching, environment is set at first import
|
||||
// All modules should detect the same environment (which will be Testing due to test setup)
|
||||
expect(getEnvironment()).toBe(Environment.Testing);
|
||||
// Production-specific defaults should be consistent
|
||||
expect(postgresConfig.POSTGRES_SSL).toBe(false); // default is false unless overridden expect(questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // checking actual property name
|
||||
expect(mongodbConfig.MONGODB_TLS).toBe(false); // checking actual property name
|
||||
expect(loggingConfig.LOG_FORMAT).toBe('json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Main Index Exports', () => {
|
||||
test('should export all configuration objects from index', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
|
||||
const config = await import('../src/index');
|
||||
|
||||
// Core utilities (no coreConfig object)
|
||||
expect(config.Environment).toBeDefined();
|
||||
expect(config.getEnvironment).toBeDefined();
|
||||
expect(config.ConfigurationError).toBeDefined();
|
||||
|
||||
// Configuration objects
|
||||
expect(config.postgresConfig).toBeDefined();
|
||||
expect(config.questdbConfig).toBeDefined();
|
||||
expect(config.mongodbConfig).toBeDefined();
|
||||
expect(config.loggingConfig).toBeDefined();
|
||||
expect(config.riskConfig).toBeDefined();
|
||||
});
|
||||
test('should export individual values from index', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
|
||||
const config = await import('../src/index');
|
||||
|
||||
// Core utilities
|
||||
expect(config.Environment).toBeDefined();
|
||||
expect(config.getEnvironment).toBeDefined();
|
||||
|
||||
// Individual configuration values exported from modules
|
||||
expect(config.POSTGRES_HOST).toBeDefined();
|
||||
expect(config.POSTGRES_PORT).toBeDefined();
|
||||
expect(config.QUESTDB_HOST).toBeDefined();
|
||||
expect(config.MONGODB_HOST).toBeDefined();
|
||||
|
||||
// Risk values
|
||||
expect(config.RISK_MAX_POSITION_SIZE).toBeDefined();
|
||||
expect(config.RISK_MAX_DAILY_LOSS).toBeDefined();
|
||||
|
||||
// Logging values
|
||||
expect(config.LOG_LEVEL).toBeDefined();
|
||||
});
|
||||
test('should maintain type safety in exports', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
|
||||
const {
|
||||
Environment,
|
||||
getEnvironment,
|
||||
postgresConfig,
|
||||
questdbConfig,
|
||||
mongodbConfig,
|
||||
loggingConfig,
|
||||
riskConfig,
|
||||
POSTGRES_HOST,
|
||||
POSTGRES_PORT,
|
||||
QUESTDB_HOST,
|
||||
MONGODB_HOST,
|
||||
RISK_MAX_POSITION_SIZE,
|
||||
} = await import('../src/index');
|
||||
|
||||
// Type checking should pass
|
||||
expect(typeof POSTGRES_HOST).toBe('string');
|
||||
expect(typeof POSTGRES_PORT).toBe('number');
|
||||
expect(typeof QUESTDB_HOST).toBe('string');
|
||||
expect(typeof MONGODB_HOST).toBe('string');
|
||||
expect(typeof RISK_MAX_POSITION_SIZE).toBe('number');
|
||||
|
||||
// Configuration objects should have expected shapes
|
||||
expect(postgresConfig).toHaveProperty('POSTGRES_HOST');
|
||||
expect(postgresConfig).toHaveProperty('POSTGRES_PORT');
|
||||
expect(questdbConfig).toHaveProperty('QUESTDB_HOST');
|
||||
expect(mongodbConfig).toHaveProperty('MONGODB_HOST');
|
||||
expect(loggingConfig).toHaveProperty('LOG_LEVEL');
|
||||
expect(riskConfig).toHaveProperty('RISK_MAX_POSITION_SIZE');
|
||||
});
|
||||
});
|
||||
describe('Environment Variable Validation', () => {
|
||||
test('should validate environment variables across all modules', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'test',
|
||||
LOG_LEVEL: 'info', // valid level
|
||||
POSTGRES_HOST: 'localhost',
|
||||
POSTGRES_DATABASE: 'test',
|
||||
POSTGRES_USERNAME: 'test',
|
||||
POSTGRES_PASSWORD: 'test',
|
||||
QUESTDB_HOST: 'localhost',
|
||||
MONGODB_HOST: 'localhost',
|
||||
MONGODB_DATABASE: 'test',
|
||||
RISK_MAX_POSITION_SIZE: '0.1',
|
||||
RISK_MAX_DAILY_LOSS: '0.05',
|
||||
}); // All imports should succeed with valid config
|
||||
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
|
||||
expect(core.getEnvironment()).toBe(core.Environment.Testing); // default test env
|
||||
expect(postgres.postgresConfig.POSTGRES_HOST).toBe('localhost');
|
||||
expect(questdb.questdbConfig.QUESTDB_HOST).toBe('localhost');
|
||||
expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost');
|
||||
expect(logging.loggingConfig.LOG_LEVEL).toBe('info'); // set in test
|
||||
expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // from test env
|
||||
});
|
||||
test('should accept valid environment variables across all modules', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'development',
|
||||
LOG_LEVEL: 'debug',
|
||||
|
||||
POSTGRES_HOST: 'localhost',
|
||||
POSTGRES_PORT: '5432',
|
||||
POSTGRES_DATABASE: 'stockbot_dev',
|
||||
POSTGRES_USERNAME: 'dev_user',
|
||||
POSTGRES_PASSWORD: 'dev_pass',
|
||||
POSTGRES_SSL: 'false',
|
||||
|
||||
QUESTDB_HOST: 'localhost',
|
||||
QUESTDB_HTTP_PORT: '9000',
|
||||
QUESTDB_PG_PORT: '8812',
|
||||
|
||||
MONGODB_HOST: 'localhost',
|
||||
MONGODB_DATABASE: 'stockbot_dev',
|
||||
|
||||
RISK_MAX_POSITION_SIZE: '0.25',
|
||||
RISK_MAX_DAILY_LOSS: '0.025',
|
||||
|
||||
LOG_FORMAT: 'json',
|
||||
LOG_FILE_ENABLED: 'false',
|
||||
});
|
||||
|
||||
// All imports should succeed
|
||||
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
|
||||
// Since this is the first test to set NODE_ENV to development and modules might not be cached yet,
|
||||
// this could actually change the environment. Let's test what we actually get.
|
||||
expect(core.getEnvironment()).toBeDefined(); // Just verify it returns something valid
|
||||
expect(postgres.postgresConfig.POSTGRES_HOST).toBe('localhost');
|
||||
expect(questdb.questdbConfig.QUESTDB_HOST).toBe('localhost');
|
||||
expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost');
|
||||
expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default value
|
||||
expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // default value
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Consistency', () => {
|
||||
test('should maintain consistent SSL settings across databases', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'production',
|
||||
POSTGRES_HOST: 'prod-postgres.com',
|
||||
POSTGRES_DATABASE: 'prod_db',
|
||||
POSTGRES_USERNAME: 'prod_user',
|
||||
POSTGRES_PASSWORD: 'prod_pass',
|
||||
QUESTDB_HOST: 'prod-questdb.com',
|
||||
MONGODB_HOST: 'prod-mongo.com',
|
||||
MONGODB_DATABASE: 'prod_db',
|
||||
RISK_MAX_POSITION_SIZE: '0.1',
|
||||
RISK_MAX_DAILY_LOSS: '0.05',
|
||||
// SSL settings not explicitly set - should use defaults
|
||||
});
|
||||
|
||||
const [postgres, questdb, mongodb] = await Promise.all([
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
]);
|
||||
|
||||
// Check actual SSL property names and their default values expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default is false
|
||||
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // default is false
|
||||
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false); // default is false
|
||||
});
|
||||
test('should maintain consistent environment detection across modules', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'staging',
|
||||
...getMinimalTestEnv(),
|
||||
});
|
||||
|
||||
const [core, logging] = await Promise.all([import('../src/core'), import('../src/logging')]);
|
||||
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
|
||||
|
||||
// The setTestEnv call above doesn't actually change the real NODE_ENV because modules cache it
|
||||
// So we check that the test setup is working correctly
|
||||
expect(process.env.NODE_ENV).toBe('test'); // This is what's actually set in test environment
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and Caching', () => {
|
||||
test('should cache configuration values between imports', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
|
||||
// Import the same module multiple times
|
||||
const postgres1 = await import('../src/postgres');
|
||||
const postgres2 = await import('../src/postgres');
|
||||
const postgres3 = await import('../src/postgres');
|
||||
|
||||
// Should return the same object reference (cached)
|
||||
expect(postgres1.postgresConfig).toBe(postgres2.postgresConfig);
|
||||
expect(postgres2.postgresConfig).toBe(postgres3.postgresConfig);
|
||||
});
|
||||
|
||||
test('should handle rapid sequential imports', async () => {
|
||||
setTestEnv(getMinimalTestEnv());
|
||||
|
||||
// Import all modules simultaneously
|
||||
const startTime = Date.now();
|
||||
|
||||
await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete relatively quickly (less than 1 second)
|
||||
expect(duration).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
describe('Error Handling and Recovery', () => {
|
||||
test('should provide helpful error messages for missing variables', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'test',
|
||||
// Missing required variables
|
||||
});
|
||||
|
||||
// Most modules have defaults, so they shouldn't throw
|
||||
// But let's verify they load with defaults
|
||||
try {
|
||||
const { postgresConfig } = await import('../src/postgres');
|
||||
expect(postgresConfig).toBeDefined();
|
||||
expect(postgresConfig.POSTGRES_HOST).toBe('localhost'); // default value
|
||||
} catch (error) {
|
||||
// If it throws, check that error message is helpful
|
||||
expect((error as Error).message).toBeTruthy();
|
||||
}
|
||||
|
||||
try {
|
||||
const { riskConfig } = await import('../src/risk');
|
||||
expect(riskConfig).toBeDefined();
|
||||
expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // default value
|
||||
} catch (error) {
|
||||
// If it throws, check that error message is helpful
|
||||
expect((error as Error).message).toBeTruthy();
|
||||
}
|
||||
});
|
||||
test('should handle partial configuration failures gracefully', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'test',
|
||||
LOG_LEVEL: 'info',
|
||||
// Core config should work
|
||||
POSTGRES_HOST: 'localhost',
|
||||
POSTGRES_DATABASE: 'test',
|
||||
POSTGRES_USERNAME: 'test',
|
||||
POSTGRES_PASSWORD: 'test',
|
||||
// Postgres should work
|
||||
QUESTDB_HOST: 'localhost',
|
||||
// QuestDB should work
|
||||
// MongoDB and Risk should work with defaults
|
||||
});
|
||||
|
||||
// All these should succeed since modules have defaults
|
||||
const core = await import('../src/core');
|
||||
const postgres = await import('../src/postgres');
|
||||
const questdb = await import('../src/questdb');
|
||||
const logging = await import('../src/logging');
|
||||
const mongodb = await import('../src/mongodb');
|
||||
const risk = await import('../src/risk');
|
||||
|
||||
expect(core.Environment).toBeDefined();
|
||||
expect(postgres.postgresConfig).toBeDefined();
|
||||
expect(questdb.questdbConfig).toBeDefined();
|
||||
expect(logging.loggingConfig).toBeDefined();
|
||||
expect(mongodb.mongodbConfig).toBeDefined();
|
||||
expect(risk.riskConfig).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe('Development vs Production Differences', () => {
|
||||
test('should configure appropriately for development environment', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'development',
|
||||
...getMinimalTestEnv(),
|
||||
POSTGRES_SSL: undefined, // Should default to false
|
||||
QUESTDB_TLS_ENABLED: undefined, // Should default to false
|
||||
MONGODB_TLS: undefined, // Should default to false
|
||||
LOG_FORMAT: undefined, // Should default to json
|
||||
RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
|
||||
});
|
||||
|
||||
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
|
||||
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false);
|
||||
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false);
|
||||
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false);
|
||||
expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default
|
||||
expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true); // default
|
||||
});
|
||||
|
||||
test('should configure appropriately for production environment', async () => {
|
||||
setTestEnv({
|
||||
NODE_ENV: 'production',
|
||||
...getMinimalTestEnv(),
|
||||
POSTGRES_SSL: undefined, // Should default to false (same as dev)
|
||||
QUESTDB_TLS_ENABLED: undefined, // Should default to false
|
||||
MONGODB_TLS: undefined, // Should default to false
|
||||
LOG_FORMAT: undefined, // Should default to json
|
||||
RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
|
||||
});
|
||||
|
||||
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
|
||||
import('../src/core'),
|
||||
import('../src/postgres'),
|
||||
import('../src/questdb'),
|
||||
import('../src/mongodb'),
|
||||
import('../src/logging'),
|
||||
import('../src/risk'),
|
||||
]);
|
||||
|
||||
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
|
||||
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default doesn't change by env
|
||||
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false);
|
||||
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false);
|
||||
expect(logging.loggingConfig.LOG_FORMAT).toBe('json');
|
||||
expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
181
libs/config/test/loaders.test.ts
Normal file
181
libs/config/test/loaders.test.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
|
||||
describe('EnvLoader', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test('should load environment variables with prefix', async () => {
|
||||
process.env.TEST_APP_NAME = 'env-app';
|
||||
process.env.TEST_APP_VERSION = '1.0.0';
|
||||
process.env.TEST_DATABASE_HOST = 'env-host';
|
||||
process.env.TEST_DATABASE_PORT = '5432';
|
||||
process.env.OTHER_VAR = 'should-not-load';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { convertCase: false, nestedDelimiter: null });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.APP_NAME).toBe('env-app');
|
||||
expect(config.APP_VERSION).toBe('1.0.0');
|
||||
expect(config.DATABASE_HOST).toBe('env-host');
|
||||
expect(config.DATABASE_PORT).toBe(5432); // Should be parsed as number
|
||||
expect(config.OTHER_VAR).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should convert snake_case to camelCase', async () => {
|
||||
process.env.TEST_DATABASE_CONNECTION_STRING = 'postgres://localhost';
|
||||
process.env.TEST_API_KEY_SECRET = 'secret123';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { convertCase: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.databaseConnectionString).toBe('postgres://localhost');
|
||||
expect(config.apiKeySecret).toBe('secret123');
|
||||
});
|
||||
|
||||
test('should parse JSON values', async () => {
|
||||
process.env.TEST_SETTINGS = '{"feature": true, "limit": 100}';
|
||||
process.env.TEST_NUMBERS = '[1, 2, 3]';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { parseJson: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.SETTINGS).toEqual({ feature: true, limit: 100 });
|
||||
expect(config.NUMBERS).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('should parse boolean and number values', async () => {
|
||||
process.env.TEST_ENABLED = 'true';
|
||||
process.env.TEST_DISABLED = 'false';
|
||||
process.env.TEST_PORT = '3000';
|
||||
process.env.TEST_RATIO = '0.75';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { parseValues: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.ENABLED).toBe(true);
|
||||
expect(config.DISABLED).toBe(false);
|
||||
expect(config.PORT).toBe(3000);
|
||||
expect(config.RATIO).toBe(0.75);
|
||||
});
|
||||
|
||||
test('should handle nested object structure', async () => {
|
||||
process.env.TEST_APP__NAME = 'nested-app';
|
||||
process.env.TEST_APP__SETTINGS__ENABLED = 'true';
|
||||
process.env.TEST_DATABASE__HOST = 'localhost';
|
||||
|
||||
const loader = new EnvLoader('TEST_', {
|
||||
parseValues: true,
|
||||
nestedDelimiter: '__'
|
||||
});
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.APP).toEqual({
|
||||
NAME: 'nested-app',
|
||||
SETTINGS: {
|
||||
ENABLED: true
|
||||
}
|
||||
});
|
||||
expect(config.DATABASE).toEqual({
|
||||
HOST: 'localhost'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileLoader', () => {
|
||||
const testDir = join(process.cwd(), 'test-config');
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('should load JSON configuration file', async () => {
|
||||
const config = {
|
||||
app: { name: 'file-app', version: '1.0.0' },
|
||||
database: { host: 'localhost', port: 5432 }
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual(config);
|
||||
});
|
||||
|
||||
test('should load environment-specific configuration', async () => {
|
||||
const defaultConfig = {
|
||||
app: { name: 'app', port: 3000 },
|
||||
database: { host: 'localhost' }
|
||||
};
|
||||
|
||||
const prodConfig = {
|
||||
app: { port: 8080 },
|
||||
database: { host: 'prod-db' }
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
JSON.stringify(defaultConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'production.json'),
|
||||
JSON.stringify(prodConfig, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir, 'production');
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual({
|
||||
app: { name: 'app', port: 8080 },
|
||||
database: { host: 'prod-db' }
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing configuration files gracefully', async () => {
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual({});
|
||||
});
|
||||
|
||||
test('should throw on invalid JSON', async () => {
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
'invalid json content'
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
|
||||
await expect(loader.load()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should support custom configuration', async () => {
|
||||
const config = { custom: 'value' };
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'custom.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.loadFile('custom.json');
|
||||
|
||||
expect(loaded).toEqual(config);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/**
|
||||
* Test Setup for @stock-bot/config Library
|
||||
*
|
||||
* Provides common setup and utilities for testing configuration modules.
|
||||
*/
|
||||
|
||||
// Set NODE_ENV immediately at module load time
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// Store original environment variables
|
||||
const originalEnv = process.env;
|
||||
|
||||
// Note: Bun provides its own test globals, no need to import from @jest/globals
|
||||
beforeEach(() => {
|
||||
// Reset environment variables to original state
|
||||
process.env = { ...originalEnv };
|
||||
// Ensure NODE_ENV is set to test by default
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear environment
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to set environment variables for testing
|
||||
*/
|
||||
export function setTestEnv(vars: Record<string, string | undefined>): void {
|
||||
Object.assign(process.env, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to clear specific environment variables
|
||||
*/
|
||||
export function clearEnvVars(vars: string[]): void {
|
||||
vars.forEach(varName => {
|
||||
delete process.env[varName];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a clean environment for testing
|
||||
*/
|
||||
export function getCleanEnv(): typeof process.env {
|
||||
return {
|
||||
NODE_ENV: 'test',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create minimal required environment variables
|
||||
*/
|
||||
export function getMinimalTestEnv(): Record<string, string> {
|
||||
return {
|
||||
NODE_ENV: 'test',
|
||||
// Logging
|
||||
LOG_LEVEL: 'info', // Changed from 'error' to 'info' to match test expectations
|
||||
// Database
|
||||
POSTGRES_HOST: 'localhost',
|
||||
POSTGRES_PORT: '5432',
|
||||
POSTGRES_DATABASE: 'test_db',
|
||||
POSTGRES_USERNAME: 'test_user',
|
||||
POSTGRES_PASSWORD: 'test_pass',
|
||||
// QuestDB
|
||||
QUESTDB_HOST: 'localhost',
|
||||
QUESTDB_HTTP_PORT: '9000',
|
||||
QUESTDB_PG_PORT: '8812',
|
||||
// MongoDB
|
||||
MONGODB_HOST: 'localhost',
|
||||
MONGODB_PORT: '27017',
|
||||
MONGODB_DATABASE: 'test_db',
|
||||
MONGODB_USERNAME: 'test_user',
|
||||
MONGODB_PASSWORD: 'test_pass',
|
||||
// Dragonfly
|
||||
DRAGONFLY_HOST: 'localhost',
|
||||
DRAGONFLY_PORT: '6379',
|
||||
// Monitoring
|
||||
PROMETHEUS_PORT: '9090',
|
||||
GRAFANA_PORT: '3000',
|
||||
// Data Providers
|
||||
DATA_PROVIDER_API_KEY: 'test_key',
|
||||
// Risk
|
||||
RISK_MAX_POSITION_SIZE: '0.1',
|
||||
RISK_MAX_DAILY_LOSS: '0.05',
|
||||
// Admin
|
||||
ADMIN_PORT: '8080',
|
||||
};
|
||||
}
|
||||
|
|
@ -1,17 +1,22 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["bun-types"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"declarationDir": "./dist",
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/test/**/*",
|
||||
"**/tests/**/*"
|
||||
],
|
||||
"references": [{ "path": "../types" }]
|
||||
}
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["@stock-bot/types#build"],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.spec.ts",
|
||||
"!**/test/**",
|
||||
"!**/tests/**",
|
||||
"!**/__tests__/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Configuration Validation Script
|
||||
* Tests that all configuration modules can be loaded and validated
|
||||
*/
|
||||
|
||||
// Set test environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PORT = '3001';
|
||||
|
||||
// Database configs
|
||||
process.env.DB_HOST = 'localhost';
|
||||
process.env.DB_PORT = '5432';
|
||||
process.env.DB_NAME = 'test_db';
|
||||
process.env.DB_USER = 'test_user';
|
||||
process.env.DB_PASSWORD = 'test_pass';
|
||||
|
||||
// QuestDB configs
|
||||
process.env.QUESTDB_HOST = 'localhost';
|
||||
process.env.QUESTDB_HTTP_PORT = '9000';
|
||||
process.env.QUESTDB_PG_PORT = '8812';
|
||||
|
||||
// MongoDB configs
|
||||
process.env.MONGODB_HOST = 'localhost';
|
||||
process.env.MONGODB_PORT = '27017';
|
||||
process.env.MONGODB_DATABASE = 'test_db';
|
||||
|
||||
// Dragonfly configs
|
||||
process.env.DRAGONFLY_HOST = 'localhost';
|
||||
process.env.DRAGONFLY_PORT = '6379';
|
||||
|
||||
// Monitoring configs
|
||||
process.env.PROMETHEUS_HOST = 'localhost';
|
||||
process.env.PROMETHEUS_PORT = '9090';
|
||||
process.env.GRAFANA_HOST = 'localhost';
|
||||
process.env.GRAFANA_PORT = '3000';
|
||||
|
||||
// Loki configs
|
||||
process.env.LOKI_HOST = 'localhost';
|
||||
process.env.LOKI_PORT = '3100';
|
||||
|
||||
// Logging configs
|
||||
process.env.LOG_LEVEL = 'info';
|
||||
process.env.LOG_FORMAT = 'json';
|
||||
|
||||
try {
|
||||
console.log('🔍 Validating configuration modules...\n');
|
||||
|
||||
// Test each configuration module
|
||||
const modules = [
|
||||
{ name: 'Database', path: './dist/database.js' },
|
||||
{ name: 'QuestDB', path: './dist/questdb.js' },
|
||||
{ name: 'MongoDB', path: './dist/mongodb.js' },
|
||||
{ name: 'Dragonfly', path: './dist/dragonfly.js' },
|
||||
{ name: 'Monitoring', path: './dist/monitoring.js' },
|
||||
{ name: 'Loki', path: './dist/loki.js' },
|
||||
{ name: 'Logging', path: './dist/logging.js' },
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const module of modules) {
|
||||
try {
|
||||
const config = require(module.path);
|
||||
const configKeys = Object.keys(config);
|
||||
|
||||
if (configKeys.length === 0) {
|
||||
throw new Error('No exported configuration found');
|
||||
}
|
||||
|
||||
// Try to access the main config object
|
||||
const mainConfig = config[configKeys[0]];
|
||||
if (!mainConfig || typeof mainConfig !== 'object') {
|
||||
throw new Error('Invalid configuration object');
|
||||
}
|
||||
|
||||
console.log(`✅ ${module.name}: ${configKeys.length} config(s) loaded`);
|
||||
results.push({ name: module.name, status: 'success', configs: configKeys });
|
||||
|
||||
} catch (error) {
|
||||
console.log(`❌ ${module.name}: ${error.message}`);
|
||||
results.push({ name: module.name, status: 'error', error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Test main index exports
|
||||
try {
|
||||
const indexExports = require('./dist/index.js');
|
||||
const exportCount = Object.keys(indexExports).length;
|
||||
console.log(`\n✅ Index exports: ${exportCount} modules exported`);
|
||||
results.push({ name: 'Index', status: 'success', exports: exportCount });
|
||||
} catch (error) {
|
||||
console.log(`\n❌ Index exports: ${error.message}`);
|
||||
results.push({ name: 'Index', status: 'error', error: error.message });
|
||||
}
|
||||
|
||||
// Summary
|
||||
const successful = results.filter(r => r.status === 'success').length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`\n📊 Validation Summary:`);
|
||||
console.log(` Total modules: ${total}`);
|
||||
console.log(` Successful: ${successful}`);
|
||||
console.log(` Failed: ${total - successful}`);
|
||||
|
||||
if (successful === total) {
|
||||
console.log('\n🎉 All configuration modules validated successfully!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n⚠️ Some configuration modules failed validation.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Validation script failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue