diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e116e0 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Example configurations via environment variables (sse README.md | Configuration): + +# Example configuration via JSON: +NEST_SERVER_CONFIG='{ + "env": "dotenv", + "email": { + "mailjet": { + "api_key_private": 7, + "api_key_public": "EMAIL_MAILJET_API_KEY_PUBLIC" + }, + "smtp": { + "auth": { + "pass": "EMAIL_SMTP_AUTH_PASS", + "user": "EMAIL_SMTP_AUTH_USER" + } + } + }, + "jwt": { + "refresh": { + "secret": "JWT_REFRESH_SECRET" + }, + "secret": "JWT_SECRET" + } +}' + +# Example configuration via single Nest Server Config environment variables: +NSC__EMAIL__DEFAULT_SENDER__EMAIL=jon.doe@ethereal.email diff --git a/README.md b/README.md index a494553..42e07d4 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,46 @@ Project use following scripts (via `package.json`): - `npm run link:nest-server` (for `yalc add @lenne.tech/nest-server && yalc link @lenne.tech/nest-server && npm install`) - `npm run unlink:nest-server` (for `yalc remove @lenne.tech/nest-server && npm install`) +## Configuration + +The configuration of the server is done via the `src/config.env.ts` file. This file is a TypeScript file that exports +an object with the configuration values. It is automatically integrated into the `ConfigService` +(see src/core/common/services/config.service.ts). + +### Environment variables + +To protect sensitive data and to avoid committing them to the repository the `.env` file can be used. +An example `.env` file is provided in the `.env.example` file. + +There are multiple ways to manipulate or extend the configuration via environment variables: +1. Via "normal" integration of the environment variables into the `src/config.env.ts` +2. Via JSON in the `NEST_SERVER_CONFIG` environment variable +3. Via single environment variables with the prefix `NSC__` (Nest Server Config) + +#### Normal environment variables +Using `dotenv` (see https://www.dotenv.org/) environment variables can directly integrated into the +`src/config.env.ts` via `process.env`. E.g.: +```typescript +export const config = { + development: { + port: process.env.PORT || 3000, + }, +}; +``` + +#### JSON +The `NEST_SERVER_CONFIG` is the environment variable for the server configuration. +The value of `NEST_SERVER_CONFIG` must be a (multiline) JSON string that will be parsed by the server +(see config.env.ts). The keys will override the other configuration values via deep merge +(see https://lodash.com/docs/4.17.15#merge, without array merging). + +#### Single config variables +The prefix `NSC__` (**N**est **S**erver **C**onfig) can be used to set single configuration values via environment +variables. The key is the name of the configuration value in uppercase and with double underscores (`__`) instead of +dots. Single underscores are used to separate compound terms like `DEFAULT_SENDER` for `defaultSender`. +For example, the configuration value `email.defaultSender.name` can be set via the environment variable +`NSC__EMAIL_DEFAULT_SENDER_NAME`. + ## Documentation The API and developer documentation can automatically be generated. diff --git a/package-lock.json b/package-lock.json index 58a6352..081e895 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lenne.tech/nest-server", - "version": "10.7.1", + "version": "10.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lenne.tech/nest-server", - "version": "10.7.1", + "version": "10.8.0", "license": "MIT", "dependencies": { "@apollo/gateway": "2.9.3", @@ -30,6 +30,7 @@ "class-validator": "0.14.1", "compression": "1.7.5", "cookie-parser": "1.4.7", + "dotenv": "16.4.7", "ejs": "3.1.10", "graphql": "16.9.0", "graphql-query-complexity": "1.0.0", diff --git a/package.json b/package.json index b62ebf3..e7910d2 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lenne.tech/nest-server", - "version": "10.7.1", + "version": "10.8.0", "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).", "keywords": [ "node", @@ -83,6 +83,7 @@ "class-validator": "0.14.1", "compression": "1.7.5", "cookie-parser": "1.4.7", + "dotenv": "16.4.7", "ejs": "3.1.10", "graphql": "16.9.0", "graphql-query-complexity": "1.0.0", diff --git a/spectaql.yml b/spectaql.yml index 093db99..bb6239f 100644 --- a/spectaql.yml +++ b/spectaql.yml @@ -11,7 +11,7 @@ servers: info: title: lT Nest Server description: Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases). - version: 10.7.1 + version: 10.8.0 contact: name: lenne.Tech GmbH url: https://lenne.tech diff --git a/src/config.env.ts b/src/config.env.ts index 0f8f81b..c7dea7e 100644 --- a/src/config.env.ts +++ b/src/config.env.ts @@ -1,12 +1,14 @@ import { CronExpression } from '@nestjs/schedule'; +import * as dotenv from 'dotenv'; import { join } from 'path'; -import { merge } from './core/common/helpers/config.helper'; +import { getEnvironmentConfig } from './core/common/helpers/config.helper'; import { IServerOptions } from './core/common/interfaces/server-options.interface'; /** * Configuration for the different environments */ +dotenv.config(); const config: { [env: string]: IServerOptions } = { // =========================================================================== // Development environment @@ -330,47 +332,6 @@ const config: { [env: string]: IServerOptions } = { }; /** - * Environment specific config - * - * default: local + * Export config merged with other configs and environment variables as default */ -const env = process.env['NODE' + '_ENV'] || 'local'; -const envConfig = config[env] || config.local; -console.info(`Configured for: ${envConfig.env}${env !== envConfig.env ? ` (requested: ${env})` : ''}`); -// Merge with localConfig (e.g. config.json) -if (envConfig.loadLocalConfig) { - let localConfig; - if (typeof envConfig.loadLocalConfig === 'string') { - import(envConfig.loadLocalConfig) - .then((loadedConfig) => { - localConfig = loadedConfig.default || loadedConfig; - merge(envConfig, localConfig); - }) - .catch(() => { - console.info(`Configuration ${envConfig.loadLocalConfig} not found!`); - }); - } else { - // get config from src directory - import(join(__dirname, 'config.json')) - .then((loadedConfig) => { - localConfig = loadedConfig.default || loadedConfig; - merge(envConfig, localConfig); - }) - .catch(() => { - // if not found try to find in project directory - import(join(__dirname, '..', 'config.json')) - .then((loadedConfig) => { - localConfig = loadedConfig.default || loadedConfig; - merge(envConfig, localConfig); - }) - .catch(() => { - console.info('No local config.json found!'); - }); - }); - } -} - -/** - * Export envConfig as default - */ -export default envConfig; +export default getEnvironmentConfig({ config }); diff --git a/src/core/common/helpers/config.helper.ts b/src/core/common/helpers/config.helper.ts index 11fd08a..6403ac5 100644 --- a/src/core/common/helpers/config.helper.ts +++ b/src/core/common/helpers/config.helper.ts @@ -1,3 +1,6 @@ +import * as dotenv from 'dotenv'; +import { join } from 'path'; + import _ = require('lodash'); /** @@ -46,3 +49,128 @@ export function merge(obj: Record, ...sources: any[]): any { } }); } + +/** + * Get environment configuration (deeply merged into config object set via options) + * + * The configuration is extended via deep merge in the following order: + * 1. config[env] (if set) + * 2. + * + * @param options options for processing + * @param options.config config object with different environments as main keys (see config.env.ts) to merge environment configurations into (default: {}) + * @param options.defaultEnv default environment to use if no NODE_ENV is set (default: 'local') + */ +export function getEnvironmentConfig(options: { config?: Record; defaultEnv?: string }) { + const { config, defaultEnv } = { + config: {}, + defaultEnv: 'local', + ...options, + }; + + dotenv.config(); + const env = process.env['NODE' + '_ENV'] || defaultEnv; + const envConfig = config[env] || config.local || {}; + + // Merge with localConfig (e.g. config.json) + if (envConfig.loadLocalConfig) { + let localConfig: Record; + if (typeof envConfig.loadLocalConfig === 'string') { + import(envConfig.loadLocalConfig) + .then((loadedConfig) => { + localConfig = loadedConfig.default || loadedConfig; + merge(envConfig, localConfig); + }) + .catch(() => { + console.info(`Configuration ${envConfig.loadLocalConfig} not found!`); + }); + } else { + // get config from src directory + import(join(__dirname, 'config.json')) + .then((loadedConfig) => { + localConfig = loadedConfig.default || loadedConfig; + merge(envConfig, localConfig); + }) + .catch(() => { + // if not found try to find in project directory + import(join(__dirname, '..', 'config.json')) + .then((loadedConfig) => { + localConfig = loadedConfig.default || loadedConfig; + merge(envConfig, localConfig); + }) + .catch(() => { + console.info('No local config.json found!'); + }); + }); + } + } + + // .env handling via dotenv + if (process.env['NEST_SERVER_CONFIG']) { + try { + const dotEnvConfig = JSON.parse(process.env['NEST_SERVER_CONFIG']); + if (dotEnvConfig && Object.keys(dotEnvConfig).length > 0) { + merge(envConfig, dotEnvConfig); + console.info('NEST_SERVER_CONFIG used from .env'); + } + } catch (e) { + console.error('Error parsing NEST_SERVER_CONFIG from .env: ', e); + console.error( + 'Maybe the JSON is invalid? Please check the value of NEST_SERVER_CONFIG in .env file (e.g. via https://jsonlint.com/)', + ); + } + } + + // Merge with environment variables + const environmentObject = getEnvironmentObject(); + const environmentObjectKeyCount = Object.keys(environmentObject).length; + if (environmentObjectKeyCount > 0) { + merge(envConfig, environmentObject); + console.info( + `Environment object from the environment integrated into the configuration with ${environmentObjectKeyCount} keys`, + ); + } + + console.info(`Configured for: ${envConfig.env}${env !== envConfig.env ? ` (requested: ${env})` : ''}`); + return envConfig; +} + +/** + * Get environment object from environment variables + */ +export function getEnvironmentObject(options?: { prefix?: string; processEnv?: Record }) { + const config = { + prefix: 'NSC__', + processEnv: process.env, + ...options, + }; + const output = {}; + + Object.entries(config.processEnv) + .filter(([key]) => key.startsWith(config.prefix)) + .forEach(([key, value]) => { + // Remove prefix from key + const adjustedKey = key.slice(config.prefix?.length || 0); + + // Convert key to path + const path = adjustedKey.split('__').map(part => + part + .split('_') + .map((s, i) => (i === 0 ? s.toLowerCase() : s[0].toUpperCase() + s.slice(1).toLowerCase())) + .join(''), + ); + + // Set value in output object + let current = output; + for (let i = 0; i < path.length; i++) { + const segment = path[i]; + if (i === path.length - 1) { + current[segment] = value; + } else { + current = current[segment] = current[segment] || {}; + } + } + }); + + return output; +}