npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@woodchopper/property-mapper

v0.0.4

Published

A solution to map structures in TypeScript Node projects. This solution provides decorators and types to make it easier to use.

Downloads

39

Readme

Property mapper

A solution to map structures in typescript Node projects. This solution provides decorators and types to make it easier to use.

Quick start

Install the dependency

npm install @woodchopper/property-mapper

Mapper example:

import {AbstractMapper, Mapping} from "@woodchopper/property-mapper";

@Mapping(
    { source: 'fName', target: 'firstName' },
    { source: 'lName', target: 'lastName' }
)
export class MyFirstMapper extends AbstractMapper<UserInfo, UserDetails> {}

Usage:

const userInfo: UserInfo = {
    fName: 'foo',
    lName: 'bar'
}

const userMapper = new MyFirstMapper();

const userDetails = userMapper.map(userInfo);

/*
userDetails.firstName is 'foo'
userDetails.lastName is 'bar'
 */

Usage

Mapping annotation can decorate both classes and methods.

Dedicated mapper

When using the decorator at class level, the class needs to extends AbstractMapper<S, T>. S is the type of the source object while T is the target type.

AbstractMapper contains the methods map and arrayMap that cast the objets given in argument from S to T. This is the responsibility of your @Mapping decorators to map the input to an output that match T.

For those given types:

type UserInfo = {
    fName: string
    lName: string
    age: number
};

type UserDetails = {
    firstName: string
    lastName: string
    age: number
};

A correct mapper would be:

@Mapping(
    { source: 'fName', target: 'firstName' },
    { source: 'lName', target: 'lastName' }
)
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

or

