made config async
This commit is contained in:
parent
92d4b90987
commit
caf1c5fcaf
6 changed files with 91 additions and 143 deletions
|
|
@ -1,7 +1,7 @@
|
|||
// Framework imports
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { initializeConfigSync } from '@stock-bot/config';
|
||||
import { initializeConfig } from '@stock-bot/config';
|
||||
// Library imports
|
||||
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
|
||||
import { connectMongoDB } from '@stock-bot/mongodb-client';
|
||||
|
|
@ -12,7 +12,8 @@ import { ProxyManager } from '@stock-bot/utils';
|
|||
// Local imports
|
||||
import { exchangeRoutes, healthRoutes, queueRoutes } from './routes';
|
||||
|
||||
const config = initializeConfigSync();
|
||||
const config = initializeConfig();
|
||||
console.log('Configuration loaded:', config);
|
||||
const serviceConfig = config.service;
|
||||
const databaseConfig = config.database;
|
||||
const queueConfig = config.queue;
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize the configuration by loading from all sources
|
||||
* Initialize the configuration by loading from all sources synchronously.
|
||||
*/
|
||||
async initialize(schema?: ConfigSchema): Promise<T> {
|
||||
initialize(schema?: ConfigSchema): T {
|
||||
if (this.config) {
|
||||
return this.config;
|
||||
}
|
||||
|
|
@ -48,7 +48,8 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
// Load configurations from all sources
|
||||
const configs: Record<string, unknown>[] = [];
|
||||
for (const loader of sortedLoaders) {
|
||||
const config = await loader.load();
|
||||
// Assuming all loaders now have a synchronous `load` method
|
||||
const config = loader.load();
|
||||
if (config && Object.keys(config).length > 0) {
|
||||
configs.push(config);
|
||||
}
|
||||
|
|
@ -82,51 +83,6 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the configuration synchronously (only env vars, no file loading)
|
||||
*/
|
||||
initializeSync(schema?: ConfigSchema): T {
|
||||
if (this.config) {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
this.schema = schema;
|
||||
|
||||
// Only use EnvLoader for sync initialization
|
||||
const envLoader = this.loaders.find(loader => loader.constructor.name === 'EnvLoader');
|
||||
if (!envLoader) {
|
||||
throw new ConfigError('No EnvLoader found for synchronous initialization');
|
||||
}
|
||||
|
||||
// Load env vars synchronously
|
||||
const envLoaderInstance = envLoader as any;
|
||||
const config = envLoaderInstance.loadSync ? envLoaderInstance.loadSync() : {};
|
||||
|
||||
// Add environment if not present
|
||||
if (typeof config === 'object' && config !== null && !('environment' in config)) {
|
||||
(config as Record<string, unknown>)['environment'] = this.environment;
|
||||
}
|
||||
|
||||
// Validate if schema provided
|
||||
if (this.schema) {
|
||||
try {
|
||||
this.config = this.schema.parse(config) as T;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new ConfigValidationError(
|
||||
'Configuration validation failed',
|
||||
error.errors
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
this.config = config as T;
|
||||
}
|
||||
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
|
|
@ -176,7 +132,7 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
throw new ConfigError('Configuration not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
const updated = this.deepMerge(this.config as any, updates as any) as T;
|
||||
const updated = this.deepMerge(this.config as Record<string, unknown>, updates as Record<string, unknown>) as T;
|
||||
|
||||
// Re-validate if schema is present
|
||||
if (this.schema) {
|
||||
|
|
@ -240,8 +196,8 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
}
|
||||
|
||||
private deepMerge(...objects: Record<string, any>[]): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
private deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const obj of objects) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
|
|
@ -253,7 +209,10 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
!(value instanceof Date) &&
|
||||
!(value instanceof RegExp)
|
||||
) {
|
||||
result[key] = this.deepMerge(result[key] || {}, value);
|
||||
result[key] = this.deepMerge(
|
||||
(result[key] as Record<string, unknown>) || {} as Record<string, unknown>,
|
||||
value as Record<string, unknown>
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,8 @@
|
|||
// Export all schemas
|
||||
export * from './schemas';
|
||||
|
||||
// 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 { EnvLoader } from './loaders/env.loader';
|
||||
import { FileLoader } from './loaders/file.loader';
|
||||
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;
|
||||
|
|
@ -37,30 +17,36 @@ function loadCriticalEnvVarsSync(): void {
|
|||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||
const lines = envContent.split('\n');
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const equalIndex = trimmed.indexOf('=');
|
||||
if (equalIndex === -1) continue;
|
||||
|
||||
if (equalIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = trimmed.substring(0, equalIndex).trim();
|
||||
let value = trimmed.substring(equalIndex + 1).trim();
|
||||
|
||||
|
||||
// Remove surrounding quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
|
||||
// Only set if not already set
|
||||
if (!(key in process.env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore errors - env file is optional
|
||||
}
|
||||
}
|
||||
|
|
@ -69,26 +55,16 @@ function loadCriticalEnvVarsSync(): void {
|
|||
loadCriticalEnvVarsSync();
|
||||
|
||||
/**
|
||||
* Initialize configuration synchronously (env vars only)
|
||||
* This should be called at the very start of the application
|
||||
* Initialize the global configuration synchronously.
|
||||
*
|
||||
* This loads configuration from all sources in the correct hierarchy:
|
||||
* 1. Schema defaults (lowest priority)
|
||||
* 2. default.json
|
||||
* 3. [environment].json (e.g., development.json)
|
||||
* 4. .env file values
|
||||
* 5. process.env values (highest priority)
|
||||
*/
|
||||
export function initializeConfigSync(): AppConfig {
|
||||
if (!configInstance) {
|
||||
configInstance = new ConfigManager<AppConfig>({
|
||||
loaders: [
|
||||
new EnvLoader(''), // Environment variables only for sync
|
||||
]
|
||||
});
|
||||
}
|
||||
return configInstance.initializeSync(appConfigSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the global configuration
|
||||
*/
|
||||
export async function initializeConfig(
|
||||
configPath?: string
|
||||
): Promise<AppConfig> {
|
||||
export function initializeConfig(configPath?: string): AppConfig {
|
||||
if (!configInstance) {
|
||||
configInstance = new ConfigManager<AppConfig>({
|
||||
configPath,
|
||||
|
|
@ -98,21 +74,21 @@ export async function initializeConfig(
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize configuration for a service in a monorepo
|
||||
* 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> {
|
||||
export function initializeServiceConfig(): 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 FileLoader('./config', environment), // Service config
|
||||
new EnvLoader(''), // Environment variables
|
||||
]
|
||||
],
|
||||
});
|
||||
}
|
||||
return configInstance.initialize(appConfigSchema);
|
||||
|
|
@ -185,4 +161,24 @@ export function isProduction(): boolean {
|
|||
|
||||
export function isTest(): boolean {
|
||||
return getConfig().environment === 'test';
|
||||
}
|
||||
}
|
||||
|
||||
// Export all schemas
|
||||
export * from './schemas';
|
||||
|
||||
// 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';
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ export class EnvLoader implements ConfigLoader {
|
|||
};
|
||||
}
|
||||
|
||||
async load(): Promise<Record<string, unknown>> {
|
||||
return this.loadSync();
|
||||
}
|
||||
|
||||
loadSync(): Record<string, unknown> {
|
||||
load(): Record<string, unknown> {
|
||||
try {
|
||||
// Load root .env file - try multiple possible locations
|
||||
const possiblePaths = ['./.env', '../.env', '../../.env'];
|
||||
|
|
@ -67,7 +63,7 @@ export class EnvLoader implements ConfigLoader {
|
|||
}
|
||||
}
|
||||
|
||||
private setConfigValue(config: Record<string, any>, key: string, value: string): void {
|
||||
private setConfigValue(config: Record<string, unknown>, key: string, value: string): void {
|
||||
const parsedValue = this.parseValue(value);
|
||||
|
||||
try {
|
||||
|
|
@ -104,7 +100,7 @@ export class EnvLoader implements ConfigLoader {
|
|||
}
|
||||
}
|
||||
|
||||
private setNestedValue(obj: Record<string, any>, path: string[], value: unknown): boolean {
|
||||
private setNestedValue(obj: Record<string, unknown>, path: string[], value: unknown): boolean {
|
||||
if (path.length === 0) {
|
||||
return false; // Cannot set value on empty path
|
||||
}
|
||||
|
|
@ -115,13 +111,13 @@ export class EnvLoader implements ConfigLoader {
|
|||
|
||||
try {
|
||||
const target = path.reduce((acc, key) => {
|
||||
if (!acc[key]) {
|
||||
if (!acc[key] || typeof acc[key] !== 'object') {
|
||||
acc[key] = {};
|
||||
}
|
||||
return acc[key];
|
||||
return acc[key] as Record<string, unknown>;
|
||||
}, obj);
|
||||
|
||||
target[lastKey] = value;
|
||||
(target as Record<string, unknown>)[lastKey] = value;
|
||||
return true;
|
||||
} catch {
|
||||
// If we can't assign to any property (readonly), skip this env var silently
|
||||
|
|
@ -257,11 +253,11 @@ export class EnvLoader implements ConfigLoader {
|
|||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
// File not found is not an error (env files are optional)
|
||||
if (error.code !== 'ENOENT') {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Warning: Could not load env file ${filePath}:`, error.message);
|
||||
console.warn(`Warning: Could not load env file ${filePath}:`, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { ConfigLoader } from '../types';
|
||||
import { ConfigLoaderError } from '../errors';
|
||||
|
|
@ -11,18 +11,18 @@ export class FileLoader implements ConfigLoader {
|
|||
private environment: string
|
||||
) {}
|
||||
|
||||
async load(): Promise<Record<string, unknown>> {
|
||||
load(): Record<string, unknown> {
|
||||
try {
|
||||
const configs: Record<string, unknown>[] = [];
|
||||
|
||||
// Load default config
|
||||
const defaultConfig = await this.loadFile('default.json');
|
||||
const defaultConfig = this.loadFile('default.json');
|
||||
if (defaultConfig) {
|
||||
configs.push(defaultConfig);
|
||||
}
|
||||
|
||||
// Load environment-specific config
|
||||
const envConfig = await this.loadFile(`${this.environment}.json`);
|
||||
const envConfig = this.loadFile(`${this.environment}.json`);
|
||||
if (envConfig) {
|
||||
configs.push(envConfig);
|
||||
}
|
||||
|
|
@ -37,30 +37,26 @@ export class FileLoader implements ConfigLoader {
|
|||
}
|
||||
}
|
||||
|
||||
private async loadFile(filename: string): Promise<Record<string, unknown> | null> {
|
||||
private loadFile(filename: string): Record<string, unknown> | null {
|
||||
const filepath = join(this.configPath, filename);
|
||||
|
||||
try {
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error: unknown) {
|
||||
// File not found is not an error (configs are optional)
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
|
||||
if (!existsSync(filepath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = readFileSync(filepath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
private deepMerge(...objects: Record<string, any>[]): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
private deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
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 if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
||||
result[key] = this.deepMerge(result[key] as Record<string, unknown> || {}, value as Record<string, unknown>);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||
export type Environment = 'development' | 'test' | 'production';
|
||||
|
||||
export interface ConfigLoader {
|
||||
load(): Promise<Record<string, unknown>>;
|
||||
load(): Record<string, unknown>;
|
||||
readonly priority: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue