simplified a lot of stuff
This commit is contained in:
parent
b845a8eade
commit
885b484a37
20 changed files with 360 additions and 1335 deletions
|
|
@ -22,20 +22,19 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
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
|
||||
new EnvLoader(''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the configuration by loading from all sources synchronously.
|
||||
* Initialize the configuration by loading from all sources
|
||||
*/
|
||||
initialize(schema?: ConfigSchema): T {
|
||||
if (this.config) {
|
||||
|
|
@ -50,7 +49,6 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
// Load configurations from all sources
|
||||
const configs: Record<string, unknown>[] = [];
|
||||
for (const loader of sortedLoaders) {
|
||||
// Assuming all loaders now have a synchronous `load` method
|
||||
const config = loader.load();
|
||||
if (config && Object.keys(config).length > 0) {
|
||||
configs.push(config);
|
||||
|
|
@ -58,14 +56,10 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
|
||||
// Merge all configurations
|
||||
const mergedConfig = this.deepMerge(...configs) as T;
|
||||
const mergedConfig = this.merge(...configs) as T;
|
||||
|
||||
// Add environment if not present
|
||||
if (
|
||||
typeof mergedConfig === 'object' &&
|
||||
mergedConfig !== null &&
|
||||
!('environment' in mergedConfig)
|
||||
) {
|
||||
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) {
|
||||
(mergedConfig as Record<string, unknown>)['environment'] = this.environment;
|
||||
}
|
||||
|
||||
|
|
@ -79,12 +73,9 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
path: err.path.join('.'),
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
expected: (err as any).expected,
|
||||
received: (err as any).received,
|
||||
}));
|
||||
|
||||
this.logger.error('Configuration validation failed:', errorDetails);
|
||||
|
||||
throw new ConfigValidationError('Configuration validation failed', error.errors);
|
||||
}
|
||||
throw error;
|
||||
|
|
@ -138,19 +129,18 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update configuration at runtime (useful for testing)
|
||||
* Update configuration at runtime
|
||||
*/
|
||||
set(updates: DeepPartial<T>): void {
|
||||
if (!this.config) {
|
||||
throw new ConfigError('Configuration not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
const updated = this.deepMerge(
|
||||
const updated = this.merge(
|
||||
this.config as Record<string, unknown>,
|
||||
updates as Record<string, unknown>
|
||||
) as T;
|
||||
|
||||
// Re-validate if schema is present
|
||||
if (this.schema) {
|
||||
try {
|
||||
this.config = this.schema.parse(updated) as T;
|
||||
|
|
@ -176,7 +166,7 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset configuration (useful for testing)
|
||||
* Reset configuration
|
||||
*/
|
||||
reset(): void {
|
||||
this.config = null;
|
||||
|
|
@ -190,13 +180,6 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
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) {
|
||||
|
|
@ -212,48 +195,32 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
}
|
||||
}
|
||||
|
||||
private deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
|
||||
const seen = new WeakSet();
|
||||
|
||||
const merge = (...objs: Record<string, unknown>[]): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {};
|
||||
/**
|
||||
* Simple deep merge without circular reference handling
|
||||
*/
|
||||
private merge(...objects: Record<string, unknown>[]): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const obj of objs) {
|
||||
if (seen.has(obj)) {
|
||||
// Skip circular reference instead of throwing
|
||||
return result;
|
||||
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.merge(
|
||||
(result[key] as Record<string, unknown>) || {},
|
||||
value as Record<string, unknown>
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
seen.add(obj);
|
||||
|
||||
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)
|
||||
) {
|
||||
if (seen.has(value)) {
|
||||
// Skip circular reference - don't merge this value
|
||||
continue;
|
||||
}
|
||||
result[key] = merge(
|
||||
(result[key] as Record<string, unknown>) || ({} as Record<string, unknown>),
|
||||
value as Record<string, unknown>
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
seen.delete(obj);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return merge(...objects);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,192 +1,22 @@
|
|||
// Import necessary types
|
||||
import { z } from 'zod';
|
||||
import { EnvLoader } from './loaders/env.loader';
|
||||
import { FileLoader } from './loaders/file.loader';
|
||||
import { ConfigManager } from './config-manager';
|
||||
import { ConfigError } from './errors';
|
||||
import type { BaseAppConfig } from './schemas';
|
||||
import { baseAppSchema } from './schemas';
|
||||
|
||||
// Legacy singleton instance for backward compatibility
|
||||
let configInstance: ConfigManager<BaseAppConfig> | null = null;
|
||||
|
||||
// Synchronously load critical env vars for early initialization
|
||||
function loadCriticalEnvVarsSync(): void {
|
||||
// Load .env file synchronously if it exists
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const envPath = path.resolve(process.cwd(), '.env');
|
||||
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;
|
||||
}
|
||||
|
||||
const equalIndex = trimmed.indexOf('=');
|
||||
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("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
// Only set if not already set
|
||||
if (!(key in process.env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors - env file is optional
|
||||
}
|
||||
}
|
||||
|
||||
// Load critical env vars immediately
|
||||
loadCriticalEnvVarsSync();
|
||||
|
||||
/**
|
||||
* 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 function initializeServiceConfig(): BaseAppConfig {
|
||||
if (!configInstance) {
|
||||
const environment = process.env.NODE_ENV || 'development';
|
||||
configInstance = new ConfigManager<BaseAppConfig>({
|
||||
loaders: [
|
||||
new FileLoader('../../config', environment), // Root config
|
||||
new FileLoader('./config', environment), // Service config
|
||||
new EnvLoader(''), // Environment variables
|
||||
],
|
||||
});
|
||||
}
|
||||
return configInstance.initialize(baseAppSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
export function getConfig(): BaseAppConfig {
|
||||
if (!configInstance) {
|
||||
throw new ConfigError('Configuration not initialized. Call initializeConfig() first.');
|
||||
}
|
||||
return configInstance.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration manager instance
|
||||
*/
|
||||
export function getConfigManager(): ConfigManager<BaseAppConfig> {
|
||||
if (!configInstance) {
|
||||
throw new ConfigError('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 getLogConfig() {
|
||||
return getConfig().log;
|
||||
}
|
||||
|
||||
export function getQueueConfig() {
|
||||
return getConfig().queue;
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic config builder for creating app-specific configurations
|
||||
* @param schema - Zod schema for your app config
|
||||
* @param options - Config manager options
|
||||
* @returns Initialized config manager instance
|
||||
*/
|
||||
export function createAppConfig<T extends z.ZodSchema>(
|
||||
schema: T,
|
||||
options?: {
|
||||
configPath?: string;
|
||||
environment?: 'development' | 'test' | 'production';
|
||||
loaders?: any[];
|
||||
}
|
||||
): ConfigManager<z.infer<T>> {
|
||||
const manager = new ConfigManager<z.infer<T>>(options);
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and initialize app config in one step
|
||||
*/
|
||||
export function initializeAppConfig<T extends z.ZodSchema>(
|
||||
schema: T,
|
||||
options?: {
|
||||
configPath?: string;
|
||||
environment?: 'development' | 'test' | 'production';
|
||||
loaders?: any[];
|
||||
}
|
||||
): z.infer<T> {
|
||||
const manager = createAppConfig(schema, options);
|
||||
return manager.initialize(schema);
|
||||
}
|
||||
|
||||
// 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 only what's actually used
|
||||
export { ConfigManager } from './config-manager';
|
||||
export { toUnifiedConfig } from './schemas/unified-app.schema';
|
||||
|
||||
// Export utilities
|
||||
export * from './utils/secrets';
|
||||
export * from './utils/validation';
|
||||
// Export used types
|
||||
export type { BaseAppConfig, UnifiedAppConfig } from './schemas';
|
||||
|
||||
// Export schemas that are used by apps
|
||||
export {
|
||||
baseAppSchema,
|
||||
dragonflyConfigSchema,
|
||||
mongodbConfigSchema,
|
||||
postgresConfigSchema,
|
||||
questdbConfigSchema,
|
||||
} from './schemas';
|
||||
|
||||
// createAppConfig function for apps/stock
|
||||
export function createAppConfig<T>(schema: any, options?: any): ConfigManager<T> {
|
||||
return new ConfigManager<T>(options);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue