Skip to content

fishen/auto-mapping

Repository files navigation

auto-mapping

Map and convert objects automatically in typescript.

Features

  • Map object to an instance of a class by annotation;
  • Map object to another object which only has the mapped property;
  • Convert property types automatically;
  • Custom conversion;
  • Multiple data source mappings;
  • Map extensions;

Installation

$ npm install --save auto-mapping

Getting started

Please import the reflect-metadata module in the project entry file before use.

To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
  }
}
import 'reflect-metadata';
import { mapping, map, select } from 'auto-mapping';

class Person {
    @mapping({ type: String })
    name: string;
    @mapping(Boolean) // short form
    gender: boolean;
    @mapping({ path: 'others.number', type: Number })
    age: number;
    extraData = 1;
}
const data={
    name: 'fisher',
    gender: 1,
    others: { number: '18' },
};
console.info("Mapped result:", map(data, Person));
console.info("Selected result:", select(data, Person));
# output
Mapped result: Person { extraData: 1, name: 'fisher', gender: true, age: 18 }
Selected result: { name: 'fisher', gender: true, age: 18 }

Use with reflect-metadata

If you have already imported reflect-metadata module into your project, it will infer type automatic except array type. The array type must declare the type parameter at any time.

import 'reflect-metadata';
import { mapping, map } from 'auto-mapping';

class Person {
    @mapping()
    name: string;
    @mapping()
    gender: boolean;
    @mapping({ path: 'others.number' })
    age: number
}
const data={
    name: 'fisher',
    gender: 1,
    others: { number: '18' },
};
console.info(map(data, Person));
# output
Person { name: 'fisher', gender: true, age: 18 }

API

@mapping(options: object | function)

Configuring property mapping information, If the argument is a function, then it is equivalent to { type: options }.

  • domain(string): The parent path of current property, the option domain will be ignored when used with path.
  • type(function | [function]): The property decalre type, it is always necessary if the property type is an array. Tt is optional if the module 'refleat-metadata' has been imported in your project. It also can be used to set custom conversion function, it will use default value if the conversion throws an error.
  • path(string): The property path in the source object, such as 'a.b.c','a.b[0].c', default is the current path name. Use the dot symbol ('.') to indicates the current path.
  • order(number): The order for the property generated, default is 0.
  • source(string): The source object name, it is required if you want to map data from multiple data sources.
  • default(any): Default value, multiple data sources can specify multiple different default values.

map(source:object, constructor:function, options?:object)

Map an object to an instance of the specified type.

  • source(object): Data source object
  • constructor(function): The type of instance, the constructor function of the class
  • options(string, optional): Mapping options.
    • source(string): The source object name, it is required if you want to map data from multiple data sources.
    • useDefaultSource(boolean): Use the default mapping configuration when the current source configuration is missing, default is true.
    • nullable(boolean): Whether to allow the value to be set to null.
    • allowNaN(booelan): Whether to allow the value to be set to NaN.
    • converters(Map): Custom conversion function, only valid during the current mapping.

map.setDefaultConverter(type:function, converter:function)

Set the global conversion function for the specified type

  • type(function): The type of value to convert.
  • converter(function): The conversion function.

The function setDefaultConverter will affect the global type conversion. If you want to use it locally, please set the converters option of the map function.

import "reflect-metadata";
import { mapping, map } from "auto-mapping";

map.setDefaultConverter(String, (val) => (val === undefined || val === null) ? '' : String(val));
map.setDefaultConverter(Number, (val) => isNaN(Number(val)) ? 0 : Number(val));

class Person {
    @mapping()
    public cityName: string;
    @mapping(() => "default")
    public intro: string;
    @mapping()
    public num: number;
}

console.info(map({}, Person));
# output
Person { name: '', desc: 'default', num: 0 }

Array

Because the type of the array cannot be automatically derived, the type parameter must be specified at all times and the value must be an array.

import { mapping, map } from 'auto-mapping';

class ArrayTest {
    @mapping({ type: [Number] })
    numbers: number[];
}
const data={
    numbers:['1','2','3']
};
const result = map(data, ArrayTest);
console.info(result);
# output
ArrayTest { numbers: [ 1, 2, 3 ] }

If an property is declared as array, but the source value is not an array, the result is wrapped into an array.

import { mapping, map } from 'auto-mapping';

