From d724557a5a88cdce5f83d806803c0a6da558a689 Mon Sep 17 00:00:00 2001 From: Ugo evola Date: Mon, 26 Sep 2022 18:11:39 +0200 Subject: [PATCH] v3.1.0 --- CHANGELOG.md | 8 +++ README.md | 68 ++++++++++++------- package.json | 2 +- ...s-mappings.ts => get-options-functions.ts} | 0 src/core/mapping.ts | 9 ++- src/utils/utils.ts | 14 +++- test/simple/simple.dto.ts | 6 +- test/simple/simple.entity.ts | 14 ++-- 8 files changed, 79 insertions(+), 42 deletions(-) rename src/core/{get-options-mappings.ts => get-options-functions.ts} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0712d79..08124ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# [3.1.0](https://github.com/ugoevola/ts-mapstruct/releases/tag/v3.1.0) + +### Added +- You have now 2 solutions to expose properies : +> - with @Expose decorator of class-transformer +> - Come back of getters / setters (from v1.0.1). If you define a getter or a setter for a property, it will be exposed. # [3.0.3](https://github.com/ugoevola/ts-mapstruct/releases/tag/v3.0.3) ### Added @@ -29,6 +35,8 @@ Now, all the selection calculations are done when the mapper is instantiated, an - parameters with underscors are now recognized # [2.0.0](https://github.com/ugoevola/ts-mapstruct/releases/tag/v2.0.0) > **NOTE**: This version is deprecated. +### Breaking Changed +- The getters / setters have no more influence to map an object. It is now necessary to expose each attribute. ### Added - **@BeforeMapping**: perform some actions before the mapping process - **@AfterMapping**: perform some actions after the mapping process diff --git a/README.md b/README.md index 7e05897..075622c 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,6 @@ But it can be used in any typescript project. ## Usage For the exemple, I will take a **UserMapper** that maps a **UserDto** into **UserEntity**. ### Classes -You must expose the properties of the target class by using an apropriate decorator.
-Otherwise, you will retrieve an empty Object, and probably have MapperExceptions.
-In this example, I'm using @Expose() decorator of the [class-tranformer library](https://www.npmjs.com/package/class-transformer#enforcing-type-safe-instance) ```ts export class UserDto { @Expose() private fname: string; @@ -79,6 +76,10 @@ export class FriendEntity { } } ``` +> **Note**: You must expose the properties of the target class by using an apropriate decorator.
+> Otherwise, you will retrieve an empty Object, and probably have MapperExceptions.
+> - In this example, I'm using @Expose() decorator of the [class-tranformer library](https://www.npmjs.com/package/class-transformer#enforcing-type-safe-instance) +> - You can also define well named getters/setters for properties. ### Mapper ```ts @@ -120,7 +121,8 @@ export class UserMapper { ``` ### Usage ```ts -@Mapper() +// NestJs (decorate your mapper with @Injectable) +@Injectable() export class UserService { constructor(private userMapper: UserMapper) {} @@ -133,6 +135,13 @@ export class UserService { } } ``` +```ts +// TypeScript +const userDto = new UserDto() +//... +const userMapper = new UserMapper() +const userEntity = userMapper.entityFromDto(userDto) +``` ### Type conversion The TS code is trans-compiled in JS before being executed, so the types of the source objects are kept on the end object. @@ -156,18 +165,18 @@ The library allows you to define the targeted type for each property: If you have multiple depths in your object, you can target the right property with the right type like this: ```ts @Mappings( - { - target: 'bestFriend', - expression: 'getBestFriend(userDto.friends)', - type: FriendEntity - }, - { target: 'friends', type: FriendEntity }, - { target: 'friends.bdate', type: Date }, - { target: 'bestFriend.bdate', type: Date } - ) - entityFromDto(_userDto: UserDto): UserEntity { - return new UserEntity; - } + { + target: 'bestFriend', + expression: 'getBestFriend(userDto.friends)', + type: FriendEntity + }, + { target: 'friends', type: FriendEntity }, + { target: 'friends.bdate', type: Date }, + { target: 'bestFriend.bdate', type: Date } +) +entityFromDto(_userDto: UserDto): UserEntity { + return new UserEntity; +} ``` Below are examples of options that may exist: @@ -270,8 +279,7 @@ export class UserMapper { } ``` - -Note: if you return object from your @AfterMapping or @BeforeMapping function, it will not be considered. +> **Note**: if you return object from your @AfterMapping or @BeforeMapping function, it will not be considered. ### @MappingTarget The MappingTarget allows you to pass the resulting object throw the methods to perform some actions on it. @@ -328,7 +336,7 @@ export class UserMapper { } ``` -> **Notes**: @MappingTarget is not used in the same way depending on the type of method in which it is used: +> **Note**: @MappingTarget is not used in the same way depending on the type of method in which it is used: > - In an @BeforeMapping method, the argument bound to the @MappingTarget decorator must also be found in the mapping method. Otherwise @BeforeMapping will not be invoked. > - In an @AfterMapping method, the argument bound to the @MappingTarget does not have to be in the mapping method. However, you must provide the return type of the mapping method for the @AfterMapping method to be invoked. @@ -376,7 +384,9 @@ The thrown exceptions are extends of the HttpException of nestjs/common. Injectable() export class UserMapper { - // this will throw a BadExpressionExceptionMapper because the expression for fullName can't be evaluated (unknownMethod does not exist) + // this will throw a BadExpressionExceptionMapper + // because the expression for fullName + // can't be evaluated (unknownMethod does not exist) @Mappings( { target: 'fullName', expression: 'unknownMethod()' } ) @@ -392,7 +402,8 @@ export class UserMapper { Injectable() export class UserMapper { - // This will throw an IllegalArgumentNameExceptionMapper because getConcatProperties is a reserved name used for supplied mapping funcions + // This will throw an IllegalArgumentNameExceptionMapper + // because getConcatProperties is a reserved name used for supplied mapping funcions // All supplied mapping function name are forbidden for naming the arguments. // cf. Supplied Mapping Functions // this exception is thrown as soon as there is an expression in one provided MappingOptions @@ -411,7 +422,9 @@ export class UserMapper { Injectable() export class UserMapper { - // this will throw an InvalidMappingOptionsExceptionMapper because you provide multiple sources (value and source) for cn in one MappingOption + // this will throw an InvalidMappingOptionsExceptionMapper + // because you provide multiple sources (value and source) + // for cn in one MappingOption @Mappings( { target: 'cn', value: 'Ugo', source: 'userDto.fname' } ) @@ -427,8 +440,9 @@ export class UserMapper { Injectable() export class UserMapper { - // this will throw an InvalidMappingTargetExceptionMapper because - // the provided @MappingTarget object does not have the type of the returned mapping function + // this will throw an InvalidMappingTargetExceptionMapper + // because the provided @MappingTarget object + // does not have the type of the returned mapping function @Mappings() invalidMappingTargetExceptionMapper (@MappingTarget() _userDto: UserDto): UserEntity { return new UserEntity() @@ -442,7 +456,8 @@ export class UserMapper { Injectable() export class UserMapper { - // this will throw an InvalidSourceExceptionMapper because userDto.unknownProperty does not exist + // this will throw an InvalidSourceExceptionMapper + // because userDto.unknownProperty does not exist @Mappings( { target: 'cn', source: 'userDto.unknownProperty' } ) @@ -458,7 +473,8 @@ export class UserMapper { Injectable() export class UserMapper { - // this will throw an InvalidTargetExceptionMapper because unknown does not exist on UserEntity + // this will throw an InvalidTargetExceptionMapper + // because unknown does not exist on UserEntity @Mappings( { target: 'unknown', source: 'userDto.fname' } ) diff --git a/package.json b/package.json index caa8b08..7afbe49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-mapstruct", - "version": "3.0.3", + "version": "3.1.0", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/src/core/get-options-mappings.ts b/src/core/get-options-functions.ts similarity index 100% rename from src/core/get-options-mappings.ts rename to src/core/get-options-functions.ts diff --git a/src/core/mapping.ts b/src/core/mapping.ts index 26edd1d..e7fd9cc 100644 --- a/src/core/mapping.ts +++ b/src/core/mapping.ts @@ -4,10 +4,11 @@ import { getSourceArguments, control, retrieveMappingTarget, - clean + clean, + exposePropertiesFromGettersOrSetters } from '../utils/utils' import { SupplierDescriptor } from '../models/supplier-descriptor' -import { getOptionsMapping } from './get-options-mappings' +import { getOptionsMapping } from './get-options-functions' import { ArgumentDescriptor } from '../models/argument-descriptor' import { mapImplicitProperties } from './mapping-for-implicit' import { MappingOptions } from '../models/mapping-options' @@ -21,7 +22,9 @@ export const mapping = ( mappingOptions: MappingOptions[] ): void => { control(mapperClass, mappingMethodName, ...mappingOptions) - const targetedType: T = descriptor.value.call() + const targetedType: T = exposePropertiesFromGettersOrSetters( + descriptor.value.call() + ) const sourceNames: string[] = getArgumentNames(descriptor.value.toString()) const sourceArgs: ArgumentDescriptor[] = getSourceArguments( mapperClass, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 3806215..b9ba750 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,10 +1,11 @@ import { ClassConstructor, ClassTransformOptions, + Expose, plainToInstance } from 'class-transformer' import { validateSync } from 'class-validator' -import { isNil } from 'lodash' +import { isNil, lowerFirst } from 'lodash' import { IllegalArgumentNameExceptionMapper } from '../exceptions/illegal-argument-name.exception' import { InvalidMappingOptionsExceptionMapper } from '../exceptions/invalid-mapping-options.exception' import { InvalidMappingTargetExceptionMapper } from '../exceptions/invalid-mapping-target.exception' @@ -115,3 +116,14 @@ export const clean = (object: T): T => { }) return object } + +export const exposePropertiesFromGettersOrSetters = (targetedType: T): T => { + Object.getOwnPropertyNames(targetedType.constructor.prototype) + .filter(key => key.startsWith('get') || key.startsWith('set')) + .map(key => lowerFirst(key.substring(3))) + .filter((v, i, a) => a.indexOf(v) === i) + .forEach(propertyName => { + Expose()(targetedType.constructor, propertyName) + }) + return targetedType +} diff --git a/test/simple/simple.dto.ts b/test/simple/simple.dto.ts index 013d818..575e909 100644 --- a/test/simple/simple.dto.ts +++ b/test/simple/simple.dto.ts @@ -3,15 +3,15 @@ export class SimpleDto { private property2: any private property3: any - getPropety1(): void { + getProperty1(): void { return this.property1 } - getPropety2(): void { + getProperty2(): void { return this.property2 } - getPropety3(): void { + getProperty3(): void { return this.property3 } diff --git a/test/simple/simple.entity.ts b/test/simple/simple.entity.ts index 754665b..269cccb 100644 --- a/test/simple/simple.entity.ts +++ b/test/simple/simple.entity.ts @@ -1,19 +1,17 @@ -import { Expose } from 'class-transformer' - export class Simple { - @Expose() private property1: any - @Expose() private property2: any - @Expose() private property3: any + private property1: any + private property2: any + private property3: any - getPropety1(): void { + getProperty1(): void { return this.property1 } - getPropety2(): void { + getProperty2(): void { return this.property2 } - getPropety3(): void { + getProperty3(): void { return this.property3 }