From 62e0283098f57a00984f5f280ef13e6c9ad06717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Thu, 4 Oct 2018 16:28:51 +0200 Subject: [PATCH] feature(core) instantiate class dynamically (ModuleRef) --- package.json | 50 ++++++----- .../exceptions/invalid-class.exception.ts | 8 ++ packages/core/errors/messages.ts | 3 + packages/core/injector/container-scanner.ts | 65 +++++++++++++++ packages/core/injector/injector.ts | 12 +-- packages/core/injector/module-ref.ts | 82 +++++++++---------- packages/core/injector/module.ts | 8 ++ packages/core/nest-application-context.ts | 28 +++++-- 8 files changed, 175 insertions(+), 81 deletions(-) create mode 100644 packages/core/errors/exceptions/invalid-class.exception.ts create mode 100644 packages/core/injector/container-scanner.ts diff --git a/package.json b/package.json index 86b9e083ed9..dd3c6b35075 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,14 @@ "scripts": { "coverage": "nyc report --reporter=text-lcov | coveralls", "precommit": "lint-staged", - "test": "nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec", - "integration-test": "mocha integration/**/*.spec.ts --reporter spec --require ts-node/register", - "lint": "tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"", - "format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status", + "test": + "nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec", + "integration-test": + "mocha integration/**/*.spec.ts --reporter spec --require ts-node/register", + "lint": + "tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"", + "format": + "prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status", "build": "gulp build && gulp move", "build:lib": "gulp build --dist bundle", "postinstall": "opencollective", @@ -17,11 +21,16 @@ "prepare:rc": "npm run build:lib && npm run copy-docs", "prepare:next": "npm run build:lib && npm run copy-docs", "prepare:beta": "npm run build:lib && npm run copy-docs", - "publish": "npm run prepare && ./node_modules/.bin/lerna publish --exact -m \"chore(@nestjs) publish %s release\"", - "publish:rc": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"", - "publish:next": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"", - "publish:beta": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"", - "publish:test": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\"" + "publish": + "npm run prepare && ./node_modules/.bin/lerna publish --exact -m \"chore(@nestjs) publish %s release\"", + "publish:rc": + "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"", + "publish:next": + "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"", + "publish:beta": + "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"", + "publish:test": + "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\"" }, "engines": { "node": ">= 8.9.0" @@ -130,9 +139,7 @@ } }, "nyc": { - "include": [ - "packages/**/*.ts" - ], + "include": ["packages/**/*.ts"], "exclude": [ "node_modules/", "packages/**/*.spec.ts", @@ -147,27 +154,18 @@ "packages/microservices/microservices-module.ts", "packages/core/middleware/middleware-module.ts", "packages/core/injector/module-ref.ts", + "packages/core/injector/container-scanner.ts", "packages/common/cache/**/*", "packages/common/serializer/**/*", "packages/common/services/logger.service.ts" ], - "extension": [ - ".ts" - ], - "require": [ - "ts-node/register" - ], - "reporter": [ - "text-summary", - "html" - ], + "extension": [".ts"], + "require": ["ts-node/register"], + "reporter": ["text-summary", "html"], "sourceMap": true, "instrument": true }, "lint-staged": { - "packages/**/*.{ts,json}": [ - "npm run format", - "git add" - ] + "packages/**/*.{ts,json}": ["npm run format", "git add"] } } diff --git a/packages/core/errors/exceptions/invalid-class.exception.ts b/packages/core/errors/exceptions/invalid-class.exception.ts new file mode 100644 index 00000000000..48a601ba45d --- /dev/null +++ b/packages/core/errors/exceptions/invalid-class.exception.ts @@ -0,0 +1,8 @@ +import { INVALID_CLASS_MESSAGE } from '../messages'; +import { RuntimeException } from './runtime.exception'; + +export class InvalidClassException extends RuntimeException { + constructor(value: any) { + super(INVALID_CLASS_MESSAGE`${value}`); + } +} diff --git a/packages/core/errors/messages.ts b/packages/core/errors/messages.ts index 73a947eec23..6c221b8fac4 100644 --- a/packages/core/errors/messages.ts +++ b/packages/core/errors/messages.ts @@ -38,6 +38,9 @@ export const INVALID_MODULE_MESSAGE = (text, scope: string) => export const UNKNOWN_EXPORT_MESSAGE = (text, module: string) => `Nest cannot export a component/module that is not a part of the currently processed module (${module}). Please verify whether each exported unit is available in this particular context.`; +export const INVALID_CLASS_MESSAGE = (text, value: any) => + `ModuleRef cannot instantiate class (${value} is not constructable).`; + export const INVALID_MIDDLEWARE_CONFIGURATION = `Invalid middleware configuration passed inside the module 'configure()' method.`; export const UNKNOWN_REQUEST_MAPPING = `Request mapping properties not defined in the @RequestMapping() annotation!`; export const UNHANDLED_RUNTIME_EXCEPTION = `Unhandled Runtime Exception.`; diff --git a/packages/core/injector/container-scanner.ts b/packages/core/injector/container-scanner.ts new file mode 100644 index 00000000000..a0b2c10ae4b --- /dev/null +++ b/packages/core/injector/container-scanner.ts @@ -0,0 +1,65 @@ +import { Type } from '@nestjs/common'; +import { isFunction } from '@nestjs/common/utils/shared.utils'; +import { UnknownElementException } from '../errors/exceptions/unknown-element.exception'; +import { InstanceWrapper, NestContainer } from './container'; +import { Module } from './module'; + +export class ContainerScanner { + private flatContainer: Partial; + + constructor(private readonly container: NestContainer) {} + + public find( + typeOrToken: Type | string | symbol, + ): TResult { + this.initFlatContainer(); + return this.findInstanceByPrototypeOrToken( + typeOrToken, + this.flatContainer, + ); + } + + public findInstanceByPrototypeOrToken( + metatypeOrToken: Type | string | symbol, + contextModule: Partial, + ): TResult { + const dependencies = new Map([ + ...contextModule.components, + ...contextModule.routes, + ...contextModule.injectables, + ]); + const name = isFunction(metatypeOrToken) + ? (metatypeOrToken as Function).name + : metatypeOrToken; + const instanceWrapper = dependencies.get(name as string); + if (!instanceWrapper) { + throw new UnknownElementException(); + } + return (instanceWrapper as InstanceWrapper).instance; + } + + private initFlatContainer() { + if (this.flatContainer) { + return undefined; + } + const modules = this.container.getModules(); + const initialValue = { + components: [], + routes: [], + injectables: [], + }; + const merge = ( + initial: Map | T[], + arr: Map, + ) => [...initial, ...arr]; + + this.flatContainer = ([...modules.values()].reduce( + (current, next) => ({ + components: merge(current.components, next.components), + routes: merge(current.routes, next.routes), + injectables: merge(current.injectables, next.injectables), + }), + initialValue, + ) as any) as Partial; + } +} diff --git a/packages/core/injector/injector.ts b/packages/core/injector/injector.ts index 3508f962dc2..3dff1b7b2e4 100644 --- a/packages/core/injector/injector.ts +++ b/packages/core/injector/injector.ts @@ -120,7 +120,7 @@ export class Injector { public async loadInstance( wrapper: InstanceWrapper, - collection, + collection: Map>, module: Module, ) { if (wrapper.isPending) { @@ -129,19 +129,19 @@ export class Injector { const done = this.applyDoneHook(wrapper); const { name, inject } = wrapper; - const targetMetatype = collection.get(name); - if (isUndefined(targetMetatype)) { + const targetWrapper = collection.get(name); + if (isUndefined(targetWrapper)) { throw new RuntimeException(); } - if (targetMetatype.isResolved) { - return void 0; + if (targetWrapper.isResolved) { + return undefined; } await this.resolveConstructorParams( wrapper, module, inject, async instances => - this.instantiateClass(instances, wrapper, targetMetatype, done), + this.instantiateClass(instances, wrapper, targetWrapper, done), ); } diff --git a/packages/core/injector/module-ref.ts b/packages/core/injector/module-ref.ts index c1c49d0c49a..9ed2b1b97b9 100644 --- a/packages/core/injector/module-ref.ts +++ b/packages/core/injector/module-ref.ts @@ -1,65 +1,61 @@ import { Type } from '@nestjs/common'; -import { isFunction } from '@nestjs/common/utils/shared.utils'; -import { UnknownElementException } from '../errors/exceptions/unknown-element.exception'; -import { InstanceWrapper, NestContainer } from './container'; +import { NestContainer } from './container'; +import { ContainerScanner } from './container-scanner'; +import { Injector } from './injector'; import { Module } from './module'; export abstract class ModuleRef { - private flattenModuleFixture: Partial; + private readonly injector = new Injector(); + private readonly containerScanner: ContainerScanner; - constructor(protected readonly container: NestContainer) {} + constructor(protected readonly container: NestContainer) { + this.containerScanner = new ContainerScanner(container); + } public abstract get( typeOrToken: Type | string | symbol, options?: { strict: boolean }, ): TResult; + public abstract create(type: Type): Promise; + protected find( typeOrToken: Type | string | symbol, ): TResult { - this.initFlattenModule(); - return this.findInstanceByPrototypeOrToken( - typeOrToken, - this.flattenModuleFixture, - ); + return this.containerScanner.find(typeOrToken); + } + + protected async instantiateClass( + type: Type, + module: Module, + ): Promise { + const wrapper = { + name: type.name, + metatype: type, + instance: undefined, + isResolved: false, + }; + return new Promise(async (resolve, reject) => { + try { + await this.injector.resolveConstructorParams( + wrapper, + module, + undefined, + async instances => resolve(new type(...instances)), + ); + } catch (err) { + reject(err); + } + }); } protected findInstanceByPrototypeOrToken( metatypeOrToken: Type | string | symbol, contextModule: Partial, ): TResult { - const dependencies = new Map([ - ...contextModule.components, - ...contextModule.routes, - ...contextModule.injectables, - ]); - const name = isFunction(metatypeOrToken) - ? (metatypeOrToken as any).name - : metatypeOrToken; - const instanceWrapper = dependencies.get(name); - if (!instanceWrapper) { - throw new UnknownElementException(); - } - return (instanceWrapper as InstanceWrapper).instance; - } - - private initFlattenModule() { - if (this.flattenModuleFixture) { - return void 0; - } - const modules = this.container.getModules(); - const initialValue = { - components: [], - routes: [], - injectables: [], - }; - this.flattenModuleFixture = [...modules.values()].reduce( - (flatten, curr) => ({ - components: [...flatten.components, ...curr.components], - routes: [...flatten.routes, ...curr.routes], - injectables: [...flatten.injectables, ...curr.injectables], - }), - initialValue, - ) as any; + return this.containerScanner.findInstanceByPrototypeOrToken< + TInput, + TResult + >(metatypeOrToken, contextModule); } } diff --git a/packages/core/injector/module.ts b/packages/core/injector/module.ts index 5ed8de72c9d..e42e0d72550 100644 --- a/packages/core/injector/module.ts +++ b/packages/core/injector/module.ts @@ -13,6 +13,7 @@ import { isSymbol, isUndefined, } from '@nestjs/common/utils/shared.utils'; +import { InvalidClassException } from '../errors/exceptions/invalid-class.exception'; import { RuntimeException } from '../errors/exceptions/runtime.exception'; import { UnknownExportException } from '../errors/exceptions/unknown-export.exception'; import { ApplicationReferenceHost } from '../helpers/application-ref-host'; @@ -364,6 +365,13 @@ export class Module { self, ); } + + public async create(type: Type): Promise { + if (!(type && isFunction(type) && type.prototype)) { + throw new InvalidClassException(type); + } + return this.instantiateClass(type, self); + } }; } } diff --git a/packages/core/nest-application-context.ts b/packages/core/nest-application-context.ts index f6228a94b95..d514d0e7624 100644 --- a/packages/core/nest-application-context.ts +++ b/packages/core/nest-application-context.ts @@ -11,20 +11,20 @@ import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils'; import iterate from 'iterare'; import { UnknownModuleException } from './errors/exceptions/unknown-module.exception'; import { NestContainer } from './injector/container'; +import { ContainerScanner } from './injector/container-scanner'; import { Module } from './injector/module'; -import { ModuleRef } from './injector/module-ref'; import { ModuleTokenFactory } from './injector/module-token-factory'; -export class NestApplicationContext extends ModuleRef - implements INestApplicationContext { +export class NestApplicationContext implements INestApplicationContext { private readonly moduleTokenFactory = new ModuleTokenFactory(); + private readonly containerScanner: ContainerScanner; constructor( - container: NestContainer, + protected readonly container: NestContainer, private readonly scope: Type[], - protected contextModule: Module, + private contextModule: Module, ) { - super(container); + this.containerScanner = new ContainerScanner(container); } public selectContextModule() { @@ -168,4 +168,20 @@ export class NestApplicationContext extends ModuleRef (instance as OnApplicationBootstrap).onApplicationBootstrap, ); } + + protected find( + typeOrToken: Type | string | symbol, + ): TResult { + return this.containerScanner.find(typeOrToken); + } + + protected findInstanceByPrototypeOrToken( + metatypeOrToken: Type | string | symbol, + contextModule: Partial, + ): TResult { + return this.containerScanner.findInstanceByPrototypeOrToken< + TInput, + TResult + >(metatypeOrToken, contextModule); + } }