class ArrayTest {
    @mapping({ type: [Number], path:'number' })
    numbers: number[];
}
const data={
    number: '1'
};
console.info(map(data, ArrayTest));
# output
ArrayTest { numbers: [ 1 ] }

Multiple Data Source

The default data source mapping config named default, you can set multiple configurations by source option to map multiple data sources.

import { mapping, map } from 'auto-mapping';

class Person {
    @mapping()
    @mapping({ source: 'other', path: 'person.name' })
    name: string;
}
const dataSource1 = {
    name: 'fisher'
};
const dataSource2 = {
    person: { name: 'jack' }
};
const result1 = map(dataSource1, Person);
const result2 = map(dataSource2, Person, { source: 'other' });

console.info(result1, result2);
# output
Person { name: 'fisher' } Person { name: 'jack' }

Custom Conversion

The signature of the custom conversion function is as follows:

function(value: any, source: any, dest: any, options?: object){}
  • value(any): The value which current path matched in the source object;
  • source(any): The source object;
  • dest(any): The instance of the dest class;
  • options(object, optional): Mapping options;

Custom conversion function can be set by type parameter. If you just pass a function to the annotation, like this mapping(fn), then it is equivalent to mapping({ type: fn }).

import { mapping, map } from 'auto-mapping';

function trim(value: string) {
    return value && value.trim();
}

function fullName(_value: any, source: any) {
    return `${source.firstName} ${source.lastName}`;
}

function info(_value: any, _source: any, dest: Person) {
    return `I am ${dest.name} who come from ${dest.city}`;
}

class Person {
    age: number = 18;
    @mapping(fullName)
    name: string;
    @mapping({ type: trim, path: 'city.cityName' })
    city: string;
    @mapping(info)
    info: string;
}
const dataSource = {
    firstName: 'Lei',
    lastName: 'Lee',
    city: {
        cityId: 1,
        cityName: '    NEW YORK    '
    },
};
console.info(map(dataSource, Person));
# ouput
Person {
  age: 18,
  name: 'Lei Lee',
  city: 'NEW YORK',
  info: 'I am Lei Lee who come from NEW YORK.' 
}

Extensions

[MAPPING](src:object, options?:object){...}

Do some processing before mapping.

import "reflect-metadata";
import { mapping, map, MAPPING } from "auto-mapping";

class Person {
    @mapping()
    public num: number = 1;
    @mapping()
    public name: string;
    [MAPPING](src: any, options: any) {
        src.name = "fisher";
    }
}
const result = map({ num: 10 }, Person);
console.info(result, result instanceof Person);
# output
Person { num: 100, name: 'fisher' } true

If the MAPPING function returns a value that is not undefined, it will be passed to the map method as a source object.

[MAPPED](src:object, options?:object){...}

Sometimes you may need to do some extra finishing work, such as dynamically adding some properties, you can do this through the MAPPED symbol function.

import "reflect-metadata";
import { mapping, map, MAPPED } from "auto-mapping";

class Person {
    @mapping()
    public gender: boolean;
    @mapping()
    public num: number = 1;
    [MAPPED](src: any, options: any) {
        // set value manually
        this.gender = true;
        // override original value
        this.num = 100;
        // dynamic assignment
        Object.assign(this, { a: 1, b: 2 })
    }
}
const result = map({ num: 10 }, Person);
console.info(result, result instanceof Person);
# output
Person { num: 100, gender: true, a: 1, b: 2 } true

⚠️ Note that when using the MAPPED function, if the map result is null, then any property that accesses this will throw an error.

If the function returns a value that is not undefined, it will replace the map result value.

...
[MAPPED](src: any, options: any) {
    this.gender = true;
    this.num = 100;
    return Object.assign({}, this, { a: 1, b: 2 })
}
...
# output
{ num: 100, gender: true, a: 1, b: 2 } false

Update Logs

1.3.0

  • added select function.

1.2.1

  • compatible for mini-programs.

1.2.0

  • added mapping options nullableallowNaNconverters;
  • added new api map.setDefaultConverter to config global default type conversion;

1.1.0

  • refacted module using reflect-metadata;
  • added two extension methods MAPPING and MAPPED;
  • marked the after method as a deprecated method and replaced with the MAPPED method.

About

map and convert object in typescript

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published