Skip to content

A fast, simple, and powerful library that helps you design your application layers correctly and ensures reliable mapping of your entities. The library actively uses TypeScript's type checking capabilities and the pre-assembly of mapping rules to achieve maximum speed and comfort for the developer.

Notifications You must be signed in to change notification settings

placDev/mappers-nest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project Logo

mappers

A fast, simple, and powerful library that helps you design your application layers correctly and ensures reliable mapping of your entities. The library actively uses TypeScript's type checking capabilities and the pre-assembly of mapping rules to achieve maximum speed and comfort for the developer.
This solution provides you with all the necessary tools to solve class conversion problems, providing a convenient declarative syntax and the ability to reliably validate the resulting instances.

Documentation

Installation

Install @mappers/nest:

npm i @mappers/nest

This is the official integration package for the NestJS framework for Core

Idea

Help TypeScript developers build cleaner applications based on Object-Oriented Programming concepts with separation into layers that communicate through independent contracts.

For example:

Presentation Layer Domain Logic Layer Data Access Layer
DTO Domain Entity
Layer of data transfer from
controllers and their validation.
Layer of business logic.
Models reflecting business
entities and their methods.
Layer of data access.
Isolates details of working
with a database or cache.

Or any other set of layers you need :)

Examples

You can see the example by clicking on the link Examples

Glossary

1) MappersModule

The library integration module in the NestJS application. It must be added to the top-level module. Provides access to the Mapper instance via DI (@InjectMapper()).
Pay special attention, the module must be imported with the call .forRoot(). Using the .forRoot() method, you can specify the default validator.

import { MappersModule } from '@mappers/nest';

@Module({
  imports: [ MappersModule.forRoot() ]
})
export class AppModule {}
2) Mapper

The entity through which entity mapping operations are performed according to the specified rules.
To inject the mapper into your services and controllers, use the @InjectMapper() decorator.

@Injectable()
class OrdersService {
    constructor(@InjectMapper() private mapper: MapperInterface) {}
    async getAll(): Promise<Order[]> {
        const orderEntities: OrderEntity[] = await this.orderRepository.find();
        return this.mapper.map(orderEntities, OrderEntity, Order);
    }
}

@Controller()
class OrderController {
  constructor(@InjectMapper() private mapper: MapperInterface) {}
  async getAll(): Promise<OrderDto[]> {
      const orders: Order[] = await this.ordersService.getAll();
      return this.mapper.autoMap(orders, OrderDto);
  }
}
2) Profiles

The class through which mapping rules are assembled. We recommend creating profiles by grouping rules in them by related entities.
To work correctly, all your profiles must be extended from the BasicMapperProfile.
Please note that only one instance of a specific profile can be created, otherwise a ProfileError error will be thrown.

class OrderProfile extends BaseMapperProfile {
  async define(mapper: ProfileMapperInterface) {
    mapper.addRule(OrderEntity, Order)
    // ...
    mapper.addRule(Order, OrderDto)
    // ...
  }
}

//...
@Module({
  providers: [...providers, OrderProfile]
})
export class SomeModule {}
3) Rules

A specific mapping rule from one class to another, described in the profile. A single profile can contain multiple rules. Rules can refer to other rules when mapping nested objects.
Note that for one pair of classes (Order and OrderDto for example) There can only be one mapping rule in all profiles, otherwise a RuleError error will be thrown.

class OrderProfile extends BaseMapperProfile {
    async define(mapper: ProfileMapperInterface) {
        mapper
            .addRule(OrderEntity, Order)
            .callConstructor()
            .properties((x) => [x.id, x.type])
            .property((x) => x.date, (x) => x.date, (value) => this.dataConverter.convert(value))
            .byRule((x) => x.user, (x) => x.user, mapper.withRule(UserEntity, User))
            .validate(OrderValidator);
        
        mapper.addRule(Order, OrderDto)
        // ...
    }
}
4) Validators

A class that provides verification of each transformed object according to a custom scenario.
You can define a default validator (you don't have to pass an argument to .validate() for a rule) or define a custom validator for a specific rule.
To work correctly, all your validators must be extended from the BaseMapperValidator.
Note that the validator class can have only one instance, otherwise a ValidatorError error will be thrown.

export class OrderValidator extends BaseMapperValidator {
    async validate(item: Order) {
        // Validation logic. Called for each object.
    }
}

//...
@Module({
  providers: [...providers, OrderValidator]
})
export class SomeModule {}

Mapper

map

A method of the Mapper object that transforms an entity or an array of entities (values) through a rule that will be found through a pair of classes (from and to).
If the conversion rule was not found, a RuleError error will be thrown.
Examples:

await this.mapper.map(order, Order, OrderDto);
await this.mapper.map(orders, Order, OrderDto);

Typing:

function map<V extends F, F, T>(values: V[], from: ConstructorType<F>, to: ConstructorType<T>): Promise<T[]>;
function map<V extends F, F, T>(values: V, from: ConstructorType<F>, to: ConstructorType<T>): Promise<T>;

autoMap

It works in the same way as map, but the type we are converting from (the values or from) is calculated automatically.
You only need to specify the class to which the conversion should take place.

await this.mapper.autoMap(order, OrderDto);
await this.mapper.autoMap(orders, OrderDto);

Typing:

function autoMap<V, T>(values: V[], to: ConstructorType<T>): Promise<T[]>;
function autoMap<V, T>(values: V, to: ConstructorType<T>): Promise<T>;

Profiles

DI

You can use dependency injection into your profiles, but it is important to remember that profiles and all dependencies that are injected into it must necessarily use the scope Singleton (DEFAULT).
This is due to optimizations that are applied to the rules at the application launch stage.

define

The method that will be called during the collection of profile data (at the start of the application).
Through it, mapping rules are registered via the Mapper object (passed in the argument mapper).

@Injectable() // Only Singlton scope (DEFAULT)
class OrderProfile extends BaseMapperProfile {
  constructor(private ownerService: OwnerService) { // Only Singlton dependencies
      super();
  }
  
  async define(mapper: ProfileMapperInterface) {
    mapper
        .addRule(OrderEntity, Order)
        .callConstructor(Order, (call, from) => call(0, null, from.name))
        .properties((x) => [x.id, x.type])
        .property((x) => x.age, (x) => x.age)
        .property((x) => x.date, (x) => x.date, async (_, from) => await this.dateService.get(from.id))
        .complex((x) => x.items, (x) => x.items)
        .fill(async (from) => await this.ownerService.getByOrderId(from.id), (x) => x.owner)
        .byRule((x) => x.user, (x) => x.user, mapper.withRule(UserEntity, User))
        .validate(OrderValidator);

    mapper.addRule(Order, OrderDto)
    // ...
  }
}

//...
@Module({
  providers: [...providers, OrderProfile]
})
export class SomeModule {}

Typing:

function define(mapper: ProfileMapperInterface): Promise<void>;

Rules

addRule

The method of registration of the mapping rule. Accepts two arguments From and To, which are constructor functions (classes). From is the type of input data (the ones we map), To is the type of output values (what we map). Please note that the application can have only one rule for one pair of From and To, otherwise a RuleError error will be thrown.

mapper.addRule(FromClass, ToClass)

Typing:

function addRule<F, T>(from: ConstructorType<F>, to: ConstructorType<T>): MapRule<F, T>;

property

A method for mapping primitive properties of an object. The first argument (propertyFrom) determines which property to take the data from, and the second argument (propertyTo) determines where to place it.
The third optional argument is a transformer function that allows you to transform data from one property to another by changing its type and value. Accepts three optional arguments: the field value from the original object (property), the original object (from), and the object that is currently being mapped (to). The transformer function can be asynchronous.

property((x) => x.name, (y) => y.name)
property((x) => x.name, (y) => y.name, () => 'Cats')
property((x) => x.name, (y) => y.name, async () => 'Cats')
property((x) => x.name, (y) => y.name, async (value) => `My ${value}`)
property((x) => x.name, (y) => y.name, async (value, from) => `My ${value} ${from.age}`)

Typing:

function property<C>(propertyFrom: (value: Primitive<ClassFields<From>>) => C, propertyTo: (value: Primitive<ClassFields<To>>) => C): MapRule<From, To>;
function property<C, V>(propertyFrom: (value: Primitive<ClassFields<From>>) => C, propertyTo: (value: Primitive<ClassFields<To>>) => V, transform: (property: C, from: From, to: To) => Promise<NotVoid<V>>): MapRule<From, To>;
function property<C, V>(propertyFrom: (value: Primitive<ClassFields<From>>) => C, propertyTo: (value: Primitive<ClassFields<To>>) => V, transform: (property: C, from: From, to: To) => NotVoid<V>): MapRule<From, To>;

properties

A method that allows you to conveniently map properties corresponding by name and type. The rule must specify an array of properties of the objects that need to be matched. This way, the values of the properties specified in the rule will be transferred to the final object without changes.
Please note that only primitive properties with the same type and name can be changed using this method.

properties((x) => [x.name, x.age, x.isActive])

Typing:

function properties(intersectionCallback: (intersection: IntersectionProperties<ClassFields<From>, ClassFields<To>>) => IntersectionProperty[]): MapRule<From, To>;

complex

A method for defining mapping rules for complex structures (not primitives). It has two modes of operation: through deep cloning and transformers functions. The first argument (propertyFrom) determines which property to extract data from, and the second argument (propertyTo) determines where to put it. If the third argument is missing, the deep optimized cloning mode is used (implemented via the static Cloner.deep() method).
The third optional argument is the transformer function, through which you can implement custom data transformation logic. Accepts three optional arguments: the field value from the original object (property), the original object (from), and the object that is currently being mapped (to). The transformer function can be asynchronous. You can also use the deep cloning feature in it by importing the Cloner class from the package. The transformer function can be asynchronous.
Please note that the application can have only one rule for one pair of From and To, otherwise a RuleError error will be thrown.

complex((x) => x.ordersRaw, (y) => y.orders)
complex((x) => x.ordersRaw, (y) => y.orders, () => [new Order()])
complex((x) => x.ordersRaw, (y) => y.orders, async () => [new Order()])
complex((x) => x.ordersRaw, (y) => y.orders, async (value) => value.map(x => new Order(x)))
complex((x) => x.ordersRaw, (y) => y.orders, async (value, from) => {
    if(!from.isActive) {
        return [];
    }
    
    return value.map(x => new Order(x));
})

Typing:

function complex<C, V extends C>(propertyFrom: (value: NonPrimitive<ClassFields<From>>) => C, propertyTo: (value: NonPrimitive<ClassFields<To>>) => V): MapRule<From, To>;
function complex<C, V, N extends V>(propertyFrom: (value: NonPrimitive<ClassFields<From>>) => C, propertyTo: (value: NonPrimitive<ClassFields<To>>) => V, transform: (property: C, from: From, to: To) => Promise<NotVoid<N>>): MapRule<From, To>;
function complex<C, V, N extends V>(propertyFrom: (value: NonPrimitive<ClassFields<From>>) => C, propertyTo: (value: NonPrimitive<ClassFields<To>>) => V, transform: (property: C, from: From, to: To) => NotVoid<N>): MapRule<From, To>;

byRule

This method is used if you need to transform an object or an array of objects with a prototype according to the rule. The specified rule can be added in another profile. The first argument (propertyFrom) determines which property to extract data from, and the second argument (propertyTo) determines where to put it, and the third argument (rule) should specify a pair of classes to find the mapping rule.
If the specified rule is not found after all profiles are assembled, a RuleError exception will be thrown when the application is launched.

byRule((x) => x.user, (x) => x.user, mapper.withRule(UserEntity, User))
byRule((x) => x.orders, (x) => x.orders, mapper.withRule(OrderEntity, Order))

Typing:

function byRule<Z, D>(propertyFrom: (value: NonPrimitive<ClassFields<From>>) => Z, propertyTo: (value: NonPrimitive<ClassFields<To>>) => D, rule: ProxyRule<Z, D>): MapRule<From, To>;

fill

A method that adds rules for filling in object properties that are not present in the original object. So you have a need, depending on some data at the mapping stage, to calculate the value (based on some properties of the source object, for example) which should be written to the property, you can add this rule.
The method takes two arguments: an asynchronous or synchronous function that will return the value that will be written to the property (filler) and a function that points to the property that should be filled (propertyTo). The filter function can take two arguments: the original object (from), and the object that is currently being mapped (to).
Please note that such rules have the lowest priority, and if the specified property is already specified in any of the rules, you will receive a FillError error at the profile assembly stage.

fill(async (from) => await this.userRepository.find({ userId: from.userId }), (x) => x.user)
fill(() => Math.random(), (x) => x.randomNumber)

Typing:

function fill<Z>(filler: (from: From, to: To) => Promise<NotVoid<Z>> | NotVoid<Z>, propertyTo: (value: ClassFields<To>) => Z): MapRule<From, To>;

callConstructor

This method allows you to add a rule that, when mapping, will call the prototype constructor that is being converted to. This can be especially useful if your entity has some logic that needs to be called in the class constructor. Remember that this rule has a low priority.

This rule is presented in two modes:
1) If the constructor has no parameters (call callConstructor() without arguments).
2) If the constructor has parameters or before calling the constructor, it is necessary to perform some logic, the first argument (toConstructor) must be passed to the type class in which the conversion is taking place and the second argument (callConstructorCallback).

