import { SELF_DECLARED_DEPS_METADATA } from '@nestjs/common/constants'; import type { Constructor } from '@rsdk/common'; import { LoggerFactory } from '@rsdk/logging'; import { APP_NAME } from '../../app-metadata/app-metadata.const'; import { NoInitException } from '../../exceptions'; import type { ConfigContext } from '../context/config.context'; import { ConfigPropertyMetadataNotFound } from '../exceptions'; import { isPropertyToken } from '../metadata'; import type { ConfigSource } from '../sources'; import { ConfigSourceDIException, isReloadable } from '../sources'; import type { RawValues } from '../types'; /** * Содержит логику инициализации дополнительных источников конфигурации */ export class AdditionalSourceInitializer { /** * Промис, состояние которого отражает состояние инициализации дополнительных источников конфигурации * @private */ private initializePromise: Promise | undefined; private logger = LoggerFactory.create(AdditionalSourceInitializer); constructor( private context: ConfigContext, private options: Constructor[], ) {} /** * Инициализирует дополнительные источники конфигурации * ATTENTION: Можно вызывать сколько угодно раз, однако результат будет идентичным в буквальном смысле, * * AdditionalSourceModuleInitializer.initialize() === AdditionalSourceModuleInitializer.initialize() // true * * Это сделано намеренно из-за того как nestjs работает с разрешением зависимостей, а именно `Promise.all`. * Соответственно провайдеров конфигурации может быть много и всем им необходимо инициализировать * источники, проверка на `isInitialized` очевидно создаёт неразрешимые проблемы из-за асинхронности. */ async initialize(): Promise { if (this.initializePromise) { return this.initializePromise; } this.initializePromise = this.initializeAdditionalSources(); return this.initializePromise; } /** * Initializing additional configuration sources. * Loading values and merging them into the storage. * NOTE: standalone properties from env can be used to * configure additional sources */ private async initializeAdditionalSources(): Promise { const options = { sources: this.options }; if (!options.sources || options.sources.length === 0) { this.logger.info('No additional sources configured'); return; } const { sources } = options; const { events, vars } = this.context; const logger = this.logger; logger.info('Initializing additional configuration sources'); for (const ctor of sources) { // We should restore the order to initialize for our ctor init later const args = this.getConfigSourceArguments(ctor); const source = new ctor(...args); logger.info('Loading configuration from source', { source: source.name, }); const loaded = await source.load(); /** * Loading and merging data into storage */ const { updatedKeys } = vars.merge(loaded); if (updatedKeys.length > 0) { logger.warn(`${updatedKeys.length} values overwritten`, { keys: updatedKeys, source: source.name, }); } /** * In case the source is reloadable we subscribe to it's * 'reload' event */ if (isReloadable(source)) { logger.info('Setting up configuration reload', { source: source.name, }); source.init(); } } events.on('reload', (source: ConfigSource, newValues: RawValues) => { const { createdKeys, updatedKeys } = vars.merge(newValues); if (createdKeys.length > 0 || updatedKeys.length > 0) { events.emit('merge', source, { createdKeys, updatedKeys, }); } }); logger.info('Additional sources initialized successfully'); } /** * Returns the arguments for creating the `ConfigSource` instance * @param ctor * @private */ private getConfigSourceArguments(ctor: Constructor): unknown[] { // Injectable config properties! const metadata = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, ctor) as { index: number; param: unknown; }[]; // No metadata - no args if (!metadata) { return []; } const sortedMetadata = [...metadata].sort((a, b) => a.index - b.index); const injectables = new Map().set( APP_NAME, this.context.prefix, ); const args = sortedMetadata.map((metadata) => { if (injectables.has(metadata.param)) { return injectables.get(metadata.param); } if (!isPropertyToken(metadata.param)) { throw new ConfigSourceDIException(ctor, metadata.param); } const propMeta = this.context.standalonePropertyMetadata.get( metadata.param, ); if (!propMeta) { throw new ConfigPropertyMetadataNotFound(metadata.param); } const property = this.context.vars.extract(propMeta); if (!property) { throw new NoInitException( 'Config property', metadata.param as string, 'You probably should add @DeclareProperty decorator to source class.', ); } return property; }); return args; } }