1.0.0 • Published 7 months ago

@mappers/core v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

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/core:

npm i @mappers/core
FrameworkPackage
Nest JSNestJS Integration Library

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 LayerDomain Logic LayerData Access Layer
DTODomainEntity
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) Mapper

The entity through which entity mapping operations are performed according to the specified rules.

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

class OrderController {
    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)
        // ...
    }
}
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)
            .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.
    }
}

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

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).

class OrderProfile extends BaseMapperProfile {
    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)
        // ...
    }
}

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

Set default validator

MapperSettings.setSettings({
    defaultValidator: DefaultMapperValidator,
});

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.

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

Typing:

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

Raw usage

If you want to use libraries without framework integrations or write your own integration, you can use a low-level flow.

Without DI

MapperSettings.setSettings({
    defaultValidator: DefaultMapperValidator, // Installing the default validator
});

MapperSettings.addProfile(SomeProfile); // Adding the mapping profile constructor function
MapperSettings.addCustomValidatorInstance(new SomeMapperValidator()); // Defining a custom validator

MapperSettings.collectProfiles(); // We collect and optimize profiles when launching the application

mapper = MapperSettings.getMapper(); // Getting the mapper

For DI

Please note, when working in the CollectType.DI an attempt to reinitialize objects extending BaseMapperProfile or BaseMapperValidator will be ignored. All instances extending BaseMapperProfile and BaseMapperValidator should use scope singleton, both for themselves and for the dependencies that are injected into them. This is necessary for the proper operation of optimization processes that run when the application is launched.

MapperSettings.setSettings({
    collectType: CollectType.DI,
    defaultValidator: DefaultMapperValidator, // Installing the default validator
});

// Registering dependencies via DI
// It is necessary for DII to create profile instances and validators

MapperSettings.collectProfileInstances(); // Добавляем в маппер профили, инстансы которых были созданны через DI

mapper = MapperSettings.getMapper(); // Getting the mappe

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
1.0.0

7 months ago

0.0.20

8 months ago

0.0.19

8 months ago

0.0.18

8 months ago

0.0.17

8 months ago

0.0.16

8 months ago

0.0.15

8 months ago

0.0.13

9 months ago

0.0.11

9 months ago

0.0.10

9 months ago

0.0.9

9 months ago

0.0.8

9 months ago

0.0.7

9 months ago

0.0.6

9 months ago

0.0.5

9 months ago

0.0.4

9 months ago

0.0.3

9 months ago

0.0.2

9 months ago

0.0.1

9 months ago