import { flatten } from 'flat'; import * as fs from 'fs'; import { isString, merge as _merge, set } from 'lodash'; import * as path from 'path'; import { getFsStats, isNonArrayObject } from '../lib'; import { ConfigSettingsPath, EnvConfigSettings } from '../types'; import { ConfigSettingsBuilder } from './ConfigSettingsBuilder'; import { defaultConfigSettingsBuilderOptions } from './constants'; import { ConfigSettingsBuilderOptions } from './types'; const RefToken = '$ref='; export const transformFileSettings = async ( settings: EnvConfigSettings | T, rootPath: string, options?: ConfigSettingsBuilderOptions ): Promise | T> => { return resolveRefs(settings, rootPath, options); }; const isRefValue = (val: string): boolean => { return isString(val) ? val .trim() .toLowerCase() .startsWith(RefToken) : false; }; /** * Determines whether a settings object contains $ref tokens for propety values * @param settings Settings file that may contain $ref tokens */ export const hasRef = (settings: EnvConfigSettings | T): boolean => { if (settings) { const flattened = flatten(settings); return Object.keys(flattened).some(prop => { return isRefValue(flattened[prop]); }); } return false; }; /** * Transforms properties in the object that begin with a $ref token followed by a relative path to settings file with * values to be used for the property. * @param settings Settings object that may contain $ref tokens for property values * @param rootPath Root path from which $ref paths will be resolved * @param options ConfigSettingsBuilderOptions passed to ConfigSettingsBuilder to build referenced settings files */ export const resolveRefs = async ( settings: EnvConfigSettings | T, rootPath: string, options?: ConfigSettingsBuilderOptions ): Promise | T> => { if (!isNonArrayObject(settings)) { return settings; } options = _merge({}, defaultConfigSettingsBuilderOptions, options); const stats = getFsStats(rootPath); if (!stats) { throw new Error(`Cannot resolve refs. Path does not exist: ${rootPath}`); } const rootDir = stats.isDirectory() ? rootPath : path.dirname(rootPath); const resolvedSettings = { ...(settings as object), }; const flattened = flatten(settings); for (const prop of Object.keys(flattened)) { const val = flattened[prop]; if (isString(val)) { // looking for anything starting with ${refToken} if (isRefValue(val)) { const refVal = val.trim().substr(RefToken.length); const refPath = path.resolve(rootDir, refVal); if (fs.existsSync(refPath)) { let propRootPath: string; let baseName: string | undefined; const propStats = fs.lstatSync(refPath); if (propStats.isDirectory()) { propRootPath = path.dirname(refPath); baseName = path.basename(refPath); } else { propRootPath = refPath; } const builtSettings = await ConfigSettingsBuilder.buildFromPaths(propRootPath, baseName, options); set(resolvedSettings, prop, builtSettings); } } } } return resolvedSettings as EnvConfigSettings | T; };