A callConstructorCallback is a function (synchronous or asynchronous) in which you can describe the logic before creating an object. The function takes two arguments: the function (call) that must be called to start creating an object (it takes the same arguments as the class constructor function) and the initial object (from).

class OrderDto {
    constructor(age: number, name:string) {
        //...
    }
}
//...

callConstructor()
callConstructor(OrderDto, async (call, from) => {
    call(from.age, from.name);
})

Typing:

function callConstructor(): MapRule<From, To>;
function callConstructor<ToConstructor extends ConstructorType<To>>(toConstructor: ToConstructor, callConstructorCallback: CallConstructorCallback<ToConstructor, From>): MapRule<From, To>;

validate

The method indicates the need for validation of mapping results. When calling with an empty argument, the default validator specified in the settings will be used.
The function argument (validator) points to a class that should implement the validation logic specifically for this entity. Note that the specified validator must extend the BaseMapperValidator.

validate()
validate(SomeMapperValidator)

Typing:

function validate<T extends BaseMapperValidator>(): MapRule<From, To>;
function validate<T extends BaseMapperValidator>(validator: MapperValidator<T, To>): MapRule<From, To>;

Validators

DI

You can use dependency injection into your validators, but it is important to remember that validators and all dependencies that are injected into it must necessarily use the scope Singleton (DEFAULT).
This is due to optimizations that are applied to the rules at the application launch stage.

