Skip to content

Commit

Permalink
Merge pull request #382 from lenneTech/develop
Browse files Browse the repository at this point in the history
Release 10.8.0
  • Loading branch information
kaihaase authored Dec 17, 2024
2 parents 16634f6 + d993315 commit 3921bd6
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 48 deletions.
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion spectaql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 5 additions & 44 deletions src/config.env.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 });
128 changes: 128 additions & 0 deletions src/core/common/helpers/config.helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as dotenv from 'dotenv';
import { join } from 'path';

import _ = require('lodash');

/**
Expand Down Expand Up @@ -46,3 +49,128 @@ export function merge(obj: Record<string, any>, ...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<string, any>; 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<string, any>;
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<string, number | string> }) {
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;
}

0 comments on commit 3921bd6

Please sign in to comment.