moved folders around
This commit is contained in:
parent
4f89affc2b
commit
36cb84b343
202 changed files with 1160 additions and 660 deletions
228
libs/core/config/src/config-manager.ts
Normal file
228
libs/core/config/src/config-manager.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import { join } from 'path';
|
||||
import { z } from 'zod';
|
||||
import { EnvLoader } from './loaders/env.loader';
|
||||
import { FileLoader } from './loaders/file.loader';
|
||||
import { ConfigError, ConfigValidationError } from './errors';
|
||||
import {
|
||||
ConfigLoader,
|
||||
ConfigManagerOptions,
|
||||
ConfigSchema,
|
||||
DeepPartial,
|
||||
Environment,
|
||||
} from './types';
|
||||
|
||||
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 synchronously.
|
||||
*/
|
||||
initialize(schema?: ConfigSchema): 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) {
|
||||
// Assuming all loaders now have a synchronous `load` method
|
||||
const config = 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 Record<string, unknown>)['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: unknown = config;
|
||||
|
||||
for (const key of keys) {
|
||||
if (value && typeof value === 'object' && key in value) {
|
||||
value = (value as Record<string, unknown>)[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 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;
|
||||
} 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, 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) &&
|
||||
!(value instanceof Date) &&
|
||||
!(value instanceof RegExp)
|
||||
) {
|
||||
result[key] = this.deepMerge(
|
||||
(result[key] as Record<string, unknown>) || ({} as Record<string, unknown>),
|
||||
value as Record<string, unknown>
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue