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

@sontx/mapstructjs

v1.0.0-alpha.1

Published

Mapping source object to target object with javascript.

Downloads

2

Readme

mapstructjs

CI npm version

Multi-layered applications often require mapping between different object models (e.g. entities and DTOs). Writing such mapping code is a tedious and error-prone task. mapstructjs aims at simplifying this work by automating it as much as possible.

A playground where you can see all samples and modify code to see how it works: mapstructjs playground

Installation

npm install --save @sontx/mapstructjs

Usage

Let's assume we have a class representing staffs (e.g. an entity) and an accompanying data transfer object (DTO).

Both types are rather similar, only the company name and company rank properties are from the nested object.

// staff.entity.ts
class StaffEntity {
  name: string;
  age: number;
  comapny: {
    name: string;
    rank: {
      number: number;
      type: string;
    }
  }
}
// staff.dto.ts
class StaffDto {
  name: string;
  age: number;
  companyName: string;
  companyRank: number;
}

Basic usage

  1. Define an object mapper
class ObjectMapper {
  @MapMethod({
    properties: [
      // map target's name prop with source's name prop
      'name',
      'age',
      // map target's companyName prop with source's 'company -> name' prop
      ['companyName', 'company.name'],
      // map target's companyRank prop with source's 'company -> rank -> number' prop
      { target: 'companyRank', source: 'company.rank.number' },
    ],
  })
  toDto: (source: any) => any;// the implementation for this method will be injected by the library
}
  1. Create object mapper instance by using ObjectMapperFactory
const objectMapper = new ObjectMapperFactory().create(ObjectMapper);
  1. Mapping source to target
const staffEntity: StaffEntity = {
  name: 'son',
  age: 20,
  company: {
    name: 'fsoft',
    rank: {
      number: 10,
      type: 'good',
    },
  },
};

const staffDto = objectMapper.toDto(staffEntity);
  1. The staffDto value will be
{
 "name": "son",
 "age": 20,
 "companyName": "fsoft",
 "companyRank": 10
}

Mapping with @Property annotation

Fields that are annotated with @Property will be mapped from the source automatically, for unknown field names you have to specify the appropriate source property by using @Mapping.

  1. Define target class
class StaffDto {
  @Property()
  name: string;

  @Property()
  age: number;

  @Property()// there is no matched prop name in the source, so we have to use @Mapping for this field
  companyName: string;

  @Property()
  companyRank: number;
}
  1. Define object mapper
class ObjectMapper {
  @Mapping({ target: 'companyName', source: 'company.name' })
  @Mapping({ target: 'companyRank', source: 'company.rank' })
  @MapMethod({ targetType: StaffDto })
  toDto: (source: any) => StaffDto;
}
  1. The rest steps will look like the above steps

Transform fields

You can customize transforming the source's prop to the target's prop by defining transform in @Mapping.

All steps look like the above, but step 2.

class ObjectMapper {
  @Mapping({ target: 'companyName', transform: (_, context) => `Company is ${context.source.company.name}`})
  @Mapping({ target: 'companyRank', source: 'company.rank' })
  @MapMethod({ targetType: StaffDto })
  toDto: (source: any) => StaffDto;
}

The output

{
 "name": "son",
 "age": 20,
 "companyName": "Company is fsoft",
 "companyRank": 10
}

Link with other object mappers

Let's assume we have mapper1 that can map a company entity to its dto, and mapper2 has a mapping property that also needs to map a company entity to dto. To reuse these types of logic, we can "link" mapper1 to mapper2, the library will look up the correct map method while mapping each property by its source and target type.

  1. Define entities and DTOs
// company.entity.ts
class CompanyEntity {
  name: string;
  rank: {
    number: number;
    type: string;
  };
}

// staff.entity.ts
class StaffEntity {
  @Property()
  name: string;

  @Property()
  age: number;

  @Property(CompanyEntity)
  company: CompanyEntity;
}

// company.dto.ts
class CompanyDto {
  name: string;
  rank: number;
}

// staff.dto.ts
class StaffDto {
  @Property()
  name: string;

  @Property()
  age: number;

  @Property(CompanyDto)
  company: CompanyDto;
}
  1. Define object mapper for mapping company entity to its dto
class CompanyObjectMapper {
  @Mapping({ target: 'name' })
  @Mapping({ target: 'rank', source: 'rank.number' })
  @MapMethod({ targetType: CompanyDto, sourceType: CompanyEntity })
  toDto: (source: CompanyEntity) => CompanyDto;
}
  1. Define object mapper for mapping staff entity to its dto
// we link this object mapper to the company object mapper,
// so the library can lookup to find the best suitable mapping method for mapping company
@Mapper([CompanyObjectMapper])
class ObjectMapper {
  @MapMethod({ targetType: StaffDto, sourceType: StaffEntity })
  toDto: (source: StaffEntity) => StaffDto;
}

The output

{
  "name": "son",
  "age": 20,
  // this result is from CompanyObjectMapper.dto
  "company": {
    "rank": 10,
    "name": "fsoft"
  }
}

Link with other services

Let's assume we have a WelcomeService that needs to call while mapping our staff's name.

class WelcomeService {
  welcome(name: string) {
    return `Welcome ${name}`;
  }
}

Inject this service into the object mapper

class ObjectMapper {
  // this service instance will be resolved when the object mapper is created
  @Inject(WelcomeService)
  private welcomeService: WelcomeService;

  @Mapping({
    target: 'name',
    transform: (sourceValue, context) => {
      const self = context.getObjectMapper<ObjectMapper>();
      return self.welcomeService.welcome(self.uppercaseService.uppercase(sourceValue));
    },
  })
  @MapMethod({
    properties: ['name', 'age'],
  })
  toDto: (source: any) => any;
}

The output

{
  "name": "Welcome son",
  "age": 20
}

If you want to handle creating WelcomeService, call this one before objectMapperFactory.create

objectMapperFactory.options.instanceResolver?.register(WelcomeService, () => new WelcomeService());

After and before hooks

The mapping flow will look like: create context (1) -> call before hook (2) -> do map -> call after hook (3) -> return target (4)

  1. Mapping context is created, target object is also created in this phase.
  2. Call all matched before hooks.
  3. Map source object to target object, custom transform will be called in this phase.
  4. Call all matched after hooks, you can overwrite the return target value in this hook (only the first override is accepted).
  5. Return the final target object, this can be the created object in step 1 or the overwritten object in step 4.

Hooks are defined in the object mapper class

class ObjectMapper {
  // .....map methods.....

  @BeforeMapping()
  before(context: MappingContext) {
    console.log('Before mapping');
  }

  @AfterMapping()
  after(context: MappingContext) {
    console.log('After mapping');
    // overwrite the target value
    return {
      name: 'noem',
      age: 19
    }
  }
}

You can use injected services inside hooks

class ObjectMapper {
  @Inject(WelcomeService)
  private welcomeService: WelcomeService;

  // .....map methods.....

  @AfterMapping()
  after(context: MappingContext) {
    return {
      ...context.target,
      name: this.welcomeService.welcome(context.target.name),
    };
  }
}

Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

Distributed under the MIT License. See LICENSE.txt for more information.

Contact

Tran Xuan Son - @sontx0 - [email protected]

Acknowledgments

Use this space to list resources you find helpful and would like to give credit to. I've included a few of my favorites to kick things off!