From 46dd65c13f6e055e376dcf29ff537e17dc8982c4 Mon Sep 17 00:00:00 2001 From: Diluka Date: Thu, 16 Apr 2020 13:25:59 +0000 Subject: [PATCH 1/2] feat(@nestjs/swagger): add `IntersectionType` helper see nestjs/graphql#776 --- lib/type-helpers/index.ts | 1 + lib/type-helpers/intersection-type.helper.ts | 53 +++++++++++ .../intersection-type.helper.spec.ts | 89 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 lib/type-helpers/intersection-type.helper.ts create mode 100644 test/type-helpers/intersection-type.helper.spec.ts diff --git a/lib/type-helpers/index.ts b/lib/type-helpers/index.ts index fba5b5df9..dbd86ed6b 100644 --- a/lib/type-helpers/index.ts +++ b/lib/type-helpers/index.ts @@ -1,3 +1,4 @@ +export * from './intersection-type.helper'; export * from './omit-type.helper'; export * from './partial-type.helper'; export * from './pick-type.helper'; diff --git a/lib/type-helpers/intersection-type.helper.ts b/lib/type-helpers/intersection-type.helper.ts new file mode 100644 index 000000000..df61b0e0f --- /dev/null +++ b/lib/type-helpers/intersection-type.helper.ts @@ -0,0 +1,53 @@ +import { Type } from '@nestjs/common'; +import { DECORATORS } from '../constants'; +import { ApiProperty } from '../decorators'; +import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; +import { + inheritTransformationMetadata, + inheritValidationMetadata +} from './type-helpers.utils'; + +const modelPropertiesAccessor = new ModelPropertiesAccessor(); + +export function IntersectionType( + classARef: Type, + classBRef: Type +): Type { + const fieldsOfA = modelPropertiesAccessor.getModelProperties( + classARef.prototype + ); + const fieldsOfB = modelPropertiesAccessor.getModelProperties( + classBRef.prototype + ); + + abstract class IntersectionTypeClass {} + inheritValidationMetadata(classARef, IntersectionTypeClass); + inheritTransformationMetadata(classARef, IntersectionTypeClass); + inheritValidationMetadata(classBRef, IntersectionTypeClass); + inheritTransformationMetadata(classBRef, IntersectionTypeClass); + + fieldsOfA.forEach((propertyKey) => { + const metadata = Reflect.getMetadata( + DECORATORS.API_MODEL_PROPERTIES, + classARef.prototype, + propertyKey + ); + const decoratorFactory = ApiProperty(metadata); + decoratorFactory(IntersectionTypeClass.prototype, propertyKey); + }); + + fieldsOfB.forEach((propertyKey) => { + const metadata = Reflect.getMetadata( + DECORATORS.API_MODEL_PROPERTIES, + classBRef.prototype, + propertyKey + ); + const decoratorFactory = ApiProperty(metadata); + decoratorFactory(IntersectionTypeClass.prototype, propertyKey); + }); + + Object.defineProperty(IntersectionTypeClass, 'name', { + value: `Intersection${classARef.name}${classBRef.name}` + }); + return IntersectionTypeClass as Type; +} diff --git a/test/type-helpers/intersection-type.helper.spec.ts b/test/type-helpers/intersection-type.helper.spec.ts new file mode 100644 index 000000000..a5c3646d0 --- /dev/null +++ b/test/type-helpers/intersection-type.helper.spec.ts @@ -0,0 +1,89 @@ +import { Type } from '@nestjs/common'; +import { classToClass, Expose, Transform } from 'class-transformer'; +import { IsString, validate } from 'class-validator'; +import { ApiProperty } from '../../lib/decorators'; +import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; +import { IntersectionType } from '../../lib/type-helpers'; +import { getValidationMetadataByTarget } from './type-helpers.test-utils'; + +describe('IntersectionType', () => { + class CreateUserDto { + @ApiProperty({ required: true }) + login: string; + + @Expose() + @Transform((str) => str + '_transformed') + @IsString() + @ApiProperty({ minLength: 10 }) + password: string; + } + + class UserDto { + @IsString() + @ApiProperty({ required: false }) + firstName: string; + } + + class UpdateUserDto extends IntersectionType(UserDto, CreateUserDto) {} + + let modelPropertiesAccessor: ModelPropertiesAccessor; + + beforeEach(() => { + modelPropertiesAccessor = new ModelPropertiesAccessor(); + }); + + describe('OpenAPI metadata', () => { + it('should return combined class', () => { + expect( + modelPropertiesAccessor.getModelProperties( + (UpdateUserDto.prototype as any) as Type + ) + ).toEqual(['firstName', 'login', 'password']); + }); + }); + describe('Validation metadata', () => { + it('should inherit metadata with all properties', () => { + const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map( + (item) => item.propertyName + ); + expect(validationKeys).toEqual(['firstName', 'password']); + }); + describe('when object does not fulfil validation rules', () => { + it('"validate" should return validation errors', async () => { + const updateDto = new UpdateUserDto(); + updateDto.password = 1234567 as any; + updateDto.firstName = 'test'; + + const validationErrors = await validate(updateDto); + + expect(validationErrors.length).toEqual(1); + expect(validationErrors[0].constraints).toEqual({ + isString: 'password must be a string' + }); + }); + }); + describe('otherwise', () => { + it('"validate" should return an empty array', async () => { + const updateDto = new UpdateUserDto(); + updateDto.login = '1234567891011'; + updateDto.password = '1234567891011'; + updateDto.firstName = '1234567891011'; + + const validationErrors = await validate(updateDto); + expect(validationErrors.length).toEqual(0); + }); + }); + }); + + describe('Transformer metadata', () => { + it('should inherit transformer metadata', () => { + const password = '1234567891011'; + const updateDto = new UpdateUserDto(); + updateDto.password = password; + updateDto.firstName = 'test'; + + const transformedDto = classToClass(updateDto); + expect(transformedDto.password).toEqual(password + '_transformed'); + }); + }); +}); From 7393bbf401fa8fb49f0e6e36ed557a72b4aac0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Fri, 17 Apr 2020 13:52:08 +0200 Subject: [PATCH 2/2] feat(): use mapped types, fix plugin integration --- lib/services/model-properties-accessor.ts | 2 +- lib/type-helpers/intersection-type.helper.ts | 18 ++- lib/type-helpers/mapped-types.utils.ts | 43 ++++++ lib/type-helpers/omit-type.helper.ts | 16 +- lib/type-helpers/partial-type.helper.ts | 17 ++- lib/type-helpers/pick-type.helper.ts | 16 +- lib/type-helpers/type-helpers.utils.ts | 140 ------------------ package-lock.json | 5 + package.json | 1 + .../intersection-type.helper.spec.ts | 74 +++------ test/type-helpers/omit-type.helper.spec.ts | 69 +++------ test/type-helpers/partial-type.helper.spec.ts | 72 +++------ test/type-helpers/pick-type.helper.spec.ts | 68 +++------ 13 files changed, 184 insertions(+), 357 deletions(-) create mode 100644 lib/type-helpers/mapped-types.utils.ts delete mode 100644 lib/type-helpers/type-helpers.utils.ts diff --git a/lib/services/model-properties-accessor.ts b/lib/services/model-properties-accessor.ts index 3a7bd31af..013e0f545 100644 --- a/lib/services/model-properties-accessor.ts +++ b/lib/services/model-properties-accessor.ts @@ -30,7 +30,7 @@ export class ModelPropertiesAccessor { } const metadata = prototype.constructor[METADATA_FACTORY_NAME](); const properties = Object.keys(metadata); - properties.forEach(key => { + properties.forEach((key) => { createApiPropertyDecorator(metadata[key], false)(classPrototype, key); }); } while ( diff --git a/lib/type-helpers/intersection-type.helper.ts b/lib/type-helpers/intersection-type.helper.ts index df61b0e0f..8b6640ff1 100644 --- a/lib/type-helpers/intersection-type.helper.ts +++ b/lib/type-helpers/intersection-type.helper.ts @@ -1,11 +1,12 @@ import { Type } from '@nestjs/common'; -import { DECORATORS } from '../constants'; -import { ApiProperty } from '../decorators'; -import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; import { inheritTransformationMetadata, inheritValidationMetadata -} from './type-helpers.utils'; +} from '@nestjs/mapped-types'; +import { DECORATORS } from '../constants'; +import { ApiProperty } from '../decorators'; +import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; +import { clonePluginMetadataFactory } from './mapped-types.utils'; const modelPropertiesAccessor = new ModelPropertiesAccessor(); @@ -26,6 +27,15 @@ export function IntersectionType( inheritValidationMetadata(classBRef, IntersectionTypeClass); inheritTransformationMetadata(classBRef, IntersectionTypeClass); + clonePluginMetadataFactory( + IntersectionTypeClass as Type, + classARef.prototype + ); + clonePluginMetadataFactory( + IntersectionTypeClass as Type, + classBRef.prototype + ); + fieldsOfA.forEach((propertyKey) => { const metadata = Reflect.getMetadata( DECORATORS.API_MODEL_PROPERTIES, diff --git a/lib/type-helpers/mapped-types.utils.ts b/lib/type-helpers/mapped-types.utils.ts new file mode 100644 index 000000000..60589e2d4 --- /dev/null +++ b/lib/type-helpers/mapped-types.utils.ts @@ -0,0 +1,43 @@ +import { Type } from '@nestjs/common'; +import { identity } from 'lodash'; +import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; + +export function clonePluginMetadataFactory( + target: Type, + parent: Type, + transformFn: (metadata: Record) => Record = identity +) { + let targetMetadata = {}; + + do { + if (!parent.constructor) { + return; + } + if (!parent.constructor[METADATA_FACTORY_NAME]) { + continue; + } + const parentMetadata = parent.constructor[METADATA_FACTORY_NAME](); + targetMetadata = { + ...parentMetadata, + ...targetMetadata + }; + } while ( + (parent = Reflect.getPrototypeOf(parent) as Type) && + parent !== Object.prototype && + parent + ); + targetMetadata = transformFn(targetMetadata); + + if (target[METADATA_FACTORY_NAME]) { + const originalFactory = target[METADATA_FACTORY_NAME]; + target[METADATA_FACTORY_NAME] = () => { + const originalMetadata = originalFactory(); + return { + ...originalMetadata, + ...targetMetadata + }; + }; + } else { + target[METADATA_FACTORY_NAME] = () => targetMetadata; + } +} diff --git a/lib/type-helpers/omit-type.helper.ts b/lib/type-helpers/omit-type.helper.ts index 8de59d2aa..c82ad039b 100644 --- a/lib/type-helpers/omit-type.helper.ts +++ b/lib/type-helpers/omit-type.helper.ts @@ -1,11 +1,13 @@ import { Type } from '@nestjs/common'; -import { DECORATORS } from '../constants'; -import { ApiProperty } from '../decorators'; -import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; import { inheritTransformationMetadata, inheritValidationMetadata -} from './type-helpers.utils'; +} from '@nestjs/mapped-types'; +import { omit } from 'lodash'; +import { DECORATORS } from '../constants'; +import { ApiProperty } from '../decorators'; +import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; +import { clonePluginMetadataFactory } from './mapped-types.utils'; const modelPropertiesAccessor = new ModelPropertiesAccessor(); @@ -24,6 +26,12 @@ export function OmitType( inheritValidationMetadata(classRef, OmitTypeClass, isInheritedPredicate); inheritTransformationMetadata(classRef, OmitTypeClass, isInheritedPredicate); + clonePluginMetadataFactory( + OmitTypeClass as Type, + classRef.prototype, + (metadata: Record) => omit(metadata, keys) + ); + fields.forEach((propertyKey) => { const metadata = Reflect.getMetadata( DECORATORS.API_MODEL_PROPERTIES, diff --git a/lib/type-helpers/partial-type.helper.ts b/lib/type-helpers/partial-type.helper.ts index a71f31e1e..425b163c7 100644 --- a/lib/type-helpers/partial-type.helper.ts +++ b/lib/type-helpers/partial-type.helper.ts @@ -1,12 +1,14 @@ import { Type } from '@nestjs/common'; -import { DECORATORS } from '../constants'; -import { ApiProperty } from '../decorators'; -import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; import { applyIsOptionalDecorator, inheritTransformationMetadata, inheritValidationMetadata -} from './type-helpers.utils'; +} from '@nestjs/mapped-types'; +import { mapValues } from 'lodash'; +import { DECORATORS } from '../constants'; +import { ApiProperty } from '../decorators'; +import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; +import { clonePluginMetadataFactory } from './mapped-types.utils'; const modelPropertiesAccessor = new ModelPropertiesAccessor(); @@ -17,6 +19,13 @@ export function PartialType(classRef: Type): Type> { inheritValidationMetadata(classRef, PartialTypeClass); inheritTransformationMetadata(classRef, PartialTypeClass); + clonePluginMetadataFactory( + PartialTypeClass as Type, + classRef.prototype, + (metadata: Record) => + mapValues(metadata, (item) => ({ ...item, required: false })) + ); + fields.forEach((key) => { const metadata = Reflect.getMetadata( diff --git a/lib/type-helpers/pick-type.helper.ts b/lib/type-helpers/pick-type.helper.ts index 546937ad5..34145207e 100644 --- a/lib/type-helpers/pick-type.helper.ts +++ b/lib/type-helpers/pick-type.helper.ts @@ -1,11 +1,13 @@ import { Type } from '@nestjs/common'; -import { DECORATORS } from '../constants'; -import { ApiProperty } from '../decorators'; -import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; import { inheritTransformationMetadata, inheritValidationMetadata -} from './type-helpers.utils'; +} from '@nestjs/mapped-types'; +import { pick } from 'lodash'; +import { DECORATORS } from '../constants'; +import { ApiProperty } from '../decorators'; +import { ModelPropertiesAccessor } from '../services/model-properties-accessor'; +import { clonePluginMetadataFactory } from './mapped-types.utils'; const modelPropertiesAccessor = new ModelPropertiesAccessor(); @@ -24,6 +26,12 @@ export function PickType( inheritValidationMetadata(classRef, PickTypeClass, isInheritedPredicate); inheritTransformationMetadata(classRef, PickTypeClass, isInheritedPredicate); + clonePluginMetadataFactory( + PickTypeClass as Type, + classRef.prototype, + (metadata: Record) => pick(metadata, keys) + ); + fields.forEach((propertyKey) => { const metadata = Reflect.getMetadata( DECORATORS.API_MODEL_PROPERTIES, diff --git a/lib/type-helpers/type-helpers.utils.ts b/lib/type-helpers/type-helpers.utils.ts deleted file mode 100644 index fba9426eb..000000000 --- a/lib/type-helpers/type-helpers.utils.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Logger, Type } from '@nestjs/common'; - -/* eslint-disable @typescript-eslint/no-var-requires */ -const logger = new Logger('TypeHelpers'); - -export function applyIsOptionalDecorator( - targetClass: Function, - propertyKey: string -) { - if (!isClassValidatorAvailable()) { - return; - } - const classValidator: typeof import('class-validator') = require('class-validator'); - const decoratorFactory = classValidator.IsOptional(); - decoratorFactory(targetClass.prototype, propertyKey); -} - -export function inheritValidationMetadata( - parentClass: Type, - targetClass: Function, - isPropertyInherited?: (key: string) => boolean -) { - if (!isClassValidatorAvailable()) { - return; - } - try { - const classValidator: typeof import('class-validator') = require('class-validator'); - const metadataStorage = classValidator.getFromContainer( - classValidator.MetadataStorage - ); - const targetMetadata = metadataStorage.getTargetValidationMetadatas( - parentClass, - null - ); - targetMetadata - .filter( - ({ propertyName }) => - !isPropertyInherited || isPropertyInherited(propertyName) - ) - .forEach((value) => - metadataStorage.addValidationMetadata({ - ...value, - target: targetClass - }) - ); - } catch (err) { - logger.error( - `Validation ("class-validator") metadata cannot be inherited for "${parentClass.name}" class.` - ); - logger.error(err); - } -} - -type TransformMetadataKey = - | '_excludeMetadatas' - | '_exposeMetadatas' - | '_typeMetadatas' - | '_transformMetadatas'; - -export function inheritTransformationMetadata( - parentClass: Type, - targetClass: Function, - isPropertyInherited?: (key: string) => boolean -) { - if (!isClassTransformerAvailable()) { - return; - } - try { - const transformMetadataKeys: TransformMetadataKey[] = [ - '_excludeMetadatas', - '_exposeMetadatas', - '_transformMetadatas', - '_typeMetadatas' - ]; - transformMetadataKeys.forEach((key) => - inheritTransformerMetadata( - key, - parentClass, - targetClass, - isPropertyInherited - ) - ); - } catch (err) { - logger.error( - `Transformer ("class-transformer") metadata cannot be inherited for "${parentClass.name}" class.` - ); - logger.error(err); - } -} - -function inheritTransformerMetadata( - key: TransformMetadataKey, - parentClass: Type, - targetClass: Function, - isPropertyInherited?: (key: string) => boolean -) { - const classTransformer: typeof import('class-transformer/storage') = require('class-transformer/storage'); - const metadataStorage = classTransformer.defaultMetadataStorage; - - if (metadataStorage[key].has(parentClass)) { - const metadataMap = metadataStorage[key] as Map>; - const parentMetadata = metadataMap.get(parentClass); - - const targetMetadataEntries: Iterable<[string, any]> = Array.from( - parentMetadata.entries() - ) - .filter(([key]) => !isPropertyInherited || isPropertyInherited(key)) - .map(([key, metadata]) => { - if (Array.isArray(metadata)) { - // "_transformMetadatas" is an array of elements - const targetMetadata = metadata.map((item) => ({ - ...item, - target: targetClass - })); - return [key, targetMetadata]; - } - return [key, { ...metadata, target: targetClass }]; - }); - - metadataMap.set(targetClass, new Map(targetMetadataEntries)); - } -} - -function isClassValidatorAvailable() { - try { - require('class-validator'); - return true; - } catch { - return false; - } -} - -function isClassTransformerAvailable() { - try { - require('class-transformer'); - return true; - } catch { - return false; - } -} diff --git a/package-lock.json b/package-lock.json index 1cf0ccf60..9f7ff7209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1473,6 +1473,11 @@ } } }, + "@nestjs/mapped-types": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.0.1.tgz", + "integrity": "sha512-4G4Ui7Sj0UqXiZsUFk/6cPD3K7uZEFSElzkOftaJ3/lXW+HUi1/vfWXabF53qrzO1enTRQDxt1plDbP6SsqXEg==" + }, "@nestjs/platform-express": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-7.0.8.tgz", diff --git a/package.json b/package.json index 563a870cf..6b9e28840 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "release": "release-it" }, "dependencies": { + "@nestjs/mapped-types": "0.0.1", "lodash": "4.17.15", "path-to-regexp": "3.2.0" }, diff --git a/test/type-helpers/intersection-type.helper.spec.ts b/test/type-helpers/intersection-type.helper.spec.ts index a5c3646d0..19911f8b7 100644 --- a/test/type-helpers/intersection-type.helper.spec.ts +++ b/test/type-helpers/intersection-type.helper.spec.ts @@ -1,10 +1,10 @@ import { Type } from '@nestjs/common'; -import { classToClass, Expose, Transform } from 'class-transformer'; -import { IsString, validate } from 'class-validator'; +import { Expose, Transform } from 'class-transformer'; +import { IsString } from 'class-validator'; import { ApiProperty } from '../../lib/decorators'; +import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; import { IntersectionType } from '../../lib/type-helpers'; -import { getValidationMetadataByTarget } from './type-helpers.test-utils'; describe('IntersectionType', () => { class CreateUserDto { @@ -16,12 +16,20 @@ describe('IntersectionType', () => { @IsString() @ApiProperty({ minLength: 10 }) password: string; + + static [METADATA_FACTORY_NAME]() { + return { dateOfBirth: { required: true, type: () => String } }; + } } class UserDto { @IsString() @ApiProperty({ required: false }) firstName: string; + + static [METADATA_FACTORY_NAME]() { + return { dateOfBirth2: { required: true, type: () => String } }; + } } class UpdateUserDto extends IntersectionType(UserDto, CreateUserDto) {} @@ -34,56 +42,16 @@ describe('IntersectionType', () => { describe('OpenAPI metadata', () => { it('should return combined class', () => { - expect( - modelPropertiesAccessor.getModelProperties( - (UpdateUserDto.prototype as any) as Type - ) - ).toEqual(['firstName', 'login', 'password']); - }); - }); - describe('Validation metadata', () => { - it('should inherit metadata with all properties', () => { - const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map( - (item) => item.propertyName - ); - expect(validationKeys).toEqual(['firstName', 'password']); - }); - describe('when object does not fulfil validation rules', () => { - it('"validate" should return validation errors', async () => { - const updateDto = new UpdateUserDto(); - updateDto.password = 1234567 as any; - updateDto.firstName = 'test'; - - const validationErrors = await validate(updateDto); - - expect(validationErrors.length).toEqual(1); - expect(validationErrors[0].constraints).toEqual({ - isString: 'password must be a string' - }); - }); - }); - describe('otherwise', () => { - it('"validate" should return an empty array', async () => { - const updateDto = new UpdateUserDto(); - updateDto.login = '1234567891011'; - updateDto.password = '1234567891011'; - updateDto.firstName = '1234567891011'; - - const validationErrors = await validate(updateDto); - expect(validationErrors.length).toEqual(0); - }); - }); - }); - - describe('Transformer metadata', () => { - it('should inherit transformer metadata', () => { - const password = '1234567891011'; - const updateDto = new UpdateUserDto(); - updateDto.password = password; - updateDto.firstName = 'test'; - - const transformedDto = classToClass(updateDto); - expect(transformedDto.password).toEqual(password + '_transformed'); + const prototype = (UpdateUserDto.prototype as any) as Type; + + modelPropertiesAccessor.applyMetadataFactory(prototype); + expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ + 'firstName', + 'login', + 'password', + 'dateOfBirth2', + 'dateOfBirth' + ]); }); }); }); diff --git a/test/type-helpers/omit-type.helper.spec.ts b/test/type-helpers/omit-type.helper.spec.ts index f48c8684b..e98b8eb3d 100644 --- a/test/type-helpers/omit-type.helper.spec.ts +++ b/test/type-helpers/omit-type.helper.spec.ts @@ -1,10 +1,10 @@ import { Type } from '@nestjs/common'; -import { classToClass, Transform } from 'class-transformer'; -import { MinLength, validate } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { MinLength } from 'class-validator'; import { ApiProperty } from '../../lib/decorators'; +import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; import { OmitType } from '../../lib/type-helpers'; -import { getValidationMetadataByTarget } from './type-helpers.test-utils'; describe('OmitType', () => { class CreateUserDto { @@ -16,9 +16,18 @@ describe('OmitType', () => { @MinLength(10) @ApiProperty({ minLength: 10 }) password: string; + + lastName: string; + + static [METADATA_FACTORY_NAME]() { + return { + firstName: { required: true, type: () => String }, + lastName: { required: true, type: () => String } + }; + } } - class UpdateUserDto extends OmitType(CreateUserDto, ['login']) {} + class UpdateUserDto extends OmitType(CreateUserDto, ['login', 'lastName']) {} let modelPropertiesAccessor: ModelPropertiesAccessor; @@ -28,53 +37,13 @@ describe('OmitType', () => { describe('OpenAPI metadata', () => { it('should omit "login" property', () => { - expect( - modelPropertiesAccessor.getModelProperties( - (UpdateUserDto.prototype as any) as Type - ) - ).toEqual(['password']); - }); - }); - - describe('Validation metadata', () => { - it('should inherit metadata with "login" property excluded', () => { - const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map( - (item) => item.propertyName - ); - expect(validationKeys).toEqual(['password']); - }); - describe('when object does not fulfil validation rules', () => { - it('"validate" should return validation errors', async () => { - const updateDto = new UpdateUserDto(); - updateDto.password = '1234567'; - - const validationErrors = await validate(updateDto); - - expect(validationErrors.length).toEqual(1); - expect(validationErrors[0].constraints).toEqual({ - minLength: 'password must be longer than or equal to 10 characters' - }); - }); - }); - describe('otherwise', () => { - it('"validate" should return an empty array', async () => { - const updateDto = new UpdateUserDto(); - updateDto.password = '1234567891011'; - - const validationErrors = await validate(updateDto); - expect(validationErrors.length).toEqual(0); - }); - }); - }); - - describe('Transformer metadata', () => { - it('should inherit transformer metadata', () => { - const password = '1234567891011'; - const updateDto = new UpdateUserDto(); - updateDto.password = password; + const prototype = (UpdateUserDto.prototype as any) as Type; - const transformedDto = classToClass(updateDto); - expect(transformedDto.password).toEqual(password + '_transformed'); + modelPropertiesAccessor.applyMetadataFactory(prototype); + expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ + 'password', + 'firstName' + ]); }); }); }); diff --git a/test/type-helpers/partial-type.helper.spec.ts b/test/type-helpers/partial-type.helper.spec.ts index 62d13471e..2d77a6b39 100644 --- a/test/type-helpers/partial-type.helper.spec.ts +++ b/test/type-helpers/partial-type.helper.spec.ts @@ -1,11 +1,11 @@ import { Type } from '@nestjs/common'; -import { classToClass, Expose, Transform } from 'class-transformer'; -import { IsString, validate } from 'class-validator'; +import { Expose, Transform } from 'class-transformer'; +import { IsString } from 'class-validator'; import { DECORATORS } from '../../lib/constants'; import { ApiProperty } from '../../lib/decorators'; +import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; import { PartialType } from '../../lib/type-helpers'; -import { getValidationMetadataByTarget } from './type-helpers.test-utils'; describe('PartialType', () => { class CreateUserDto { @@ -17,6 +17,13 @@ describe('PartialType', () => { @IsString() @ApiProperty({ minLength: 10 }) password: string; + + static [METADATA_FACTORY_NAME]() { + return { + firstName: { required: true, type: String }, + lastName: { required: true, type: String } + }; + } } class UpdateUserDto extends PartialType(CreateUserDto) {} @@ -29,11 +36,15 @@ describe('PartialType', () => { describe('OpenAPI metadata', () => { it('should return partial class', () => { - expect( - modelPropertiesAccessor.getModelProperties( - (UpdateUserDto.prototype as any) as Type - ) - ).toEqual(['login', 'password']); + const prototype = (UpdateUserDto.prototype as any) as Type; + + modelPropertiesAccessor.applyMetadataFactory(prototype); + expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ + 'login', + 'password', + 'firstName', + 'lastName' + ]); }); it('should set "required" option to "false" for each property', () => { @@ -46,6 +57,7 @@ describe('PartialType', () => { key ); }); + expect(metadata[0]).toEqual({ isArray: false, required: false, @@ -57,47 +69,11 @@ describe('PartialType', () => { minLength: 10, type: String }); - }); - }); - describe('Validation metadata', () => { - it('should inherit metadata and apply @IsOptional() to each property', () => { - const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map( - (item) => item.propertyName - ); - expect(validationKeys).toEqual(['password', 'login', 'password']); - }); - describe('when object does not fulfil validation rules', () => { - it('"validate" should return validation errors', async () => { - const updateDto = new UpdateUserDto(); - updateDto.password = 1234567 as any; - - const validationErrors = await validate(updateDto); - - expect(validationErrors.length).toEqual(1); - expect(validationErrors[0].constraints).toEqual({ - isString: 'password must be a string' - }); - }); - }); - describe('otherwise', () => { - it('"validate" should return an empty array', async () => { - const updateDto = new UpdateUserDto(); - updateDto.login = '1234567891011'; - - const validationErrors = await validate(updateDto); - expect(validationErrors.length).toEqual(0); + expect(metadata[2]).toEqual({ + isArray: false, + required: false, + type: String }); }); }); - - describe('Transformer metadata', () => { - it('should inherit transformer metadata', () => { - const password = '1234567891011'; - const updateDto = new UpdateUserDto(); - updateDto.password = password; - - const transformedDto = classToClass(updateDto); - expect(transformedDto.password).toEqual(password + '_transformed'); - }); - }); }); diff --git a/test/type-helpers/pick-type.helper.spec.ts b/test/type-helpers/pick-type.helper.spec.ts index 11439b81d..979728ee5 100644 --- a/test/type-helpers/pick-type.helper.spec.ts +++ b/test/type-helpers/pick-type.helper.spec.ts @@ -1,10 +1,10 @@ import { Type } from '@nestjs/common'; -import { classToClass, Transform } from 'class-transformer'; -import { MinLength, validate } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { MinLength } from 'class-validator'; import { ApiProperty } from '../../lib/decorators'; +import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor'; import { PickType } from '../../lib/type-helpers'; -import { getValidationMetadataByTarget } from './type-helpers.test-utils'; describe('PickType', () => { class CreateUserDto { @@ -16,9 +16,18 @@ describe('PickType', () => { @MinLength(10) @ApiProperty({ minLength: 10 }) password: string; + + firstName: string; + + static [METADATA_FACTORY_NAME]() { + return { + firstName: { required: true, type: () => String }, + lastName: { required: true, type: () => String } + }; + } } - class UpdateUserDto extends PickType(CreateUserDto, ['login']) {} + class UpdateUserDto extends PickType(CreateUserDto, ['login', 'firstName']) {} let modelPropertiesAccessor: ModelPropertiesAccessor; @@ -28,52 +37,13 @@ describe('PickType', () => { describe('OpenAPI metadata', () => { it('should pick "login" property', () => { - expect( - modelPropertiesAccessor.getModelProperties( - (UpdateUserDto.prototype as any) as Type - ) - ).toEqual(['login']); - }); - }); - describe('Validation metadata', () => { - it('should inherit metadata with "password" property excluded', () => { - const validationKeys = getValidationMetadataByTarget(UpdateUserDto).map( - (item) => item.propertyName - ); - expect(validationKeys).toEqual(['login']); - }); - describe('when object does not fulfil validation rules', () => { - it('"validate" should return validation errors', async () => { - const updateDto = new UpdateUserDto(); - updateDto.login = '1234567'; - - const validationErrors = await validate(updateDto); - - expect(validationErrors.length).toEqual(1); - expect(validationErrors[0].constraints).toEqual({ - minLength: 'login must be longer than or equal to 10 characters' - }); - }); - }); - describe('otherwise', () => { - it('"validate" should return an empty array', async () => { - const updateDto = new UpdateUserDto(); - updateDto.login = '1234567891011'; - - const validationErrors = await validate(updateDto); - expect(validationErrors.length).toEqual(0); - }); - }); - }); - - describe('Transformer metadata', () => { - it('should inherit transformer metadata', () => { - const login = '1234567891011'; - const updateDto = new UpdateUserDto(); - updateDto.login = login; + const prototype = (UpdateUserDto.prototype as any) as Type; - const transformedDto = classToClass(updateDto); - expect(transformedDto.login).toEqual(login + '_transformed'); + modelPropertiesAccessor.applyMetadataFactory(prototype); + expect(modelPropertiesAccessor.getModelProperties(prototype)).toEqual([ + 'login', + 'firstName' + ]); }); }); });