@nestjsplus/lib-starter
Version:
Boilerplate for building installable NestJS libraries
300 lines (299 loc) • 12.8 kB
JavaScript
"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;