UNPKG

@snow-tzu/type-config

Version:

Core configuration management system with Spring Boot-like features

316 lines (315 loc) 13.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigManager = void 0; const path = __importStar(require("path")); const class_validator_1 = require("class-validator"); const decorators_1 = require("./decorators"); const sources_1 = require("./sources"); const placeholder_resolver_1 = require("./placeholder-resolver"); const map_binder_1 = require("./map-binder"); class ConfigManager { constructor(options = {}) { this.options = options; this.config = {}; this.sources = []; this.initialized = false; this.configInstances = new Map(); this.profile = options.profile || process.env.NODE_ENV || 'development'; this.validateOnBind = options.validateOnBind ?? true; this.enablePlaceholderResolution = options.enablePlaceholderResolution ?? true; this.placeholderResolver = new placeholder_resolver_1.PlaceholderResolver(); this.mapBinder = new map_binder_1.MapBinder(); if (options.encryptionKey) { this.encryptionHelper = new sources_1.EncryptionHelper(options.encryptionKey); } } async initialize() { if (this.initialized) return; const configDir = this.options.configDir || './config'; this.sources.push(new sources_1.FileConfigSource(path.join(configDir, 'application.json'), 100), new sources_1.FileConfigSource(path.join(configDir, 'application.yml'), 100), new sources_1.FileConfigSource(path.join(configDir, `application-${this.profile}.json`), 150), new sources_1.FileConfigSource(path.join(configDir, `application-${this.profile}.yml`), 150), new sources_1.EnvConfigSource(this.options.envPrefix, 200)); if (this.options.additionalSources) { this.sources.push(...this.options.additionalSources); } this.sources.sort((a, b) => a.priority - b.priority); await this.reload(); this.initialized = true; } async reload() { const newConfig = {}; for (const source of this.sources) { try { const data = await source.load(); this.deepMerge(newConfig, data); } catch (err) { console.warn(`Failed to load config source ${source.name}:`, err); } } let resolvedConfig = newConfig; if (this.enablePlaceholderResolution) { resolvedConfig = this.resolveEnvironmentVariables(newConfig); } if (this.encryptionHelper) { this.config = this.encryptionHelper.decryptObject(resolvedConfig); } else { this.config = resolvedConfig; } this.configInstances.clear(); } resolveEnvironmentVariables(config) { return this.placeholderResolver.resolveObject(config); } get(path, defaultValue) { const keys = path.split('.'); let value = this.config; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return defaultValue; } } return value; } getAll() { return { ...this.config }; } getProfile() { return this.profile; } bind(ConfigClass) { if (this.configInstances.has(ConfigClass)) { return this.configInstances.get(ConfigClass); } const prefix = Reflect.getMetadata(decorators_1.CONFIG_PREFIX_KEY, ConfigClass); if (!prefix) { throw new Error(`Class ${ConfigClass.name} must be decorated with @ConfigurationProperties`); } const instance = new ConfigClass(); const requiredProps = Reflect.getMetadata(decorators_1.REQUIRED_PROPS_KEY, ConfigClass) || []; const defaults = Reflect.getMetadata(decorators_1.DEFAULTS_KEY, ConfigClass) || {}; const shouldValidate = Reflect.getMetadata(decorators_1.VALIDATE_KEY, ConfigClass); const allProperties = this.getAllProperties(instance, ConfigClass); for (const [propertyKey, propertyPath] of allProperties) { const fullPath = `${prefix}.${propertyPath}`; const defaultVal = defaults[propertyKey]; const value = this.get(fullPath, defaultVal); const type = Reflect.getMetadata('design:type', instance, propertyKey); const isNestedClass = type && this.isConfigurationClass(type); if (value !== undefined || isNestedClass) { instance[propertyKey] = this.convertAndBindType(value, instance, propertyKey); } } for (const prop of requiredProps) { if (instance[prop] === undefined || instance[prop] === null) { throw new Error(`Required configuration property '${prefix}.${prop}' is missing`); } } if (this.validateOnBind && shouldValidate) { this.validateInstanceSync(instance, ConfigClass.name); } this.configInstances.set(ConfigClass, instance); return instance; } validateInstanceSync(instance, className) { const errors = (0, class_validator_1.validateSync)(instance); if (errors.length > 0) { const messages = this.formatValidationErrors(errors); throw new Error(`Validation failed for ${className}:\n${messages}`); } } formatValidationErrors(errors, prefix = '') { return errors .map(error => { const constraints = error.constraints ? Object.values(error.constraints).join(', ') : ''; let message = ` ${prefix}- ${error.property}: ${constraints}`; if (error.children && error.children.length > 0) { const childMessages = this.formatValidationErrors(error.children, prefix + ' '); message += '\n' + childMessages; } return message; }) .join('\n'); } isConfigurationClass(type) { if (!type || typeof type !== 'function') { return false; } const hasDefaults = Reflect.hasMetadata(decorators_1.DEFAULTS_KEY, type); const hasRequired = Reflect.hasMetadata(decorators_1.REQUIRED_PROPS_KEY, type); const hasValidate = Reflect.hasMetadata(decorators_1.VALIDATE_KEY, type); const hasConfigProps = Reflect.hasMetadata(decorators_1.CONFIG_PROPERTIES_KEY, type); return hasDefaults || hasRequired || hasValidate || hasConfigProps; } getAllProperties(_instance, ConfigClass) { const properties = Reflect.getMetadata(decorators_1.CONFIG_PROPERTIES_KEY, ConfigClass) || {}; const result = new Map(); for (const [key, path] of Object.entries(properties)) { result.set(key, path); } const defaults = Reflect.getMetadata(decorators_1.DEFAULTS_KEY, ConfigClass) || {}; const requiredProps = Reflect.getMetadata(decorators_1.REQUIRED_PROPS_KEY, ConfigClass) || []; for (const key of Object.keys(defaults)) { if (!result.has(key)) { result.set(key, key); } } for (const key of requiredProps) { if (!result.has(key)) { result.set(key, key); } } return result; } getAllPropertiesForNestedClass(_instance, NestedClass, configValue) { const properties = Reflect.getMetadata(decorators_1.CONFIG_PROPERTIES_KEY, NestedClass) || {}; const result = new Map(); for (const [key, path] of Object.entries(properties)) { result.set(key, path); } const defaults = Reflect.getMetadata(decorators_1.DEFAULTS_KEY, NestedClass) || {}; for (const key of Object.keys(defaults)) { if (!result.has(key)) { result.set(key, key); } } const requiredProps = Reflect.getMetadata(decorators_1.REQUIRED_PROPS_KEY, NestedClass) || []; for (const key of requiredProps) { if (!result.has(key)) { result.set(key, key); } } if (configValue && typeof configValue === 'object' && !Array.isArray(configValue)) { for (const key of Object.keys(configValue)) { if (!result.has(key)) { result.set(key, key); } } } return result; } bindNestedClass(value, NestedClass, propertyPath) { if (!value || typeof value !== 'object' || Array.isArray(value)) { const shouldValidate = Reflect.getMetadata(decorators_1.VALIDATE_KEY, NestedClass); const defaults = Reflect.getMetadata(decorators_1.DEFAULTS_KEY, NestedClass) || {}; const hasDefaults = Object.keys(defaults).length > 0; const shouldCreateInstance = (this.validateOnBind && shouldValidate) || hasDefaults; if (shouldCreateInstance && (value === null || value === undefined)) { value = {}; } else { return value; } } const instance = new NestedClass(); const defaults = Reflect.getMetadata(decorators_1.DEFAULTS_KEY, NestedClass) || {}; const allProps = this.getAllPropertiesForNestedClass(instance, NestedClass, value); for (const [propKey, propPath] of allProps) { const defaultVal = defaults[propKey]; const propValue = value[propPath] !== undefined ? value[propPath] : defaultVal; const type = Reflect.getMetadata('design:type', instance, propKey); const isNestedClass = type && this.isConfigurationClass(type); if (propValue !== undefined || isNestedClass) { instance[propKey] = this.convertAndBindType(propValue, instance, propKey); } } const requiredProps = Reflect.getMetadata(decorators_1.REQUIRED_PROPS_KEY, NestedClass) || []; for (const prop of requiredProps) { if (instance[prop] === undefined || instance[prop] === null) { throw new Error(`Required configuration property '${propertyPath}.${prop}' is missing`); } } const shouldValidate = Reflect.getMetadata(decorators_1.VALIDATE_KEY, NestedClass); if (this.validateOnBind && shouldValidate) { const errors = (0, class_validator_1.validateSync)(instance); if (errors.length > 0) { const messages = this.formatValidationErrors(errors); throw new Error(`Validation failed for ${NestedClass.name} at path '${propertyPath}':\n${messages}`); } } return instance; } convertAndBindType(value, instance, propertyKey) { const type = Reflect.getMetadata('design:type', instance, propertyKey); if (!type) return value; if (type === Map) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`Cannot bind primitive value to Map<string, T> for property '${propertyKey}'`); } const map = this.mapBinder.objectToMap(value); return map; } if (type === Object && this.mapBinder.isRecordProperty(instance, propertyKey)) { return value; } if (this.isConfigurationClass(type)) { return this.bindNestedClass(value, type, propertyKey); } switch (type.name) { case 'Number': return Number(value); case 'Boolean': return value === 'true' || value === true; case 'String': return String(value); case 'Array': return Array.isArray(value) ? value : [value]; default: return value; } } deepMerge(target, source) { for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; this.deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } } async dispose() { this.configInstances.clear(); } } exports.ConfigManager = ConfigManager;