Skip to content

Commit

Permalink
2.4.0 Add Enhanced Config Service (#190)
Browse files Browse the repository at this point in the history
* Add Enhanced Config Service (#189)

* base config service initial commit

* included files in the index component

* updated package lock file

* added unit tests for base config service

* fixed env override regex

* updated package lock file

---------

Co-authored-by: tanghel <[email protected]>

* updated package lock

* replaced references to localhost

---------

Co-authored-by: Horia Schiau <[email protected]>
  • Loading branch information
tanghel and mad2sm0key authored Nov 24, 2023
1 parent a52797e commit 2617f99
Show file tree
Hide file tree
Showing 6 changed files with 397 additions and 8 deletions.
69 changes: 62 additions & 7 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"peerDependencies": {
"@multiversx/sdk-nestjs-monitoring": "^2.0.0",
"@nestjs/common": "^10.x",
"@nestjs/config": "^3.x",
"@nestjs/core": "^10.x",
"@nestjs/swagger": "^7.x"
},
Expand Down
185 changes: 185 additions & 0 deletions packages/common/src/common/config/base.config.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class BaseConfigService {
constructor(protected readonly configService: ConfigService) { }

get<T = any>(key: string): T | undefined {
const envOverride = this.tryGetEnvOverride<T>(key);
if (envOverride || envOverride === false) {
return envOverride;
}

const configValue = this.configService.get<T>(key);

if (configValue === undefined) {
return undefined;
}

if (typeof configValue !== 'string') {
return configValue;
}

return this.tryGetEnvValue<T>(key, configValue);
}

private tryGetEnvOverride<T>(key: string): T | undefined {
const overridePrefix = 'MVX_OVERRIDE_';
const envKey = key
// Replace any non-uppercase sequence before an uppercase letter or number with that letter/number prefixed by an underscore
.replace(/([a-z])([A-Z])/g, '$1_$2')
// Handle the scenario where a group of uppercase letters is followed by a lowercase letter
.replace(/([A-Z])([A-Z])([a-z])/g, '$1_$2$3')
// Replace non-alphanumeric characters with underscores
.replace(/[^a-zA-Z0-9]/g, '_')
// Convert the whole string to uppercase
.toUpperCase();

const envValue = this.configService.get(overridePrefix + envKey);
if (!envValue) {
return undefined;
}

const strValue = this.trimPrefix(envValue, 'str:');
if (strValue) {
return strValue as T;
}

const numValue = this.trimPrefix(envValue, 'num:');
if (numValue) {
return this.parseValueAsNumber(numValue) as T;
}

const boolValue = this.trimPrefix(envValue, 'bool:');
if (boolValue) {
return this.parseValueAsBoolean(boolValue) as T;
}

const jsonValue = this.trimPrefix(envValue, 'json:');
if (jsonValue) {
return this.parseValueAsJson(jsonValue) as T;
}

const arrStrValue = this.trimPrefix(envValue, 'arr:str:');
if (arrStrValue) {
return this.parseValueAsArray(arrStrValue, 'str') as T;
}

const arrNumValue = this.trimPrefix(envValue, 'arr:num:');
if (arrNumValue) {
return this.parseValueAsArray(arrNumValue, 'num') as T;
}

return envValue as T;
}

private trimPrefix(key: string, prefix: string): string | undefined {
if (key.startsWith(prefix)) {
return key.slice(prefix.length);
}

return undefined;
}

private tryGetEnvValue<T>(key: string, value: string): T | undefined {
const regex = /\$\{([^}]+)\}/;
const match = value.match(regex);

if (!match) {
return value as T;
}

let keyName = match[1];
const keySegments = keyName.split(':');

if (keySegments.length === 0) {
throw new Error(`Could not parse config key ${key}`);
}

if (keySegments.length === 1) {
return this.configService.get(keyName);
}

keyName = keySegments.pop() as string;
if (keyName === '') {
throw new Error(`Could not parse config key ${key}`);
}

const envValue = this.configService.get(keyName);

if (keySegments[0] === 'arr') {
const expectedType = keySegments.length === 1 ? 'str' : keySegments[1];

return this.parseValueAsArray(envValue, expectedType);
}

if (envValue === undefined) {
return envValue;
}

return this.parseValue(envValue, keySegments[0]);
}

private parseValue(value: string, valueType: string): any {
switch (valueType) {
case 'str':
return value;
case 'bool':
return this.parseValueAsBoolean(value);
case 'num':
return this.parseValueAsNumber(value);
case 'json':
return this.parseValueAsJson(value);
default:
throw new Error(`Cannot parse config value ${value} as ${valueType}`);
}
}

private parseValueAsJson(value: string): any {
try {
return JSON.parse(value);
} catch (error) {
throw new Error(`Could not parse config value ${value} as json`);
}
}

private parseValueAsNumber(value: string): number {
if (value.trim() === '') {
throw new Error(`Cannot parse config value ${value} as a number`);
}

if (isNaN(Number(value))) {
throw new Error(`Cannot parse config value ${value} as a number`);
}

return Number(value);
}

private parseValueAsBoolean(value: string): boolean {
if (value === 'true') {
return true;
}

if (value === 'false') {
return false;
}

throw new Error(`Cannot parse config value ${value} as a boolean`);
}

private parseValueAsArray(value: string | undefined, valueType: string): any {
if (value === undefined || value === '') {
return [];
}

const elements = value.split(',');
if (valueType === 'str') {
return elements;
}

const result = elements.map(element => this.parseValue(element, valueType));

return result;
}
}
2 changes: 2 additions & 0 deletions packages/common/src/common/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './base.config.service';
export * from './erdnest.config.service';
2 changes: 1 addition & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export * from './pipes/parse.token.pipe';
export * from './pipes/parse.token.or.nft.pipe';
export * from './pipes/parse.transaction.hash.pipe';
export * from './common/entities/amount';
export * from './common/config/erdnest.config.service';
export * from './common/config';
export * from './common/swappable-settings';
export * from './common/logging/logging.module';
export * from './common/complexity/apply.complexity';
Expand Down
Loading

0 comments on commit 2617f99

Please sign in to comment.