Skip to content

Commit

Permalink
config module rework
Browse files Browse the repository at this point in the history
  • Loading branch information
Enity committed May 15, 2024
1 parent 4e17a7f commit 712c4e7
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 64 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,17 @@ The built-in solution simply lacks behind competitors in the market. Features th

### The solution

The solution is to use the [@monkee/turbo-config](https://github.com/monke-systems/monke-turbo-config). It provides all the necessary features and is easily integrated into the NestJs application.
The starter provides a pre-configured setup of the library. All included modules are configurable.
This starter provides a high-level tool for configuring your applications. Many configuration rules are strictly enforced, and this is done intentionally.

TOOD: Add more information about the configuration
Key points:

1. The application is configured using YML files with the ability to override settings through YML, environment variables, or CLI arguments.
2. There is always a default configuration file named config.default.yml.
3. Separate YML files can be created for each environment. The environment name is set by the NEST_CONFIG_ENV environment variable. Examples: config.production.yml, config.test.yml
4. The root directory by default is "src/resources". The root directory can be reassigned via the NEST_CONFIG_ROOT environment variable. You should properly configure the production container build
5. If the application is started with NEST_CONFIG_GENERATE_REF=true, config reference and json schema is automatically generated

The starter uses the [@monkee/turbo-config](https://github.com/monke-systems/monke-turbo-config) library under the hood. Refer to the documentation for more details.

## Logging

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test:integration": "jest test",
"lint": "eslint \"{src,test}/**/*.ts\"",
"lint:fix": "eslint \"{src,test}/**/*.ts\" --fix",
"start-example-app": "NODE_ENV=development ts-node test/example-app/main.ts",
"start-example-app": "NODE_ENV=development NEST_CONFIG_ENV=dev NEST_CONFIG_GENERATE_REF=true ts-node test/example-app/main.ts",
"prepublishOnly": "npm run build",
"clean": "rm -rf lib"
},
Expand Down
2 changes: 1 addition & 1 deletion src/modules/actuator/actuator-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ActuatorServer

if (this.opts.config.enabled) {
this.server.listen(this.opts.config.port, '0.0.0.0', () => {
this.logger.log(`Actuator is running on port ${this.opts.config.port}`);
this.logger.log(`Actuator listening on port :${this.opts.config.port}`);
});
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/modules/config/config-module-opts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
export type ConfigModuleOpts = {
configClass: new () => object;
config: {
configRootDir: string;
configEnv?: string;
generateDefaultConfigIfNotExist?: boolean;
generateDocAndSchema?: boolean;
configDocPath?: string;
configSchemaPath?: string;
loadEnvFiles?: boolean;
ymlFiles?: string[];
envFiles?: string[];
};
};
172 changes: 130 additions & 42 deletions src/modules/config/config.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { writeFile } from 'fs/promises';
import * as fs from 'fs/promises';
import { writeFile } from 'node:fs/promises';
import * as path from 'node:path';
import type { BuildConfigOptions } from '@monkee/turbo-config';
import { buildConfig, generateConfigDoc } from '@monkee/turbo-config';
import type { DynamicModule, Provider } from '@nestjs/common';
import { Logger } from '@nestjs/common';
import type { JSONSchema7 } from 'json-schema';
import type { ConfigModuleOpts } from './config-module-opts';
import { defaultConfigText } from './default-config-text';

const DEFAULT_CONFIG_NAME = 'config.default.yml';

export class ConfigModule {
static forRoot(opts: ConfigModuleOpts): DynamicModule {
Expand All @@ -17,46 +23,8 @@ export class ConfigModule {
const providers: Provider[] = [
{
provide: opts.configClass,
useFactory: async () => {
const monkeeOpts: BuildConfigOptions = {
ymlFiles: opts.config.ymlFiles,
envFiles: opts.config.envFiles,
loadEnvFiles: opts.config.loadEnvFiles === true,
throwOnValidationError: false,
throwIfYmlNotExist: false,
throwIfEnvFileNotExist: false,
};

const { config, jsonSchema, validationErrors } = await buildConfig(
opts.configClass,
monkeeOpts,
);

if (opts.config.generateDocAndSchema === true) {
if (opts.config.configDocPath !== undefined) {
await generateConfigDoc(jsonSchema, {
writeToFile: opts.config.configDocPath,
});
}

if (opts.config.configSchemaPath !== undefined) {
await writeFile(
opts.config.configSchemaPath,
JSON.stringify(jsonSchema),
'utf-8',
);
}
}

if (validationErrors.length > 0) {
throw new Error(
`Config validation failed: \n${validationErrors
.map((e) => e.toString())
.join('\n')}`,
);
}

return config;
useFactory: (): Promise<object> => {
return ConfigModule.buildConfig(opts);
},
},
];
Expand All @@ -68,4 +36,124 @@ export class ConfigModule {
global,
};
}

private static async buildConfig(opts: ConfigModuleOpts): Promise<object> {
const logger = new Logger(ConfigModule.name);

let defaultConfigPath: string | undefined;
try {
defaultConfigPath = await ConfigModule.getValidatedConfigPath(
opts.config.configRootDir,
DEFAULT_CONFIG_NAME,
);
} catch (e) {
if (opts.config.generateDefaultConfigIfNotExist === true) {
logger.warn(
`Default config file not found: ${DEFAULT_CONFIG_NAME}. Generating empty config file`,
);
await ConfigModule.generateDefaultConfigFile(opts.config.configRootDir);
defaultConfigPath = await ConfigModule.getValidatedConfigPath(
opts.config.configRootDir,
DEFAULT_CONFIG_NAME,
);
} else {
throw e;
}
}

const ymlFiles = [defaultConfigPath];

if (opts.config.configEnv !== undefined) {
const envConfigPath = await ConfigModule.getValidatedConfigPath(
opts.config.configRootDir,
`config.${opts.config.configEnv}.yml`,
);

ymlFiles.push(envConfigPath);
}

const monkeeOpts: BuildConfigOptions = {
ymlFiles,
envFiles: [],
loadEnvFiles: false,
throwOnValidationError: false,
throwIfYmlNotExist: false,
throwIfEnvFileNotExist: false,
};

const { config, jsonSchema, validationErrors } = await buildConfig(
opts.configClass,
monkeeOpts,
);

if (opts.config.generateDocAndSchema === true) {
if (opts.config.configDocPath !== undefined) {
await generateConfigDoc(jsonSchema, {
writeToFile: opts.config.configDocPath,
});
}

const configSchemaPath = path.resolve(
opts.config.configRootDir,
'config.schema.json',
);

ConfigModule.makeAllJsonSchemaFieldsOptional(jsonSchema);

await writeFile(configSchemaPath, JSON.stringify(jsonSchema), 'utf-8');
}

if (validationErrors.length > 0) {
throw new Error(
`Config validation failed: \n${validationErrors
.map((e) => e.toString())
.join('\n')}`,
);
}

return config;
}

private static async getValidatedConfigPath(
rootDir: string,
fileName: string,
): Promise<string> {
const fullPath = path.resolve(rootDir, fileName);

try {
await fs.access(fullPath, fs.constants.F_OK);
} catch (e) {
throw new Error(
`Nest config file not found or access denied: ${fullPath}`,
);
}

return fullPath;
}

private static async generateDefaultConfigFile(rootDir: string) {
const defaultConfigPath = path.resolve(rootDir, DEFAULT_CONFIG_NAME);

await fs.mkdir(rootDir, { recursive: true });

await writeFile(defaultConfigPath, defaultConfigText, 'utf-8');
}

private static makeAllJsonSchemaFieldsOptional = (schema: JSONSchema7) => {
if (schema.required !== undefined) {
schema.required = undefined;
}

if (schema.properties !== undefined) {
for (const key of Object.keys(schema.properties)) {
const prop = schema.properties[key] as JSONSchema7;

if (prop.type === 'object') {
ConfigModule.makeAllJsonSchemaFieldsOptional(prop);
} else {
prop.required = undefined;
}
}
}
};
}
5 changes: 5 additions & 0 deletions src/modules/config/default-config-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const defaultConfigText = `# yaml-language-server: $schema=config.schema.json
nest:
`;
6 changes: 0 additions & 6 deletions src/starter/starter-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ export class LoggingConfig implements LoggingModuleConfig {
@ConfigField()
@IsEnum(LOG_LEVEL)
level!: LOG_LEVEL;

@ConfigField()
enableHttpRequestContext!: boolean;

@ConfigField()
enableHttpTracing!: boolean;
}

export class GracefulShutdownConfig implements GracefulShutdownModuleConfig {
Expand Down
12 changes: 5 additions & 7 deletions src/starter/starter-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ClsModule } from 'nestjs-cls';
import { ActuatorModule, ActuatorService } from '../modules/actuator';
import { ConfigModule } from '../modules/config';
import { HealthcheckModule } from '../modules/healthcheck';
import { LoggingModule } from '../modules/logging/logging.module';
import { LoggingModule } from '../modules/logging';
import { PrometheusModule, PrometheusRegistry } from '../modules/prometheus';
import type { NestStarterConfig } from './starter-config';

Expand All @@ -15,13 +15,11 @@ export const createStarterModules = <T extends NestStarterConfig>(
ConfigModule.forRoot({
configClass,
config: {
ymlFiles: [
'src/resources/default-config.yml',
'resources/default-config.yml',
],
generateDocAndSchema: process.env.NODE_ENV === 'development',
configRootDir: process.env.NEST_CONFIG_ROOT ?? 'src/resources',
configEnv: process.env.NEST_CONFIG_ENV,
generateDocAndSchema: process.env.NEST_CONFIG_GENERATE_REF === 'true',
generateDefaultConfigIfNotExist: process.env.NODE_ENV === 'development',
configDocPath: 'CONFIG_REFERENCE.MD',
configSchemaPath: 'src/resources/config-schema.json',
},
}),
ActuatorModule.forRootAsync({
Expand Down

0 comments on commit 712c4e7

Please sign in to comment.