/** * Configuration is loaded in predictable order. You should take * into account chat all repeating keys will be overwritten on each step: * * 1. .env file if there is any * 2. process environment * 3. динамические source'ы (если сделать последовательно - то в порядке указания) */ import type { Constructor } from '@rsdk/common'; import { LoggerFactory } from '@rsdk/logging'; import type { RsdkMetadataProvider } from '@rsdk/metadata'; import dotenv from 'dotenv'; import type { Config } from '../config.abstract'; import type { ConfigModuleOptions } from '../config.module'; import { ConfigPropertyMetadataNotFound } from '../exceptions'; import type { PropertyToken } from '../metadata'; import { getPropertyToken } from '../metadata'; import { ConfigMetadataProvider } from '../metadata/config-metadata.provider'; import { ReloadEvents } from '../reload'; import { assertAppNameStrategyType } from '../strategy/app-name-strategy.validator'; import type { PropertyMetadata } from '../types'; import { Vars } from '../vars.class'; export class ConfigContext { readonly events = new ReloadEvents(); readonly vars: Vars; readonly prefix: string; /** * Метадата о необходимых приложению значениях конфигурации * Зачем храним? * Потому что пайплайн такой: * 1. Собираем данные о том какие поля нам нужны * 2. Приложение запрашивает поле из конфигурации * 3. Ищем метаданные * 4. По метаданным вытягиваем значение * 5. Отдаём значение * * Это важно так как позволяет прозрачным образом контролировать этот процесс и инициализировать поля в тот момент когда они потребуются * А не в тот момент когда их объявили * Основной кейс, где это применяется - тесты * @private */ readonly standalonePropertyMetadata = new Map(); /** * Проинициализированные секции конфигов * @private */ private readonly initializedConfigs = new Map, Config>(); /** * undefined является валидным значением по умолчанию (`defaultValue`) * @private */ private readonly properties = new Map(); /** * We don't use class as context here because it is undefined if * tsc target is ES2022 */ private readonly logger = LoggerFactory.create('PlatformConfigModule'); constructor( rsdkMetadataProvider: RsdkMetadataProvider, options?: ConfigModuleOptions, ) { if (options?.appNameStrategy) { assertAppNameStrategyType(options.appNameStrategy); } /** * Should be invoked BEFORE bootstrapping nest application. * * It takes 2 main actions: * - load environment variables * * - initialize storage from options and rsdkMetadataProvider * * If any of these actions fails - application should exit. * * ATTENTION: not load values from additional source */ this.prefix = options?.appName ?? ''; if (this.prefix) { this.logger.info('Setting configuration keys prefix', { prefix: this.prefix, }); } this.vars = new Vars(this.prefix, options?.appNameStrategy); this.readEnvironment(); const properties = new ConfigMetadataProvider( rsdkMetadataProvider, ).getPropertiesMap(); /** * Reading and validating standalone properties from env. * * NOTE: Splitting extraction if standalone properties into 2 phases * is necessary, some configuration sources can need something * from env to be instatiated. */ this.extractStandaloneProperties(properties, 'expectedInEnv'); /** * NOTE: That properties from .env already loaded. * Now loading ones from other sources */ this.extractStandaloneProperties(properties, 'others'); } /** * Extracting specific instance of configuration section by * its class reference. Throws error if called before .bootstrap() * * ATTENTION: Конфиг будет разрешен с доступными на данный момент переменными, * то есть если дополнительные источники конфигурации ещё не были проинициализированы, * они не будут учтены в разрешении конфигурации. * * @param ctor Pass constructor of configuration section class * @returns T */ resolve(ctor: Constructor): T { const instance = this.initializedConfigs.get(ctor); if (!instance) { const instantiateSection = this.instantiateSection(ctor); this.initializedConfigs.set(ctor, instantiateSection); return instantiateSection; } // We know exactly it is T return instance as T; } resolveProperty(token: `STANDALONE_CONFIG_PROPERTY_${string}`): any { if (this.properties.has(token)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.properties.get(token)!; } const propMeta = this.standalonePropertyMetadata.get(token); if (!propMeta) { throw new ConfigPropertyMetadataNotFound(token); } const value = this.vars.extract(propMeta); this.properties.set(token, value); return value; } private readEnvironment(): void { this.logger.info('Loading variables from .env file if exists'); dotenv.config(); this.logger.info('Initializing raw parameters storage'); /** * Copying parameters from process.env, skipping entries * for which value is undefined (not sure it's possible, * but @types/node says it is). */ for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { this.vars.set(key, value); } } } private extractStandaloneProperties( properties: Map, subset: 'expectedInEnv' | 'others', ): void { for (const [alias, metadata] of properties.entries()) { if (subset === 'expectedInEnv' && !metadata.expectedInEnv) { continue; } this.standalonePropertyMetadata.set(getPropertyToken(alias), metadata); } } /** * Инициализирует инстанс секции конфига * ATTENTION: не проверяет есть ли ещё проинициализированные, будьте внимательны во избежание неожиданного поведение * @param ctor * @private */ private instantiateSection(ctor: Constructor): T { const instance = new ctor(this.events); this.logger.debug('Initializing configuration section instance', { ctor: ctor.name, }); this.events.on('merge', () => { instance.tryUpdate(this.vars); }); instance.read(this.vars); return instance; } }