@snow-tzu/type-config
Version:
Core configuration management system with Spring Boot-like features
316 lines (315 loc) • 13.8 kB
JavaScript
"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 `);
}
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;