From 96da5bd9c53ee85537527dbe60b49612cf3ca9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Wed, 1 Aug 2018 22:31:39 +0200 Subject: [PATCH] feature(@nestjs/core) add @Optional() decorator #847 --- packages/common/constants.ts | 1 + packages/common/decorators/core/index.ts | 21 +++--- .../decorators/core/optional.decorator.ts | 12 ++++ packages/common/index.ts | 68 +++++++++---------- packages/common/pipes/validation.pipe.ts | 3 +- packages/core/injector/injector.ts | 46 +++++++++---- packages/core/test/injector/injector.spec.ts | 4 +- tsconfig.json | 5 +- 8 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 packages/common/decorators/core/optional.decorator.ts diff --git a/packages/common/constants.ts b/packages/common/constants.ts index 5a9d1ecf2..bbdc7cdaa 100644 --- a/packages/common/constants.ts +++ b/packages/common/constants.ts @@ -12,6 +12,7 @@ export const GLOBAL_MODULE_METADATA = '__globalModule__'; export const PATH_METADATA = 'path'; export const PARAMTYPES_METADATA = 'design:paramtypes'; export const SELF_DECLARED_DEPS_METADATA = 'self:paramtypes'; +export const OPTIONAL_DEPS_METADATA = 'optional:paramtypes'; export const METHOD_METADATA = 'method'; export const ROUTE_ARGS_METADATA = '__routeArguments__'; export const CUSTOM_ROUTE_AGRS_METADATA = '__customRouteArgs__'; diff --git a/packages/common/decorators/core/index.ts b/packages/common/decorators/core/index.ts index b40b25d66..b899b38e0 100644 --- a/packages/common/decorators/core/index.ts +++ b/packages/common/decorators/core/index.ts @@ -1,11 +1,12 @@ -export * from './controller.decorator'; -export * from './component.decorator'; -export * from './dependencies.decorator'; -export * from './inject.decorator'; -export * from './catch.decorator'; -export * from './exception-filters.decorator'; -export * from './use-pipes.decorator'; -export * from './use-guards.decorator'; -export * from './reflect-metadata.decorator'; -export * from './use-interceptors.decorator'; export * from './bind.decorator'; +export * from './catch.decorator'; +export * from './component.decorator'; +export * from './controller.decorator'; +export * from './dependencies.decorator'; +export * from './exception-filters.decorator'; +export * from './inject.decorator'; +export * from './optional.decorator'; +export * from './reflect-metadata.decorator'; +export * from './use-guards.decorator'; +export * from './use-interceptors.decorator'; +export * from './use-pipes.decorator'; diff --git a/packages/common/decorators/core/optional.decorator.ts b/packages/common/decorators/core/optional.decorator.ts new file mode 100644 index 000000000..736e60a90 --- /dev/null +++ b/packages/common/decorators/core/optional.decorator.ts @@ -0,0 +1,12 @@ +import 'reflect-metadata'; +import { OPTIONAL_DEPS_METADATA } from '../../constants'; + +/** + * Sets dependency as an optional one. + */ +export function Optional(): ParameterDecorator { + return (target, key, index) => { + const args = Reflect.getMetadata(OPTIONAL_DEPS_METADATA, target) || []; + Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [...args, index], target); + }; +} diff --git a/packages/common/index.ts b/packages/common/index.ts index 27e0b77dd..61f76437d 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -7,39 +7,39 @@ export * from './decorators'; export * from './enums'; -export { - NestModule, - INestApplication, - INestMicroservice, - NestMiddleware, - MiddlewareFunction, - MiddlewareConsumer, - OnModuleInit, - ExceptionFilter, - WebSocketAdapter, - PipeTransform, - Paramtype, - ArgumentMetadata, - OnModuleDestroy, - ExecutionContext, - CanActivate, - RpcExceptionFilter, - WsExceptionFilter, - NestInterceptor, - DynamicModule, - INestApplicationContext, - HttpServer, - Provider, - Type, - HttpServerFactory, - ArgumentsHost, - INestExpressApplication, - INestFastifyApplication, - ForwardReference, -} from './interfaces'; -export * from './interceptors'; -export * from './services/logger.service'; -export * from './pipes'; -export * from './utils'; export * from './exceptions'; export * from './http'; +export * from './interceptors'; +export { + ArgumentMetadata, + ArgumentsHost, + CanActivate, + DynamicModule, + ExceptionFilter, + ExecutionContext, + ForwardReference, + HttpServer, + HttpServerFactory, + INestApplication, + INestApplicationContext, + INestExpressApplication, + INestFastifyApplication, + INestMicroservice, + MiddlewareConsumer, + MiddlewareFunction, + NestInterceptor, + NestMiddleware, + NestModule, + OnModuleDestroy, + OnModuleInit, + Paramtype, + PipeTransform, + Provider, + RpcExceptionFilter, + Type, + WebSocketAdapter, + WsExceptionFilter, +} from './interfaces'; +export * from './pipes'; +export * from './services/logger.service'; +export * from './utils'; diff --git a/packages/common/pipes/validation.pipe.ts b/packages/common/pipes/validation.pipe.ts index 2af1fc110..f365ef645 100644 --- a/packages/common/pipes/validation.pipe.ts +++ b/packages/common/pipes/validation.pipe.ts @@ -1,3 +1,4 @@ +import { Optional } from '../decorators'; import { ArgumentMetadata, BadRequestException } from '../index'; import { ValidatorOptions } from '../interfaces/external/validator-options.interface'; import { PipeTransform } from '../interfaces/features/pipe-transform.interface'; @@ -19,7 +20,7 @@ export class ValidationPipe implements PipeTransform { protected isDetailedOutputDisabled: boolean; protected validatorOptions: ValidatorOptions; - constructor(options?: ValidationPipeOptions) { + constructor(@Optional() options?: ValidationPipeOptions) { options = options || {}; const { transform, disableErrorMessages, ...validatorOptions } = options; this.isTransformEnabled = !!transform; diff --git a/packages/core/injector/injector.ts b/packages/core/injector/injector.ts index 8dedc60d7..46d5f2e57 100644 --- a/packages/core/injector/injector.ts +++ b/packages/core/injector/injector.ts @@ -1,4 +1,4 @@ -import { PARAMTYPES_METADATA, SELF_DECLARED_DEPS_METADATA } from '@nestjs/common/constants'; +import { OPTIONAL_DEPS_METADATA, PARAMTYPES_METADATA, SELF_DECLARED_DEPS_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'; @@ -99,7 +99,7 @@ export class Injector { await this.loadInstance(wrapper, components, module); } - public applyDoneSubject(wrapper: InstanceWrapper): () => void { + public applyDoneHook(wrapper: InstanceWrapper): () => void { let done: () => void; wrapper.done$ = new Promise((resolve, reject) => { done = resolve; @@ -116,13 +116,15 @@ export class Injector { if (wrapper.isPending) { return await wrapper.done$; } - const done = this.applyDoneSubject(wrapper); + const done = this.applyDoneHook(wrapper); const { metatype, name, inject } = wrapper; const currentMetatype = collection.get(name); if (isUndefined(currentMetatype)) { throw new RuntimeException(); } - if (currentMetatype.isResolved) return null; + if (currentMetatype.isResolved) { + return void 0; + } await this.resolveConstructorParams( wrapper, @@ -153,22 +155,34 @@ export class Injector { callback: (args) => void, ) { let isResolved = true; + const dependencies = isNil(inject) ? this.reflectConstructorParams(wrapper.metatype) : inject; + const optionalDependenciesIds = isNil(inject) + ? this.reflectOptionalParams(wrapper.metatype) + : []; const instances = await Promise.all( dependencies.map(async (param, index) => { - const paramWrapper = await this.resolveSingleParam( - wrapper, - param, - { index, dependencies }, - module, - ); - if (!paramWrapper.isResolved && !paramWrapper.forwardRef) { - isResolved = false; + try { + const paramWrapper = await this.resolveSingleParam( + wrapper, + param, + { index, dependencies }, + module, + ); + if (!paramWrapper.isResolved && !paramWrapper.forwardRef) { + isResolved = false; + } + return paramWrapper.instance; + } catch (err) { + const isOptional = optionalDependenciesIds.includes(index); + if (!isOptional) { + throw err; + } + return null; } - return paramWrapper.instance; }), ); isResolved && (await callback(instances)); @@ -182,6 +196,10 @@ export class Injector { return paramtypes; } + public reflectOptionalParams(type: Type): any[] { + return Reflect.getMetadata(OPTIONAL_DEPS_METADATA, type) || []; + } + public reflectSelfParams(type: Type): any[] { return Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, type) || []; } @@ -313,4 +331,4 @@ export class Injector { }; return modules.concat.apply(modules, modules.map(flatten)); } -} \ No newline at end of file +} diff --git a/packages/core/test/injector/injector.spec.ts b/packages/core/test/injector/injector.spec.ts index 3df35abdc..43263601b 100644 --- a/packages/core/test/injector/injector.spec.ts +++ b/packages/core/test/injector/injector.spec.ts @@ -101,7 +101,7 @@ describe('Injector', () => { expect(result).to.be.eql(value); }); - it('should return null when metatype is resolved', async () => { + it('should return undefined when metatype is resolved', async () => { const value = 'test'; const result = await injector.loadInstance( { @@ -113,7 +113,7 @@ describe('Injector', () => { moduleDeps.components, moduleDeps, ); - expect(result).to.be.null; + expect(result).to.be.undefined; }); }); diff --git a/tsconfig.json b/tsconfig.json index 77fc094f2..b632befda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,10 @@ "target": "es6", "sourceMap": true, "allowJs": true, - "outDir": "dist" + "outDir": "dist", + "lib": [ + "es7" + ] }, "include": [ "packages/**/*",