UNPKG

@nestjsplus/lib-starter

Version:

Boilerplate for building installable NestJS libraries

300 lines (299 loc) 12.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); const common_1 = require("@nestjs/common"); const dotenv = require("dotenv"); const path = require("path"); const clc = require("cli-color"); const Joi = require("@hapi/joi"); const constants_1 = require("./constants"); const errors_1 = require("./errors"); const abstract_config_1 = require("./abstract.config"); let ConfigManager = class ConfigManager extends abstract_config_1.AbstractConfigManager { constructor() { super(...arguments); this.traceProduction = false; } loadAndValidateEnvFile() { this.processOptions(); this.resolveEnvFilePath(); let loadedConfig; try { loadedConfig = this.loadEnvFile(); } catch (error) { if (error.message === constants_1.NO_ENV_FILE_ERROR) { if (!this.options.allowMissingEnvFile) { this.handleFatalError(`Fatal error loading environment. The following file is missing: \n${error.badpath}`); } else { constants_1.dbg.cfg('> No env file found. All env vars will come from external environment, if defined.'); loadedConfig = {}; } } } const { schema, required } = this.processConfigSpec(); const { updatedConfig, missingKeyErrors } = this.cascadeRequiredVars(loadedConfig, required); this.traceProduction && constants_1.dbg.trace('> resolveMap: \n', this.resolveMap); const { error: validationErrors, value: validatedConfig } = Joi.validate(updatedConfig, schema, { abortEarly: false, allowUnknown: this.options.allowExtras, }); this.traceProduction && constants_1.dbg.cfg('> Validated result: ', validatedConfig); if (missingKeyErrors.length > 0 || validationErrors) { let validationErrorMessages = []; if (validationErrors) { validationErrorMessages = this.extractValidationErrorMessages(validationErrors); } this.processConfigErrors(missingKeyErrors, validationErrorMessages); if (this.options.onError === 'throw') { this.logger.error('Invalid configuration -- see log file and / or Exception for details'); throw new errors_1.InvalidConfigurationError(missingKeyErrors, validationErrorMessages); } else { this.handleFatalError('Invalid configuration'); } } else { this.envConfig = validatedConfig; } } processOptions() { if (!this.options.onError) { this.options.onError = 'exit'; } let environmentKey; if (this.options.envKey) { environmentKey = this.options.envKey; } else if (process.env.NODE_ENV) { environmentKey = 'NODE_ENV'; } else { if (!this.options.useFile && !this.options.defaultEnvironment) { this.handleFatalError('Fatal error. No envKey specified, and `NODE_ENV` is not defined.'); } } this.environment = process.env[environmentKey]; if (typeof this.environment === 'undefined' && this.options.hasOwnProperty('defaultEnvironment')) { this.environment = this.options.defaultEnvironment; } if (!this.options.useFile && !this.isValidEnvironment(this.environment)) { this.handleFatalError(`Bad environment key: ${this.options.envKey}`); } if (this.options.onError && !['continue', 'throw', 'exit'].includes(this.options.onError)) { this.logger.warn(`Invalid onError value ('${this.options.onError}') specified in ConfigManagerModule.register(). Using 'exit' instead.`); this.options.onError = 'exit'; } if (this.environment !== 'production' || process.env.TRACE_PRODUCTION) { this.traceProduction = true; } constants_1.dbg.cfg('> cfg options: ', this.options); constants_1.dbg.cfg(`> environment (using ${environmentKey}): ${this.environment}`); } isValidEnvironment(environment) { if (typeof environment === 'undefined') { return false; } return true; } resolveEnvFilePath() { let envRoot = ''; if (process.mainModule && process.mainModule.filename) { envRoot = path.resolve(path.dirname(process.mainModule.filename), '..'); } else { envRoot = process.cwd(); } if (!envRoot) { this.handleFatalError('Could not locate root directory'); } constants_1.dbg.cfg('> envRoot: ', envRoot); constants_1.dbg.cfg('> resolving envfile path...'); if (this.options.useFile) { constants_1.dbg.cfg('> ... from "useFile" with file:', this.options.useFile); this.envFilePath = path.resolve(envRoot, this.options.useFile); return; } if (this.options.useFunction) { constants_1.dbg.cfg(`> ... from "useFunction" with \n\trootFolder: ${envRoot}\n\tenviroment: ${this.environment}`); this.envFilePath = this.options.useFunction(envRoot, this.environment); return; } if (!this.options.useEnv) { this.handleFatalError('Invalid or missing configuration options.'); } constants_1.dbg.cfg('> ... using environment'); let envRootSubfolder = 'config'; if (typeof this.options.useEnv === 'object' && this.options.useEnv.folder) { envRootSubfolder = this.options.useEnv.folder; } const envPrefix = this.environment; const filePath = path.resolve(envRoot, envRootSubfolder, envPrefix); this.envFilePath = filePath + '.env'; } loadEnvFile() { constants_1.dbg.cfg(clc.yellow('> Parsing dotenv config file: ', this.envFilePath)); const config = dotenv.config({ path: this.envFilePath, }); if (config.error) { let errorMessage; if (config.error.code === 'ENOENT') { throw new errors_1.MissingEnvFileError(config.error.message); } else { errorMessage = `Fatal unknown error loading environment: ${config.error.message}`; this.handleFatalError(errorMessage); } } this.traceProduction && constants_1.dbg.cfg('> Parsed config: ', config.parsed); return config.parsed; } cascadeRequiredVars(loadedConfig, requiredConfig) { const updatedConfig = JSON.parse(JSON.stringify(loadedConfig)); const missingKeyErrors = []; const resolveMap = {}; Object.entries(requiredConfig).forEach(([key, value]) => { resolveMap[key] = { dotenv: updatedConfig[key] ? updatedConfig[key] : '--', env: this.procEnv[key] ? this.procEnv[key] : '--', default: requiredConfig[key].default ? requiredConfig[key].default : '--', resolvedFrom: this.procEnv[key] ? 'env' : updatedConfig[key] ? 'dotenv' : '--', isExtra: false, }; updatedConfig[key] = process.env[key]; if (requiredConfig[key].required && !updatedConfig[key]) { missingKeyErrors.push(`"${key}" is required, but missing`); } if (!updatedConfig[key]) { updatedConfig[key] = requiredConfig[key].default; resolveMap[key].resolvedFrom = 'default'; } resolveMap[key].resolvedValue = updatedConfig[key]; }); if (this.options.allowExtras) { Object.entries(updatedConfig).forEach(([key, value]) => { if (!requiredConfig[key]) { resolveMap[key] = { dotenv: updatedConfig[key] ? updatedConfig[key] : '--', env: this.procEnv[key] ? this.procEnv[key] : '--', default: '--', resolvedFrom: this.procEnv[key] ? 'env' : updatedConfig[key] ? 'dotenv' : '--', isExtra: true, }; resolveMap[key].resolvedValue = updatedConfig[key]; } }); } this.traceProduction && constants_1.dbg.cfg('> updatedConfig (after cascade): ', updatedConfig); this.resolveMap = resolveMap; return { updatedConfig, missingKeyErrors }; } provideConfigSpec(environment) { throw new errors_1.MissingOverrideError(); } processConfigSpec() { let configSpec; try { configSpec = this.provideConfigSpec(this.environment); } catch (error) { let errorMessage; if (error instanceof errors_1.MissingOverrideError) { errorMessage = 'Fatal error: required method provideConfigSpec missing from class extending ConfigService'; } else { errorMessage = `Unhandled error from overridden provideConfigSpec: ${error.message}`; } this.handleFatalError(errorMessage); } if (!configSpec) { throw new Error('no schema'); } const schema = {}; const required = {}; Object.keys(configSpec).map(key => { if (!configSpec[key].validate) { this.handleFatalError(`Missing required validate field in configSchema for key: ${key}`); } schema[key] = configSpec[key].validate; if (configSpec[key].required) { required[key] = { required: true, }; } else { if (!configSpec[key].default) { this.handleFatalError(`Missing required default field in configSchema for key: ${key}`); } required[key] = { required: false, default: configSpec[key].default, }; } }); constants_1.dbg.cfg(`> Loaded ${Object.keys(configSpec).length} configuration spec keys.`); return { schema, required }; } processConfigErrors(missingKeys, validationErrors) { if (missingKeys.length > 0) { this.logger.error(`Configuration error. The following required environment variables are missing: \n--> ${missingKeys.join('\n--> ')}`); } if (validationErrors.length > 0) { this.logger.error(`Configuration error. The following environment variables failed validation: \n--> ${validationErrors.join('\n--> ')}`); } } extractValidationErrorMessages(errors) { const errorMessages = []; for (const detail of errors.details) { errorMessages.push(detail.message); } return errorMessages; } handleFatalError(message) { switch (this.options.onError) { case 'throw': this.logger.error(`${message} -- See exception for details`); throw new Error(message); case 'continue': this.logger.error(`An error was encountered in configuration, but 'continue' was specified.`); this.logger.error('This may cause unpredictable results!'); break; default: case 'exit': this.logger.error(`${message} -- App will now exit`); process.exit(0); break; } } get(key) { return this.envConfig[key]; } trace() { return this.resolveMap; } }; ConfigManager = __decorate([ common_1.Injectable() ], ConfigManager); exports.ConfigManager = ConfigManager;