import { flatten } from 'flat'; import * as fs from 'fs'; import { get, isEmpty, isFunction, isObject, isString, set } from 'lodash'; import { merge } from '../lib'; import { defaultConfigSettingsBuilderOptions, defaultMergeOptions } from './constants'; import { EnvConfigSettingsFileLoader } from './EnvConfigSettingsFileLoader'; import { isBuildConfigSettingsBuilderPathOption, isEnvConfigSettings } from './lib'; import { ConfigSettingsBuilderOptions } from './types'; import { ConfigSettingsArg, ConfigSettingsPath, EnvConfigSettings } from '../types'; export class ConfigSettingsBuilder { public get NODE_ENV(): string { return process.env.NODE_ENV; } public static buildFromPaths = async ( rootPaths: string | string[], baseName: string, options?: ConfigSettingsBuilderOptions ): Promise => { const build = async (_rootConfigPath: string, _baseName: string): Promise => { if (fs.existsSync(_rootConfigPath)) { const pathOptions: ConfigSettingsPath = { baseName: _baseName, rootPath: _rootConfigPath, }; const builder = new ConfigSettingsBuilder(pathOptions, options); return builder.build(); } else { // throw new Error(`buildPackageConfig failed. Root path '${_rootConfigPath}' not found`); // TBD: quietly return empty if path not found? } // tslint:disable-next-line: no-object-literal-type-assertion return {} as T; }; const paths: string[] = Array.isArray(rootPaths) ? rootPaths : [rootPaths]; const settings = await Promise.all(paths.map(p => build(p, baseName))); options = ConfigSettingsBuilder.buildOptions(options); const merged = merge(settings, options.mergeOptions); return merged; }; public static build = async ( args: ConfigSettingsArg | Array>, options?: ConfigSettingsBuilderOptions ): Promise => { const builder = new ConfigSettingsBuilder(args, options); return builder.build(); }; private static buildOptions = (options?: ConfigSettingsBuilderOptions): ConfigSettingsBuilderOptions => { return merge([defaultConfigSettingsBuilderOptions, options]); }; private _rawArgs: Array>; private _normalizedArgs: Array>; private _options: ConfigSettingsBuilderOptions; private _settings: TSettings; constructor( args: ConfigSettingsArg | Array>, options?: ConfigSettingsBuilderOptions ) { this._rawArgs = Array.isArray(args) ? args : [args]; this._options = ConfigSettingsBuilder.buildOptions(options); } public build = async (): Promise => { if (!this._settings) { try { this._normalizedArgs = await this.normalizeArgs(this._rawArgs); const merged = this._normalizedArgs.reduce((acc, cur) => { const base = cur.base || {}; const nodeEnvRawSettings = (cur.nodeEnv && cur.nodeEnv[this.NODE_ENV]) || {}; return this.merge(acc, base, nodeEnvRawSettings); }, {}); // await this.loadSsmToEnvironmentVariables(); this._settings = this.mergeEnvironmentOverrides(merged) as TSettings; } catch (e) { throw new Error(`ConfigSettingsBuilder build failed: ${e.message}`); } } return this._settings; }; private loadFromPathOptions = async (options: ConfigSettingsPath) => { const loader = new EnvConfigSettingsFileLoader(options.rootPath, options.baseName, this._options); return loader.load(); }; private normalizeArgs = async ( args: Array> ): Promise>> => { const normalized: Array> = []; for (let arg of args) { if (isFunction(arg)) { arg = await Promise.resolve((arg as (() => Promise>))()); } if (isBuildConfigSettingsBuilderPathOption(arg)) { const pathOptions = arg as ConfigSettingsPath; const settings = await this.loadFromPathOptions(pathOptions); if (settings) { normalized.push(settings); } } else if (isEnvConfigSettings(arg as EnvConfigSettings | TSettings)) { normalized.push(arg as EnvConfigSettings); } else if (isObject(arg)) { normalized.push({ base: arg as TSettings, }); } else { throw new Error('Unexpected PackageConfigBuilder argument'); } } return normalized; }; // tslint:disable-next-line: no-any private mergeEnvironmentOverrides = (settings: any): any => { // merge multiple sets to one const rawSettings = this._normalizedArgs; const mergedEnvVars = rawSettings.reduce((acc, cur) => { if (cur.envVars) { acc = this.merge(acc, cur.envVars); } return acc; }, {}); // map env vals to settings if (!isEmpty(mergedEnvVars)) { const flattened = flatten(mergedEnvVars); for (const prop of Object.keys(flattened)) { const envVarName = flattened[prop]; if (isString(envVarName)) { const envVal = process.env[envVarName]; if (envVal) { // const curVal = get(settings, prop) set(settings, prop, envVal); } else { if (!get(settings, prop)) { throw new Error( `Environment Variable '${envVarName}' is not set for '${prop}' and no value defined in Settings` ); } } } } } return settings; }; private merge = (...settings: T[]) => { return merge(settings, this._options.mergeOptions); }; }