@Mapping({ source: 'fName', target: 'firstName' })
@Mapping({ source: 'lName', target: 'lastName' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

A property that is not mapped will simply go from the source to the target with the same key and value. In our example, age will be present on object of type UserDetails after the mapping.

Basic mapping instructions

source and target can define paths to properties. This path expression is defined by a string. the source expression support jsonPath syntax.

Example:

@Mapping({ source: 'details.age', target: 'personnalDetails.age' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

Another example with jsonPath:

@Mapping({ source: '$.details.phoneNumbers[:1].number', target: '$.personnalDetails.mainPhoneNumber' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

With multiple source

If the source provide multiple results, multipleSources needs to be used instead of source.

@Mapping({ multipleSources: '$.details.phoneNumbers[:].number', target: '$.personnalDetails.phoneNumbers' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

Side note: a jsonPath expression always returns an array of results. When using source: the first element of the results will be used. When using multipleSources: all the elements of the result will be used.

With transformation

The mapping instruction can provide transformations:

Example:

@Mapping({ source: 'lName', target: 'lastName', transform: (name: string) => name.toUpperCase() })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

Or with a source that provide an array:

@Mapping({ source: '$.details.phoneNumbers[:].number', target: '$.personnalDetails.phoneNumbers', transformEach: (phoneNumber: string) => phoneNumber.trim() })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

When source and target share the same property name, you can use sourceTarget:

@Mapping({ sourceTarget: 'lastName', transform: (name: string) => name.toUpperCase() })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

With arguments

Arguments can be passed to the mapper:

@Mapping({ source: 'lName', target: 'lastName', transform: (name: string, title: string) => title + ' ' + name })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}
const userMapper = new UserMapper();

const userDetails = userMapper.map(userInfo, 'Dr.');

Using multiple arguments will result to this:

@Mapping({ /* ... */ transform: (name: string, arg1: any, arg2: any, arg3: any) => /* ... */ })
/* ... */
userMapper.map(userInfo, arg1, arg2, arg3)

If you need to deal with a lot of arguments, a wise way to use it is by using a context object:

@Mapping({ source: 'lName', target: 'lastName', transform: (name: string, context: {}) => context.title + ' ' + name })
@Mapping({ sourceTarget: 'phoneNumber', transform: (phoneNumber: string, context: {}) => context.phonePrefix + phoneNumber })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}
const userMapper = new UserMapper();

const userDetails = userMapper.map(userInfo, { title: 'Dr.', phonePrefix: '+32' });

Using another AbstractMapper as dependency

Transformations can also use other mappers. This is useful when dealing with nested Objects of multiple types

type UserInfo = {
    fName: string
    lName: string
};

type UserDetails = {
    firstName: string
    lastName: string
};

type Website = {
    numberOfActiveUsers: number,
    users: UserInfo[]
}

type WebsiteDetails = {
    activity: string,
    users: UserDetails
}

A correct mapper would be:

@Mapping(
    { source: 'numberOfActiveUsers', target: 'activity', transform: (v: number) => v + ' of active users' },
    { sourceTarget: 'users', transformEach: UserMapper }
)
export class WebsiteMapper extends AbstractMapper<Website, WebsiteDetails> {}

or

@Mapping(
    { source: 'numberOfActiveUsers', target: 'activity', transform: (v: number) => v + ' of active users' },
    { sourceTarget: 'users', transformEach: new UserMapper() }
)
export class WebsiteMapper extends AbstractMapper<Website, WebsiteDetails> {}

For some reason, you could provide your own implementation of UserMapper by injecting it:

@Mapping(
    {source: 'numberOfActiveUsers', target: 'activity', transform: (v: number) => v + ' of active users'},
    {sourceTarget: 'users', transformEach: UserMapper}
)
export class WebsiteMapper extends AbstractMapper<Website, WebsiteDetails> {
    constructor(private userMapper: UserMapper) {
        super();
    }
}
/**
 * overrides the mappings for firstName and lastname defined in UserMapper
 */
@Mapping({ target: 'firstName', transform: () => 'HIDDEN' })
@Mapping({ target: 'lastName', transform: () => 'HIDDEN' })
class GDPRCompliantUserMapper extends UserMapper {}
const websiteMapper = new WebsiteMapper(new GDPRCompliantUserMapper());
const websiteDetails = websiteMapper.map(website);

Remove instruction

type UserInfo = {
    fName: string
    lName: string
};

type UserDetails = {
    firstName: string
    lastName: string
};

All those examples provide ways from mapping UserInfo to UserDetails. While we expect user details to be a plain object of this form:

{
    "firstName": "John",
    "lastName": "Doe"
}

It is in fact of this form:

{
    "firstName": "John",
    "lastName": "Doe",
    "fName": "John",
    "lName": "Doe"
}

The properties of the source object remains. When working with this object, if you stick to the definition of the type UserDetails, you should not have any issue.

If those remaining properties bother you in you dev, you need to explicitly remove them:

@Mapping({ source: 'fName', target: 'firstName' }, { remove: 'fName'})
@Mapping({ source: 'lName', target: 'lastName' }, { remove: 'lName'})
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

Or

@Ignore('fName', 'lName')
@Mapping({ source: 'fName', target: 'firstName' })
@Mapping({ source: 'lName', target: 'lastName' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}

You will get:

{
    "firstName": "John",
    "lastName": "Doe"
}

Angular injection

When working with the annotation @Injectable from Angular, you could get issues while trying to inject a mapper. You need to explicitly provide the mapper in you Module:

@Injectable()
@Mapping({ source: 'fName', target: 'firstName' })
@Mapping({ source: 'lName', target: 'lastName' })
export class UserMapper extends AbstractMapper<UserInfo, UserDetails> {}
@NgModule({
    /* ... */
  providers: [
      /* ... */
    {
      provide: MapperClassService,
      useValue: new MapperClassService()
    }
      /* ... */
  ],
    /* ... */
})
export class AppModule { }

Mapping on methods

The decorator can also be used on class methods. This feature can be used to perform some small mapping.

Example:

type UserInfo = {
    fName: string
    lName: string
    inscriptionDate: Date
};
class UserClient {
    constructor(/* an async http client */) {}

    @Mapping({sourceTarget: 'inscriptionDate', transform: (date: string) => new Date(date)})
    getUser(id: string): Observable<UserInfo> {
        /* some async call to an API */
    }
}

Here the Mapping instruction convert the date in format string to an object of type Date. So that the model UserInfo is respected.

Those method mappings can be performed on methods returning objects of type Observable, Promise or plain objects.

It can also be used on Array return types:

class UserClient {
    constructor(/* an async http client */) {}

    @Mapping({sourceTarget: 'inscriptionDate', transform: (date: string) => new Date(date)})
    getAllUsers(id: string): Observable<UserInfo[]> {
        /* some async call to an API */
    }
}