From be6d90ace2b0b2496fc4cd51619d8216e2f4a808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sun, 24 Jun 2018 13:26:55 +0200 Subject: [PATCH] feature(@nestjs/core) enable deffered dynamic modules --- integration/typeorm/e2e/typeorm-async.spec.ts | 29 ++++++++ integration/typeorm/src/app-async.module.ts | 11 +++ integration/typeorm/src/database.module.ts | 28 ++++++++ .../modules/module-metadata.interface.ts | 22 ++++-- packages/core/injector/compiler.ts | 25 ++++--- packages/core/injector/container.ts | 16 +++-- packages/core/nest-factory.ts | 41 ++++++----- packages/core/scanner.ts | 68 ++++++++++--------- packages/core/test/injector/compiler.spec.ts | 9 ++- packages/core/test/injector/container.spec.ts | 16 ++--- packages/core/test/scanner.spec.ts | 41 ++++++----- 11 files changed, 198 insertions(+), 108 deletions(-) create mode 100644 integration/typeorm/e2e/typeorm-async.spec.ts create mode 100644 integration/typeorm/src/app-async.module.ts create mode 100644 integration/typeorm/src/database.module.ts diff --git a/integration/typeorm/e2e/typeorm-async.spec.ts b/integration/typeorm/e2e/typeorm-async.spec.ts new file mode 100644 index 00000000000..cc3ad7ddea0 --- /dev/null +++ b/integration/typeorm/e2e/typeorm-async.spec.ts @@ -0,0 +1,29 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { AsyncApplicationModule } from './../src/app-async.module'; + +describe('TypeOrm (async configuration)', () => { + let server; + let app: INestApplication; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AsyncApplicationModule], + }).compile(); + + app = module.createNestApplication(); + server = app.getHttpServer(); + await app.init(); + }); + + it(`should return created entity`, () => { + return request(server) + .post('/photo') + .expect(201, { name: 'Nest', description: 'Is great!', views: 6000 }); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/integration/typeorm/src/app-async.module.ts b/integration/typeorm/src/app-async.module.ts new file mode 100644 index 00000000000..4dfd7e9442a --- /dev/null +++ b/integration/typeorm/src/app-async.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from './database.module'; +import { PhotoModule } from './photo/photo.module'; + +@Module({ + imports: [ + DatabaseModule.forRoot(), + PhotoModule, + ], +}) +export class AsyncApplicationModule {} diff --git a/integration/typeorm/src/database.module.ts b/integration/typeorm/src/database.module.ts new file mode 100644 index 00000000000..ea766899ac1 --- /dev/null +++ b/integration/typeorm/src/database.module.ts @@ -0,0 +1,28 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Photo } from './photo/photo.entity'; + +@Module({}) +export class DatabaseModule { + static async forRoot(): Promise { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return { + module: DatabaseModule, + imports: [ + TypeOrmModule.forRoot({ + type: 'mysql', + host: 'localhost', + port: 3306, + username: 'root', + password: 'root', + database: 'test', + entities: [Photo], + synchronize: true, + keepConnectionAlive: true, + retryAttempts: 2, + retryDelay: 1000, + }), + ], + }; + } +} \ No newline at end of file diff --git a/packages/common/interfaces/modules/module-metadata.interface.ts b/packages/common/interfaces/modules/module-metadata.interface.ts index 8375aadf2bb..64d372f012e 100644 --- a/packages/common/interfaces/modules/module-metadata.interface.ts +++ b/packages/common/interfaces/modules/module-metadata.interface.ts @@ -1,17 +1,25 @@ -import { NestModule } from './nest-module.interface'; -import { Controller } from '../controllers/controller.interface'; -import { DynamicModule } from './dynamic-module.interface'; import { Type } from '../type.interface'; -import { Provider } from './provider.interface'; +import { DynamicModule } from './dynamic-module.interface'; import { ForwardReference } from './forward-reference.interface'; +import { Provider } from './provider.interface'; export interface ModuleMetadata { - imports?: Array | DynamicModule | ForwardReference>; + imports?: Array< + Type | DynamicModule | Promise | ForwardReference + >; controllers?: Type[]; providers?: Provider[]; - exports?: Array; + exports?: Array< + | DynamicModule + | Promise + | string + | Provider + | ForwardReference + >; /** @deprecated */ - modules?: Array | DynamicModule | ForwardReference>; + modules?: Array< + Type | DynamicModule | Promise | ForwardReference + >; /** @deprecated */ components?: Provider[]; } diff --git a/packages/core/injector/compiler.ts b/packages/core/injector/compiler.ts index 47845b73017..ce2e8d93847 100644 --- a/packages/core/injector/compiler.ts +++ b/packages/core/injector/compiler.ts @@ -1,4 +1,4 @@ -import { Type, DynamicModule } from '@nestjs/common/interfaces'; +import { DynamicModule, Type } from '@nestjs/common/interfaces'; import { ModuleTokenFactory } from './module-token-factory'; export interface ModuleFactory { @@ -10,21 +10,22 @@ export interface ModuleFactory { export class ModuleCompiler { private readonly moduleTokenFactory = new ModuleTokenFactory(); - public compile( - metatype: Type | DynamicModule, + public async compile( + metatype: Type | DynamicModule | Promise, scope: Type[], - ): ModuleFactory { - const { type, dynamicMetadata } = this.extractMetadata(metatype); + ): Promise { + const { type, dynamicMetadata } = await this.extractMetadata(metatype); const token = this.moduleTokenFactory.create(type, scope, dynamicMetadata); return { type, dynamicMetadata, token }; } - public extractMetadata( - metatype: Type | DynamicModule, - ): { + public async extractMetadata( + metatype: Type | DynamicModule | Promise, + ): Promise<{ type: Type; dynamicMetadata?: Partial | undefined; - } { + }> { + metatype = this.isDefferedModule(metatype) ? await metatype : metatype; if (!this.isDynamicModule(metatype)) { return { type: metatype }; } @@ -37,4 +38,10 @@ export class ModuleCompiler { ): module is DynamicModule { return !!(module as DynamicModule).module; } + + public isDefferedModule( + module: Type | DynamicModule | Promise, + ): module is Promise { + return module && module instanceof Promise; + } } diff --git a/packages/core/injector/container.ts b/packages/core/injector/container.ts index 4644dad5c1a..967fa8e56c5 100644 --- a/packages/core/injector/container.ts +++ b/packages/core/injector/container.ts @@ -35,11 +35,14 @@ export class NestContainer { return this.applicationRef; } - public addModule(metatype: Type | DynamicModule, scope: Type[]) { + public async addModule( + metatype: Type | DynamicModule | Promise, + scope: Type[], + ) { if (!metatype) { throw new InvalidModuleException(scope); } - const { type, dynamicMetadata, token } = this.moduleCompiler.compile( + const { type, dynamicMetadata, token } = await this.moduleCompiler.compile( metatype, scope, ); @@ -87,7 +90,7 @@ export class NestContainer { return this.modules; } - public addRelatedModule( + public async addRelatedModule( relatedModule: Type | DynamicModule, token: string, ) { @@ -97,9 +100,10 @@ export class NestContainer { const parent = module.metatype; const scope = [].concat(module.scope, parent); - const { - token: relatedModuleToken, - } = this.moduleCompiler.compile(relatedModule, scope); + const { token: relatedModuleToken } = await this.moduleCompiler.compile( + relatedModule, + scope, + ); const related = this.modules.get(relatedModuleToken); module.addRelatedModule(related); } diff --git a/packages/core/nest-factory.ts b/packages/core/nest-factory.ts index 5a4f983ff19..ebc1af35e49 100644 --- a/packages/core/nest-factory.ts +++ b/packages/core/nest-factory.ts @@ -1,31 +1,30 @@ -import { DependenciesScanner } from './scanner'; -import { InstanceLoader } from './injector/instance-loader'; -import { NestContainer } from './injector/container'; -import { ExceptionsZone } from './errors/exceptions-zone'; -import { Logger } from '@nestjs/common/services/logger.service'; -import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; -import { messages } from './constants'; -import { NestApplication } from './nest-application'; -import { isFunction } from '@nestjs/common/utils/shared.utils'; -import { ExpressFactory } from './adapters/express-factory'; import { + HttpServer, INestApplication, - INestMicroservice, INestApplicationContext, - HttpServer, + INestMicroservice, } from '@nestjs/common'; -import { MetadataScanner } from './metadata-scanner'; -import { NestApplicationContext } from './nest-application-context'; -import { HttpsOptions } from '@nestjs/common/interfaces/external/https-options.interface'; -import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface'; +import { MicroserviceOptions } from '@nestjs/common/interfaces/microservices/microservice-configuration.interface'; import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface'; -import { ApplicationConfig } from './application-config'; -import { ExpressAdapter } from './adapters/express-adapter'; +import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface'; +import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; import { INestExpressApplication } from '@nestjs/common/interfaces/nest-express-application.interface'; -import { FastifyAdapter } from './adapters/fastify-adapter'; import { INestFastifyApplication } from '@nestjs/common/interfaces/nest-fastify-application.interface'; -import { MicroserviceOptions } from '@nestjs/common/interfaces/microservices/microservice-configuration.interface'; +import { Logger } from '@nestjs/common/services/logger.service'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; +import { isFunction } from '@nestjs/common/utils/shared.utils'; +import { ExpressAdapter } from './adapters/express-adapter'; +import { ExpressFactory } from './adapters/express-factory'; +import { FastifyAdapter } from './adapters/fastify-adapter'; +import { ApplicationConfig } from './application-config'; +import { messages } from './constants'; +import { ExceptionsZone } from './errors/exceptions-zone'; +import { NestContainer } from './injector/container'; +import { InstanceLoader } from './injector/instance-loader'; +import { MetadataScanner } from './metadata-scanner'; +import { NestApplication } from './nest-application'; +import { NestApplicationContext } from './nest-application-context'; +import { DependenciesScanner } from './scanner'; export class NestFactoryStatic { private readonly logger = new Logger('NestFactory', true); @@ -149,7 +148,7 @@ export class NestFactoryStatic { try { this.logger.log(messages.APPLICATION_START); await ExceptionsZone.asyncRun(async () => { - dependenciesScanner.scan(module); + await dependenciesScanner.scan(module); await instanceLoader.createInstancesOfDependencies(); dependenciesScanner.applyApplicationProviders(); }); diff --git a/packages/core/scanner.ts b/packages/core/scanner.ts index 5cd9daba3fc..06a8ce4959c 100644 --- a/packages/core/scanner.ts +++ b/packages/core/scanner.ts @@ -1,27 +1,27 @@ -import 'reflect-metadata'; -import { NestContainer } from './injector/container'; -import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; -import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; +import { DynamicModule } from '@nestjs/common'; import { - metadata, - GATEWAY_MIDDLEWARES, EXCEPTION_FILTERS_METADATA, + GATEWAY_MIDDLEWARES, GUARDS_METADATA, INTERCEPTORS_METADATA, + metadata, PIPES_METADATA, ROUTE_ARGS_METADATA, } from '@nestjs/common/constants'; +import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; +import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; import { Type } from '@nestjs/common/interfaces/type.interface'; -import { MetadataScanner } from '../core/metadata-scanner'; -import { DynamicModule } from '@nestjs/common'; -import { ApplicationConfig } from './application-config'; import { + isFunction, isNil, isUndefined, - isFunction, } from '@nestjs/common/utils/shared.utils'; -import { APP_INTERCEPTOR, APP_PIPE, APP_GUARD, APP_FILTER } from './constants'; +import 'reflect-metadata'; +import { MetadataScanner } from '../core/metadata-scanner'; +import { ApplicationConfig } from './application-config'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from './constants'; import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception'; +import { NestContainer } from './injector/container'; interface ApplicationProviderWrapper { moduleToken: string; @@ -36,43 +36,43 @@ export class DependenciesScanner { private readonly applicationConfig = new ApplicationConfig(), ) {} - public scan(module: Type) { - this.scanForModules(module); - this.scanModulesForDependencies(); + public async scan(module: Type) { + await this.scanForModules(module); + await this.scanModulesForDependencies(); this.container.bindGlobalScope(); } - public scanForModules( + public async scanForModules( module: Type | DynamicModule, scope: Type[] = [], ) { - this.storeModule(module, scope); + await this.storeModule(module, scope); const modules = this.reflectMetadata(module, metadata.MODULES); - modules.map(innerModule => { - this.scanForModules(innerModule, [].concat(scope, module)); - }); + for (const innerModule of modules) { + await this.scanForModules(innerModule, [].concat(scope, module)); + } } - public storeModule(module: any, scope: Type[]) { + public async storeModule(module: any, scope: Type[]) { if (module && module.forwardRef) { - return this.container.addModule(module.forwardRef(), scope); + return await this.container.addModule(module.forwardRef(), scope); } - this.container.addModule(module, scope); + await this.container.addModule(module, scope); } - public scanModulesForDependencies() { + public async scanModulesForDependencies() { const modules = this.container.getModules(); - modules.forEach(({ metatype }, token) => { - this.reflectRelatedModules(metatype, token, metatype.name); + for (const [token, { metatype }] of modules) { + await this.reflectRelatedModules(metatype, token, metatype.name); this.reflectComponents(metatype, token); this.reflectControllers(metatype, token); this.reflectExports(metatype, token); - }); + } } - public reflectRelatedModules( + public async reflectRelatedModules( module: Type, token: string, context: string, @@ -88,7 +88,9 @@ export class DependenciesScanner { metadata.IMPORTS as 'imports', ), ]; - modules.map(related => this.storeRelatedModule(related, token, context)); + for (const related of modules) { + await this.storeRelatedModule(related, token, context); + } } public reflectComponents(module: Type, token: string) { @@ -214,14 +216,18 @@ export class DependenciesScanner { return descriptor ? Reflect.getMetadata(key, descriptor.value) : undefined; } - public storeRelatedModule(related: any, token: string, context: string) { + public async storeRelatedModule( + related: any, + token: string, + context: string, + ) { if (isUndefined(related)) { throw new CircularDependencyException(context); } if (related && related.forwardRef) { - return this.container.addRelatedModule(related.forwardRef(), token); + return await this.container.addRelatedModule(related.forwardRef(), token); } - this.container.addRelatedModule(related, token); + await this.container.addRelatedModule(related, token); } public storeComponent(component, token: string) { diff --git a/packages/core/test/injector/compiler.spec.ts b/packages/core/test/injector/compiler.spec.ts index bdf30fee0a6..f938c3608dc 100644 --- a/packages/core/test/injector/compiler.spec.ts +++ b/packages/core/test/injector/compiler.spec.ts @@ -1,4 +1,3 @@ -import * as sinon from 'sinon'; import { expect } from 'chai'; import { ModuleCompiler } from '../../injector/compiler'; @@ -10,19 +9,19 @@ describe('ModuleCompiler', () => { describe('extractMetadata', () => { describe('when module is a dynamic module', () => { - it('should return object with "type" and "dynamicMetadata" property', () => { + it('should return object with "type" and "dynamicMetadata" property', async () => { const obj = { module: 'test', providers: [] }; const { module, ...dynamicMetadata } = obj; - expect(compiler.extractMetadata(obj as any)).to.be.deep.equal({ + expect(await compiler.extractMetadata(obj as any)).to.be.deep.equal({ type: module, dynamicMetadata, }); }); }); describe('when module is a not dynamic module', () => { - it('should return object with "type" property', () => { + it('should return object with "type" property', async () => { const type = 'test'; - expect(compiler.extractMetadata(type as any)).to.be.deep.equal({ + expect(await compiler.extractMetadata(type as any)).to.be.deep.equal({ type, }); }); diff --git a/packages/core/test/injector/container.spec.ts b/packages/core/test/injector/container.spec.ts index 5a3c451a0c4..72f72242764 100644 --- a/packages/core/test/injector/container.spec.ts +++ b/packages/core/test/injector/container.spec.ts @@ -1,9 +1,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { NestContainer } from '../../injector/container'; import { Module } from '../../../common/decorators/modules/module.decorator'; -import { UnknownModuleException } from '../../errors/exceptions/unknown-module.exception'; import { Global } from '../../../common/index'; +import { UnknownModuleException } from '../../errors/exceptions/unknown-module.exception'; +import { NestContainer } from '../../injector/container'; describe('NestContainer', () => { let container: NestContainer; @@ -52,24 +52,24 @@ describe('NestContainer', () => { }); describe('addModule', () => { - it('should not add module if already exists in collection', () => { + it('should not add module if already exists in collection', async () => { const modules = new Map(); const setSpy = sinon.spy(modules, 'set'); (container as any).modules = modules; - container.addModule(TestModule as any, []); - container.addModule(TestModule as any, []); + await container.addModule(TestModule as any, []); + await container.addModule(TestModule as any, []); expect(setSpy.calledOnce).to.be.true; }); it('should throws an exception when metatype is not defined', () => { - expect(() => container.addModule(undefined, [])).to.throws(); + expect(container.addModule(undefined, [])).to.eventually.throws(); }); - it('should add global module when module is global', () => { + it('should add global module when module is global', async () => { const addGlobalModuleSpy = sinon.spy(container, 'addGlobalModule'); - container.addModule(GlobalTestModule as any, []); + await container.addModule(GlobalTestModule as any, []); expect(addGlobalModuleSpy.calledOnce).to.be.true; }); }); diff --git a/packages/core/test/scanner.spec.ts b/packages/core/test/scanner.spec.ts index c5553ed47fe..3537f7d1250 100644 --- a/packages/core/test/scanner.spec.ts +++ b/packages/core/test/scanner.spec.ts @@ -1,16 +1,15 @@ -import * as sinon from 'sinon'; import { expect } from 'chai'; -import { DependenciesScanner } from './../scanner'; -import { NestContainer } from './../injector/container'; -import { Module } from '../../common/decorators/modules/module.decorator'; -import { NestModule } from '../../common/interfaces/modules/nest-module.interface'; +import * as sinon from 'sinon'; +import { GUARDS_METADATA } from '../../common/constants'; import { Component } from '../../common/decorators/core/component.decorator'; -import { UseGuards } from '../../common/decorators/core/use-guards.decorator'; import { Controller } from '../../common/decorators/core/controller.decorator'; -import { MetadataScanner } from '../metadata-scanner'; -import { GUARDS_METADATA } from '../../common/constants'; +import { UseGuards } from '../../common/decorators/core/use-guards.decorator'; +import { Module } from '../../common/decorators/modules/module.decorator'; import { ApplicationConfig } from '../application-config'; -import { APP_INTERCEPTOR, APP_GUARD, APP_PIPE, APP_FILTER } from '../constants'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '../constants'; +import { MetadataScanner } from '../metadata-scanner'; +import { NestContainer } from './../injector/container'; +import { DependenciesScanner } from './../scanner'; class Guard {} @@ -53,30 +52,30 @@ describe('DependenciesScanner', () => { mockContainer.restore(); }); - it('should "storeModule" call twice (2 modules) container method "addModule"', () => { + it('should "storeModule" call twice (2 modules) container method "addModule"', async () => { const expectation = mockContainer.expects('addModule').twice(); - scanner.scan(TestModule as any); + await scanner.scan(TestModule as any); expectation.verify(); }); - it('should "storeComponent" call twice (2 components) container method "addComponent"', () => { + it('should "storeComponent" call twice (2 components) container method "addComponent"', async () => { const expectation = mockContainer.expects('addComponent').twice(); const stub = sinon.stub(scanner, 'storeExportedComponent'); - scanner.scan(TestModule as any); + await scanner.scan(TestModule as any); expectation.verify(); stub.restore(); }); - it('should "storeRoute" call twice (2 components) container method "addController"', () => { + it('should "storeRoute" call twice (2 components) container method "addController"', async () => { const expectation = mockContainer.expects('addController').twice(); - scanner.scan(TestModule as any); + await scanner.scan(TestModule as any); expectation.verify(); }); - it('should "storeExportedComponent" call once (1 component) container method "addExportedComponent"', () => { + it('should "storeExportedComponent" call once (1 component) container method "addExportedComponent"', async () => { const expectation = mockContainer.expects('addExportedComponent').once(); - scanner.scan(TestModule as any); + await scanner.scan(TestModule as any); expectation.verify(); }); @@ -156,18 +155,18 @@ describe('DependenciesScanner', () => { }); describe('storeRelatedModule', () => { - it('should call forwardRef() when forwardRef property exists', () => { + it('should call forwardRef() when forwardRef property exists', async () => { const module = { forwardRef: sinon.stub().returns({}) }; sinon.stub(container, 'addRelatedModule').returns({}); - scanner.storeRelatedModule(module as any, [] as any, 'test'); + await scanner.storeRelatedModule(module as any, [] as any, 'test'); expect(module.forwardRef.called).to.be.true; }); describe('when "related" is nil', () => { it('should throw exception', () => { - expect(() => + expect( scanner.storeRelatedModule(undefined, [] as any, 'test'), - ).to.throws(); + ).to.eventually.throws(); }); }); });