automapper-nartc v6.1.3
AutoMapper - Nartc
This is a fork of automapper-ts
by Bert Loedeman. My goal is to re-create this awesome library with a more strong-type approach while learning TypeScript
myself.
Documentations
Github Pages https://nartc.github.io/automapper-nartc/
Motivations
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.
Features
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.
So far, the following is supported:
- Basic Mapping between two classes
- Basic Mapping for nested classes
- Array/List Mapping
- Flattening
- ReverseMap - Very basic
reverseMapping
feature. Use for primitives models only if you can. - Value Converters
- Value Resolvers
- Async
- Before/After Callback
- Naming Conventions
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.
Future features:
- Type Converters - Help needed
- Value Transformers
Will not support:
- Null Substitution - It makes more sense to use
fromValue()
instead of implementnullSubstitution()
. Please let me know of a use-case wherenullSubstitution()
makes sense.
Contributions are appreciated.
Implementation note:
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.
Installation
npm install --save automapper-nartc
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
Usage
- Assuming you have couple of
Domain Models
as follows:
class Address {
address: string;
city: string;
state: string;
zip: string;
}
class Profile {
bio: string;
phone: string;
email: string;
addresses: Address[];
constructor() {
this.addresses = [];
}
}
class User {
firstName: string;
lastName: string;
password: string;
profile: Profile;
}
- And you also have couple of
View Models
(orDTOs
):
class ProfileVm {
bio: string;
email: string;
addressStrings: string[];
}
class UserVm {
fullName: string;
profile: ProfileVm;
firstName?: string;
lastName?: string;
}
- Decorate all of your properties with
@Expose()
.@Expose
is imported fromclass-transformer
. This will allow the engine to be aware of all the properties available in a certain class.
class User {
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
password: string;
@Expose()
profile: Profile;
}
class UserVm {
@Expose()
fullName: string;
@Expose()
profile: ProfileVm;
@Expose()
firstName?: string;
@Expose()
lastName?: string;
}
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
.
class User {
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
password: string;
@Expose()
@Type(() => Profile)
profile: Profile;
}
class UserVm {
@Expose()
fullName: string;
@Expose()
@Type(() => ProfileVm)
profile: ProfileVm;
@Expose()
firstName?: string;
@Expose()
lastName?: string;
}
However, automapper-nartc
provides a short-hand decorator @ExposedType()
instead of explicitly use @Expose()
and @Type()
on a nested model property.
class UserVm {
@Expose()
fullName: string;
@ExposedType(() => ProfileVm)
profile: ProfileVm;
@Expose()
firstName?: string;
@Expose()
lastName?: string;
}
- Next, import
Mapper
fromautomapper-nartc
. You can also just instantiate a new instance ofAutoMapper
if you want to manage your instance. - Initialize
Mapper
withinitialize()
method.initialize()
expects aConfiguration
callback that will give you access to theConfiguration
object. There are two methods on theConfiguration
object that you can use to setup yourMapper
createMap()
:createMap()
expects a source as the first argument and the destination as the second argument.createMap()
returnsCreateMapFluentFunctions<TSource, TDestination>
(Read more at API Reference).
import { Mapper, MappingProfileBase } from 'automapper-nartc';
Mapper.initialize(config => {
config.createMap(User, UserVm); // create a mapping from User to UserVm (one direction)
config.createMap(Profile, ProfileVm)
.forMember('addressStrings', opts => opts.mapFrom(s => s.addresses.map(... /* map to addressString however you like */)));
});
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.
Mapper.initialize(config => {
config
.createMap(User, UserVm) // create a mapping from User to UserVm (one direction)
.forMember('fullName', opts =>
opts.mapFrom(source => source.firstName + ' ' + source.lastName)
); // You will get type-inference here
});
addProfile()
:addProfile()
expects a new instance of a class which extendsMappingProfileBase
. Usually, you can just initialize yourMapper
withconfig.createMap
and setup all your mappings that way. But more than often, it is better to separate your mappings intoProfile
which will create the mappings for specific set of source and destination
import { MappingProfileBase } from 'automapper-nartc';
export class UserProfile extends MappingProfileBase {
constructor() {
super(); // this is required since it will take UserProfile and get the string "UserProfile" to assign to profileName
}
// configure() is required since it is an abstract method. configure() will be called automatically by Mapper.
// This is where you will setup your mapping with the class method: createMap
configure(mapper: AutoMapper) {
mapper
.createMap(User, UserVm)
.forMember('fullName', opts =>
opts.mapFrom(source => source.firstName + ' ' + source.lastName)
); // You will get type-inference here
}
}
// in another file
Mapper.initialize(config => {
config.addProfile(new UserProfile());
});
- When you're ready to map, call
Mapper.map()
.
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
console.log('instance of UserVm?', userVm instanceof UserVm); // true
Callbacks
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.
There are two ways you can provide the callbacks: Map
level and Mapping
level.
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.
- Map level: all
map()
methods have the third parameter which has a shape ofMapActionOptions: {beforeMap: Function, afterMap: Function}
. If any of the callbacks is provided, it will be called in correct chronological order.
/**
* In this case, both callbacks will be called with the following arguments.
*
* @param {User} source
* @param {UserVm} destination
* @param {Mapping<User, UserVm>} mapping
*/
const userVm = Mapper.map(user, UserVm, {
beforeMap: (source, destination, mapping) => {},
afterMap: (source, destination, mapping) => {}
});
- Mapping level: callbacks on the
Mapping
level will be called for ALL map operations on the two models unless you provide diferent callbacks to specificmap
operation (akaMap
level)
/**
* In this case, both callbacks will be called with the following arguments.
*
* @param {User} source
* @param {UserVm} destination
* @param {Mapping<User, UserVm>} mapping
*/
Mapper.initialize(config => {
config
.createMap(User, UserVm)
.beforeMap((source, destination, mapping) => {})
.afterMap((source, destination, mapping) => {}); // create a mapping from User to UserVm (one direction)
});
NOTE 1: Map
level callbacks will overide Mapping
level callbacks if both are provided
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.
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()
- Use
Mapper.mapArray()
if you want to map fromTSource[]
toTDestination[]
.
Demo
Codesandbox Demo Codesandbox
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago