├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── discussion.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── code-of-conduct.md ├── index.d.ts ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src ├── automapper.ts ├── base.ts ├── index.ts ├── naming │ ├── camel-case-naming-convention.ts │ ├── index.ts │ └── pascal-case-naming-convention.ts ├── profile.ts └── types.ts ├── test └── automapper.test.ts ├── tools ├── gh-pages-publish.ts └── semantic-release-prepare.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Discussion 3 | about: A discussion about any ideas 4 | title: "[Discussion]" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | example 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '11' 10 | - '8' 11 | script: 12 | - npm run test:prod 13 | 14 | jobs: 15 | include: 16 | - stage: deploy_docs 17 | before_script: 18 | - npm run build 19 | node_js: 20 | - '11' 21 | script: if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run deploy-docs; fi 22 | - stage: npm_release 23 | before_script: 24 | - npm run build 25 | node_js: 26 | - '11' 27 | script: if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run semantic-release; fi 28 | branches: 29 | except: 30 | - /^v\d+\.\d+\.\d+$/ 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We're really glad you're reading this, because we need volunteer developers to help this project come to fruition. 👏 2 | 3 | ## Instructions 4 | 5 | These steps will guide you through contributing to this project: 6 | 7 | - Fork the repo 8 | - Clone it and install dependencies 9 | 10 | git clone https://github.com/YOUR-USERNAME/typescript-library-starter 11 | npm install 12 | 13 | Keep in mind that after running `npm install` the git repo is reset. So a good way to cope with this is to have a copy of the folder to push the changes, and the other to try them. 14 | 15 | Make and commit your changes. Make sure the commands npm run build and npm run test:prod are working. 16 | 17 | Finally send a [GitHub Pull Request](https://github.com/alexjoverm/typescript-library-starter/compare?expand=1) with a clear list of what you've done (read more [about pull requests](https://help.github.com/articles/about-pull-requests/)). Make sure all of your commits are atomic (one feature per commit). 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Chau 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated Warning: 2 | #### This package is no longer maintained as I just refactored the whole mapping logic and moved to a new package: [@nartc/automapper](https://github.com/nartc/mapper). You can still use this `automapper-nartc` but it will no longer be maintained and updated. 3 | 4 | 5 | # AutoMapper - Nartc 6 | 7 | This is a fork of `automapper-ts` by [Bert Loedeman](https://github.com/loedeman). My goal is to re-create this awesome library with a more strong-type approach while learning `TypeScript` myself. 8 | 9 | ## Documentations 10 | 11 | Github Pages 12 | [https://nartc.github.io/automapper-nartc/](https://nartc.github.io/automapper-nartc/) 13 | 14 | ## Motivations 15 | 16 | I know that `AutoMapper` is pretty weak in `TypeScript` because of how `Reflection` works in `TypeScript`. However, it'd be nice to have some type of `Mapper` that works for `NodeJS` development. 17 | 18 | ## Features 19 | 20 | Features are limited since I am, by no mean, a `TypeScript` nor an `AutoMapper` expert which I'm planning to research more to provide more `AutoMapper` features to this library. 21 | 22 | So far, the following is supported: 23 | 24 | - [x] Basic Mapping between two classes 25 | - [x] Basic Mapping for nested classes 26 | - [x] Array/List Mapping 27 | - [x] Flattening 28 | - [x] ReverseMap - Very basic `reverseMapping` feature. Use for primitives models only if you can. 29 | - [x] Value Converters 30 | - [x] Value Resolvers 31 | - [x] Async 32 | - [x] Before/After Callback 33 | - [x] Naming Conventions 34 | 35 | **NOTE: Please be advised that the current state of this library is for learning purposes and I'd appreciate any help/guides. Everything is still in beta and DO NOT USE in production.** 36 | 37 | #### Future features: 38 | 39 | - [ ] Type Converters - Help needed 40 | - [ ] Value Transformers 41 | 42 | #### Will not support: 43 | 44 | - [x] Null Substitution - It makes more sense to use `fromValue()` instead of implement `nullSubstitution()`. Please let me know of a use-case where `nullSubstitution()` makes sense. 45 | 46 | Contributions are appreciated. 47 | 48 | #### Implementation note: 49 | 50 | I have plans in the near future to update how `forMember()` method works in terms of the method's signature. I might change it to a lambda expression to support `reverseMapping` better. But I am open to suggestions. 51 | 52 | ## Installation 53 | 54 | ```shell 55 | npm install --save automapper-nartc 56 | ``` 57 | 58 | **NOTE: `automapper-nartc` has a `peerDependency` of `reflect-metadata` and a dependency of `class-transformer`. `class-transformer` will also be installed when you install this library. Please also turn on `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig` ** 59 | 60 | ## Usage 61 | 62 | 1. Assuming you have couple of `Domain Models` as follows: 63 | 64 | ```typescript 65 | class Address { 66 | address: string; 67 | city: string; 68 | state: string; 69 | zip: string; 70 | } 71 | 72 | class Profile { 73 | bio: string; 74 | phone: string; 75 | email: string; 76 | addresses: Address[]; 77 | 78 | constructor() { 79 | this.addresses = []; 80 | } 81 | } 82 | 83 | class User { 84 | firstName: string; 85 | lastName: string; 86 | password: string; 87 | profile: Profile; 88 | } 89 | ``` 90 | 91 | 2. And you also have couple of `View Models` (or `DTOs`): 92 | 93 | ```typescript 94 | class ProfileVm { 95 | bio: string; 96 | email: string; 97 | addressStrings: string[]; 98 | } 99 | 100 | class UserVm { 101 | fullName: string; 102 | profile: ProfileVm; 103 | firstName?: string; 104 | lastName?: string; 105 | } 106 | ``` 107 | 108 | 3. Decorate all of your properties with `@Expose()`. `@Expose` is imported from `class-transformer`. This will allow the engine to be aware of all the properties available in a certain **class**. 109 | 110 | ```typescript 111 | class User { 112 | @Expose() 113 | firstName: string; 114 | @Expose() 115 | lastName: string; 116 | @Expose() 117 | password: string; 118 | @Expose() 119 | profile: Profile; 120 | } 121 | 122 | class UserVm { 123 | @Expose() 124 | fullName: string; 125 | @Expose() 126 | profile: ProfileVm; 127 | @Expose() 128 | firstName?: string; 129 | @Expose() 130 | lastName?: string; 131 | } 132 | ``` 133 | 134 | **NOTE: If you have nested model, like `profile` in this case, you will want to use `@Type()` on those as well. `@Type()` is also imported from `class-transformer`.** 135 | 136 | ```typescript 137 | class User { 138 | @Expose() 139 | firstName: string; 140 | @Expose() 141 | lastName: string; 142 | @Expose() 143 | password: string; 144 | @Expose() 145 | @Type(() => Profile) 146 | profile: Profile; 147 | } 148 | 149 | class UserVm { 150 | @Expose() 151 | fullName: string; 152 | @Expose() 153 | @Type(() => ProfileVm) 154 | profile: ProfileVm; 155 | @Expose() 156 | firstName?: string; 157 | @Expose() 158 | lastName?: string; 159 | } 160 | ``` 161 | 162 | However, `automapper-nartc` provides a short-hand decorator `@ExposedType()` instead of explicitly use `@Expose()` and `@Type()` on a nested model property. 163 | 164 | ```typescript 165 | class UserVm { 166 | @Expose() 167 | fullName: string; 168 | @ExposedType(() => ProfileVm) 169 | profile: ProfileVm; 170 | @Expose() 171 | firstName?: string; 172 | @Expose() 173 | lastName?: string; 174 | } 175 | ``` 176 | 177 | 4. Next, import `Mapper` from `automapper-nartc`. You can also just instantiate a new instance of `AutoMapper` if you want to manage your instance. 178 | 5. Initialize `Mapper` with `initialize()` method. `initialize()` expects a `Configuration` callback that will give you access to the `Configuration` object. There are two methods on the `Configuration` object that you can use to setup your `Mapper` 179 | 180 | - `createMap()`: `createMap()` expects a **source** as the first argument and the **destination** as the second argument. `createMap()` returns `CreateMapFluentFunctions` (Read more at [API Reference](https://nartc.github.io/automapper-nartc/index.html)). 181 | 182 | ```typescript 183 | import { Mapper, MappingProfileBase } from 'automapper-nartc'; 184 | 185 | Mapper.initialize(config => { 186 | config.createMap(User, UserVm); // create a mapping from User to UserVm (one direction) 187 | config.createMap(Profile, ProfileVm) 188 | .forMember('addressStrings', opts => opts.mapFrom(s => s.addresses.map(... /* map to addressString however you like */))); 189 | }); 190 | ``` 191 | 192 | `createMap()` will establish basic mappings for: `primitives` and `nested mapping` that have the same field name on the **source** and **destination** (eg: `userVm.firstName` will be automatically mapped from `user.firstName`). In addition, you can use `forMember()` to gain more control on how to map a field on the **destination**. 193 | 194 | ```typescript 195 | Mapper.initialize(config => { 196 | config 197 | .createMap(User, UserVm) // create a mapping from User to UserVm (one direction) 198 | .forMember('fullName', opts => 199 | opts.mapFrom(source => source.firstName + ' ' + source.lastName) 200 | ); // You will get type-inference here 201 | }); 202 | ``` 203 | 204 | - `addProfile()`: `addProfile()` expects a new instance of a class which extends `MappingProfileBase`. Usually, you can just initialize your `Mapper` with `config.createMap` and setup all your mappings that way. But more than often, it is better to separate your mappings into `Profile` which will create the mappings for specific set of **source** and **destination** 205 | 206 | ```typescript 207 | import { MappingProfileBase } from 'automapper-nartc'; 208 | 209 | export class UserProfile extends MappingProfileBase { 210 | constructor() { 211 | super(); // this is required since it will take UserProfile and get the string "UserProfile" to assign to profileName 212 | } 213 | 214 | // configure() is required since it is an abstract method. configure() will be called automatically by Mapper. 215 | // This is where you will setup your mapping with the class method: createMap 216 | configure(mapper: AutoMapper) { 217 | mapper 218 | .createMap(User, UserVm) 219 | .forMember('fullName', opts => 220 | opts.mapFrom(source => source.firstName + ' ' + source.lastName) 221 | ); // You will get type-inference here 222 | } 223 | } 224 | 225 | // in another file 226 | Mapper.initialize(config => { 227 | config.addProfile(new UserProfile()); 228 | }); 229 | ``` 230 | 231 | 5. When you're ready to map, call `Mapper.map()`. 232 | 233 | ```typescript 234 | const userVm = Mapper.map(user, UserVm); // this will return an instance of UserVm and assign it to userVm with all the fields assigned properly from User 235 | 236 | console.log('instance of UserVm?', userVm instanceof UserVm); // true 237 | ``` 238 | 239 | #### Callbacks 240 | 241 | `automapper-nartc` provides `beforeMap` and `afterMap` callbacks which are called **before** a mapping operator occurs and/or **after** a mapping operator occurs, if said callbacks are provided. 242 | 243 | There are two ways you can provide the callbacks: `Map` level and `Mapping` level. 244 | 245 | **NOTE: `Map` level refers to the actual map operation when any of the `map()` methods are called. `Mapping` level refers to the actual `Mapping` between two models when `createMap()` is called.** 246 | 247 | - **Map** level: all `map()` methods have the third parameter which has a shape of `MapActionOptions: {beforeMap: Function, afterMap: Function}`. If any of the callbacks is provided, it will be called in correct chronological order. 248 | 249 | ```typescript 250 | /** 251 | * In this case, both callbacks will be called with the following arguments. 252 | * 253 | * @param {User} source 254 | * @param {UserVm} destination 255 | * @param {Mapping} mapping 256 | */ 257 | const userVm = Mapper.map(user, UserVm, { 258 | beforeMap: (source, destination, mapping) => {}, 259 | afterMap: (source, destination, mapping) => {} 260 | }); 261 | ``` 262 | 263 | - **Mapping** level: callbacks on the `Mapping` level will be called for ALL map operations on the two models unless you provide diferent callbacks to specific `map` operation (aka `Map` level) 264 | 265 | ```typescript 266 | /** 267 | * In this case, both callbacks will be called with the following arguments. 268 | * 269 | * @param {User} source 270 | * @param {UserVm} destination 271 | * @param {Mapping} mapping 272 | */ 273 | Mapper.initialize(config => { 274 | config 275 | .createMap(User, UserVm) 276 | .beforeMap((source, destination, mapping) => {}) 277 | .afterMap((source, destination, mapping) => {}); // create a mapping from User to UserVm (one direction) 278 | }); 279 | ``` 280 | 281 | **NOTE 1: `Map` level callbacks will overide `Mapping` level callbacks if both are provided** 282 | 283 | **NOTE 2: The callbacks are called with `source`, `destination` and `mapping`. **ANYTHING** you do to the `source` and `destination` will be carried over to the `source` and `destination` being mapped (mutation) so please be cautious. It might be handy/dangerous at the same time given the dynamic characteristic of **JavaScript**.** 284 | 285 | **NOTE 3: `mapArray()` will ignore `Mapping` level callbacks because that would be a performance issue if callbacks were to be called on every single item in an array. Provide `Map` level callbacks for `mapArray()` if you want to have callbacks on `mapArray()`** 286 | 287 | 6. Use `Mapper.mapArray()` if you want to map from `TSource[]` to `TDestination[]`. 288 | 289 | ## Demo 290 | 291 | Codesandbox Demo 292 | [Codesandbox](https://codesandbox.io/s/automapper-nartc-example-l96nw) 293 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexjovermorales@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'automapper-nartc/automapper' { 2 | import { ExposeOptions, TypeHelpOptions, TypeOptions } from 'class-transformer'; 3 | import { AutoMapperBase } from 'automapper-nartc/base'; 4 | import { Configuration, Constructable, CreateMapFluentFunctions, MapActionOptions, MappingProfile } from 'automapper-nartc/types'; 5 | /** 6 | * Combined Expose and Type from class-transformer 7 | * 8 | * @param {(type?: TypeHelpOptions) => Function} typeFn 9 | * @param {ExposeOptions} exposeOptions 10 | * @param {TypeOptions} typeOptions 11 | */ 12 | export const ExposedType: (typeFn: (type?: TypeHelpOptions | undefined) => Function, exposeOptions?: ExposeOptions | undefined, typeOptions?: TypeOptions | undefined) => PropertyDecorator; 13 | export class AutoMapper extends AutoMapperBase { 14 | private static _instance; 15 | private readonly _profiles; 16 | /** 17 | * @static - Get the Mapper instance 18 | */ 19 | static getInstance(): AutoMapper; 20 | constructor(); 21 | /** 22 | * Initialize Mapper 23 | * 24 | * @example 25 | * 26 | * 27 | * ```ts 28 | * Mapper.initialize(config => { 29 | * config.addProfile(new Profile()); 30 | * config.createMap(Source, Destination); 31 | * }) 32 | * ``` 33 | * 34 | * @param {(config: Configuration) => void} configFn - Config function callback 35 | * 36 | */ 37 | initialize(configFn: (config: Configuration) => void): void; 38 | /** 39 | * Map from Source to Destination 40 | * 41 | * @example 42 | * 43 | * 44 | * ```ts 45 | * const user = new User(); 46 | * user.firstName = 'John'; 47 | * user.lastName = 'Doe'; 48 | * 49 | * const userVm = Mapper.map(user, UserVm); 50 | * ``` 51 | * 52 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 53 | * @param {Constructable} destination - the Destination model to receive the 54 | * mapped values 55 | * @param {MapActionOptions} option - Optional mapping option 56 | */ 57 | map(sourceObj: TSource, destination: Constructable, option?: MapActionOptions): TDestination; 58 | /** 59 | * Map from Source to Destination Async. Mapping operation will be run as a micro task. 60 | * 61 | * @example 62 | * 63 | * 64 | * ```ts 65 | * const user = new User(); 66 | * user.firstName = 'John'; 67 | * user.lastName = 'Doe'; 68 | * 69 | * const userVm = await Mapper.mapAsync(user, UserVm); 70 | * ``` 71 | * 72 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 73 | * @param {Constructable} destination - the Destination model to receive the 74 | * mapped values 75 | * 76 | * @param {MapActionOptions} option - Optional mapping option 77 | * @returns {Promise} Promise that resolves TDestination 78 | */ 79 | mapAsync(sourceObj: TSource, destination: Constructable, option?: MapActionOptions): Promise; 80 | /** 81 | * Map from a list of Source to a list of Destination 82 | * 83 | * @example 84 | * 85 | * 86 | * ```ts 87 | * const addresses = []; 88 | * addresses.push(new Address(), new Address()); 89 | * 90 | * const addressesVm = Mapper.mapArray(addresses, AddressVm); 91 | * ``` 92 | * 93 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 94 | * @param {Constructable} destination - the Destination model to receive the 95 | * mapped values 96 | * @param {MapActionOptions} option - Optional mapping option 97 | */ 98 | mapArray(sourceObj: TSource[], destination: Constructable, option?: MapActionOptions): TDestination[]; 99 | /** 100 | * Map from a list of Source to a list of Destination async. Mapping operation will be run 101 | * as a micro task. 102 | * 103 | * @example 104 | * 105 | * 106 | * ```ts 107 | * const addresses = []; 108 | * addresses.push(new Address(), new Address()); 109 | * 110 | * const addressesVm = await Mapper.mapArrayAsync(addresses, AddressVm); 111 | * ``` 112 | * 113 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 114 | * @param {Constructable} destination - the Destination model to receive the 115 | * mapped values 116 | * @param {MapActionOptions} option - Optional mapping option 117 | * @returns {Promise>} Promise that resolves a TDestination[] 118 | */ 119 | mapArrayAsync(sourceObj: TSource[], destination: Constructable, option?: MapActionOptions): Promise; 120 | /** 121 | * Add MappingProfile to the current instance of AutoMapper 122 | * 123 | * @param {MappingProfile} profile - Profile being added 124 | */ 125 | addProfile(profile: MappingProfile): AutoMapper; 126 | /** 127 | * Create a mapping between Source and Destination without initializing the Mapper 128 | * 129 | * @param {Constructable} source - the Source model 130 | * @param {Constructable} destination - the Destination model 131 | */ 132 | createMap(source: Constructable, destination: Constructable): CreateMapFluentFunctions; 133 | /** 134 | * Dispose Mappings and Profiles created on the Mapper singleton 135 | */ 136 | dispose(): void; 137 | private _createMappingFluentFunctions; 138 | private _createMapForMember; 139 | private _createReverseMap; 140 | private _createMapForPath; 141 | } 142 | /** 143 | * @instance AutoMapper singleton 144 | */ 145 | export const Mapper: AutoMapper; 146 | 147 | } 148 | declare module 'automapper-nartc/base' { 149 | import { Constructable, ForMemberExpression, ForPathDestinationFn, MapActionOptions, Mapping, TransformationType } from 'automapper-nartc/types'; 150 | export abstract class AutoMapperBase { 151 | protected readonly _mappings: { 152 | [key: string]: Mapping; 153 | }; 154 | protected constructor(); 155 | protected getTransformationType(forMemberFn: ForMemberExpression): TransformationType; 158 | protected _mapArray(sourceArray: TSource[], mapping: Mapping, option?: MapActionOptions): TDestination[]; 163 | protected _map(sourceObj: TSource, mapping: Mapping, option?: MapActionOptions, isArrayMap?: boolean): TDestination; 168 | protected _mapAsync(sourceObj: TSource, mapping: Mapping, option?: MapActionOptions): Promise; 173 | protected _mapArrayAsync(sourceArray: TSource[], mapping: Mapping, option?: MapActionOptions): Promise; 178 | private static _assertMappingErrors; 179 | protected _createMappingObject(source: Constructable, destination: Constructable): Mapping; 184 | protected _createReverseMappingObject(mapping: Mapping): Mapping; 189 | protected _getKeyFromMemberFn(fn: ForPathDestinationFn): keyof T; 192 | protected _getMapping(source: Constructable, destination: Constructable): Mapping; 193 | protected _getMappingForDestination(destination: Constructable): Mapping; 194 | protected _dispose(): void; 195 | private _hasMapping; 196 | private static _getMappingKey; 197 | private static _isClass; 198 | private static _isDate; 199 | private static _isArray; 200 | private static _isResolver; 201 | private _getMappingForNestedKey; 202 | private static _getSourcePropertyKey; 203 | } 204 | 205 | } 206 | declare module 'automapper-nartc/index' { 207 | export * from 'automapper-nartc/base'; 208 | export * from 'automapper-nartc/types'; 209 | export * from 'automapper-nartc/automapper'; 210 | export * from 'automapper-nartc/profile'; 211 | export * from 'automapper-nartc/naming/index'; 212 | 213 | } 214 | declare module 'automapper-nartc/naming/camel-case-naming-convention' { 215 | import { NamingConvention } from 'automapper-nartc/types'; 216 | export class CamelCaseNamingConvention implements NamingConvention { 217 | separatorCharacter: string; 218 | splittingExpression: RegExp; 219 | transformPropertyName(sourceNameParts: string[]): string; 220 | } 221 | 222 | } 223 | declare module 'automapper-nartc/naming/index' { 224 | export * from 'automapper-nartc/naming/camel-case-naming-convention'; 225 | export * from 'automapper-nartc/naming/pascal-case-naming-convention'; 226 | 227 | } 228 | declare module 'automapper-nartc/naming/pascal-case-naming-convention' { 229 | import { NamingConvention } from 'automapper-nartc/types'; 230 | export class PascalCaseNamingConvention implements NamingConvention { 231 | separatorCharacter: string; 232 | splittingExpression: RegExp; 233 | transformPropertyName(sourceNameParts: string[]): string; 234 | } 235 | 236 | } 237 | declare module 'automapper-nartc/profile' { 238 | import { AutoMapper } from 'automapper-nartc/automapper'; 239 | import { MappingProfile } from 'automapper-nartc/types'; 240 | /** 241 | * Abstract class for all mapping Profiles 242 | * 243 | */ 244 | export abstract class MappingProfileBase implements MappingProfile { 245 | /** 246 | * @property {string} profileName - the name of the Profile 247 | */ 248 | profileName: string; 249 | /** 250 | * @constructor - initialize the profile with the profileName 251 | */ 252 | protected constructor(); 253 | /** 254 | * @abstract configure() method to be called when using with Mapper.initialize() 255 | * 256 | * @param {AutoMapper} mapper - AutoMapper instance to add this Profile on 257 | */ 258 | abstract configure(mapper: AutoMapper): void; 259 | } 260 | 261 | } 262 | declare module 'automapper-nartc/types' { 263 | import { AutoMapper } from 'automapper-nartc/automapper'; 264 | export type Unpacked = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise ? U : T; 265 | export enum TransformationType { 266 | /** 267 | * when `opts.ignore()` is used on `forMember()` 268 | */ 269 | Ignore = 0, 270 | /** 271 | * when `opts.mapFrom()` is used on `forMember()` 272 | */ 273 | MapFrom = 1, 274 | /** 275 | * when `opts.condition()` is used on `forMember()` 276 | */ 277 | Condition = 2, 278 | /** 279 | * when `opts.fromValue()` is used on `forMember()` 280 | */ 281 | FromValue = 3, 282 | /** 283 | * when `opts.mapWith()` is used on `forMember()` 284 | */ 285 | MapWith = 4, 286 | /** 287 | * when `opts.convertUsing()` is used on `forMember()` 288 | */ 289 | ConvertUsing = 5 290 | } 291 | /** 292 | * A new-able type 293 | */ 294 | export type Constructable = new (...args: any[]) => T; 297 | export type MapActionOptions = { 302 | beforeMap?: BeforeAfterMapAction; 303 | afterMap?: BeforeAfterMapAction; 304 | }; 305 | export type BeforeAfterMapAction = (source: TSource, destination: TDestination, mapping?: Mapping) => void; 306 | export interface Converter { 307 | convert(source: TSource): TDestination; 308 | } 309 | export interface Resolver { 314 | resolve(source: TSource, destination: TDestination, transformation: MappingTransformation): K; 315 | } 316 | /** 317 | * Value Selector from a source type 318 | * 319 | * @example 320 | * 321 | * ```ts 322 | * source => source.foo.bar 323 | * ``` 324 | */ 325 | export type ValueSelector = (source: TSource) => TDestination[K]; 330 | export type MapFromCallback = ValueSelector | Resolver; 335 | /** 336 | * Condition Predicate from a source 337 | */ 338 | export type ConditionPredicate = (source: TSource) => boolean; 341 | /** 342 | * Options for mapWith 343 | */ 344 | export type MapWithOptions = { 349 | destination: Constructable>; 350 | value: ValueSelector; 351 | }; 352 | export type ConvertUsingOptions = { 357 | converter: Converter; 358 | value?: (source: TSource) => TSource[keyof TSource]; 359 | }; 360 | export interface DestinationMemberConfigOptions { 365 | mapFrom(cb: MapFromCallback): void; 366 | mapWith(destination: Constructable>, value: ValueSelector): void; 367 | condition(predicate: ConditionPredicate): void; 368 | fromValue(value: TDestination[K]): void; 369 | ignore(): void; 370 | convertUsing(converter: Converter, value?: (source: TSource) => TConvertSource): void; 371 | } 372 | export interface ForMemberExpression { 377 | (opts: DestinationMemberConfigOptions): void; 378 | } 379 | export type ForPathDestinationFn = (destination: TDestination) => TDestination[keyof TDestination]; 382 | export interface CreateReverseMapFluentFunctions { 387 | forPath(destination: ForPathDestinationFn, forPathFn: ForMemberExpression): CreateReverseMapFluentFunctions; 388 | } 389 | export interface CreateMapFluentFunctions { 394 | forMember(key: K, expression: ForMemberExpression): CreateMapFluentFunctions; 395 | beforeMap(action: BeforeAfterMapAction): CreateMapFluentFunctions; 396 | afterMap(action: BeforeAfterMapAction): CreateMapFluentFunctions; 397 | reverseMap(): CreateReverseMapFluentFunctions; 398 | setSourceNamingConvention(namingConvention: NamingConvention): CreateMapFluentFunctions; 399 | setDestinationNamingConvention(namingConvention: NamingConvention): CreateMapFluentFunctions; 400 | } 401 | export interface Configuration { 402 | addProfile(profile: MappingProfile): void; 403 | createMap(source: Constructable, destination: Constructable): CreateMapFluentFunctions; 404 | } 405 | export interface MappingTransformation { 410 | transformationType: TransformationType; 411 | mapFrom: MapFromCallback; 412 | mapWith: MapWithOptions; 413 | condition: ConditionPredicate; 414 | fromValue: TDestination[keyof TDestination]; 415 | convertUsing: ConvertUsingOptions; 416 | } 417 | export interface MappingProperty { 422 | destinationKey: keyof TDestination; 423 | transformation: MappingTransformation; 424 | } 425 | export interface Mapping { 430 | source: Constructable; 431 | destination: Constructable; 432 | sourceKey: string; 433 | destinationKey: string; 434 | properties: Map>; 435 | sourceMemberNamingConvention: NamingConvention; 436 | destinationMemberNamingConvention: NamingConvention; 437 | beforeMapAction?: BeforeAfterMapAction; 438 | afterMapAction?: BeforeAfterMapAction; 439 | } 440 | export interface MappingProfile { 441 | profileName: string; 442 | configure: (mapper: AutoMapper) => void; 443 | } 444 | export type NamingConvention = { 445 | splittingExpression: RegExp; 446 | separatorCharacter: string; 447 | transformPropertyName: (sourcePropNameParts: string[]) => string; 448 | }; 449 | 450 | } 451 | declare module 'automapper-nartc' { 452 | import main = require('automapper-nartc/index'); 453 | export = main; 454 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automapper-nartc", 3 | "version": "0.0.0-development", 4 | "description": "", 5 | "keywords": [], 6 | "main": "dist/automapper.umd.js", 7 | "module": "dist/automapper.es5.js", 8 | "typings": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "author": "Chau ", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/nartc/automapper-nartc" 17 | }, 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=6.0.0" 21 | }, 22 | "scripts": { 23 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 24 | "prebuild": "rimraf dist", 25 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src", 26 | "start": "rollup -c rollup.config.ts -w", 27 | "test": "jest --coverage", 28 | "test:watch": "jest --coverage --watch", 29 | "test:prod": "npm run lint && npm run test -- --no-cache", 30 | "deploy-docs": "ts-node tools/gh-pages-publish", 31 | "report-coverage": "cat ./coverage/lcov.ixnfo | coveralls", 32 | "commit": "git-cz", 33 | "semantic-release": "semantic-release", 34 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 35 | "precommit": "npm run gen:dts && lint-staged", 36 | "prepush": "npm run test:prod && npm run build", 37 | "commitmsg": "commitlint -E HUSKY_GIT_PARAMS", 38 | "gen:dts": "npm-dts generate" 39 | }, 40 | "lint-staged": { 41 | "{src,test}/**/*.ts": [ 42 | "prettier --write", 43 | "git add" 44 | ] 45 | }, 46 | "config": { 47 | "commitizen": { 48 | "path": "node_modules/cz-conventional-changelog" 49 | } 50 | }, 51 | "jest": { 52 | "transform": { 53 | ".(ts|tsx)": "ts-jest" 54 | }, 55 | "testEnvironment": "node", 56 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 57 | "moduleFileExtensions": [ 58 | "ts", 59 | "tsx", 60 | "js" 61 | ], 62 | "coveragePathIgnorePatterns": [ 63 | "/node_modules/", 64 | "/test/" 65 | ], 66 | "collectCoverageFrom": [ 67 | "src/automapper.{js,ts}" 68 | ] 69 | }, 70 | "prettier": { 71 | "semi": true, 72 | "singleQuote": true 73 | }, 74 | "commitlint": { 75 | "extends": [ 76 | "@commitlint/config-conventional" 77 | ] 78 | }, 79 | "devDependencies": { 80 | "@commitlint/cli": "8.2.0", 81 | "@commitlint/config-conventional": "8.2.0", 82 | "@types/jest": "24.0.18", 83 | "@types/lodash.get": "4.4.6", 84 | "@types/lodash.isempty": "4.4.6", 85 | "@types/lodash.lowercase": "4.3.6", 86 | "@types/node": "12.7.12", 87 | "colors": "1.4.0", 88 | "commitizen": "4.0.3", 89 | "coveralls": "3.0.6", 90 | "cross-env": "6.0.3", 91 | "cz-conventional-changelog": "3.0.2", 92 | "husky": "3.0.8", 93 | "jest": "24.9.0", 94 | "jest-config": "24.9.0", 95 | "lint-staged": "9.4.2", 96 | "lodash.camelcase": "4.3.0", 97 | "npm-dts": "1.1.10", 98 | "prettier": "1.18.2", 99 | "prompt": "1.0.0", 100 | "reflect-metadata": "0.1.13", 101 | "replace-in-file": "4.1.3", 102 | "rimraf": "3.0.0", 103 | "rollup": "1.23.1", 104 | "rollup-plugin-commonjs": "10.1.0", 105 | "rollup-plugin-json": "4.0.0", 106 | "rollup-plugin-node-resolve": "5.2.0", 107 | "rollup-plugin-sourcemaps": "0.4.2", 108 | "rollup-plugin-typescript2": "0.24.3", 109 | "semantic-release": "15.13.24", 110 | "shelljs": "0.8.3", 111 | "ts-jest": "24.1.0", 112 | "ts-node": "8.4.1", 113 | "tslint": "5.20.0", 114 | "tslint-config-prettier": "1.18.0", 115 | "tslint-config-standard": "8.0.1", 116 | "typedoc": "0.15.0", 117 | "typescript": "3.6.3" 118 | }, 119 | "dependencies": { 120 | "class-transformer": "0.2.3", 121 | "lodash.get": "4.4.2", 122 | "lodash.isempty": "4.4.0", 123 | "lodash.lowercase": "4.3.0" 124 | }, 125 | "peerDependencies": { 126 | "reflect-metadata": "0.1.13" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import sourceMaps from 'rollup-plugin-sourcemaps'; 4 | import camelCase from 'lodash.camelcase'; 5 | import typescript from 'rollup-plugin-typescript2'; 6 | import json from 'rollup-plugin-json'; 7 | 8 | const pkg = require('./package.json'); 9 | 10 | const libraryName = 'automapper'; 11 | 12 | export default { 13 | input: `src/${ libraryName }.ts`, 14 | output: [ 15 | { 16 | file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true, globals: { 17 | 'lodash': 'lodash', 18 | 'class-transformer': 'class-transformer' 19 | } 20 | }, 21 | { 22 | file: pkg.module, format: 'es', sourcemap: true, globals: { 23 | 'lodash': 'lodash', 24 | 'class-transformer': 'class-transformer' 25 | } 26 | } 27 | ], 28 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 29 | external: ['lodash', 'class-transformer'], 30 | watch: { 31 | include: 'src/**' 32 | }, 33 | plugins: [ 34 | // Allow json resolution 35 | json(), 36 | // Compile TypeScript files 37 | typescript({ useTsconfigDeclarationDir: true }), 38 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 39 | commonjs(), 40 | // Allow node_modules resolution, so you can use 'external' to control 41 | // which external modules to include in the bundle 42 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 43 | resolve(), 44 | 45 | // Resolve source maps to the original source 46 | sourceMaps() 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /src/automapper.ts: -------------------------------------------------------------------------------- 1 | import { Expose, ExposeOptions, Type, TypeHelpOptions, TypeOptions } from 'class-transformer'; 2 | import { AutoMapperBase } from './base'; 3 | import { 4 | ConditionPredicate, 5 | Configuration, 6 | Constructable, 7 | ConvertUsingOptions, 8 | CreateMapFluentFunctions, 9 | CreateReverseMapFluentFunctions, 10 | DestinationMemberConfigOptions, 11 | ForMemberExpression, 12 | MapActionOptions, 13 | MapFromCallback, 14 | Mapping, 15 | MappingProfile, 16 | MappingProperty, 17 | MapWithOptions 18 | } from './types'; 19 | 20 | /** 21 | * Combined Expose and Type from class-transformer 22 | * 23 | * @param {(type?: TypeHelpOptions) => Function} typeFn 24 | * @param {ExposeOptions} exposeOptions 25 | * @param {TypeOptions} typeOptions 26 | */ 27 | export const ExposedType = ( 28 | typeFn: (type?: TypeHelpOptions) => Function, 29 | exposeOptions?: ExposeOptions, 30 | typeOptions?: TypeOptions 31 | ): PropertyDecorator => (target: any, propertyKey) => { 32 | Expose(exposeOptions)(target, propertyKey as string); 33 | Type(typeFn, typeOptions)(target, propertyKey as string); 34 | }; 35 | 36 | export class AutoMapper extends AutoMapperBase { 37 | private static _instance: AutoMapper = new AutoMapper(); 38 | 39 | private readonly _profiles!: { [key: string]: MappingProfile }; 40 | 41 | /** 42 | * @static - Get the Mapper instance 43 | */ 44 | public static getInstance(): AutoMapper { 45 | return this._instance; 46 | } 47 | 48 | constructor() { 49 | super(); 50 | this._profiles = {}; 51 | if (!AutoMapper._instance) { 52 | AutoMapper._instance = this; 53 | } 54 | } 55 | 56 | /** 57 | * Initialize Mapper 58 | * 59 | * @example 60 | * 61 | * 62 | * ```ts 63 | * Mapper.initialize(config => { 64 | * config.addProfile(new Profile()); 65 | * config.createMap(Source, Destination); 66 | * }) 67 | * ``` 68 | * 69 | * @param {(config: Configuration) => void} configFn - Config function callback 70 | * 71 | */ 72 | public initialize(configFn: (config: Configuration) => void): void { 73 | const configuration: Configuration = { 74 | addProfile: (profile: MappingProfile): AutoMapper => { 75 | return this.addProfile(profile); 76 | }, 77 | createMap: ( 78 | source: Constructable, 79 | destination: Constructable 80 | ): CreateMapFluentFunctions => { 81 | return this.createMap(source, destination); 82 | } 83 | }; 84 | 85 | configFn(configuration); 86 | } 87 | 88 | /** 89 | * Map from Source to Destination 90 | * 91 | * @example 92 | * 93 | * 94 | * ```ts 95 | * const user = new User(); 96 | * user.firstName = 'John'; 97 | * user.lastName = 'Doe'; 98 | * 99 | * const userVm = Mapper.map(user, UserVm); 100 | * ``` 101 | * 102 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 103 | * @param {Constructable} destination - the Destination model to receive the 104 | * mapped values 105 | * @param {MapActionOptions} option - Optional mapping option 106 | */ 107 | public map( 108 | sourceObj: TSource, 109 | destination: Constructable, 110 | option?: MapActionOptions 111 | ): TDestination { 112 | const mapping = super._getMappingForDestination(destination); 113 | return super._map(sourceObj, mapping, option); 114 | } 115 | 116 | /** 117 | * Map from Source to Destination Async. Mapping operation will be run as a micro task. 118 | * 119 | * @example 120 | * 121 | * 122 | * ```ts 123 | * const user = new User(); 124 | * user.firstName = 'John'; 125 | * user.lastName = 'Doe'; 126 | * 127 | * const userVm = await Mapper.mapAsync(user, UserVm); 128 | * ``` 129 | * 130 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 131 | * @param {Constructable} destination - the Destination model to receive the 132 | * mapped values 133 | * 134 | * @param {MapActionOptions} option - Optional mapping option 135 | * @returns {Promise} Promise that resolves TDestination 136 | */ 137 | public mapAsync( 138 | sourceObj: TSource, 139 | destination: Constructable, 140 | option?: MapActionOptions 141 | ): Promise { 142 | const mapping = super._getMappingForDestination(destination); 143 | return super._mapAsync(sourceObj, mapping, option); 144 | } 145 | 146 | /** 147 | * Map from a list of Source to a list of Destination 148 | * 149 | * @example 150 | * 151 | * 152 | * ```ts 153 | * const addresses = []; 154 | * addresses.push(new Address(), new Address()); 155 | * 156 | * const addressesVm = Mapper.mapArray(addresses, AddressVm); 157 | * ``` 158 | * 159 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 160 | * @param {Constructable} destination - the Destination model to receive the 161 | * mapped values 162 | * @param {MapActionOptions} option - Optional mapping option 163 | */ 164 | public mapArray( 165 | sourceObj: TSource[], 166 | destination: Constructable, 167 | option?: MapActionOptions 168 | ): TDestination[] { 169 | const mapping = super._getMappingForDestination(destination); 170 | return super._mapArray(sourceObj, mapping, option); 171 | } 172 | 173 | /** 174 | * Map from a list of Source to a list of Destination async. Mapping operation will be run 175 | * as a micro task. 176 | * 177 | * @example 178 | * 179 | * 180 | * ```ts 181 | * const addresses = []; 182 | * addresses.push(new Address(), new Address()); 183 | * 184 | * const addressesVm = await Mapper.mapArrayAsync(addresses, AddressVm); 185 | * ``` 186 | * 187 | * @param {TSource} sourceObj - the sourceObj that are going to be mapped 188 | * @param {Constructable} destination - the Destination model to receive the 189 | * mapped values 190 | * @param {MapActionOptions} option - Optional mapping option 191 | * @returns {Promise>} Promise that resolves a TDestination[] 192 | */ 193 | public mapArrayAsync( 194 | sourceObj: TSource[], 195 | destination: Constructable, 196 | option?: MapActionOptions 197 | ): Promise { 198 | const mapping = super._getMappingForDestination(destination); 199 | return super._mapArrayAsync(sourceObj, mapping, option); 200 | } 201 | 202 | /** 203 | * Add MappingProfile to the current instance of AutoMapper 204 | * 205 | * @param {MappingProfile} profile - Profile being added 206 | */ 207 | public addProfile(profile: MappingProfile): AutoMapper { 208 | if (this._profiles[profile.profileName]) { 209 | throw new Error(`${profile.profileName} is already existed on the current Mapper instance`); 210 | } 211 | 212 | profile.configure(this); 213 | this._profiles[profile.profileName] = Object.freeze(profile); 214 | return this; 215 | } 216 | 217 | /** 218 | * Create a mapping between Source and Destination without initializing the Mapper 219 | * 220 | * @param {Constructable} source - the Source model 221 | * @param {Constructable} destination - the Destination model 222 | */ 223 | public createMap( 224 | source: Constructable, 225 | destination: Constructable 226 | ): CreateMapFluentFunctions { 227 | const mapping = super._createMappingObject(source, destination); 228 | return this._createMappingFluentFunctions(mapping); 229 | } 230 | 231 | /** 232 | * Dispose Mappings and Profiles created on the Mapper singleton 233 | */ 234 | public dispose(): void { 235 | Object.keys(this._profiles).forEach(key => { 236 | delete this._profiles[key]; 237 | }); 238 | super._dispose(); 239 | } 240 | 241 | private _createMappingFluentFunctions( 242 | mapping: Mapping 243 | ): CreateMapFluentFunctions { 244 | const fluentFunctions: CreateMapFluentFunctions = { 245 | forMember: (destinationKey, forMemberFn) => { 246 | return this._createMapForMember(mapping, destinationKey, forMemberFn, fluentFunctions); 247 | }, 248 | reverseMap: () => { 249 | return this._createReverseMap(mapping); 250 | }, 251 | beforeMap: action => { 252 | mapping.beforeMapAction = action; 253 | return fluentFunctions; 254 | }, 255 | afterMap: action => { 256 | mapping.afterMapAction = action; 257 | return fluentFunctions; 258 | }, 259 | setSourceNamingConvention: namingConvention => { 260 | mapping.sourceMemberNamingConvention = namingConvention; 261 | return fluentFunctions; 262 | }, 263 | setDestinationNamingConvention: namingConvention => { 264 | mapping.destinationMemberNamingConvention = namingConvention; 265 | return fluentFunctions; 266 | } 267 | }; 268 | 269 | return fluentFunctions; 270 | } 271 | 272 | private _createMapForMember( 273 | mapping: Mapping, 274 | key: keyof TDestination, 275 | fn: ForMemberExpression, 276 | fluentFunctions: CreateMapFluentFunctions 277 | ): CreateMapFluentFunctions { 278 | const transformationType = super.getTransformationType(fn); 279 | let mapFrom: MapFromCallback; 280 | let condition: ConditionPredicate; 281 | let fromValue: TDestination[keyof TDestination]; 282 | let mapWith: MapWithOptions; 283 | let convertUsing: ConvertUsingOptions; 284 | 285 | const opts: DestinationMemberConfigOptions = { 286 | mapFrom: cb => { 287 | mapFrom = cb; 288 | }, 289 | mapWith: (destination, value) => { 290 | mapWith = { destination, value }; 291 | }, 292 | condition: predicate => { 293 | condition = predicate; 294 | }, 295 | ignore(): void { 296 | // do nothing 297 | }, 298 | fromValue: value => { 299 | fromValue = value; 300 | }, 301 | convertUsing: (converter, value) => { 302 | convertUsing = { converter, value }; 303 | } 304 | }; 305 | 306 | fn(opts); 307 | 308 | const mappingProperty: MappingProperty = { 309 | destinationKey: key, 310 | transformation: Object.freeze({ 311 | transformationType, 312 | // @ts-ignore 313 | mapFrom, 314 | // @ts-ignore 315 | condition, 316 | // @ts-ignore 317 | fromValue, 318 | // @ts-ignore 319 | mapWith, 320 | // @ts-ignore 321 | convertUsing 322 | }) 323 | }; 324 | 325 | mapping.properties.set(key, Object.freeze(mappingProperty)); 326 | 327 | return fluentFunctions; 328 | } 329 | 330 | private _createReverseMap( 331 | mapping: Mapping 332 | ): CreateReverseMapFluentFunctions { 333 | const reverseMapping = super._createReverseMappingObject(mapping); 334 | 335 | const reverseMapFluentFunctions: CreateReverseMapFluentFunctions = { 336 | forPath: (destination, forPathFn) => { 337 | const destinationKey = super._getKeyFromMemberFn(destination); 338 | return this._createMapForPath( 339 | reverseMapping, 340 | destinationKey, 341 | forPathFn, 342 | reverseMapFluentFunctions 343 | ); 344 | } 345 | }; 346 | 347 | return reverseMapFluentFunctions; 348 | } 349 | 350 | private _createMapForPath( 351 | mapping: Mapping, 352 | key: keyof TSource, 353 | fn: ForMemberExpression, 354 | fluentFunctions: CreateReverseMapFluentFunctions 355 | ): CreateReverseMapFluentFunctions { 356 | const transformationType = super.getTransformationType(fn); 357 | 358 | let mapFrom: MapFromCallback; 359 | let condition: ConditionPredicate; 360 | let fromValue: TSource[keyof TSource]; 361 | let mapWith: MapWithOptions; 362 | let convertUsing: ConvertUsingOptions; 363 | 364 | const opts: DestinationMemberConfigOptions = { 365 | mapFrom: cb => { 366 | mapFrom = cb; 367 | }, 368 | mapWith: (destination, value) => { 369 | mapWith = { destination, value }; 370 | }, 371 | condition: predicate => { 372 | condition = predicate; 373 | }, 374 | ignore(): void { 375 | // do nothing 376 | }, 377 | fromValue: value => { 378 | fromValue = value; 379 | }, 380 | convertUsing: (converter, value) => { 381 | convertUsing = { converter, value }; 382 | } 383 | }; 384 | 385 | fn(opts); 386 | 387 | const mappingProperty: MappingProperty = { 388 | destinationKey: key, 389 | transformation: Object.freeze({ 390 | transformationType, 391 | // @ts-ignore 392 | mapFrom, 393 | // @ts-ignore 394 | condition, 395 | // @ts-ignore 396 | fromValue, 397 | // @ts-ignore 398 | mapWith, 399 | // @ts-ignore 400 | convertUsing 401 | }) 402 | }; 403 | 404 | mapping.properties.set(key, Object.freeze(mappingProperty)); 405 | return fluentFunctions; 406 | } 407 | } 408 | 409 | /** 410 | * @instance AutoMapper singleton 411 | */ 412 | export const Mapper = AutoMapper.getInstance(); 413 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from 'class-transformer'; 2 | import get from 'lodash.get'; 3 | import isEmpty from 'lodash.isempty'; 4 | import { CamelCaseNamingConvention } from './naming'; 5 | import { 6 | Constructable, 7 | ForMemberExpression, 8 | ForPathDestinationFn, 9 | MapActionOptions, 10 | MapFromCallback, 11 | Mapping, 12 | NamingConvention, 13 | Resolver, 14 | TransformationType, 15 | ValueSelector 16 | } from './types'; 17 | 18 | export abstract class AutoMapperBase { 19 | protected readonly _mappings!: { [key: string]: Mapping }; 20 | 21 | protected constructor() { 22 | this._mappings = {}; 23 | } 24 | 25 | protected getTransformationType< 26 | TSource extends { [key in keyof TSource]: any } = any, 27 | TDestination extends {} = any 28 | >(forMemberFn: ForMemberExpression): TransformationType { 29 | const fnString = forMemberFn.toString(); 30 | if (fnString.includes('ignore')) { 31 | return TransformationType.Ignore; 32 | } 33 | 34 | if (fnString.includes('condition')) { 35 | return TransformationType.Condition; 36 | } 37 | 38 | if (fnString.includes('fromValue')) { 39 | return TransformationType.FromValue; 40 | } 41 | 42 | if (fnString.includes('mapWith')) { 43 | return TransformationType.MapWith; 44 | } 45 | 46 | if (fnString.includes('convertUsing')) { 47 | return TransformationType.ConvertUsing; 48 | } 49 | 50 | return TransformationType.MapFrom; 51 | } 52 | 53 | protected _mapArray< 54 | TSource extends { [key in keyof TSource]: any } = any, 55 | TDestination extends { [key in keyof TDestination]: any } = any 56 | >( 57 | sourceArray: TSource[], 58 | mapping: Mapping, 59 | option: MapActionOptions = { 60 | beforeMap: undefined, 61 | afterMap: undefined 62 | } 63 | ): TDestination[] { 64 | let destination: TDestination[] = []; 65 | const { beforeMap, afterMap } = option; 66 | 67 | if (beforeMap) { 68 | beforeMap(sourceArray, destination, { ...mapping }); 69 | } 70 | 71 | destination = sourceArray.map(s => this._map(s, mapping, {}, true)); 72 | 73 | if (afterMap) { 74 | afterMap(sourceArray, destination, { ...mapping }); 75 | } 76 | 77 | return destination; 78 | } 79 | 80 | protected _map< 81 | TSource extends { [key in keyof TSource]: any } = any, 82 | TDestination extends { [key in keyof TDestination]: any } = any 83 | >( 84 | sourceObj: TSource, 85 | mapping: Mapping, 86 | option: MapActionOptions = { beforeMap: undefined, afterMap: undefined }, 87 | isArrayMap: boolean = false 88 | ): TDestination { 89 | sourceObj = plainToClass(mapping.source, sourceObj); 90 | const { beforeMap, afterMap } = option; 91 | const { 92 | destination, 93 | properties, 94 | afterMapAction, 95 | beforeMapAction, 96 | sourceMemberNamingConvention, 97 | destinationMemberNamingConvention 98 | } = mapping; 99 | const destinationObj = plainToClass(destination, new destination()); 100 | const configProps = [...properties.keys()]; 101 | 102 | const destinationKeys = Object.keys(destinationObj); 103 | const destinationKeysLen = destinationKeys.length; 104 | 105 | if (!isArrayMap) { 106 | if (beforeMap) { 107 | beforeMap(sourceObj, destinationObj, { ...mapping }); 108 | } else if (beforeMapAction) { 109 | beforeMapAction(sourceObj, destinationObj, { ...mapping }); 110 | } 111 | } 112 | 113 | for (let i = 0; i < destinationKeysLen; i++) { 114 | const key = destinationKeys[i] as keyof TDestination; 115 | const sourceKey = AutoMapperBase._getSourcePropertyKey( 116 | destinationMemberNamingConvention, 117 | sourceMemberNamingConvention, 118 | key as string 119 | ); 120 | if (configProps.includes(key)) { 121 | continue; 122 | } 123 | 124 | if (!sourceObj.hasOwnProperty(sourceKey)) { 125 | const keys = sourceKey 126 | .split(sourceMemberNamingConvention.splittingExpression) 127 | .filter(Boolean); 128 | if (keys.length === 1 || !sourceObj.hasOwnProperty(keys[0])) { 129 | continue; 130 | } 131 | 132 | const flatten = get(sourceObj, keys.join('.')); 133 | if (typeof flatten === 'object') { 134 | continue; 135 | } 136 | destinationObj[key] = flatten; 137 | continue; 138 | } 139 | 140 | const sourceVal: TSource[keyof TSource] = sourceObj[sourceKey as keyof TSource]; 141 | if (sourceVal === undefined || sourceVal === null) { 142 | delete destinationObj[key]; 143 | continue; 144 | } 145 | 146 | if (typeof sourceVal === 'object') { 147 | if (AutoMapperBase._isDate(sourceVal)) { 148 | destinationObj[key] = new Date(sourceVal) as TDestination[keyof TDestination]; 149 | continue; 150 | } 151 | 152 | if (AutoMapperBase._isArray(sourceVal)) { 153 | if (isEmpty(sourceVal[0])) { 154 | destinationObj[key] = [] as any; 155 | continue; 156 | } 157 | 158 | if (typeof sourceVal[0] !== 'object') { 159 | destinationObj[key] = sourceVal.slice(); 160 | continue; 161 | } 162 | 163 | const nestedMapping = this._getMappingForNestedKey< 164 | TSource[keyof TSource], 165 | TDestination[keyof TDestination] 166 | >(sourceVal[0]); 167 | destinationObj[key] = this._mapArray(sourceVal, nestedMapping) as any; 168 | continue; 169 | } 170 | } 171 | 172 | if ( 173 | (typeof sourceVal === 'object' || typeof sourceVal === 'function') && 174 | AutoMapperBase._isClass(sourceVal) 175 | ) { 176 | const nestedMapping = this._getMappingForNestedKey< 177 | TSource[keyof TSource], 178 | TDestination[keyof TDestination] 179 | >(sourceVal); 180 | destinationObj[key] = this._map(sourceVal, nestedMapping); 181 | continue; 182 | } 183 | 184 | destinationObj[key] = sourceVal; 185 | } 186 | 187 | const propKeys: Array = []; 188 | for (let prop of properties.values()) { 189 | propKeys.push(prop.destinationKey); 190 | const propSourceKey = AutoMapperBase._getSourcePropertyKey( 191 | destinationMemberNamingConvention, 192 | sourceMemberNamingConvention, 193 | prop.destinationKey as string 194 | ); 195 | if (prop.transformation.transformationType === TransformationType.Ignore) { 196 | destinationObj[prop.destinationKey] = null as any; 197 | continue; 198 | } 199 | 200 | if (prop.transformation.transformationType === TransformationType.Condition) { 201 | const condition = prop.transformation.condition(sourceObj); 202 | if (condition) { 203 | destinationObj[prop.destinationKey] = (sourceObj as any)[propSourceKey] || null; 204 | console.warn(`${propSourceKey} does not exist on ${mapping.source}`); 205 | } 206 | continue; 207 | } 208 | 209 | if (prop.transformation.transformationType === TransformationType.FromValue) { 210 | destinationObj[prop.destinationKey] = prop.transformation.fromValue; 211 | continue; 212 | } 213 | 214 | if (prop.transformation.transformationType === TransformationType.MapWith) { 215 | const _mapping = this._getMappingForDestination(prop.transformation.mapWith.destination); 216 | const _source = prop.transformation.mapWith.value(sourceObj); 217 | 218 | if (isEmpty(_source)) { 219 | console.warn(`${propSourceKey} does not exist on ${_mapping.source}`); 220 | destinationObj[prop.destinationKey] = null as any; 221 | continue; 222 | } 223 | 224 | if (!AutoMapperBase._isClass(_source)) { 225 | console.warn( 226 | `${prop.destinationKey} is type ${prop.transformation.mapWith.destination.name} but ${_source} is a primitive. No mapping was executed` 227 | ); 228 | destinationObj[prop.destinationKey] = null as any; 229 | continue; 230 | } 231 | 232 | if (AutoMapperBase._isArray(_source)) { 233 | destinationObj[prop.destinationKey] = isEmpty(_source[0]) 234 | ? [] 235 | : (this._mapArray(_source, _mapping as Mapping) as any); 236 | continue; 237 | } 238 | 239 | destinationObj[prop.destinationKey] = this._map(_source, _mapping as Mapping); 240 | continue; 241 | } 242 | 243 | if (prop.transformation.transformationType === TransformationType.ConvertUsing) { 244 | const { converter, value } = prop.transformation.convertUsing; 245 | if (value == null) { 246 | const _source = (sourceObj as any)[propSourceKey]; 247 | 248 | if (_source == null) { 249 | console.warn(`${propSourceKey} does not exist on ${mapping.source}`); 250 | destinationObj[prop.destinationKey] = null as any; 251 | continue; 252 | } 253 | 254 | destinationObj[prop.destinationKey] = converter.convert(_source); 255 | continue; 256 | } 257 | 258 | destinationObj[prop.destinationKey] = converter.convert(value(sourceObj)); 259 | continue; 260 | } 261 | 262 | if (AutoMapperBase._isResolver(prop.transformation.mapFrom)) { 263 | destinationObj[prop.destinationKey] = prop.transformation.mapFrom.resolve( 264 | sourceObj, 265 | destinationObj, 266 | prop.transformation 267 | ); 268 | continue; 269 | } 270 | 271 | destinationObj[prop.destinationKey] = (prop.transformation.mapFrom as ValueSelector)( 272 | sourceObj 273 | ); 274 | } 275 | 276 | AutoMapperBase._assertMappingErrors(destinationObj, propKeys); 277 | 278 | if (!isArrayMap) { 279 | if (afterMap) { 280 | afterMap(sourceObj, destinationObj, { ...mapping }); 281 | } else if (afterMapAction) { 282 | afterMapAction(sourceObj, destinationObj, { ...mapping }); 283 | } 284 | } 285 | 286 | return destinationObj; 287 | } 288 | 289 | protected _mapAsync< 290 | TSource extends { [key in keyof TSource]: any } = any, 291 | TDestination extends { [key in keyof TDestination]: any } = any 292 | >( 293 | sourceObj: TSource, 294 | mapping: Mapping, 295 | option: MapActionOptions = { 296 | beforeMap: undefined, 297 | afterMap: undefined 298 | } 299 | ): Promise { 300 | return Promise.resolve().then(() => this._map(sourceObj, mapping, option)); 301 | } 302 | 303 | protected _mapArrayAsync< 304 | TSource extends { [key in keyof TSource]: any } = any, 305 | TDestination extends { [key in keyof TDestination]: any } = any 306 | >( 307 | sourceArray: TSource[], 308 | mapping: Mapping, 309 | option: MapActionOptions = { 310 | beforeMap: undefined, 311 | afterMap: undefined 312 | } 313 | ): Promise { 314 | return Promise.resolve().then(() => this._mapArray(sourceArray, mapping, option)); 315 | } 316 | 317 | private static _assertMappingErrors( 318 | obj: T, 319 | propKeys: Array 320 | ): void { 321 | const keys = Object.keys(obj); 322 | const unmappedKeys: string[] = []; 323 | for (let i = 0; i < keys.length; i++) { 324 | const key = keys[i]; 325 | if (propKeys.includes(key as keyof T)) { 326 | continue; 327 | } 328 | (obj[key as keyof T] === null || obj[key as keyof T] === undefined) && unmappedKeys.push(key); 329 | } 330 | 331 | if (unmappedKeys.length) { 332 | throw new Error(`The following keys are unmapped on ${obj.constructor.name || ''}: 333 | ---------------------------- 334 | ${unmappedKeys.join('\n')} 335 | `); 336 | } 337 | } 338 | 339 | protected _createMappingObject< 340 | TSource extends { [key in keyof TSource]: any } = any, 341 | TDestination extends { [key in keyof TDestination]: any } = any 342 | >( 343 | source: Constructable, 344 | destination: Constructable 345 | ): Mapping { 346 | const key = this._hasMapping(source, destination); 347 | 348 | const mapping: Mapping = { 349 | source, 350 | destination, 351 | sourceKey: source.name, 352 | destinationKey: destination.name, 353 | properties: new Map(), 354 | sourceMemberNamingConvention: new CamelCaseNamingConvention(), 355 | destinationMemberNamingConvention: new CamelCaseNamingConvention() 356 | }; 357 | 358 | this._mappings[key] = Object.seal(mapping); 359 | return mapping; 360 | } 361 | 362 | protected _createReverseMappingObject< 363 | TSource extends { [key in keyof TSource]: any } = any, 364 | TDestination extends { [key in keyof TDestination]: any } = any 365 | >(mapping: Mapping): Mapping { 366 | const key = this._hasMapping(mapping.destination, mapping.source); 367 | const reverseMapping = { 368 | source: mapping.destination, 369 | destination: mapping.source, 370 | sourceKey: mapping.destination.name, 371 | destinationKey: mapping.source.name, 372 | properties: new Map(), 373 | sourceMemberNamingConvention: mapping.destinationMemberNamingConvention, 374 | destinationMemberNamingConvention: mapping.sourceMemberNamingConvention 375 | }; 376 | 377 | // TODO: Implement reverse nested mapping 378 | // if (mapping.properties.size) { 379 | // for (const prop of mapping.properties.values()) { 380 | // const keys = lowerCase(prop.destinationKey as string).split(' '); 381 | // if (keys.length <= 1) { 382 | // continue; 383 | // } 384 | // 385 | // const mappingProperty: MappingProperty = { 386 | // destinationKey: keys.join('.') as keyof TDestination, 387 | // transformation: { 388 | // transformationType: TransformationType.FromValue, 389 | // fromValue: 390 | // } 391 | // }; 392 | // } 393 | // } 394 | 395 | this._mappings[key] = Object.seal(reverseMapping); 396 | 397 | return reverseMapping; 398 | } 399 | 400 | // TODO: This is not right. 401 | protected _getKeyFromMemberFn( 402 | fn: ForPathDestinationFn 403 | ): keyof T { 404 | return fn.toString().includes('function') 405 | ? ((fn 406 | .toString() 407 | .split('return') 408 | .pop() as string) 409 | .replace(/[};]/gm, '') 410 | .trim() 411 | .split('.') 412 | .pop() as keyof T) 413 | : ((fn 414 | .toString() 415 | .split('=>') 416 | .pop() as string) 417 | .trim() 418 | .split('.') 419 | .pop() as keyof T); 420 | } 421 | 422 | protected _getMapping( 423 | source: Constructable, 424 | destination: Constructable 425 | ): Mapping { 426 | const sourceName = source.prototype.constructor.name; 427 | const destinationName = destination.prototype.constructor.name; 428 | const mapping = this._mappings[AutoMapperBase._getMappingKey(sourceName, destinationName)]; 429 | 430 | if (!mapping) { 431 | throw new Error( 432 | `Mapping not found for source ${sourceName} and destination ${destinationName}` 433 | ); 434 | } 435 | 436 | return mapping; 437 | } 438 | 439 | protected _getMappingForDestination( 440 | destination: Constructable 441 | ): Mapping { 442 | const destinationName = destination.prototype.constructor.name; 443 | const sourceKey = Object.keys(this._mappings) 444 | .filter(key => key.includes(destinationName)) 445 | .find(key => this._mappings[key].destinationKey === destinationName); 446 | 447 | const sourceName = this._mappings[sourceKey as string].sourceKey; 448 | const mapping = this._mappings[AutoMapperBase._getMappingKey(sourceName, destinationName)]; 449 | 450 | if (!mapping) { 451 | throw new Error( 452 | `Mapping not found for source ${sourceName} and destination ${destinationName}` 453 | ); 454 | } 455 | 456 | return mapping; 457 | } 458 | 459 | protected _dispose() { 460 | Object.keys(this._mappings).forEach(key => { 461 | delete this._mappings[key]; 462 | }); 463 | } 464 | 465 | private _hasMapping( 466 | source: Constructable, 467 | destination: Constructable 468 | ): string { 469 | const key = AutoMapperBase._getMappingKey(source.name, destination.name); 470 | if (this._mappings[key]) { 471 | throw new Error( 472 | `Mapping for source ${source.name} and destination ${destination.name} is already existed` 473 | ); 474 | } 475 | 476 | return key; 477 | } 478 | 479 | private static _getMappingKey(sourceKey: string, destinationKey: string): string { 480 | return sourceKey + '->' + destinationKey; 481 | } 482 | 483 | private static _isClass(fn: Constructable): boolean { 484 | return ( 485 | fn.constructor && 486 | (/^\s*function/.test(fn.constructor.toString()) || 487 | /^\s*class/.test(fn.constructor.toString())) && 488 | fn.constructor.toString().includes(fn.constructor.name) 489 | ); 490 | } 491 | 492 | private static _isDate(fn: Constructable): boolean { 493 | return fn && Object.prototype.toString.call(fn) === '[object Date]' && !isNaN(fn as any); 494 | } 495 | 496 | private static _isArray(fn: Constructable): boolean { 497 | return Array.isArray(fn) && Object.prototype.toString.call(fn) === '[object Array]'; 498 | } 499 | 500 | private static _isResolver(fn: MapFromCallback): fn is Resolver { 501 | return 'resolve' in fn; 502 | } 503 | 504 | private _getMappingForNestedKey( 505 | val: Constructable 506 | ): Mapping { 507 | const mappingName = val.constructor.name; 508 | const destinationEntry = Object.entries(this._mappings) 509 | .filter(([key, _]) => key.includes(mappingName)) 510 | .find(([key, _]) => this._mappings[key].sourceKey === mappingName); 511 | 512 | if (!destinationEntry) { 513 | throw new Error(`Mapping not found for source ${mappingName}`); 514 | } 515 | 516 | const destination = destinationEntry[1].destination as Constructable; 517 | 518 | if (!destination) { 519 | throw new Error(`Mapping not found for source ${mappingName}`); 520 | } 521 | 522 | const mapping = this._getMapping(val, destination); 523 | 524 | if (!mapping) { 525 | throw new Error( 526 | `Mapping not found for source ${mappingName} and destination ${destination.name || 527 | destination.constructor.name}` 528 | ); 529 | } 530 | 531 | return mapping; 532 | } 533 | 534 | private static _getSourcePropertyKey( 535 | destinationMemberNamingConvention: NamingConvention, 536 | sourceMemberNamingConvention: NamingConvention, 537 | key: string 538 | ): string { 539 | const keyParts = key 540 | .split(destinationMemberNamingConvention.splittingExpression) 541 | .filter(Boolean); 542 | return !keyParts.length ? key : sourceMemberNamingConvention.transformPropertyName(keyParts); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './types'; 3 | export * from './automapper'; 4 | export * from './profile'; 5 | export * from './naming'; 6 | -------------------------------------------------------------------------------- /src/naming/camel-case-naming-convention.ts: -------------------------------------------------------------------------------- 1 | import { NamingConvention } from '../types'; 2 | 3 | export class CamelCaseNamingConvention implements NamingConvention { 4 | public separatorCharacter = ''; 5 | splittingExpression: RegExp = /(^[a-z]+(?=$|[A-Z]{1}[a-z0-9]+)|[A-Z]?[a-z0-9]+)/; 6 | 7 | public transformPropertyName(sourceNameParts: string[]): string { 8 | let result = ''; 9 | 10 | const len = sourceNameParts.length; 11 | 12 | for (let i = 0; i < len; i++) { 13 | if (i === 0) { 14 | result += sourceNameParts[i].charAt(0).toLowerCase() + sourceNameParts[i].substr(1); 15 | } else { 16 | result += sourceNameParts[i].charAt(0).toUpperCase() + sourceNameParts[i].substr(1); 17 | } 18 | } 19 | 20 | return result; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/naming/index.ts: -------------------------------------------------------------------------------- 1 | export * from './camel-case-naming-convention'; 2 | export * from './pascal-case-naming-convention'; 3 | -------------------------------------------------------------------------------- /src/naming/pascal-case-naming-convention.ts: -------------------------------------------------------------------------------- 1 | import { NamingConvention } from '../types'; 2 | 3 | export class PascalCaseNamingConvention implements NamingConvention { 4 | public separatorCharacter = ''; 5 | public splittingExpression: RegExp = /(^[A-Z]+(?=$|[A-Z]{1}[a-z0-9]+)|[A-Z]?[a-z0-9]+)/; 6 | 7 | public transformPropertyName(sourceNameParts: string[]): string { 8 | let result = ''; 9 | const len = sourceNameParts.length; 10 | 11 | for (let i = 0; i < len; i++) { 12 | result += sourceNameParts[i].charAt(0).toUpperCase() + sourceNameParts[i].substr(1); 13 | } 14 | 15 | return result; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/profile.ts: -------------------------------------------------------------------------------- 1 | import { AutoMapper } from './automapper'; 2 | import { MappingProfile } from './types'; 3 | 4 | /** 5 | * Abstract class for all mapping Profiles 6 | * 7 | */ 8 | export abstract class MappingProfileBase implements MappingProfile { 9 | /** 10 | * @property {string} profileName - the name of the Profile 11 | */ 12 | public profileName: string; 13 | 14 | /** 15 | * @constructor - initialize the profile with the profileName 16 | */ 17 | protected constructor() { 18 | this.profileName = this.constructor.name; 19 | } 20 | 21 | /** 22 | * @abstract configure() method to be called when using with Mapper.initialize() 23 | * 24 | * @param {AutoMapper} mapper - AutoMapper instance to add this Profile on 25 | */ 26 | abstract configure(mapper: AutoMapper): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { AutoMapper } from './automapper'; 2 | 3 | export type Unpacked = T extends (infer U)[] 4 | ? U 5 | : T extends (...args: any[]) => infer U 6 | ? U 7 | : T extends Promise 8 | ? U 9 | : T; 10 | 11 | export enum TransformationType { 12 | /** 13 | * when `opts.ignore()` is used on `forMember()` 14 | */ 15 | Ignore = 0, 16 | /** 17 | * when `opts.mapFrom()` is used on `forMember()` 18 | */ 19 | MapFrom = 1, 20 | /** 21 | * when `opts.condition()` is used on `forMember()` 22 | */ 23 | Condition = 2, 24 | /** 25 | * when `opts.fromValue()` is used on `forMember()` 26 | */ 27 | FromValue = 3, 28 | /** 29 | * when `opts.mapWith()` is used on `forMember()` 30 | */ 31 | MapWith = 4, 32 | /** 33 | * when `opts.convertUsing()` is used on `forMember()` 34 | */ 35 | ConvertUsing = 5 36 | } 37 | 38 | /** 39 | * A new-able type 40 | */ 41 | export type Constructable = new (...args: any[]) => T; 42 | 43 | export type MapActionOptions< 44 | TSource extends { [key in keyof TSource]: any } = any, 45 | TDestination extends { [key in keyof TDestination]: any } = any 46 | > = { 47 | beforeMap?: BeforeAfterMapAction; 48 | afterMap?: BeforeAfterMapAction; 49 | }; 50 | 51 | export type BeforeAfterMapAction = ( 52 | source: TSource, 53 | destination: TDestination, 54 | mapping?: Mapping 55 | ) => void; 56 | 57 | export interface Converter { 58 | convert(source: TSource): TDestination; 59 | } 60 | 61 | export interface Resolver< 62 | TSource extends { [key in keyof TSource]: any } = any, 63 | TDestination extends { [key in keyof TDestination]: any } = any, 64 | K extends keyof TDestination = never 65 | > { 66 | resolve( 67 | source: TSource, 68 | destination: TDestination, 69 | transformation: MappingTransformation 70 | ): TDestination[K]; 71 | } 72 | 73 | /** 74 | * Value Selector from a source type 75 | * 76 | * @example 77 | * 78 | * ```ts 79 | * source => source.foo.bar 80 | * ``` 81 | */ 82 | export type ValueSelector< 83 | TSource extends { [key in keyof TSource]: any } = any, 84 | TDestination extends { [key in keyof TDestination]: any } = any, 85 | K extends keyof TDestination = never 86 | > = (source: TSource) => TDestination[K]; 87 | 88 | export type MapFromCallback< 89 | TSource extends { [key in keyof TSource]: any } = any, 90 | TDestination extends { [key in keyof TDestination]: any } = any, 91 | K extends keyof TDestination = never 92 | > = ValueSelector | Resolver; 93 | 94 | /** 95 | * Condition Predicate from a source 96 | */ 97 | export type ConditionPredicate = ( 98 | source: TSource 99 | ) => boolean; 100 | 101 | /** 102 | * Options for mapWith 103 | */ 104 | export type MapWithOptions< 105 | TSource extends { [key in keyof TSource]: any } = any, 106 | TDestination extends { [key in keyof TDestination]: any } = any 107 | > = { 108 | destination: Constructable>; 109 | value: ValueSelector; 110 | }; 111 | 112 | export type ConvertUsingOptions< 113 | TSource extends { [key in keyof TSource]: any } = any, 114 | TDestination extends { [key in keyof TDestination]: any } = any 115 | > = { 116 | converter: Converter; 117 | value?: (source: TSource) => TSource[keyof TSource]; 118 | }; 119 | 120 | export interface DestinationMemberConfigOptions< 121 | TSource extends { [key in keyof TSource]: any } = any, 122 | TDestination extends { [key in keyof TDestination]: any } = any, 123 | K extends keyof TDestination = never 124 | > { 125 | mapFrom(cb: MapFromCallback): void; 126 | 127 | mapWith( 128 | destination: Constructable>, 129 | value: ValueSelector 130 | ): void; 131 | 132 | condition(predicate: ConditionPredicate): void; 133 | 134 | fromValue(value: TDestination[K]): void; 135 | 136 | ignore(): void; 137 | 138 | convertUsing< 139 | TConvertSource extends TSource[keyof TSource], 140 | TConvertDestination extends TDestination[K] 141 | >( 142 | converter: Converter, 143 | value?: (source: TSource) => TConvertSource 144 | ): void; 145 | } 146 | 147 | export interface ForMemberExpression< 148 | TSource extends { [key in keyof TSource]: any } = any, 149 | TDestination extends { [key in keyof TDestination]: any } = any, 150 | K extends keyof TDestination = never 151 | > { 152 | (opts: DestinationMemberConfigOptions): void; 153 | } 154 | 155 | export type ForPathDestinationFn< 156 | TDestination extends { [key in keyof TDestination]: any } = any 157 | > = (destination: TDestination) => TDestination[keyof TDestination]; 158 | 159 | export interface CreateReverseMapFluentFunctions< 160 | TDestination extends { [key in keyof TDestination]: any } = any, 161 | TSource extends { [key in keyof TSource]: any } = any 162 | > { 163 | forPath( 164 | destination: ForPathDestinationFn, 165 | forPathFn: ForMemberExpression 166 | ): CreateReverseMapFluentFunctions; 167 | } 168 | 169 | export interface CreateMapFluentFunctions< 170 | TSource extends { [key in keyof TSource]: any } = any, 171 | TDestination extends { [key in keyof TDestination]: any } = any 172 | > { 173 | forMember( 174 | key: K, 175 | expression: ForMemberExpression 176 | ): CreateMapFluentFunctions; 177 | 178 | beforeMap( 179 | action: BeforeAfterMapAction 180 | ): CreateMapFluentFunctions; 181 | 182 | afterMap( 183 | action: BeforeAfterMapAction 184 | ): CreateMapFluentFunctions; 185 | 186 | reverseMap(): CreateReverseMapFluentFunctions; 187 | 188 | setSourceNamingConvention( 189 | namingConvention: NamingConvention 190 | ): CreateMapFluentFunctions; 191 | 192 | setDestinationNamingConvention( 193 | namingConvention: NamingConvention 194 | ): CreateMapFluentFunctions; 195 | } 196 | 197 | export interface Configuration { 198 | addProfile(profile: MappingProfile): void; 199 | 200 | createMap( 201 | source: Constructable, 202 | destination: Constructable 203 | ): CreateMapFluentFunctions; 204 | } 205 | 206 | export interface MappingTransformation< 207 | TSource extends { [key in keyof TSource]: any } = any, 208 | TDestination extends { [key in keyof TDestination]: any } = any 209 | > { 210 | transformationType: TransformationType; 211 | mapFrom: MapFromCallback; 212 | mapWith: MapWithOptions; 213 | condition: ConditionPredicate; 214 | fromValue: TDestination[keyof TDestination]; 215 | convertUsing: ConvertUsingOptions; 216 | } 217 | 218 | export interface MappingProperty< 219 | TSource extends { [key in keyof TSource]: any } = any, 220 | TDestination extends { [key in keyof TDestination]: any } = any 221 | > { 222 | destinationKey: keyof TDestination; 223 | transformation: MappingTransformation; 224 | } 225 | 226 | export interface Mapping< 227 | TSource extends { [key in keyof TSource]: any } = any, 228 | TDestination extends { [key in keyof TDestination]: any } = any 229 | > { 230 | source: Constructable; 231 | destination: Constructable; 232 | sourceKey: string; 233 | destinationKey: string; 234 | properties: Map>; 235 | sourceMemberNamingConvention: NamingConvention; 236 | destinationMemberNamingConvention: NamingConvention; 237 | beforeMapAction?: BeforeAfterMapAction; 238 | afterMapAction?: BeforeAfterMapAction; 239 | } 240 | 241 | export interface MappingProfile { 242 | profileName: string; 243 | configure: (mapper: AutoMapper) => void; 244 | } 245 | 246 | export type NamingConvention = { 247 | splittingExpression: RegExp; 248 | separatorCharacter: string; 249 | transformPropertyName: (sourcePropNameParts: string[]) => string; 250 | }; 251 | -------------------------------------------------------------------------------- /test/automapper.test.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | import 'reflect-metadata'; 3 | import { 4 | AutoMapper, 5 | BeforeAfterMapAction, 6 | Converter, 7 | ExposedType, 8 | Mapper, 9 | Mapping, 10 | MappingProfileBase, 11 | MappingTransformation, 12 | Resolver 13 | } from '../src'; 14 | import { PascalCaseNamingConvention } from '../src/naming'; 15 | 16 | class User { 17 | @Expose() 18 | firstName!: string; 19 | @Expose() 20 | lastName!: string; 21 | @ExposedType(() => Nested) 22 | nested!: Nested; 23 | } 24 | 25 | class UserVm { 26 | @Expose() 27 | firstName!: string; 28 | @Expose() 29 | lastName!: string; 30 | @Expose() 31 | fullName!: string; 32 | @ExposedType(() => NestedVm) 33 | nested!: NestedVm; 34 | } 35 | 36 | class Address { 37 | @Expose() 38 | street!: string; 39 | @Expose() 40 | city!: string; 41 | @Expose() 42 | state!: string; 43 | } 44 | 45 | class AddressVm { 46 | @Expose() 47 | addressString!: string; 48 | } 49 | 50 | class Profile { 51 | @Expose() 52 | bio!: string; 53 | @Expose() 54 | avatar!: string; 55 | } 56 | 57 | class ProfileVm { 58 | @Expose() 59 | avatarUrl!: string; 60 | } 61 | 62 | class Nested { 63 | @Expose() 64 | foo!: string; 65 | @Expose() 66 | foobar!: number; 67 | @Expose() 68 | foobaz!: boolean; 69 | @Expose() 70 | foobarbar!: number; 71 | @Expose() 72 | foofoobarbar!: string; 73 | } 74 | 75 | class NestedVm { 76 | @Expose() 77 | bar!: string; 78 | @Expose() 79 | barfoo!: number; 80 | @Expose() 81 | bazfoo!: boolean; 82 | @Expose() 83 | barbarfoo!: number; 84 | @Expose() 85 | barbarfoofoo!: Date; 86 | } 87 | 88 | class EmployeeAddress { 89 | @Expose() 90 | Street!: string; 91 | @Expose() 92 | City!: string; 93 | @Expose() 94 | State!: string; 95 | } 96 | 97 | class Employee { 98 | @Expose() 99 | Name!: string; 100 | @Expose() 101 | Department!: string; 102 | @ExposedType(() => EmployeeAddress) 103 | Address!: EmployeeAddress; 104 | } 105 | 106 | class EmployeeVm { 107 | @Expose() 108 | nameAndDepartment!: string; 109 | @Expose() 110 | addressStreet!: string; 111 | @Expose() 112 | addressCity!: string; 113 | @Expose() 114 | addressState!: string; 115 | } 116 | 117 | class DateFormatter implements Converter { 118 | convert(source: string): Date { 119 | return new Date(source); 120 | } 121 | } 122 | 123 | class StringFormatter implements Converter { 124 | convert(source: Date): string { 125 | return source.toISOString(); 126 | } 127 | } 128 | 129 | class CityToStateResolver implements Resolver { 130 | resolve( 131 | source: Employee, 132 | destination: EmployeeVm, 133 | transformation: MappingTransformation 134 | ): EmployeeVm['addressCity'] { 135 | return source.Address.State; 136 | } 137 | } 138 | 139 | class AddressProfile extends MappingProfileBase { 140 | constructor() { 141 | super(); 142 | } 143 | 144 | configure(mapper: AutoMapper): void { 145 | mapper 146 | .createMap(Address, AddressVm) 147 | .forMember('addressString', opts => 148 | opts.mapFrom(s => s.street + ' ' + s.city + ' ' + s.state) 149 | ); 150 | } 151 | } 152 | 153 | describe('automapper-nartc', () => { 154 | it('AutoMapper exposes a singleton', () => { 155 | expect(Mapper).toBeInstanceOf(AutoMapper); 156 | const _instance = AutoMapper.getInstance(); 157 | expect(_instance).toBeInstanceOf(AutoMapper); 158 | expect(_instance).toBe(Mapper); 159 | }); 160 | 161 | it('AutoMapper is instantiable', () => { 162 | const _mapper = new AutoMapper(); 163 | expect(_mapper).toBeInstanceOf(AutoMapper); 164 | expect(_mapper).not.toBe(Mapper); 165 | }); 166 | 167 | it('AutoMapper instance is disposable', () => { 168 | const _mapper = new AutoMapper(); 169 | 170 | Mapper.createMap(Profile, ProfileVm) 171 | .reverseMap() 172 | .forPath(d => d.avatar, opts => opts.mapFrom(s => s.avatarUrl)) 173 | .forPath(d => d.bio, opts => opts.ignore()); 174 | Mapper.initialize(cfg => { 175 | cfg.createMap(User, UserVm); 176 | cfg.addProfile(new AddressProfile()); 177 | }); 178 | 179 | expect(Mapper).not.toEqual(_mapper); 180 | Mapper.dispose(); 181 | expect(Mapper).toEqual(_mapper); 182 | }); 183 | 184 | it('AutoMapper fluent API for Profile', () => { 185 | const _instance = Mapper.addProfile(new AddressProfile()); 186 | expect(_instance).toBeInstanceOf(AutoMapper); 187 | expect(_instance).toEqual(Mapper); 188 | Mapper.dispose(); 189 | }); 190 | 191 | it('AutoMapper fluent API throw error for adding duplicate Profile', () => { 192 | const profile = new AddressProfile(); 193 | let message: string = ''; 194 | try { 195 | Mapper.addProfile(profile).addProfile(profile); 196 | } catch (e) { 197 | message = e.message; 198 | } 199 | 200 | expect(message).toBeDefined(); 201 | expect(message).toEqual( 202 | `${profile.profileName} is already existed on the current Mapper instance` 203 | ); 204 | 205 | Mapper.dispose(); 206 | }); 207 | }); 208 | 209 | describe('automapper-nartc: mapping', () => { 210 | let employee: Employee; 211 | let user: User; 212 | let users: User[] = []; 213 | let address: Address; 214 | let addresses: Address[] = []; 215 | let profile: Profile; 216 | let profiles: Profile[] = []; 217 | 218 | beforeEach(() => { 219 | Mapper.createMap(Employee, EmployeeVm) 220 | .setSourceNamingConvention(new PascalCaseNamingConvention()) 221 | .forMember('nameAndDepartment', opts => opts.mapFrom(s => s.Name + ' ' + s.Department)) 222 | .forMember('addressCity', opts => opts.mapFrom(new CityToStateResolver())); 223 | Mapper.createMap(Profile, ProfileVm) 224 | .forMember('avatarUrl', opts => opts.mapFrom(s => s.avatar)) 225 | .reverseMap() 226 | .forPath(d => d.avatar, opts => opts.mapFrom(s => s.avatarUrl)) 227 | .forPath(d => d.bio, opts => opts.ignore()); 228 | Mapper.initialize(cfg => { 229 | cfg 230 | .createMap(Nested, NestedVm) 231 | .forMember('bar', opts => opts.mapFrom(s => s.foo)) 232 | .forMember('barfoo', opts => opts.ignore()) 233 | .forMember('bazfoo', opts => opts.fromValue(false)) 234 | .forMember('barbarfoo', opts => opts.condition(s => s.foobaz)) 235 | .forMember('barbarfoofoo', opts => 236 | opts.convertUsing(new DateFormatter(), source => source.foofoobarbar) 237 | ) 238 | .reverseMap() 239 | .forPath(s => s.foobarbar, opts => opts.ignore()) 240 | .forPath(s => s.foobar, opts => opts.condition(d => d.bazfoo)) 241 | .forPath(s => s.foobaz, opts => opts.fromValue(true)) 242 | .forPath( 243 | s => s.foofoobarbar, 244 | opts => opts.convertUsing(new StringFormatter(), source => source.barbarfoofoo) 245 | ); 246 | cfg 247 | .createMap(User, UserVm) 248 | .forMember('fullName', opts => opts.mapFrom(s => s.firstName + ' ' + s.lastName)) 249 | .forMember('nested', opts => opts.mapWith(NestedVm, source => source.nested)) 250 | .reverseMap() 251 | .forPath(s => s.nested, opts => opts.mapWith(Nested, source => source.nested)); 252 | cfg.addProfile(new AddressProfile()); 253 | }); 254 | 255 | user = new User(); 256 | user.firstName = 'Chau'; 257 | user.lastName = 'Tran'; 258 | user.nested = new Nested(); 259 | user.nested.foo = 'foo'; 260 | 261 | address = new Address(); 262 | address.street = 'Some'; 263 | address.city = 'City'; 264 | address.state = 'State'; 265 | 266 | profile = new Profile(); 267 | profile.bio = 'Some bio'; 268 | profile.avatar = 'Some link'; 269 | 270 | employee = new Employee(); 271 | employee.Name = 'Chau'; 272 | employee.Department = 'Code'; 273 | employee.Address = new EmployeeAddress(); 274 | employee.Address.Street = 'Some'; 275 | employee.Address.City = 'City'; 276 | employee.Address.State = 'State'; 277 | 278 | users.push(user); 279 | addresses.push(address); 280 | profiles.push(profile); 281 | }); 282 | 283 | afterEach(() => { 284 | Mapper.dispose(); 285 | users = []; 286 | addresses = []; 287 | profiles = []; 288 | }); 289 | 290 | it('map with createMap', () => { 291 | const vm = Mapper.map(profile, ProfileVm); 292 | expect(vm.avatarUrl).toEqual(profile.avatar); 293 | expect(vm).toBeInstanceOf(ProfileVm); 294 | }); 295 | 296 | it('mapAsync with createMap', async () => { 297 | const vm = await Mapper.mapAsync(profile, ProfileVm); 298 | expect(vm.avatarUrl).toEqual(profile.avatar); 299 | expect(vm).toBeInstanceOf(ProfileVm); 300 | }); 301 | 302 | it('simple reverseMap', () => { 303 | const vm = Mapper.map(profile, ProfileVm); 304 | const _profile = Mapper.map(vm, Profile); 305 | 306 | expect(_profile).toBeDefined(); 307 | expect(_profile.bio).toBeFalsy(); 308 | expect(_profile.avatar).toEqual(vm.avatarUrl); 309 | expect(_profile).toBeInstanceOf(Profile); 310 | }); 311 | 312 | it('map with config.createMap', () => { 313 | const vm = Mapper.map(user, UserVm); 314 | expect(vm.firstName).toEqual(user.firstName); 315 | expect(vm.lastName).toEqual(user.lastName); 316 | expect(vm.fullName).toEqual(user.firstName + ' ' + user.lastName); 317 | expect(vm).toBeInstanceOf(UserVm); 318 | }); 319 | 320 | it('map with nested model', () => { 321 | const vm = Mapper.map(user, UserVm); 322 | expect(vm.nested).toBeDefined(); 323 | expect(vm.nested.bar).toEqual(user.nested.foo); 324 | expect(vm.nested).toBeInstanceOf(NestedVm); 325 | }); 326 | 327 | it('map with config.addProfile', () => { 328 | const vm = Mapper.map(address, AddressVm); 329 | expect(vm.addressString).toEqual(address.street + ' ' + address.city + ' ' + address.state); 330 | expect(vm).toBeInstanceOf(AddressVm); 331 | }); 332 | 333 | it('mapArray', () => { 334 | const userVms = Mapper.mapArray(users, UserVm); 335 | const addressVms = Mapper.mapArray(addresses, AddressVm); 336 | const profileVms = Mapper.mapArray(profiles, ProfileVm); 337 | 338 | expect(userVms).toBeTruthy(); 339 | expect(userVms).toHaveLength(1); 340 | userVms.forEach(vm => expect(vm).toBeInstanceOf(UserVm)); 341 | 342 | expect(addressVms).toBeTruthy(); 343 | expect(addressVms).toHaveLength(1); 344 | addressVms.forEach(vm => expect(vm).toBeInstanceOf(AddressVm)); 345 | 346 | expect(profileVms).toBeTruthy(); 347 | expect(profileVms).toHaveLength(1); 348 | profileVms.forEach(vm => expect(vm).toBeInstanceOf(ProfileVm)); 349 | }); 350 | 351 | it('mapArrayAsync', async () => { 352 | const userVms = await Mapper.mapArrayAsync(users, UserVm); 353 | const addressVms = await Mapper.mapArrayAsync(addresses, AddressVm); 354 | const profileVms = await Mapper.mapArrayAsync(profiles, ProfileVm); 355 | 356 | expect(userVms).toBeTruthy(); 357 | expect(userVms).toHaveLength(1); 358 | userVms.forEach(vm => expect(vm).toBeInstanceOf(UserVm)); 359 | 360 | expect(addressVms).toBeTruthy(); 361 | expect(addressVms).toHaveLength(1); 362 | addressVms.forEach(vm => expect(vm).toBeInstanceOf(AddressVm)); 363 | 364 | expect(profileVms).toBeTruthy(); 365 | expect(profileVms).toHaveLength(1); 366 | profileVms.forEach(vm => expect(vm).toBeInstanceOf(ProfileVm)); 367 | }); 368 | 369 | it('beforeMap callback', () => { 370 | let _source: User; 371 | let _destination: UserVm; 372 | let _mapping: Mapping; 373 | const cb: BeforeAfterMapAction = jest.fn((source, destination, mapping) => { 374 | _source = source; 375 | _destination = destination; 376 | _mapping = mapping as Mapping; 377 | }); 378 | Mapper.map(user, UserVm, { beforeMap: cb }); 379 | 380 | expect(cb).toBeCalled(); 381 | // @ts-ignore 382 | expect(_source).toBeTruthy(); 383 | // @ts-ignore 384 | expect(_source).toEqual(user); 385 | // @ts-ignore 386 | expect(_destination).toBeTruthy(); 387 | // @ts-ignore 388 | expect(_mapping).toBeTruthy(); 389 | }); 390 | 391 | it('afterMap callback', () => { 392 | let _source: User; 393 | let _destination: UserVm; 394 | let _mapping: Mapping; 395 | const cb: BeforeAfterMapAction = jest.fn((source, destination, mapping) => { 396 | _source = source; 397 | _destination = destination; 398 | _mapping = mapping as Mapping; 399 | }); 400 | const vm = Mapper.map(user, UserVm, { afterMap: cb }); 401 | 402 | expect(cb).toBeCalled(); 403 | // @ts-ignore 404 | expect(_source).toBeTruthy(); 405 | // @ts-ignore 406 | expect(_source).toEqual(user); 407 | // @ts-ignore 408 | expect(_destination).toBeTruthy(); 409 | // @ts-ignore 410 | expect(_destination).toEqual(vm); 411 | // @ts-ignore 412 | expect(_mapping).toBeTruthy(); 413 | }); 414 | 415 | it('pascalNamingConvention', () => { 416 | const vm = Mapper.map(employee, EmployeeVm); 417 | expect(vm).toBeTruthy(); 418 | expect(vm.nameAndDepartment).toEqual(employee.Name + ' ' + employee.Department); 419 | expect(vm.addressStreet).toEqual(employee.Address.Street); 420 | expect(vm.addressCity).toEqual(employee.Address.State); 421 | }); 422 | }); 423 | -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require('shelljs'); 2 | const { readFileSync } = require('fs'); 3 | const url = require('url'); 4 | 5 | let repoUrl; 6 | let pkg = JSON.parse(readFileSync('package.json') as any); 7 | if (typeof pkg.repository === 'object') { 8 | if (!pkg.repository.hasOwnProperty('url')) { 9 | throw new Error('URL does not exist in repository section'); 10 | } 11 | repoUrl = pkg.repository.url; 12 | } else { 13 | repoUrl = pkg.repository; 14 | } 15 | 16 | let parsedUrl = url.parse(repoUrl); 17 | let repository = (parsedUrl.host || '') + (parsedUrl.path || ''); 18 | let ghToken = process.env.GH_TOKEN; 19 | 20 | echo('Deploying docs!!!'); 21 | cd('docs'); 22 | touch('.nojekyll'); 23 | exec('git init'); 24 | exec('git add .'); 25 | exec('git config user.name "Chau"'); 26 | exec('git config user.email "ctch5@mail.umsl.edu"'); 27 | exec('git commit -m "docs(docs): update gh-pages"'); 28 | exec(`git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages`); 29 | echo('Docs deployed!!'); 30 | -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { fork } = require('child_process'); 3 | const colors = require('colors'); 4 | 5 | const { readFileSync, writeFileSync } = require('fs'); 6 | const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'))); 7 | 8 | pkg.scripts.prepush = 'npm run test:prod && npm run build'; 9 | pkg.scripts.commitmsg = 'commitlint -E HUSKY_GIT_PARAMS'; 10 | 11 | writeFileSync(path.resolve(__dirname, '..', 'package.json'), JSON.stringify(pkg, null, 2)); 12 | 13 | // Call husky to set up the hooks 14 | fork(path.resolve(__dirname, '..', 'node_modules', 'husky', 'lib', 'installer', 'bin'), [ 15 | 'install' 16 | ]); 17 | 18 | console.log(); 19 | console.log(colors.green('Done!!')); 20 | console.log(); 21 | 22 | if (pkg.repository.url.trim()) { 23 | console.log(colors.cyan('Now run:')); 24 | console.log(colors.cyan(' npm install -g semantic-release-cli')); 25 | console.log(colors.cyan(' semantic-release-cli setup')); 26 | console.log(); 27 | console.log(colors.cyan('Important! Answer NO to "Generate travis.yml" question')); 28 | console.log(); 29 | console.log( 30 | colors.gray('Note: Make sure "repository.url" in your package.json is correct before') 31 | ); 32 | } else { 33 | console.log(colors.red('First you need to set the "repository.url" property in package.json')); 34 | console.log(colors.cyan('Then run:')); 35 | console.log(colors.cyan(' npm install -g semantic-release-cli')); 36 | console.log(colors.cyan(' semantic-release-cli setup')); 37 | console.log(); 38 | console.log(colors.cyan('Important! Answer NO to "Generate travis.yml" question')); 39 | } 40 | 41 | console.log(); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "declarationDir": "dist/types", 16 | "outDir": "dist/lib", 17 | "typeRoots": ["node_modules/@types"] 18 | }, 19 | "include": ["src", "index.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ], 6 | "rules": { 7 | "strict-type-predicates": false 8 | } 9 | } 10 | --------------------------------------------------------------------------------