Set default validator

In the standard configuration by the default validator, the ClassMapperValidator is installed which is a simple implements for the package class-validator.

import { MappersModule } from '@mappers/nest';

@Module({
  imports: [ 
      MappersModule.forRoot({
        defaultValidator: CustomDefaultValidator
      }) 
  ]
})
export class AppModule {}

Set custom validator by Rule

class OrderProfile extends BaseMapperProfile {
    async define(mapper: ProfileMapperInterface) {
        mapper
            .addRule(OrderEntity, Order)
            // ...
            .validate(OrderValidator);
    }
}

validate

The method that will be called for each object that has been mapped. It must be typed according to the types of the rule in which it is defined.

@Injectable() // Only Singlton scope (DEFAULT)
export class OrderValidator extends BaseMapperValidator {
  constructor(private someService: SomeService) { // Only Singlton dependencies
    super();
  }
  
  async validate(item: Order) {
      // Validation logic. Called for each object.
  }
}

//...
@Module({
  providers: [...providers, OrderValidator]
})
export class SomeModule {}

Typing:

function validate(item: any): Promise<void>

Errors description

Settings Errors

  • The function is only available when using the **TYPE** collect type
    It will be thrown out if you try to use a function with an inappropriate type of mapper settings. For example, if in CollectType.DI mode you are trying to call MapperSettings.collectProfiles().

Profile Errors

  • The object does not extend the BaseMapperValidator
    It will be thrown if the class specified as the profile does not extend the base class BaseMapperProfile.
  • An instance of the profile '**PROFILE NAME**' has already been created
    It will be thrown if you try to recreate an instance of a profile that has already been created before.

Rule Errors

  • No rules found for '**FROM NAME**'
    It will be thrown if you are trying to start mapping an object for which no mapping rules have been defined to other prototypes.
  • Rule for '**FROM PROPERTY NAME**' and '**TO PROPERTY NAME**' not found
    The rule for mapping entities based on the specified prototype was not registered when building profiles. Double-check the correctness of the definition of rules and the assembly of profiles when launching the application.
  • The rule for '**FROM PROPERTY NAME**' and '**TO PROPERTY NAME**' has already been added to the mapper
    It will be thrown out if you try to add a rule through the profile for a couple of prototypes, the rule for which has already been added earlier.

Fill Errors

  • The rule for the '**TO PROPERTY NAME**' property has already been added to the mapper
    It will be thrown if you are trying to define a rule for filling in a property for which a fill rule has already been added.
  • A rule has already been defined for the '**TO PROPERTY NAME**' property in 'properties' or 'complexity'
    It will be thrown if mapping rules of the properties or complexity type have already been defined for this property. The rules for definitions via fill have the lowest priority.

Validator Errors

  • The object does not extend the BaseMapperValidator
    It will be thrown if the class specified as the validator (for example, in .validate()) does not extend (or inherit) the base class of validators, BaseMapperValidator.
  • An instance of the validator '**VALIDATOR NAME**' has already been created
    An error will be thrown if you try to recreate the validator instance.
  • The validator '**VALIDATOR NAME**' was not found
    An error will be thrown if in the method .validate(SomeValidator) the validator whose instance has not yet been created is specified. Double-check whether this validator is registered in DI.
  • The default validator is not installed
    An error will be thrown if the default validator has not been installed (via MapperSettings.setSettings) and the rule has been registered .validate() without specifying a custom validator.
  • The default or custom validator is not defined
    It will be thrown if no default or custom validator has been installed, or the c rule has been registered .validate().
  • The validator is disabled for this rule
  • There is no custom validator defined for the rule '**FROM PROPERTY NAME** and '**TO PROPERTY NAME**' and there is no default validator

Future changes

1.1.0

  • Dynamic assembly of mapping rules Allows you to use more than just Singleton scope for DI
  • Performance improvement

About

A fast, simple, and powerful library that helps you design your application layers correctly and ensures reliable mapping of your entities. The library actively uses TypeScript's type checking capabilities and the pre-assembly of mapping rules to achieve maximum speed and comfort for the developer.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published