.`, where the hash is the SHA of the commit being reverted.
196 |
197 | ### Type
198 | Must be one of the following:
199 |
200 | * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
201 | * **chore**: Updating tasks etc; no production code change
202 | * **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
203 | * **docs**: Documentation only changes
204 | * **feat**: A new feature
205 | * **fix**: A bug fix
206 | * **perf**: A code change that improves performance
207 | * **refactor**: A code change that neither fixes a bug nor adds a feature
208 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
209 | * **test**: Adding missing tests or correcting existing tests
210 |
211 |
212 | ### Subject
213 | The subject contains succinct description of the change:
214 |
215 | * use the imperative, present tense: "change" not "changed" nor "changes"
216 | * don't capitalize first letter
217 | * no dot (.) at the end
218 |
219 | ### Body
220 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
221 | The body should include the motivation for the change and contrast this with previous behavior.
222 |
223 | ### Footer
224 | The footer should contain any information about **Breaking Changes** and is also the place to
225 | reference GitHub issues that this commit **Closes**.
226 |
227 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
228 |
229 | A detailed explanation can be found in this [document][commit-message-format].
230 |
231 |
239 |
240 |
241 |
242 |
243 | [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
244 | [corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html
245 | [dev-doc]: https://github.com/nestjs/nest/blob/master/docs/DEVELOPER.md
246 | [github]: https://github.com/nestjs/nest
247 | [gitter]: https://gitter.im/nestjs/nest
248 | [individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html
249 | [js-style-guide]: https://google.github.io/styleguide/jsguide.html
250 | [jsfiddle]: http://jsfiddle.net
251 | [plunker]: http://plnkr.co/edit
252 | [runnable]: http://runnable.com
253 |
254 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2022 Kamil Mysliwiec
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
6 | [travis-url]: https://travis-ci.org/nestjs/nest
7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
8 | [linux-url]: https://travis-ci.org/nestjs/nest
9 |
10 | A progressive Node.js framework for building efficient and scalable server-side applications.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 | ## Description
25 |
26 | Configuration module for [Nest](https://github.com/nestjs/nest) based on the [dotenv](https://github.com/motdotla/dotenv) (to load process environment variables) package.
27 |
28 | ## Installation
29 |
30 | ```bash
31 | $ npm i --save @nestjs/config
32 | ```
33 |
34 | ## Quick Start
35 |
36 | [Overview & Tutorial](https://docs.nestjs.com/techniques/configuration)
37 |
38 | ## Support
39 |
40 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
41 |
42 | ## Stay in touch
43 |
44 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
45 | - Website - [https://nestjs.com](https://nestjs.com/)
46 | - Twitter - [@nestframework](https://twitter.com/nestframework)
47 |
48 | ## License
49 |
50 | Nest is [MIT licensed](LICENSE).
51 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import eslint from '@eslint/js';
3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4 | import globals from 'globals';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | {
9 | ignores: ['tests/**'],
10 | },
11 | eslint.configs.recommended,
12 | ...tseslint.configs.recommendedTypeChecked,
13 | eslintPluginPrettierRecommended,
14 | {
15 | languageOptions: {
16 | globals: {
17 | ...globals.node,
18 | ...globals.jest,
19 | },
20 | ecmaVersion: 5,
21 | sourceType: 'module',
22 | parserOptions: {
23 | projectService: true,
24 | tsconfigRootDir: import.meta.dirname,
25 | },
26 | },
27 | },
28 | {
29 | rules: {
30 | '@typescript-eslint/no-explicit-any': 'off',
31 | '@typescript-eslint/no-unsafe-assignment': 'off',
32 | '@typescript-eslint/no-unsafe-call': 'off',
33 | '@typescript-eslint/no-unsafe-member-access': 'off',
34 | '@typescript-eslint/no-unsafe-function-type': 'off',
35 | '@typescript-eslint/no-unsafe-argument': 'off',
36 | '@typescript-eslint/no-unsafe-return': 'off',
37 | '@typescript-eslint/no-base-to-string': 'warn',
38 | },
39 | },
40 | );
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | function __export(m) {
3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4 | }
5 | exports.__esModule = true;
6 | __export(require("./dist"));
7 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/lib/conditional.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DynamicModule,
3 | ForwardReference,
4 | Logger,
5 | ModuleMetadata,
6 | Type,
7 | } from '@nestjs/common';
8 | import { ConfigModule } from './config.module';
9 |
10 | /**
11 | * Same logic as in `@nestjs/core` package.
12 | * @param instance The instance which should get the name from
13 | * @returns The name of an instance or `undefined`
14 | */
15 | const getInstanceName = (instance: unknown): string | undefined => {
16 | if ((instance as ForwardReference)?.forwardRef) {
17 | return (instance as ForwardReference).forwardRef()?.name;
18 | }
19 |
20 | if ((instance as DynamicModule).module) {
21 | return (instance as DynamicModule).module?.name;
22 | }
23 |
24 | return (instance as Type).name;
25 | };
26 |
27 | /**
28 | * @publicApi
29 | */
30 | export class ConditionalModule {
31 | /**
32 | * @publicApi
33 | */
34 | static async registerWhen(
35 | module: Required['imports'][number],
36 | condition: string | ((env: NodeJS.ProcessEnv) => boolean),
37 | options?: { timeout?: number; debug?: boolean },
38 | ) {
39 | const { timeout = 5000, debug = true } = options ?? {};
40 | const moduleName = getInstanceName(module) || module.toString();
41 |
42 | const timer = setTimeout(() => {
43 | throw new Error(
44 | `Nest was not able to resolve the config variables within ${timeout} milliseconds. Bause of this, the ConditionalModule was not able to determine if ${moduleName} should be registered or not`,
45 | );
46 | }, timeout);
47 | timer.unref();
48 |
49 | const returnModule: Required<
50 | Pick
51 | > = { module: ConditionalModule, imports: [], exports: [] };
52 | if (typeof condition === 'string') {
53 | const key = condition;
54 | condition = env => {
55 | return env[key]?.toLowerCase() !== 'false';
56 | };
57 | }
58 | await ConfigModule.envVariablesLoaded;
59 | clearTimeout(timer);
60 | const evaluation = condition(process.env);
61 | if (evaluation) {
62 | returnModule.imports.push(module);
63 | returnModule.exports.push(module as Exclude>);
64 | } else {
65 | if (debug) {
66 | Logger.debug(
67 | `${condition.toString()} evaluated to false. Skipping the registration of ${moduleName}`,
68 | ConditionalModule.name,
69 | );
70 | }
71 | }
72 | return returnModule;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/config-host.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import {
3 | CONFIGURATION_SERVICE_TOKEN,
4 | CONFIGURATION_TOKEN,
5 | } from './config.constants';
6 | import { ConfigService } from './config.service';
7 |
8 | /**
9 | * @publicApi
10 | */
11 | @Global()
12 | @Module({
13 | providers: [
14 | {
15 | provide: CONFIGURATION_TOKEN,
16 | useFactory: () => ({}),
17 | },
18 | {
19 | provide: CONFIGURATION_SERVICE_TOKEN,
20 | useClass: ConfigService,
21 | },
22 | ],
23 | exports: [CONFIGURATION_TOKEN, CONFIGURATION_SERVICE_TOKEN],
24 | })
25 | export class ConfigHostModule {}
26 |
--------------------------------------------------------------------------------
/lib/config.constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Injection tokens
3 | */
4 | export const CONFIGURATION_SERVICE_TOKEN = Symbol('CONFIG_SERVICE');
5 | export const CONFIGURATION_TOKEN = 'CONFIGURATION_TOKEN';
6 | export const CONFIGURATION_LOADER = 'CONFIGURATION_LOADER';
7 | export const VALIDATED_ENV_LOADER = 'VALIDATED_ENV_LOADER';
8 |
9 | export const PARTIAL_CONFIGURATION_KEY = 'PARTIAL_CONFIGURATION_KEY';
10 | export const PARTIAL_CONFIGURATION_PROPNAME = 'KEY';
11 | export const VALIDATED_ENV_PROPNAME = '_PROCESS_ENV_VALIDATED';
12 | export const AS_PROVIDER_METHOD_KEY = 'asProvider';
13 |
--------------------------------------------------------------------------------
/lib/config.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module } from '@nestjs/common';
2 | import { FactoryProvider } from '@nestjs/common/interfaces';
3 | import { isObject } from '@nestjs/common/utils/shared.utils';
4 | import * as dotenv from 'dotenv';
5 | import { DotenvExpandOptions, expand } from 'dotenv-expand';
6 | import * as fs from 'fs';
7 | import { resolve } from 'path';
8 | import { ConfigHostModule } from './config-host.module';
9 | import {
10 | CONFIGURATION_LOADER,
11 | CONFIGURATION_SERVICE_TOKEN,
12 | CONFIGURATION_TOKEN,
13 | VALIDATED_ENV_LOADER,
14 | VALIDATED_ENV_PROPNAME,
15 | } from './config.constants';
16 | import { ConfigService } from './config.service';
17 | import { ConfigFactory, ConfigModuleOptions } from './interfaces';
18 | import { ConfigFactoryKeyHost } from './utils';
19 | import { createConfigProvider } from './utils/create-config-factory.util';
20 | import { getRegistrationToken } from './utils/get-registration-token.util';
21 | import { mergeConfigObject } from './utils/merge-configs.util';
22 |
23 | /**
24 | * @publicApi
25 | */
26 | @Module({
27 | imports: [ConfigHostModule],
28 | providers: [
29 | {
30 | provide: ConfigService,
31 | useExisting: CONFIGURATION_SERVICE_TOKEN,
32 | },
33 | ],
34 | exports: [ConfigHostModule, ConfigService],
35 | })
36 | export class ConfigModule {
37 | /**
38 | * This promise resolves when "dotenv" completes loading environment variables.
39 | * When "ignoreEnvFile" is set to true, then it will resolve immediately after the
40 | * "ConfigModule#forRoot" method is called.
41 | */
42 | public static get envVariablesLoaded() {
43 | return this._envVariablesLoaded;
44 | }
45 |
46 | private static environmentVariablesLoadedSignal: () => void;
47 | private static readonly _envVariablesLoaded = new Promise(
48 | resolve => (ConfigModule.environmentVariablesLoadedSignal = resolve),
49 | );
50 |
51 | /**
52 | * Loads environment variables based on the "ignoreEnvFile" flag and "envFilePath" value.
53 | * Additionally, registers custom configurations globally.
54 | * @param options
55 | */
56 | static async forRoot>(
57 | options: ConfigModuleOptions = {},
58 | ): Promise {
59 | const envFilePaths = Array.isArray(options.envFilePath)
60 | ? options.envFilePath
61 | : [options.envFilePath || resolve(process.cwd(), '.env')];
62 |
63 | let validatedEnvConfig: Record | undefined = undefined;
64 | let config = options.ignoreEnvFile
65 | ? {}
66 | : this.loadEnvFile(envFilePaths, options);
67 |
68 | if (!options.ignoreEnvVars && options.validatePredefined !== false) {
69 | config = {
70 | ...config,
71 | ...process.env,
72 | };
73 | }
74 |
75 | if (options.validate) {
76 | const validatedConfig = options.validate(config);
77 | validatedEnvConfig = validatedConfig;
78 | this.assignVariablesToProcess(validatedConfig);
79 | } else if (options.validationSchema) {
80 | const validationOptions = this.getSchemaValidationOptions(options);
81 | const { error, value: validatedConfig } =
82 | options.validationSchema.validate(config, validationOptions);
83 |
84 | if (error) {
85 | throw new Error(`Config validation error: ${error.message}`);
86 | }
87 | validatedEnvConfig = validatedConfig;
88 | this.assignVariablesToProcess(validatedConfig);
89 | } else {
90 | this.assignVariablesToProcess(config);
91 | }
92 |
93 | const isConfigToLoad = options.load && options.load.length;
94 | const configFactory = await Promise.all(options.load || []);
95 | const providers = configFactory
96 | .map(factory =>
97 | createConfigProvider(factory as ConfigFactory & ConfigFactoryKeyHost),
98 | )
99 | .filter(item => item);
100 |
101 | const configProviderTokens = providers.map(item => item.provide);
102 | const configServiceProvider = {
103 | provide: ConfigService,
104 | useFactory: (configService: ConfigService) => {
105 | const untypedConfigService = configService as any;
106 | if (options.cache) {
107 | untypedConfigService.isCacheEnabled = true;
108 | }
109 | if (options.skipProcessEnv) {
110 | untypedConfigService.skipProcessEnv = true;
111 | }
112 |
113 | configService.setEnvFilePaths(envFilePaths);
114 | return configService;
115 | },
116 | inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
117 | };
118 | providers.push(configServiceProvider);
119 |
120 | if (validatedEnvConfig) {
121 | const validatedEnvConfigLoader = {
122 | provide: VALIDATED_ENV_LOADER,
123 | useFactory: (host: Record) => {
124 | host[VALIDATED_ENV_PROPNAME] = validatedEnvConfig;
125 | },
126 | inject: [CONFIGURATION_TOKEN],
127 | };
128 | providers.push(validatedEnvConfigLoader);
129 | }
130 |
131 | this.environmentVariablesLoadedSignal();
132 |
133 | return {
134 | module: ConfigModule,
135 | global: options.isGlobal,
136 | providers: isConfigToLoad
137 | ? [
138 | ...providers,
139 | {
140 | provide: CONFIGURATION_LOADER,
141 | useFactory: (
142 | host: Record,
143 | ...configurations: Record[]
144 | ) => {
145 | configurations.forEach((item, index) =>
146 | this.mergePartial(host, item, providers[index]),
147 | );
148 | },
149 | inject: [CONFIGURATION_TOKEN, ...configProviderTokens],
150 | },
151 | ]
152 | : providers,
153 | exports: [ConfigService, ...configProviderTokens],
154 | };
155 | }
156 |
157 | /**
158 | * Registers configuration object (partial registration).
159 | * @param config
160 | */
161 | static forFeature(config: ConfigFactory): DynamicModule {
162 | const configProvider = createConfigProvider(
163 | config as ConfigFactory & ConfigFactoryKeyHost,
164 | );
165 | const serviceProvider = {
166 | provide: ConfigService,
167 | useFactory: (configService: ConfigService) => configService,
168 | inject: [CONFIGURATION_SERVICE_TOKEN, configProvider.provide],
169 | };
170 |
171 | return {
172 | module: ConfigModule,
173 | providers: [
174 | configProvider,
175 | serviceProvider,
176 | {
177 | provide: CONFIGURATION_LOADER,
178 | useFactory: (
179 | host: Record,
180 | partialConfig: Record,
181 | ) => {
182 | this.mergePartial(host, partialConfig, configProvider);
183 | },
184 | inject: [CONFIGURATION_TOKEN, configProvider.provide],
185 | },
186 | ],
187 | exports: [ConfigService, configProvider.provide],
188 | };
189 | }
190 |
191 | private static loadEnvFile(
192 | envFilePaths: string[],
193 | options: ConfigModuleOptions,
194 | ): Record {
195 | let config: ReturnType = {};
196 | for (const envFilePath of envFilePaths) {
197 | if (fs.existsSync(envFilePath)) {
198 | config = Object.assign(
199 | dotenv.parse(fs.readFileSync(envFilePath)),
200 | config,
201 | );
202 | if (options.expandVariables) {
203 | const expandOptions: DotenvExpandOptions =
204 | typeof options.expandVariables === 'object'
205 | ? options.expandVariables
206 | : {};
207 | config =
208 | expand({ ...expandOptions, parsed: config }).parsed || config;
209 | }
210 | }
211 | }
212 | return config;
213 | }
214 |
215 | private static assignVariablesToProcess(
216 | config: Record,
217 | ): void {
218 | if (!isObject(config)) {
219 | return;
220 | }
221 | const keys = Object.keys(config).filter(key => !(key in process.env));
222 | keys.forEach(key => {
223 | const value = config[key];
224 | if (typeof value === 'string') {
225 | process.env[key] = value;
226 | } else if (typeof value === 'boolean' || typeof value === 'number') {
227 | process.env[key] = `${value}`;
228 | }
229 | });
230 | }
231 |
232 | private static mergePartial(
233 | host: Record,
234 | item: Record,
235 | provider: FactoryProvider,
236 | ): void {
237 | const factoryRef = provider.useFactory;
238 | const token = getRegistrationToken(factoryRef);
239 | mergeConfigObject(host, item, token);
240 | }
241 |
242 | private static getSchemaValidationOptions(
243 | options: ConfigModuleOptions,
244 | ): Record {
245 | if (options.validationOptions) {
246 | if (typeof options.validationOptions.allowUnknown === 'undefined') {
247 | options.validationOptions.allowUnknown = true;
248 | }
249 | return options.validationOptions;
250 | }
251 | return {
252 | abortEarly: false,
253 | allowUnknown: true,
254 | };
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/lib/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Optional } from '@nestjs/common';
2 | import { isUndefined } from '@nestjs/common/utils/shared.utils';
3 | import * as dotenv from 'dotenv';
4 | import fs from 'fs';
5 | import get from 'lodash/get';
6 | import has from 'lodash/has';
7 | import set from 'lodash/set';
8 | import { Subject } from 'rxjs';
9 | import {
10 | CONFIGURATION_TOKEN,
11 | VALIDATED_ENV_PROPNAME,
12 | } from './config.constants';
13 | import { ConfigChangeEvent } from './interfaces/config-change-event.interface';
14 | import { NoInferType, Path, PathValue } from './types';
15 |
16 | /**
17 | * `ValidatedResult
18 | *
19 | * If `WasValidated` is `true`, return `T`.
20 | * Otherwise, constructs the type `T` with `undefined`.
21 | */
22 | type ValidatedResult<
23 | WasValidated extends boolean,
24 | T,
25 | > = WasValidated extends true ? T : T | undefined;
26 |
27 | /**
28 | * @publicApi
29 | */
30 | export interface ConfigGetOptions {
31 | /**
32 | * If present, "get" method will try to automatically
33 | * infer a type of property based on the type argument
34 | * specified at the "ConfigService" class-level (example: ConfigService).
35 | */
36 | infer: true;
37 | }
38 |
39 | type KeyOf = keyof T extends never ? string | symbol : keyof T;
40 |
41 | /**
42 | * @publicApi
43 | */
44 | @Injectable()
45 | export class ConfigService<
46 | K = Record,
47 | WasValidated extends boolean = false,
48 | > {
49 | private set isCacheEnabled(value: boolean) {
50 | this._isCacheEnabled = value;
51 | }
52 |
53 | private get isCacheEnabled(): boolean {
54 | return this._isCacheEnabled;
55 | }
56 |
57 | private set skipProcessEnv(value: boolean) {
58 | this._skipProcessEnv = value;
59 | }
60 |
61 | private get skipProcessEnv(): boolean {
62 | return this._skipProcessEnv;
63 | }
64 |
65 | private readonly cache: Partial = {} as any;
66 | private readonly _changes$ = new Subject();
67 | private _skipProcessEnv = false;
68 | private _isCacheEnabled = false;
69 | private envFilePaths: string[] = [];
70 |
71 | constructor(
72 | @Optional()
73 | @Inject(CONFIGURATION_TOKEN)
74 | private readonly internalConfig: Record = {},
75 | ) {}
76 |
77 | /**
78 | * Returns a stream of configuration changes.
79 | * Each event contains the attribute path, the old value and the new value.
80 | */
81 | get changes$() {
82 | return this._changes$.asObservable();
83 | }
84 |
85 | /**
86 | * Get a configuration value (either custom configuration or process environment variable)
87 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
88 | * @param propertyPath
89 | */
90 | get(propertyPath: KeyOf): ValidatedResult;
91 | /**
92 | * Get a configuration value (either custom configuration or process environment variable)
93 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
94 | * @param propertyPath
95 | * @param options
96 | */
97 | get = any, R = PathValue>(
98 | propertyPath: P,
99 | options: ConfigGetOptions,
100 | ): ValidatedResult;
101 | /**
102 | * Get a configuration value (either custom configuration or process environment variable)
103 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
104 | * It returns a default value if the key does not exist.
105 | * @param propertyPath
106 | * @param defaultValue
107 | */
108 | get(propertyPath: KeyOf, defaultValue: NoInferType): T;
109 | /**
110 | * Get a configuration value (either custom configuration or process environment variable)
111 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
112 | * It returns a default value if the key does not exist.
113 | * @param propertyPath
114 | * @param defaultValue
115 | * @param options
116 | */
117 | get = any, R = PathValue>(
118 | propertyPath: P,
119 | defaultValue: NoInferType,
120 | options: ConfigGetOptions,
121 | ): Exclude;
122 | /**
123 | * Get a configuration value (either custom configuration or process environment variable)
124 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
125 | * It returns a default value if the key does not exist.
126 | * @param propertyPath
127 | * @param defaultValueOrOptions
128 | */
129 | get(
130 | propertyPath: KeyOf,
131 | defaultValueOrOptions?: T | ConfigGetOptions,
132 | options?: ConfigGetOptions,
133 | ): T | undefined {
134 | const internalValue = this.getFromInternalConfig(propertyPath);
135 | if (!isUndefined(internalValue)) {
136 | return internalValue;
137 | }
138 |
139 | const validatedEnvValue = this.getFromValidatedEnv(propertyPath);
140 | if (!isUndefined(validatedEnvValue)) {
141 | return validatedEnvValue;
142 | }
143 | const defaultValue =
144 | this.isGetOptionsObject(defaultValueOrOptions as Record) &&
145 | !options
146 | ? undefined
147 | : defaultValueOrOptions;
148 |
149 | if (!this._skipProcessEnv) {
150 | const processEnvValue = this.getFromProcessEnv(
151 | propertyPath,
152 | defaultValue,
153 | );
154 | if (!isUndefined(processEnvValue)) {
155 | return processEnvValue;
156 | }
157 | }
158 |
159 | return defaultValue as T;
160 | }
161 |
162 | /**
163 | * Get a configuration value (either custom configuration or process environment variable)
164 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
165 | * @param propertyPath
166 | */
167 | getOrThrow(propertyPath: KeyOf): Exclude;
168 | /**
169 | * Get a configuration value (either custom configuration or process environment variable)
170 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
171 | * @param propertyPath
172 | * @param options
173 | */
174 | getOrThrow = any, R = PathValue>(
175 | propertyPath: P,
176 | options: ConfigGetOptions,
177 | ): Exclude;
178 | /**
179 | * Get a configuration value (either custom configuration or process environment variable)
180 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
181 | * It returns a default value if the key does not exist.
182 | * If the default value is undefined an exception will be thrown.
183 | * @param propertyPath
184 | * @param defaultValue
185 | */
186 | getOrThrow(
187 | propertyPath: KeyOf,
188 | defaultValue: NoInferType,
189 | ): Exclude;
190 | /**
191 | * Get a configuration value (either custom configuration or process environment variable)
192 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
193 | * It returns a default value if the key does not exist.
194 | * If the default value is undefined an exception will be thrown.
195 | * @param propertyPath
196 | * @param defaultValue
197 | * @param options
198 | */
199 | getOrThrow = any, R = PathValue>(
200 | propertyPath: P,
201 | defaultValue: NoInferType,
202 | options: ConfigGetOptions,
203 | ): Exclude;
204 | /**
205 | * Get a configuration value (either custom configuration or process environment variable)
206 | * based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
207 | * It returns a default value if the key does not exist.
208 | * If the default value is undefined an exception will be thrown.
209 | * @param propertyPath
210 | * @param defaultValueOrOptions
211 | */
212 | getOrThrow(
213 | propertyPath: KeyOf,
214 | defaultValueOrOptions?: T | ConfigGetOptions,
215 | options?: ConfigGetOptions,
216 | ): Exclude {
217 | // @ts-expect-error Bypass method overloads
218 | const value = this.get(propertyPath, defaultValueOrOptions, options) as
219 | | T
220 | | undefined;
221 |
222 | if (isUndefined(value)) {
223 | throw new TypeError(
224 | `Configuration key "${propertyPath.toString()}" does not exist`,
225 | );
226 | }
227 |
228 | return value as Exclude;
229 | }
230 | /**
231 | * Sets a configuration value based on property path.
232 | * @param propertyPath
233 | * @param value
234 | */
235 | set(propertyPath: KeyOf, value: T): void {
236 | const oldValue = this.get(propertyPath);
237 | set(this.internalConfig, propertyPath, value);
238 |
239 | if (typeof propertyPath === 'string') {
240 | process.env[propertyPath] = String(value);
241 | this.updateInterpolatedEnv(propertyPath, String(value));
242 | }
243 |
244 | if (this.isCacheEnabled) {
245 | this.setInCacheIfDefined(propertyPath, value);
246 | }
247 |
248 | this._changes$.next({
249 | path: propertyPath as string,
250 | oldValue,
251 | newValue: value,
252 | });
253 | }
254 | /**
255 | * Sets env file paths from `config.module.ts` to parse.
256 | * @param paths
257 | */
258 | setEnvFilePaths(paths: string[]): void {
259 | this.envFilePaths = paths;
260 | }
261 |
262 | private getFromCache(
263 | propertyPath: KeyOf,
264 | defaultValue?: T,
265 | ): T | undefined {
266 | const cachedValue = get(this.cache, propertyPath);
267 | return isUndefined(cachedValue)
268 | ? defaultValue
269 | : (cachedValue as unknown as T);
270 | }
271 |
272 | private getFromValidatedEnv(propertyPath: KeyOf): T | undefined {
273 | const validatedEnvValue = get(
274 | this.internalConfig[VALIDATED_ENV_PROPNAME],
275 | propertyPath,
276 | );
277 | return validatedEnvValue as unknown as T;
278 | }
279 |
280 | private getFromProcessEnv(
281 | propertyPath: KeyOf,
282 | defaultValue: any,
283 | ): T | undefined {
284 | if (
285 | this.isCacheEnabled &&
286 | has(this.cache as Record, propertyPath)
287 | ) {
288 | const cachedValue = this.getFromCache(propertyPath, defaultValue);
289 | return !isUndefined(cachedValue) ? cachedValue : defaultValue;
290 | }
291 | const processValue = get(process.env, propertyPath);
292 | this.setInCacheIfDefined(propertyPath, processValue);
293 |
294 | return processValue as unknown as T;
295 | }
296 |
297 | private getFromInternalConfig(
298 | propertyPath: KeyOf,
299 | ): T | undefined {
300 | const internalValue = get(this.internalConfig, propertyPath);
301 | return internalValue;
302 | }
303 |
304 | private setInCacheIfDefined(propertyPath: KeyOf, value: any): void {
305 | if (typeof value === 'undefined') {
306 | return;
307 | }
308 | set(this.cache as Record, propertyPath, value);
309 | }
310 |
311 | private isGetOptionsObject(
312 | options: Record | undefined,
313 | ): options is ConfigGetOptions {
314 | return options && options?.infer && Object.keys(options).length === 1;
315 | }
316 |
317 | private updateInterpolatedEnv(propertyPath: string, value: string): void {
318 | let config: ReturnType = {};
319 | for (const envFilePath of this.envFilePaths) {
320 | if (fs.existsSync(envFilePath)) {
321 | config = Object.assign(
322 | dotenv.parse(fs.readFileSync(envFilePath)),
323 | config,
324 | );
325 | }
326 | }
327 |
328 | const regex = new RegExp(`\\$\\{?${propertyPath}\\}?`, 'g');
329 | for (const [k, v] of Object.entries(config)) {
330 | if (regex.test(v)) {
331 | process.env[k] = v.replace(regex, value);
332 | }
333 | }
334 | }
335 | }
336 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './conditional.module';
2 | export * from './config.module';
3 | export * from './config.service';
4 | export * from './types';
5 | export * from './utils';
6 | export * from './interfaces';
7 |
--------------------------------------------------------------------------------
/lib/interfaces/config-change-event.interface.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Represents a change in the configuration object.
3 | * Dispatched when one updates the configuration object through the `ConfigService#set` method.
4 | * @publicApi
5 | */
6 | export interface ConfigChangeEvent {
7 | path: string;
8 | oldValue: OldValue;
9 | newValue: NewValue;
10 | }
11 |
--------------------------------------------------------------------------------
/lib/interfaces/config-factory.interface.ts:
--------------------------------------------------------------------------------
1 | import { ConfigObject } from '../types';
2 |
3 | type ConfigFactoryReturnValue = T | Promise;
4 |
5 | export type ConfigFactory =
6 | () => ConfigFactoryReturnValue;
7 |
--------------------------------------------------------------------------------
/lib/interfaces/config-module-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { DotenvExpandOptions } from 'dotenv-expand';
2 | import { ConfigFactory } from './config-factory.interface';
3 |
4 | /**
5 | * @publicApi
6 | */
7 | export interface ConfigModuleOptions<
8 | ValidationOptions extends Record = Record,
9 | > {
10 | /**
11 | * If "true", values from the process.env object will be cached in the memory.
12 | * This improves the overall application performance.
13 | * See: https://github.com/nodejs/node/issues/3104
14 | */
15 | cache?: boolean;
16 |
17 | /**
18 | * If "true", registers `ConfigModule` as a global module.
19 | * See: https://docs.nestjs.com/modules#global-modules
20 | */
21 | isGlobal?: boolean;
22 |
23 | /**
24 | * If "true", environment files (`.env`) will be ignored.
25 | */
26 | ignoreEnvFile?: boolean;
27 |
28 | /**
29 | * If "true", predefined environment variables will not be validated.
30 | * @deprecated Use `validatePredefined` instead.
31 | */
32 | ignoreEnvVars?: boolean;
33 |
34 | /**
35 | * Path to the environment file(s) to be loaded.
36 | */
37 | envFilePath?: string | string[];
38 |
39 | /**
40 | * Custom function to validate environment variables. It takes an object containing environment
41 | * variables as input and outputs validated environment variables.
42 | * If exception is thrown in the function it would prevent the application from bootstrapping.
43 | * Also, environment variables can be edited through this function, changes
44 | * will be reflected in the process.env object.
45 | */
46 | validate?: (config: Record) => Record;
47 |
48 | /**
49 | * If "true", predefined environment variables will be validated.
50 | * Predefined environment variables are process variables (process.env variables) that were set before the module was imported.
51 | * For example, if you start your application with `PORT=3000 node main.js`, then `PORT` is a predefined environment variable.
52 | * Variables that were loaded by the `ConfigModule` from the .env file are not considered predefined.
53 | * @default true
54 | */
55 | validatePredefined?: boolean;
56 |
57 | /**
58 | * If "true", process environment variables (process.env) will be ignored and not picked up by the `ConfigService#get` method.
59 | * @default false
60 | */
61 | skipProcessEnv?: boolean;
62 |
63 | /**
64 | * Environment variables validation schema (Joi).
65 | */
66 | validationSchema?: any;
67 |
68 | /**
69 | * Schema validation options.
70 | * See: https://joi.dev/api/?v=17.3.0#anyvalidatevalue-options
71 | */
72 | validationOptions?: ValidationOptions;
73 |
74 | /**
75 | * Array of custom configuration files to be loaded.
76 | * See: https://docs.nestjs.com/techniques/configuration
77 | */
78 | load?: Array>;
79 |
80 | /**
81 | * A boolean value indicating the use of expanded variables, or object
82 | * containing options to pass to dotenv-expand.
83 | * If .env contains expanded variables, they'll only be parsed if
84 | * this property is set to true.
85 | */
86 | expandVariables?: boolean | DotenvExpandOptions;
87 | }
88 |
--------------------------------------------------------------------------------
/lib/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config-change-event.interface';
2 | export * from './config-factory.interface';
3 | export * from './config-module-options.interface';
4 |
--------------------------------------------------------------------------------
/lib/types/config-object.type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @publicApi
3 | */
4 | export type ConfigObject = Record;
5 |
--------------------------------------------------------------------------------
/lib/types/config.type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @publicApi
3 | */
4 | export type ConfigType any> = T extends (
5 | ...args: any
6 | ) => infer ReturnVal
7 | ? ReturnVal extends Promise
8 | ? AsyncReturnVal
9 | : ReturnVal
10 | : any;
11 |
--------------------------------------------------------------------------------
/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config-object.type';
2 | export * from './config.type';
3 | export * from './no-infer.type';
4 | export * from './path-value.type';
5 |
--------------------------------------------------------------------------------
/lib/types/no-infer.type.ts:
--------------------------------------------------------------------------------
1 | export type NoInferType = [T][T extends any ? 0 : never];
2 |
--------------------------------------------------------------------------------
/lib/types/path-value.type.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Evaluates to `true` if `T` is `any`. `false` otherwise.
3 | * (c) https://stackoverflow.com/a/68633327/5290447
4 | */
5 | type IsAny = unknown extends T
6 | ? [keyof T] extends [never]
7 | ? false
8 | : true
9 | : false;
10 |
11 | export type PathImpl = Key extends string
12 | ? IsAny extends true
13 | ? never
14 | : T[Key] extends Record
15 | ?
16 | | `${Key}.${PathImpl> &
17 | string}`
18 | | `${Key}.${Exclude & string}`
19 | : never
20 | : never;
21 |
22 | export type PathImpl2 = PathImpl | keyof T;
23 |
24 | export type Path = keyof T extends string
25 | ? PathImpl2 extends infer P
26 | ? P extends string | keyof T
27 | ? P
28 | : keyof T
29 | : keyof T
30 | : never;
31 |
32 | export type PathValue<
33 | T,
34 | P extends Path,
35 | > = P extends `${infer Key}.${infer Rest}`
36 | ? Key extends keyof T
37 | ? Rest extends Path
38 | ? PathValue
39 | : never
40 | : never
41 | : P extends keyof T
42 | ? T[P]
43 | : never;
44 |
--------------------------------------------------------------------------------
/lib/utils/create-config-factory.util.ts:
--------------------------------------------------------------------------------
1 | import { FactoryProvider } from '@nestjs/common/interfaces';
2 | import { ConfigFactory } from '../interfaces';
3 | import { getConfigToken } from './get-config-token.util';
4 | import { ConfigFactoryKeyHost } from './register-as.util';
5 | import { randomUUID } from 'crypto';
6 |
7 | /**
8 | * @publicApi
9 | */
10 | export function createConfigProvider(
11 | factory: ConfigFactory & ConfigFactoryKeyHost,
12 | ): FactoryProvider {
13 | return {
14 | provide: factory.KEY || getConfigToken(randomUUID()),
15 | useFactory: factory,
16 | inject: [],
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/lib/utils/get-config-token.util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @publicApi
3 | */
4 | export function getConfigToken(token: string | symbol): string {
5 | return `CONFIGURATION(${token.toString()})`;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/utils/get-registration-token.util.ts:
--------------------------------------------------------------------------------
1 | import { PARTIAL_CONFIGURATION_KEY } from '../config.constants';
2 |
3 | /**
4 | * @publicApi
5 | */
6 | export function getRegistrationToken(config: Record) {
7 | return config[PARTIAL_CONFIGURATION_KEY];
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './register-as.util';
2 | export * from './get-config-token.util';
3 |
--------------------------------------------------------------------------------
/lib/utils/merge-configs.util.ts:
--------------------------------------------------------------------------------
1 | import set from 'lodash/set';
2 |
3 | /**
4 | * @publicApi
5 | */
6 | export function mergeConfigObject(
7 | host: Record,
8 | partial: Record,
9 | token?: string,
10 | ): Record | undefined {
11 | if (token) {
12 | set(host, token, partial);
13 | return partial;
14 | }
15 | Object.assign(host, partial);
16 | }
17 |
--------------------------------------------------------------------------------
/lib/utils/register-as.util.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '..';
2 | import {
3 | AS_PROVIDER_METHOD_KEY,
4 | PARTIAL_CONFIGURATION_KEY,
5 | PARTIAL_CONFIGURATION_PROPNAME,
6 | } from '../config.constants';
7 | import { ConfigFactory } from '../interfaces';
8 | import { ConfigObject } from '../types';
9 | import { getConfigToken } from './get-config-token.util';
10 |
11 | /**
12 | * @publicApi
13 | */
14 | export interface ConfigFactoryKeyHost {
15 | KEY: string | symbol;
16 | asProvider(): {
17 | imports: [ReturnType];
18 | useFactory: (config: T) => T;
19 | inject: [string | symbol];
20 | };
21 | }
22 |
23 | /**
24 | * @publicApi
25 | *
26 | * Registers the configuration object behind a specified token.
27 | */
28 | export function registerAs<
29 | TConfig extends ConfigObject,
30 | TFactory extends ConfigFactory = ConfigFactory,
31 | >(
32 | token: string | symbol,
33 | configFactory: TFactory,
34 | ): TFactory & ConfigFactoryKeyHost> {
35 | const defineProperty = (key: string, value: unknown) => {
36 | Object.defineProperty(configFactory, key, {
37 | configurable: false,
38 | enumerable: false,
39 | value,
40 | writable: false,
41 | });
42 | };
43 |
44 | defineProperty(PARTIAL_CONFIGURATION_KEY, token);
45 | defineProperty(PARTIAL_CONFIGURATION_PROPNAME, getConfigToken(token));
46 | defineProperty(AS_PROVIDER_METHOD_KEY, () => ({
47 | imports: [ConfigModule.forFeature(configFactory)],
48 | useFactory: (config: unknown) => config,
49 | inject: [getConfigToken(token)],
50 | }));
51 | return configFactory as TFactory & ConfigFactoryKeyHost>;
52 | }
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nestjs/config",
3 | "version": "4.0.2",
4 | "description": "Nest - modern, fast, powerful node.js web framework (@config)",
5 | "author": "Kamil Mysliwiec",
6 | "license": "MIT",
7 | "url": "https://github.com/nestjs/config#readme",
8 | "scripts": {
9 | "build": "rimraf -rf dist && tsc -p tsconfig.json",
10 | "format": "prettier --write \"{lib,test}/**/*.ts\"",
11 | "lint": "eslint 'lib/**/*.ts' --fix",
12 | "prepublish:npm": "npm run build",
13 | "publish:npm": "npm publish --access public",
14 | "prepublish:next": "npm run build",
15 | "publish:next": "npm publish --access public --tag next",
16 | "test:integration": "jest --config ./tests/jest-e2e.json --runInBand",
17 | "prerelease": "npm run build",
18 | "release": "release-it"
19 | },
20 | "dependencies": {
21 | "dotenv": "16.4.7",
22 | "dotenv-expand": "12.0.2",
23 | "lodash": "4.17.21"
24 | },
25 | "devDependencies": {
26 | "@commitlint/cli": "19.8.1",
27 | "@commitlint/config-angular": "19.8.1",
28 | "@eslint/eslintrc": "3.3.1",
29 | "@eslint/js": "9.28.0",
30 | "@nestjs/common": "11.1.2",
31 | "@nestjs/core": "11.1.2",
32 | "@nestjs/platform-express": "11.1.2",
33 | "@nestjs/testing": "11.1.2",
34 | "@types/jest": "29.5.14",
35 | "@types/lodash": "4.17.17",
36 | "@types/node": "22.15.29",
37 | "eslint": "9.28.0",
38 | "eslint-config-prettier": "10.1.5",
39 | "eslint-plugin-prettier": "5.4.1",
40 | "globals": "16.2.0",
41 | "husky": "9.1.7",
42 | "jest": "29.7.0",
43 | "joi": "17.13.3",
44 | "lint-staged": "16.1.0",
45 | "prettier": "3.5.3",
46 | "reflect-metadata": "0.2.2",
47 | "release-it": "19.0.3",
48 | "rimraf": "6.0.1",
49 | "rxjs": "7.8.2",
50 | "ts-jest": "29.3.4",
51 | "typescript": "5.8.3",
52 | "typescript-eslint": "8.33.0"
53 | },
54 | "peerDependencies": {
55 | "@nestjs/common": "^10.0.0 || ^11.0.0",
56 | "rxjs": "^7.1.0"
57 | },
58 | "lint-staged": {
59 | "*.ts": [
60 | "prettier --write"
61 | ]
62 | },
63 | "husky": {
64 | "hooks": {
65 | "commit-msg": "commitlint -c .commitlintrc.json -E HUSKY_GIT_PARAMS",
66 | "pre-commit": "lint-staged"
67 | }
68 | },
69 | "repository": {
70 | "type": "git",
71 | "url": "https://github.com/nestjs/config"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "semanticCommits": true,
3 | "packageRules": [{
4 | "depTypeList": ["devDependencies"],
5 | "automerge": true
6 | }],
7 | "extends": [
8 | "config:base"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/tests/e2e/.env.conditional:
--------------------------------------------------------------------------------
1 | FOO="use it"
2 | FOO_FALSE="false"
3 | FOO_DYNAMIC="yes"
4 | FOO_CUSTOM="yeah!"
5 | BAR="yay"
6 | FOOBAR="do it"
7 | QUU="nested!"
--------------------------------------------------------------------------------
/tests/e2e/.env.valid:
--------------------------------------------------------------------------------
1 | PORT=4000
2 | DATABASE_NAME=test
--------------------------------------------------------------------------------
/tests/e2e/cache.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigService } from '../../lib';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('Cache', () => {
7 | let app: INestApplication;
8 | let envBackup: NodeJS.ProcessEnv;
9 | beforeAll(() => {
10 | envBackup = {
11 | ...process.env,
12 | };
13 | });
14 | describe('without cache', () => {
15 | beforeAll(async () => {
16 | process.env['NAME'] = 'TEST';
17 | const moduleRef = await Test.createTestingModule({
18 | imports: [AppModule.withEnvVars()],
19 | }).compile();
20 |
21 | app = moduleRef.createNestApplication();
22 | await app.init();
23 | });
24 |
25 | it(`should return loaded env variables from vars`, () => {
26 | const configService = app.get(ConfigService);
27 | expect(configService.get('NAME')).toEqual('TEST');
28 | });
29 |
30 | it(`should return new vars`, () => {
31 | process.env['NAME'] = 'CHANGED';
32 | const configService = app.get(ConfigService);
33 | expect(configService.get('NAME')).toEqual('CHANGED');
34 | });
35 | });
36 |
37 | describe('with cache', () => {
38 | beforeAll(async () => {
39 | process.env['NAME'] = 'TEST';
40 | const moduleRef = await Test.createTestingModule({
41 | imports: [AppModule.withCache()],
42 | }).compile();
43 |
44 | app = moduleRef.createNestApplication();
45 | await app.init();
46 | });
47 |
48 | it(`should return loaded env variables from vars`, () => {
49 | const configService = app.get(ConfigService);
50 | expect(configService.get('NAME')).toEqual('TEST');
51 | });
52 |
53 | it(`should return cached vars`, () => {
54 | process.env['NAME'] = 'CHANGED';
55 | const configService = app.get(ConfigService);
56 | expect(configService.get('NAME')).toEqual('TEST');
57 | });
58 | });
59 |
60 | afterEach(async () => {
61 | process.env = {
62 | ...envBackup,
63 | };
64 | await app.close();
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/tests/e2e/conditional.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Module } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigModule, ConditionalModule } from '../../lib';
4 | import { join } from 'path';
5 |
6 | @Injectable()
7 | class FooService {}
8 |
9 | @Injectable()
10 | class FooDynamicService {}
11 |
12 | @Module({
13 | providers: [FooService],
14 | exports: [FooService],
15 | })
16 | class FooModule {
17 | static forRoot() {
18 | return {
19 | module: FooModule,
20 | providers: [FooDynamicService],
21 | exports: [FooDynamicService],
22 | };
23 | }
24 | }
25 |
26 | @Injectable()
27 | class BarService {}
28 |
29 | @Module({
30 | providers: [BarService],
31 | exports: [BarService],
32 | })
33 | class BarModule {}
34 |
35 | @Module({
36 | providers: [
37 | {
38 | provide: 'quu',
39 | useValue: 42,
40 | },
41 | ],
42 | exports: ['quu'],
43 | })
44 | class QuuModule {}
45 |
46 | @Module({
47 | imports: [ConditionalModule.registerWhen(QuuModule, 'QUU')],
48 | exports: [ConditionalModule],
49 | })
50 | class FooBarModule {}
51 |
52 | describe('ConditionalModule', () => {
53 | it('it should work for a regular module', async () => {
54 | const modRef = await Test.createTestingModule({
55 | imports: [
56 | ConfigModule.forRoot({
57 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
58 | }),
59 | ConditionalModule.registerWhen(FooModule, 'FOO'),
60 | ],
61 | }).compile();
62 | expect(modRef.get(FooService, { strict: false })).toBeDefined();
63 | await modRef.close();
64 | });
65 | it('should work for a dynamic module', async () => {
66 | const modRef = await Test.createTestingModule({
67 | imports: [
68 | ConfigModule.forRoot({
69 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
70 | }),
71 | ConditionalModule.registerWhen(FooModule.forRoot(), 'FOO_DYNAMIC'),
72 | ],
73 | }).compile();
74 | expect(modRef.get(FooDynamicService, { strict: false })).toBeDefined();
75 | await modRef.close();
76 | });
77 | it('should not register when the value is false', async () => {
78 | const modRef = await Test.createTestingModule({
79 | imports: [
80 | ConfigModule.forRoot({
81 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
82 | }),
83 | ConditionalModule.registerWhen(FooModule, 'FOO_FALSE'),
84 | ],
85 | }).compile();
86 | expect(() => modRef.get(FooService, { strict: false })).toThrow();
87 | await modRef.close();
88 | });
89 | it('should work for a custom condition', async () => {
90 | const modRef = await Test.createTestingModule({
91 | imports: [
92 | ConfigModule.forRoot({
93 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
94 | }),
95 | ConditionalModule.registerWhen(FooModule, env => {
96 | return env.FOO_CUSTOM === 'yeah!';
97 | }),
98 | ],
99 | }).compile();
100 | expect(modRef.get(FooService, { strict: false })).toBeDefined();
101 | await modRef.close();
102 | });
103 | it('should handle two conditional modules', async () => {
104 | const modRef = await Test.createTestingModule({
105 | imports: [
106 | ConfigModule.forRoot({
107 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
108 | }),
109 | ConditionalModule.registerWhen(FooModule, 'FOO'),
110 | ConditionalModule.registerWhen(BarModule, 'BAR'),
111 | ],
112 | }).compile();
113 | expect(modRef.get(FooService, { strict: false })).toBeDefined();
114 | expect(modRef.get(BarService, { strict: false })).toBeDefined();
115 | await modRef.close();
116 | });
117 | it('should handle nested conditional module', async () => {
118 | const modRef = await Test.createTestingModule({
119 | imports: [
120 | ConfigModule.forRoot({
121 | envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
122 | }),
123 | ConditionalModule.registerWhen(FooBarModule, 'FOOBAR'),
124 | ],
125 | }).compile();
126 | expect(modRef.get('quu', { strict: false })).toBeDefined();
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/tests/e2e/for-feature.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('forFeature()', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withForFeature()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should load configuration with "forFeature()"`, () => {
18 | const host = app.get(AppModule).getDatabaseHost();
19 | expect(host).toEqual('host');
20 | });
21 |
22 | afterEach(async () => {
23 | await app.close();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/e2e/load-env-expanded-ignore-process-env.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Environment variables', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | process.env.URL = 'process-app.test';
10 |
11 | const module = await Test.createTestingModule({
12 | imports: [AppModule.withExpandedEnvVarsIgnoreProcessEnv()],
13 | }).compile();
14 |
15 | app = module.createNestApplication();
16 | await app.init();
17 | });
18 |
19 | it(`should ignore process environment variable`, () => {
20 | const envVars = app.get(AppModule).getEnvVariables();
21 | expect(envVars.EMAIL).toEqual('support@myapp.test');
22 | });
23 |
24 | afterEach(async () => {
25 | process.env.URL = undefined
26 | await app.close();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/e2e/load-env-expanded.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Environment variables', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withExpandedEnvVars()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return loaded env variables`, () => {
18 | const envVars = app.get(AppModule).getEnvVariables();
19 | expect(envVars.EMAIL).toEqual('support@myapp.test');
20 | });
21 |
22 | afterEach(async () => {
23 | await app.close();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/e2e/load-env.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigModule } from '../../lib';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('Environment variables', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const module = await Test.createTestingModule({
11 | imports: [AppModule.withEnvVars()],
12 | }).compile();
13 |
14 | app = module.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it(`should return loaded env variables`, async () => {
19 | await ConfigModule.envVariablesLoaded;
20 |
21 | const envVars = app.get(AppModule).getEnvVariables();
22 | expect(envVars.PORT).toEqual('4000');
23 | });
24 |
25 | afterEach(async () => {
26 | await app.close();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/e2e/load-files-async.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Async Files', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withLoadedAsyncConfigurations()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return loaded configuration`, () => {
18 | const host = app.get(AppModule).getDatabaseHost();
19 | expect(host).toEqual('host');
20 | });
21 |
22 | it(`should return loaded configuration (injected through constructor)`, () => {
23 | const config = app.get(AppModule).getDatabaseConfig();
24 | expect(config.host).toEqual('host');
25 | expect(config.port).toEqual(4000);
26 | });
27 |
28 | afterEach(async () => {
29 | await app.close();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/tests/e2e/load-files.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Files', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withLoadedConfigurations()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return loaded configuration`, () => {
18 | const host = app.get(AppModule).getDatabaseHost();
19 | expect(host).toEqual('host');
20 | });
21 |
22 | it(`should return loaded configuration (injected through constructor)`, () => {
23 | const config = app.get(AppModule).getDatabaseConfig();
24 | expect(config.host).toEqual('host');
25 | expect(config.port).toEqual(4000);
26 | });
27 |
28 | afterEach(async () => {
29 | await app.close();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/tests/e2e/load-multiple-env.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Environment variables (multiple env files)', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withMultipleEnvFiles()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return loaded env variables`, () => {
18 | const envVars = app.get(AppModule).getEnvVariables();
19 | expect(envVars.PORT).toEqual('3000');
20 | expect(envVars.TIMEOUT).toEqual('5000');
21 | });
22 |
23 | afterEach(async () => {
24 | await app.close();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/e2e/load-nested-files.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Nested Files', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withNestedLoadedConfigurations()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return nested loaded configuration`, () => {
18 | const host = app.get(AppModule).getNestedDatabaseHost();
19 | expect(host).toEqual('host');
20 | });
21 |
22 | afterEach(async () => {
23 | await app.close();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/e2e/load-priority.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { join } from 'path';
4 | import { ConfigService } from '../../lib';
5 | import { AppModule } from '../src/app.module';
6 |
7 | describe('Environment variables and .env files', () => {
8 | let app: INestApplication;
9 | let envBackup: NodeJS.ProcessEnv;
10 | beforeAll(() => {
11 | envBackup = {
12 | ...process.env,
13 | };
14 | });
15 | describe('without conflicts', () => {
16 | beforeAll(async () => {
17 | process.env['NAME'] = 'TEST';
18 | const moduleRef = await Test.createTestingModule({
19 | imports: [AppModule.withEnvVars()],
20 | }).compile();
21 |
22 | app = moduleRef.createNestApplication();
23 | await app.init();
24 | });
25 |
26 | it(`should return loaded env variables from vars and dotenv`, () => {
27 | const configService = app.get(ConfigService);
28 | expect(configService.get('PORT')).toEqual('4000');
29 | expect(configService.get('NAME')).toEqual('TEST');
30 | });
31 | });
32 |
33 | describe('with conflicts', () => {
34 | beforeAll(async () => {
35 | process.env['PORT'] = '8000';
36 | const moduleRef = await Test.createTestingModule({
37 | imports: [AppModule.withEnvVars()],
38 | }).compile();
39 |
40 | app = moduleRef.createNestApplication();
41 | await app.init();
42 | });
43 |
44 | it('should choose env vars over dotenv', () => {
45 | const configService = app.get(ConfigService);
46 | expect(configService.get('PORT')).toEqual('8000');
47 | });
48 | });
49 |
50 | describe('with conflicts and schema validation', () => {
51 | beforeAll(async () => {
52 | process.env['PORT'] = '8000';
53 | const moduleRef = await Test.createTestingModule({
54 | imports: [
55 | AppModule.withSchemaValidation(join(__dirname, '.env.valid')),
56 | ],
57 | }).compile();
58 |
59 | app = moduleRef.createNestApplication();
60 | await app.init();
61 | });
62 |
63 | it('should choose env vars over dotenv', () => {
64 | const configService = app.get(ConfigService);
65 | expect(configService.get('PORT')).toEqual(8000);
66 | });
67 | });
68 |
69 | describe('with conflicts of .env file and loaded configuration', () => {
70 | beforeAll(async () => {
71 | const moduleRef = await Test.createTestingModule({
72 | imports: [
73 | AppModule.withEnvVarsAndLoadedConfigurations([
74 | () => ({ PORT: '8000' }),
75 | ]),
76 | ],
77 | }).compile();
78 |
79 | app = moduleRef.createNestApplication();
80 | await app.init();
81 | });
82 |
83 | it('should choose the load configuration over .env file vars', () => {
84 | const configService = app.get(ConfigService);
85 | expect(configService.get('PORT')).toEqual('8000');
86 | });
87 | });
88 |
89 | describe('with conflicts of multiple loaded configurations', () => {
90 | beforeAll(async () => {
91 | const moduleRef = await Test.createTestingModule({
92 | imports: [
93 | AppModule.withDynamicLoadedConfigurations([
94 | () => ({ PORT: '8000' }),
95 | () => ({ PORT: '9000' }),
96 | ]),
97 | ],
98 | }).compile();
99 |
100 | app = moduleRef.createNestApplication();
101 | await app.init();
102 | });
103 |
104 | it('should choose last load configuration', () => {
105 | const configService = app.get(ConfigService);
106 | expect(configService.get('PORT')).toEqual('9000');
107 | });
108 | });
109 |
110 | afterEach(async () => {
111 | process.env = {
112 | ...envBackup,
113 | };
114 | await app.close();
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/tests/e2e/load-symbol-files.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/app.module';
4 |
5 | describe('Symbol Files', () => {
6 | let app: INestApplication;
7 |
8 | beforeEach(async () => {
9 | const module = await Test.createTestingModule({
10 | imports: [AppModule.withSymbolLoadedConfigurations()],
11 | }).compile();
12 |
13 | app = module.createNestApplication();
14 | await app.init();
15 | });
16 |
17 | it(`should return symbol loaded configuration`, () => {
18 | const config = app.get(AppModule).getSymbolDatabaseConfig();
19 | expect(config.host).toEqual('host');
20 | expect(config.port).toEqual(4000);
21 | });
22 |
23 | afterEach(async () => {
24 | await app.close();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/e2e/optional-generic.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import { ConfigService } from '../../lib';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('Optional Generic()', () => {
7 | let app: INestApplication;
8 | let moduleRef: TestingModule;
9 |
10 | beforeEach(async () => {
11 | moduleRef = await Test.createTestingModule({
12 | imports: [AppModule.withEnvVars()],
13 | }).compile();
14 |
15 | app = moduleRef.createNestApplication();
16 | await app.init();
17 | });
18 |
19 | it(`should allow a key of the interface`, () => {
20 | const configService =
21 | moduleRef.get>(ConfigService);
22 | const port = configService.get('PORT');
23 | expect(port).toBeTruthy();
24 | });
25 |
26 | it(`should infer type from a dot notation`, () => {
27 | const configService =
28 | moduleRef.get>(
29 | ConfigService,
30 | );
31 |
32 | const obj = configService.get('obj', { infer: true });
33 | const test = configService.get('obj.test', { infer: true });
34 | const testWithDefaultValue = configService.get('obj.test2', true, {
35 | infer: true,
36 | });
37 | expect(obj?.test).toEqual('true');
38 | expect(test).toEqual('true');
39 | expect(testWithDefaultValue).toBeTruthy();
40 | });
41 |
42 | it(`should infer type from a dot notation (getOrThrow)`, () => {
43 | const configService =
44 | moduleRef.get>(
45 | ConfigService,
46 | );
47 |
48 | const obj = configService.getOrThrow('obj', { infer: true });
49 | const test = configService.getOrThrow('obj.test', { infer: true });
50 | const testWithDefaultValue = configService.getOrThrow('obj.test2', true, {
51 | infer: true,
52 | });
53 | expect(obj?.test).toEqual('true');
54 | expect(test).toEqual('true');
55 | expect(testWithDefaultValue).toBeTruthy();
56 | });
57 |
58 | it(`should allow any key without a generic`, () => {
59 | const configService = moduleRef.get(ConfigService);
60 | const port = configService.get('PORT');
61 |
62 | expect(port).toBeTruthy();
63 | });
64 |
65 | it(`should allow any key without a generic and with the default value`, () => {
66 | const configService = moduleRef.get(ConfigService);
67 | const port = configService.get('PORT2', 'default');
68 |
69 | expect(port).toEqual('default');
70 | });
71 |
72 | afterEach(async () => {
73 | await app.close();
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/tests/e2e/optional.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { AppModule } from '../src/app.module';
3 | import { ConfigService } from '../../lib';
4 |
5 | describe('Optional environment variables', () => {
6 | it('should return undefined for optional variables', async () => {
7 | const module = await Test.createTestingModule({
8 | imports: [AppModule.withValidateFunction(() => ({
9 | optional: undefined,
10 | }))],
11 | }).compile();
12 |
13 | const app = module.createNestApplication();
14 | await app.init();
15 |
16 | const optional = module.get(ConfigService).get('optional')
17 |
18 | expect(optional).toEqual(undefined)
19 | });
20 |
21 | it('should not assign complex objects back to process.env', async () => {
22 | const module = await Test.createTestingModule({
23 | imports: [AppModule.withValidateFunction(() => ({
24 | complex: {hello: 'there'},
25 | }))],
26 | }).compile();
27 |
28 | const app = module.createNestApplication();
29 | await app.init();
30 |
31 | expect(process.env.complex).toEqual(undefined)
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/tests/e2e/skip-predefined.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigModule, ConfigService } from '../../lib';
4 | import { AppModule } from '../src/app.module';
5 |
6 | describe('Environment variables (skip process env)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const module = await Test.createTestingModule({
11 | imports: [AppModule.withSkipProcessEnv()],
12 | }).compile();
13 |
14 | app = module.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it(`should ignore predefined environment variables (process.env)`, async () => {
19 | process.env.RANDOM_PREDEFINED = 'test';
20 | await ConfigModule.envVariablesLoaded;
21 |
22 | const configService = app.get(ConfigService);
23 | expect(configService.get('RANDOM_PREDEFINED')).toBeUndefined();
24 | });
25 |
26 | afterEach(async () => {
27 | await app.close();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/e2e/update-env.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import { ConfigModule } from '../../lib';
4 | import { AppModule } from '../src/app.module';
5 | import { ConfigService } from '../../lib';
6 |
7 | describe('Setting environment variables', () => {
8 | let app: INestApplication;
9 | let module: TestingModule;
10 | let originalEnv: NodeJS.ProcessEnv;
11 |
12 | beforeEach(async () => {
13 | originalEnv = { ...process.env };
14 |
15 | module = await Test.createTestingModule({
16 | imports: [AppModule.withExpandedEnvVars()],
17 | }).compile();
18 |
19 | app = module.createNestApplication();
20 | await app.init();
21 | });
22 |
23 | it('should return updated value after set', async () => {
24 | const prevUrl = module.get(ConfigService).get('URL');
25 |
26 | module.get(ConfigService).set('URL', 'yourapp.test');
27 |
28 | const updatedUrl = module.get(ConfigService).get('URL');
29 |
30 | expect(prevUrl).toEqual('myapp.test');
31 | expect(updatedUrl).toEqual('yourapp.test');
32 | });
33 |
34 | it('should return value after set', async () => {
35 | const undefinedEnv = module.get(ConfigService).get('UNDEFINED_ENV');
36 |
37 | module.get(ConfigService).set('UNDEFINED_ENV', 'defined');
38 |
39 | const definedEnv = module.get(ConfigService).get('UNDEFINED_ENV');
40 |
41 | expect(undefinedEnv).toEqual(undefined);
42 | expect(definedEnv).toEqual('defined');
43 | });
44 |
45 | it('should return updated value with interpolation after set', async () => {
46 | const prevUrl = module.get(ConfigService).get('URL');
47 | const prevEmail = module.get(ConfigService).get('EMAIL');
48 |
49 | module.get(ConfigService).set('URL', 'yourapp.test');
50 |
51 | const updatedUrl = module.get(ConfigService).get('URL');
52 | const updatedEmail = module.get(ConfigService).get('EMAIL');
53 |
54 | expect(prevUrl).toEqual('myapp.test');
55 | expect(prevEmail).toEqual('support@myapp.test');
56 | expect(updatedUrl).toEqual('yourapp.test');
57 | expect(updatedEmail).toEqual('support@yourapp.test');
58 | });
59 |
60 | it(`should return updated process.env property after set`, async () => {
61 | await ConfigModule.envVariablesLoaded;
62 |
63 | module.get(ConfigService).set('URL', 'yourapp.test');
64 |
65 | const envVars = app.get(AppModule).getEnvVariables();
66 |
67 | expect(envVars.URL).toEqual('yourapp.test');
68 | expect(envVars.EMAIL).toEqual('support@yourapp.test');
69 | });
70 |
71 | afterEach(async () => {
72 | process.env = originalEnv;
73 | await app.close();
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/tests/e2e/validate-function.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { join } from 'path';
4 | import { ConfigService } from '../../lib';
5 | import { AppModule } from '../src/app.module';
6 |
7 | describe('Schema validation', () => {
8 | let app: INestApplication;
9 | const errorMessage = 'Validation error';
10 | const validate = (config: Record) => {
11 | if (!('PORT' in config && 'DATABASE_NAME' in config)) {
12 | throw new Error(errorMessage);
13 | }
14 | return {};
15 | };
16 |
17 | it(`should prevent application from bootstrapping if error is thrown due to loaded env variables`, async () => {
18 | let hasThrown = false;
19 | try {
20 | const module = await Test.createTestingModule({
21 | imports: [AppModule.withValidateFunction(validate)],
22 | }).compile();
23 |
24 | app = module.createNestApplication();
25 | await app.init();
26 | } catch (err) {
27 | hasThrown = true;
28 | expect(err.message).toEqual(errorMessage);
29 | }
30 | expect(hasThrown).toBe(true);
31 | });
32 |
33 | it(`should prevent application from bootstrapping if error is thrown even when ignoreEnvFile is true`, async () => {
34 | let hasThrown = false;
35 | try {
36 | const module = await Test.createTestingModule({
37 | imports: [AppModule.withValidateFunction(validate, undefined, true)],
38 | }).compile();
39 |
40 | app = module.createNestApplication();
41 | await app.init();
42 | } catch (err) {
43 | hasThrown = true;
44 | expect(err.message).toEqual(errorMessage);
45 | }
46 | expect(hasThrown).toBe(true);
47 | });
48 |
49 | it(`should load env variables if everything is ok`, async () => {
50 | let hasThrown = false;
51 | const module = await Test.createTestingModule({
52 | imports: [
53 | AppModule.withValidateFunction(validate, join(__dirname, '.env.valid')),
54 | ],
55 | }).compile();
56 |
57 | app = module.createNestApplication();
58 | await app.init();
59 |
60 | const configService = app.get(ConfigService);
61 | expect(typeof configService.get('PORT')).not.toBe(undefined);
62 | expect(typeof configService.get('DATABASE_NAME')).not.toBe(undefined);
63 | });
64 |
65 | it(`should parse loaded env variables`, async () => {
66 | const validateAndTransform = (config: Record) => {
67 | return {
68 | PORT: Number(config.PORT),
69 | DATABASE_NAME: config.DATABASE_NAME,
70 | };
71 | };
72 | const module = await Test.createTestingModule({
73 | imports: [
74 | AppModule.withValidateFunction(
75 | validateAndTransform,
76 | join(__dirname, '.env.valid'),
77 | ),
78 | ],
79 | }).compile();
80 |
81 | app = module.createNestApplication();
82 | await app.init();
83 |
84 | const configService = app.get(ConfigService);
85 | expect(typeof configService.get('PORT')).toEqual('number');
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/tests/e2e/validation-schema.spec.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { Test } from '@nestjs/testing';
3 | import { join } from 'path';
4 | import { ConfigService } from '../../lib';
5 | import { AppModule } from '../src/app.module';
6 |
7 | describe('Schema validation', () => {
8 | let app: INestApplication;
9 |
10 | it(`should validate loaded env variables`, async () => {
11 | try {
12 | const module = await Test.createTestingModule({
13 | imports: [AppModule.withSchemaValidation()],
14 | }).compile();
15 |
16 | app = module.createNestApplication();
17 | await app.init();
18 | } catch (err) {
19 | expect(err.message).toEqual(
20 | 'Config validation error: "PORT" is required. "DATABASE_NAME" is required',
21 | );
22 | }
23 | });
24 |
25 | it(`should validate env variables even when ignoreEnvFile is true`, async () => {
26 | try {
27 | const module = await Test.createTestingModule({
28 | imports: [AppModule.withSchemaValidation(undefined, true)],
29 | }).compile();
30 |
31 | app = module.createNestApplication();
32 | await app.init();
33 | } catch (err) {
34 | expect(err.message).toEqual(
35 | 'Config validation error: "PORT" is required. "DATABASE_NAME" is required',
36 | );
37 | }
38 | });
39 |
40 | it(`should parse loaded env variables`, async () => {
41 | const module = await Test.createTestingModule({
42 | imports: [AppModule.withSchemaValidation(join(__dirname, '.env.valid'))],
43 | }).compile();
44 |
45 | app = module.createNestApplication();
46 | await app.init();
47 |
48 | const configService = app.get(ConfigService);
49 | expect(typeof configService.get('PORT')).toEqual('number');
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tests/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": ["ts-jest", { "tsconfig": "./tests/tsconfig.json" }]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/src/.env:
--------------------------------------------------------------------------------
1 | PORT=4000
2 | TIMEOUT=5000
3 |
--------------------------------------------------------------------------------
/tests/src/.env.expanded:
--------------------------------------------------------------------------------
1 | URL=myapp.test
2 | EMAIL=support@${URL}
3 |
--------------------------------------------------------------------------------
/tests/src/.env.local:
--------------------------------------------------------------------------------
1 | PORT=3000
2 |
--------------------------------------------------------------------------------
/tests/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Inject, Module, Optional } from '@nestjs/common';
2 | import Joi from 'joi';
3 | import { join } from 'path';
4 | import { ConfigFactory, ConfigType } from '../../lib';
5 | import { ConfigModule } from '../../lib/config.module';
6 | import { ConfigService } from '../../lib/config.service';
7 | import databaseConfig from './database.config';
8 | import nestedDatabaseConfig from './nested-database.config';
9 | import symbolDatabaseConfig, { DATABASE_SYMBOL_TOKEN } from './symbol-database.config';
10 |
11 | type Config = {
12 | database: ConfigType & {
13 | driver: ConfigType;
14 | };
15 | };
16 |
17 | interface ConfigTypeAsInterface {
18 | database: ConfigType & {
19 | driver: ConfigType;
20 | };
21 | }
22 | @Module({})
23 | export class AppModule {
24 | constructor(
25 | private readonly configService: ConfigService,
26 | // The following is the same object as above but narrowing its types
27 | private readonly configServiceNarrowed: ConfigService,
28 | private readonly configServiceNarrowed2: ConfigService<
29 | ConfigTypeAsInterface,
30 | true
31 | >,
32 | @Optional()
33 | @Inject(databaseConfig.KEY)
34 | private readonly dbConfig: ConfigType,
35 | ) {}
36 |
37 | /**
38 | * This method is not meant to be used anywhere! It just here for testing
39 | * types defintions while runnig test suites (in some sort).
40 | * If some typings doesn't follows the requirements, Jest will fail due to
41 | * TypeScript errors.
42 | */
43 | private noop(): void {
44 | // Arrange
45 | const identityString = (v: string) => v;
46 | const identityNumber = (v: number) => v;
47 | // Act
48 | const knowConfig =
49 | this.configServiceNarrowed.get('database');
50 | // Assert
51 | // We don't need type assertions bellow anymore since `knowConfig` is not
52 | // expected to be `undefined` beforehand.
53 | identityString(knowConfig.host);
54 | identityNumber(knowConfig.port);
55 | identityString(knowConfig.driver.host);
56 | identityNumber(knowConfig.driver.port);
57 | }
58 |
59 | static withCache(): DynamicModule {
60 | return {
61 | module: AppModule,
62 | imports: [
63 | ConfigModule.forRoot({
64 | cache: true,
65 | envFilePath: join(__dirname, '.env'),
66 | load: [databaseConfig],
67 | }),
68 | ],
69 | };
70 | }
71 |
72 | static withSkipProcessEnv(): DynamicModule {
73 | return {
74 | module: AppModule,
75 | imports: [
76 | ConfigModule.forRoot({
77 | envFilePath: join(__dirname, '.env'),
78 | load: [() => ({ obj: { test: 'true', test2: undefined } })],
79 | skipProcessEnv: true,
80 | }),
81 | ],
82 | };
83 | }
84 |
85 | static withEnvVars(): DynamicModule {
86 | return {
87 | module: AppModule,
88 | imports: [
89 | ConfigModule.forRoot({
90 | envFilePath: join(__dirname, '.env'),
91 | load: [() => ({ obj: { test: 'true', test2: undefined } })],
92 | }),
93 | ],
94 | };
95 | }
96 |
97 | static withExpandedEnvVars(): DynamicModule {
98 | return {
99 | module: AppModule,
100 | imports: [
101 | ConfigModule.forRoot({
102 | envFilePath: join(__dirname, '.env.expanded'),
103 | expandVariables: true,
104 | }),
105 | ],
106 | };
107 | }
108 |
109 | static withExpandedEnvVarsIgnoreProcessEnv(): DynamicModule {
110 | return {
111 | module: AppModule,
112 | imports: [
113 | ConfigModule.forRoot({
114 | envFilePath: join(__dirname, '.env.expanded'),
115 | expandVariables: { processEnv: {} },
116 | }),
117 | ],
118 | };
119 | }
120 |
121 | static withEnvVarsAndLoadedConfigurations(
122 | configFactory: ConfigFactory[],
123 | ): DynamicModule {
124 | return {
125 | module: AppModule,
126 | imports: [
127 | ConfigModule.forRoot({
128 | envFilePath: join(__dirname, '.env'),
129 | load: configFactory,
130 | }),
131 | ],
132 | };
133 | }
134 |
135 | static withMultipleEnvFiles(): DynamicModule {
136 | return {
137 | module: AppModule,
138 | imports: [
139 | ConfigModule.forRoot({
140 | envFilePath: [join(__dirname, '.env.local'), join(__dirname, '.env')],
141 | }),
142 | ],
143 | };
144 | }
145 |
146 | static withLoadedConfigurations(): DynamicModule {
147 | return {
148 | module: AppModule,
149 | imports: [
150 | ConfigModule.forRoot({
151 | load: [databaseConfig],
152 | }),
153 | ],
154 | };
155 | }
156 |
157 | static withLoadedAsyncConfigurations() {
158 | return {
159 | module: AppModule,
160 | imports: [
161 | ConfigModule.forRoot({
162 | load: [Promise.resolve(databaseConfig)],
163 | }),
164 | ],
165 | };
166 | }
167 |
168 | static withNestedLoadedConfigurations(): DynamicModule {
169 | return {
170 | module: AppModule,
171 | imports: [
172 | ConfigModule.forRoot({
173 | load: [nestedDatabaseConfig],
174 | }),
175 | ],
176 | };
177 | }
178 |
179 | static withSymbolLoadedConfigurations(): DynamicModule {
180 | return {
181 | module: AppModule,
182 | imports: [
183 | ConfigModule.forRoot({
184 | load: [symbolDatabaseConfig],
185 | }),
186 | ],
187 | };
188 | }
189 |
190 | static withDynamicLoadedConfigurations(
191 | configFactory: ConfigFactory[],
192 | ): DynamicModule {
193 | return {
194 | module: AppModule,
195 | imports: [
196 | ConfigModule.forRoot({
197 | load: configFactory,
198 | }),
199 | ],
200 | };
201 | }
202 |
203 | static withSchemaValidation(
204 | envFilePath?: string,
205 | ignoreEnvFile?: boolean,
206 | ): DynamicModule {
207 | return {
208 | module: AppModule,
209 | imports: [
210 | ConfigModule.forRoot({
211 | envFilePath,
212 | ignoreEnvFile,
213 | validationSchema: Joi.object({
214 | PORT: Joi.number().required(),
215 | DATABASE_NAME: Joi.string().required(),
216 | }),
217 | }),
218 | ],
219 | };
220 | }
221 |
222 | static withValidateFunction(
223 | validate: (config: Record) => Record,
224 | envFilePath?: string,
225 | ignoreEnvFile?: boolean,
226 | ): DynamicModule {
227 | return {
228 | module: AppModule,
229 | imports: [
230 | ConfigModule.forRoot({
231 | envFilePath,
232 | ignoreEnvFile,
233 | validate,
234 | }),
235 | ],
236 | };
237 | }
238 |
239 | static withForFeature(): DynamicModule {
240 | return {
241 | module: AppModule,
242 | imports: [
243 | ConfigModule.forRoot(),
244 | ConfigModule.forFeature(databaseConfig),
245 | ],
246 | };
247 | }
248 |
249 | getEnvVariables() {
250 | return process.env;
251 | }
252 |
253 | getDatabaseHost() {
254 | return this.configService.get('database.host');
255 | }
256 |
257 | getDatabaseConfig() {
258 | return this.dbConfig;
259 | }
260 |
261 | getNestedDatabaseHost() {
262 | return this.configService.get('database.driver.host');
263 | }
264 |
265 | getSymbolDatabaseConfig() {
266 | return this.configService.get(DATABASE_SYMBOL_TOKEN)
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/tests/src/database.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '../../lib/utils';
2 |
3 | export default registerAs('database', () => ({
4 | host: 'host',
5 | port: 4000,
6 | }));
7 |
--------------------------------------------------------------------------------
/tests/src/nested-database.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '../../lib/utils';
2 |
3 | export default registerAs('database.driver', () => ({
4 | host: 'host',
5 | port: 4000,
6 | }));
7 |
--------------------------------------------------------------------------------
/tests/src/symbol-database.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '../../lib/utils';
2 |
3 | export const DATABASE_SYMBOL_TOKEN = Symbol('database');
4 |
5 | export default registerAs(DATABASE_SYMBOL_TOKEN, () => ({
6 | host: 'host',
7 | port: 4000,
8 | }));
9 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "../",
5 | "strictNullChecks": true
6 | },
7 | "include": [
8 | "**/*.spec.ts",
9 | ],
10 | "exclude": [
11 | "node_modules",
12 | "dist"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "strict": true,
6 | "removeComments": false,
7 | "noLib": false,
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "useUnknownInCatchVariables": false,
11 | "experimentalDecorators": true,
12 | "target": "ES2021",
13 | "sourceMap": false,
14 | "outDir": "./dist",
15 | "rootDir": "./lib",
16 | "skipLibCheck": true
17 | },
18 | "include": ["lib/**/*"],
19 | "exclude": ["node_modules", "**/*.spec.ts", "tests"]
20 | }
21 |
--------------------------------------------------------------------------------