import type { DynamicModule, INestMicroservice } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common/interfaces'; import type { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; import type { AbstractHttpAdapter } from '@nestjs/core'; import type { Constructor } from '@rsdk/common'; import { LoggerFactory } from '@rsdk/logging'; import type { RsdkMetadataProvider } from '@rsdk/metadata'; import { groupBy, intersection } from 'lodash'; import { APP_NAME_PATTERN } from './app-metadata/app-name.const'; import { InvalidAppNameException } from './app-metadata/exceptions'; import { ConfigMetadataProvider } from './config/metadata/config-metadata.provider'; import { Manifest } from './manifest/manifest'; import { assertAppName } from './app-metadata'; import type { Config, ConfigContext, SectionMetadata } from './config'; import { ContextAggregator } from './context.aggregator'; import { DuplicateProtocolException, NoMatchingTransportException, } from './exceptions'; import { NestLoggerAdapter } from './logging'; import { NoopHttpAdapter } from './noop.http-adapter'; import type { IHttpTransport, IMicroserviceTransport, PlatformAppMetadata, PlatformExtendedOptions, PlatformOptions, } from './types'; import { hasSpecifiedTransports, isHttpTransport, isMicroserviceTransport, serializePropertyMetadata, transformSectionMetadata, } from './types'; /** * Агрегат для хранения опций приложения, а также методы для конфигурации и запуска приложения */ export class PlatformContext { private readonly logger = LoggerFactory.create('App'); private readonly microservices = new Map< INestMicroservice, IMicroserviceTransport >(); private readonly httpTransport: IHttpTransport | undefined; private readonly aggregator: ContextAggregator; private _extendedOptions?: PlatformExtendedOptions; /** * Создаёт контекст проводя базовые проверки * - все транспорты совместимы между собой * - подключенные плагины могут работать с таким набором транспортов * * А также инициализирует конфигурацию согласно требованиям платформы * @param options * @param strictnessOptions */ constructor( readonly options: PlatformOptions, readonly strictnessOptions?: { // check appName on validity by regex skipAssertAppName?: boolean; // force set appName forceAppName?: string; }, ) { this.assertTransportCompatibility(options); this.assertPluginCompatibility(options); this.httpTransport = options.transports?.find?.(isHttpTransport); this.aggregator = new ContextAggregator(this); } get extendedOptions(): PlatformExtendedOptions { if (this._extendedOptions) { return this._extendedOptions; } const manifestData = Manifest.getData(this.options.manifestPath); const { name, description, version } = manifestData; const appName = this.strictnessOptions?.forceAppName ?? name; /** * TODO: Вынести в функцию. Да и вообще непонятно, почему мы делаем эту проверку * именно в геттере! Это же при создании контекста надо делать? */ const regexrResult = APP_NAME_PATTERN.exec(name); const appScope = regexrResult?.[1] ?? 'test-scope'; if (this.strictnessOptions?.skipAssertAppName) { this.logger.warn('app name asserting was skipped'); } else { assertAppName(appName); if (!regexrResult || !appScope) { throw new InvalidAppNameException(name); } } const extendedOptions: PlatformExtendedOptions = { appScope, description, name: appName, version, ...this.options, }; this._extendedOptions = extendedOptions; return this._extendedOptions; } async getRsdkMetadataProvider(): Promise { return await this.aggregator.getRsdkMetadataProvider(); } async getRoot(): Promise { return await this.aggregator.getRoot(); } /** * Get all important information about PlatformApp instance * @returns Readonly metadata */ async getMetadata(): Promise> { const rsdkMetadataProvider = await this.getRsdkMetadataProvider(); const confMetadata = new ConfigMetadataProvider( rsdkMetadataProvider, ).getMetadata(); const options = this.extendedOptions; const protocols = (options?.transports ?? []).map((x) => x.getProtocol()); return { name: options.name, version: options.version, description: options.description, protocols: protocols, config: { prefix: options.name, sections: [...confMetadata.sections.entries()].map( ([section, metadata]: [ section: Constructor, metadata: SectionMetadata, ]) => transformSectionMetadata({ ...metadata, constructorName: section.name, }), ), sources: [...confMetadata.sources.values()], properties: [...confMetadata.properties.values()].map( serializePropertyMetadata, ), }, }; } /** * Сделано прежде всего для совместимости с тестами. * Производит все необходимые проверки и инициализации транспортов, не запускает приложение */ async configureApp(app: INestApplication): Promise { const configContext = await this.getConfigContext(); const awaiter = this.httpTransport?.configureApp(app, configContext); if (awaiter instanceof Promise) { await awaiter; } app.enableShutdownHooks(); const transports = this.options.transports ?? ([] as any[]); for (const transport of transports.filter(isMicroserviceTransport)) { const awaiter = transport.init(app); if (awaiter instanceof Promise) { await awaiter; } const options = transport.createMicroserviceOptions(); const microservice = app.connectMicroservice(options, { inheritAppConfig: true, }); this.microservices.set(microservice, transport); } } /** * Сделано прежде всего для совместимости с тестами * @param app просто запускает приложение (не применяет никакую конфигурацию) */ async runConfiguredApplication(app: INestApplication): Promise { try { const configContext = await this.getConfigContext(); this.logger.info('Mapping controllers...'); if (this.httpTransport) { const { host, port } = this.httpTransport.createHttpOptions(configContext); await app.listen(port, host, () => this.logger.info(`Http server started on ${host}:${port}`), ); } else { await app.init(); } if (this.microservices.size > 0) { for (const [microservice, transport] of this.microservices) { await microservice.listen(); transport.onStart(); } } } catch (error) { this.logger.fatal(error as Error); // eslint-disable-next-line unicorn/no-process-exit process.exit(1); } } /** * Возвращает аргументы для `createNestApplication`. * Немедленно возникает вопрос, а почему бы не просто `createNestApplication` * Ответ: Потому что как минимум в тестах приложение создаётся хитрым способом не поддающемуся логике с точки зрения последовательности решений * * testingModuleRef.createApplication(...context.getNestFactoryCreateOptions()) * NestFactory.create(context.root, ...context.getNestFactoryCreateOptions()) */ async getNestFactoryCreateOptions(): Promise< [httpAdapter: AbstractHttpAdapter, options?: NestApplicationOptions] > { const configContext = await this.getConfigContext(); const httpAdapter = await this.httpTransport?.createAdapter(configContext); return [ httpAdapter ?? new NoopHttpAdapter(), { // Leave body parser configuration to httpTransport implementation bodyParser: false, logger: new NestLoggerAdapter(this.logger), }, ]; } private assertPluginCompatibility(options: PlatformOptions): void { if (!options.plugins) { return; } const protocols = (options.transports ?? []).map((x) => x.getProtocol()); for (const plugin of options.plugins) { if (!hasSpecifiedTransports(plugin)) { continue; } if (intersection(plugin.forTransports(), protocols).length === 0) { throw new NoMatchingTransportException(plugin); } } } private assertTransportCompatibility(options: PlatformOptions): void { const grouped = groupBy(options.transports, (x) => x.getProtocol()); for (const [protocol, implementations] of Object.entries(grouped)) { if (implementations.length > 1) { throw new DuplicateProtocolException(protocol, implementations.length); } } } private getConfigContext(): Promise { return this.aggregator.getConfigContext(); } }