import { _ } from '@kitmi/utils';
import JsonConfigProvider from './JsonConfigProvider.js';
import YamlConfigProvider from './YamlConfigProvider.js';
import EnvAwareConfigProviderF from './EnvAwareConfigProviderF.js';
import defaultSyntax, { ES6_TEMPLATE_TOKEN } from './defaultSyntax.js';
export const EnvAwareJsonConfigProvider = EnvAwareConfigProviderF('.json', JsonConfigProvider);
export const EnvAwareYamlConfigProvider = EnvAwareConfigProviderF('.yaml', YamlConfigProvider);
class ConfigLoader {
/**
* Create an environment aware JSON config loader
* @param {string} configDir
* @param {string} baseName
* @param {string} envFlag
* @param {Logger} logger
* @param {function} overrider
* @param {object} postProcessors
*/
static createEnvAwareJsonLoader(configDir, baseName, envFlag, logger, overrider, postProcessors) {
return new ConfigLoader(
new EnvAwareJsonConfigProvider(configDir, baseName, envFlag, overrider),
logger,
postProcessors
);
}
/**
* Create an environment aware YAML config loader
* @param {string} configDir
* @param {string} baseName
* @param {string} envFlag
* @param {Logger} logger
* @param {function} overrider
* @param {object} postProcessors
*/
static createEnvAwareYamlLoader(configDir, baseName, envFlag, logger, overrider, postProcessors) {
return new ConfigLoader(
new EnvAwareYamlConfigProvider(configDir, baseName, envFlag, overrider),
logger,
postProcessors
);
}
/**
* The config loader
* @constructs ConfigLoader
* @extends EventEmitter
* @example
* let fileSource = new JsonConfigProvider('path/to/config.json');
* let config = new ConfigLoader(fileSource);
* await config.load_()...;
*
* let dbSource = new DbConfigProvider(config.data.dbConnection);
* config.provider = dbSource;
* await config.reload_()...;
*
* // same as: let envAwareLoader = new ConfigLoader(
* // new (EnvAwareConfigProviderF('.json', JsonConfigProvider, 'default'))('config/dir', 'app', 'production')
* // );
* let envAwareLoader = ConfigLoader.createEnvAwareJsonLoader('config/dir', 'app', 'production');
*
* // Loader will load config/dir/app.default.json first,
* // and then load config/dir/app.production.json,
* // and finally override the default.
* let cfg = await envAwareLoader.load_();
*/
constructor(configProvider, logger, postProcessors) {
/**
* The config data source provider
* @type {object}
* @public
**/
this.provider = configProvider;
/**
* The config data
* @type {object}
* @public
**/
this.data = undefined;
/**
* Whether to do string post process automatically after loading
* @type {boolean}
* @public
*/
this.autoPostProcess = true;
/**
* Logger with log(level, message, meta) function.
* @type {Logger}
* @public
*/
this.logger = logger;
/**
* Post processors
* @private
*/
this.postProcessors = postProcessors != null ? _.defaultsDeep(postProcessors, defaultSyntax) : defaultSyntax;
}
/**
* Start loading the config files and override existing
* @param {object} variables - variables
* @returns {Promise.<object>}
*/
async load_(variables) {
const oldData = this.data;
await this.reload_(variables);
if (oldData) {
this.data = _.defaults(this.data, oldData);
}
return this.data;
}
/**
* Reload config
* @returns {Promise.<object>}
*/
async reload_(variables) {
this.data = await this.provider.load_(this.logger, true);
if (this.autoPostProcess) this.postProcess(variables);
return this.data;
}
/**
* PostProcess the loaded config
* @param {object} variables - variables
*/
postProcess(variables) {
const queue = [this.data];
this._l = this.postProcessors.prefix.length;
variables = { ...variables, $this: this.data };
const interpolateElement = (coll, key, val) => {
if (typeof val === 'string') {
coll[key] = this._tryProcessStringValue(val, variables);
} else if (_.isPlainObject(val) || _.isArray(val)) {
queue.push(val);
}
};
let offset = 0;
while (queue.length > offset) {
const node = queue[offset];
if (_.isPlainObject(node)) {
_.forOwn(node, (value, key) => {
interpolateElement(node, key, value);
});
} else {
const l = node.length;
for (let i = 0; i < l; i++) {
interpolateElement(node, i, node[i]);
}
}
offset++;
}
}
_tryProcessStringValue(strVal, variables) {
if (strVal.startsWith(this.postProcessors.prefix)) {
const colonPos = strVal.indexOf(':');
if (colonPos > this._l) {
const token = strVal.substring(this._l, colonPos);
const operator = this.postProcessors.processors[token];
if (operator) {
return operator(strVal.substring(colonPos + 1), variables);
}
throw new Error('Unsupported post processor: ' + token);
}
throw new Error('Invalid post processor syntax: ' + strVal);
} else if (strVal.startsWith('${') && strVal.endsWith('}')) {
const operator = this.postProcessors.processors[ES6_TEMPLATE_TOKEN];
return operator(strVal, variables);
}
return strVal;
}
}
export default ConfigLoader;