Yanover
Yanover

Reputation: 173

NestJs - DTO and Entities

I'm trying to use cleverly DTO and Entities in my project but it seems more complicated than it should be. I'm building a backend for managing inventory, I use NestJs and TypeOrm.

My client is sending me a set of data throught a POST request, let's say :

{
  "length": 25,
  "quantity": 100,
  "connector_A": {
    "id": "9244e41c-9da7-45b4-a1e4-4498bb9de6de"
  },
  "connector_B": {
    "id": "48426cf0-de41-499b-9c02-94c224392448"
  },
  "category": {
    "id": "f961d67f-aea0-48a3-b298-b2f78be18f1f"
  }
}

My controller has for responsability to check the field by using a custom ValidationPipe :

@Post()
  @UsePipes(new ValidationPipe())
  create(@Body() data: CableDto) {
    return this.cablesService.create(data);
}

I read in many places that in best practices, RAW data should be converted into DTO and when it comes to data insertion, I should cast my DTO into an typeOrm Entity.

I'm ok with this method, but I found it very complicated, even more when there are relations between my tables and prefix noun into it.

Here is my Entity cable

@Entity('t_cable')
export class Cable {

  @PrimaryGeneratedColumn('uuid')
  CAB_Id: string;

  @Column({
    type: "double"
  })
  CAB_Length: number;

  @Column({
    type: "int"
  })
  CAB_Quantity: number;

  @Column()
  CON_Id_A: string

  @Column()
  CON_Id_B: string
  
  @Column()
  CAT_Id: string

  @ManyToOne(type => Connector, connector => connector.CON_Id_A)
  @JoinColumn({ name: "CON_Id_A" })
  CON_A: Connector;

  @ManyToOne(type => Connector, connector => connector.CON_Id_B)
  @JoinColumn({ name: "CON_Id_B" })
  CON_B: Connector;

  @ManyToOne(type => Category, category => category.CAB_CAT_Id)
  @JoinColumn({ name: "CAT_Id" })
  CAT: Category;

}

And here is my DTO for the cable interactions :

export class CableDto {

  id: string;

  @IsOptional()
  @IsPositive()
  @Max(1000)
  length: number;
  quantity: number;

  connector_A: ConnectorDto;
  connector_B: ConnectorDto;
  category: CategoryDto

  public static from(dto: Partial<CableDto>) {
    const it = new CableDto();
    it.id = dto.id;
    it.length = dto.length;
    it.quantity = dto.quantity;
    it.connector_A = dto.connector_A
    it.connector_B = dto.connector_B
    it.category = dto.category
    return it;
  }

  public static fromEntity(entity: Cable) {
    return this.from({
      id: entity.CAB_Id,
      length: entity.CAB_Length,
      quantity: entity.CAB_Quantity,
      connector_A: ConnectorDto.fromEntity(entity.CON_A),
      connector_B: ConnectorDto.fromEntity(entity.CON_B),
       category: CategoryDto.fromEntity(entity.CAT)
    });
  }

  public static toEntity(dto: Partial<CableDto>) {
    const it = new Cable();
    if (dto.hasOwnProperty('length')) {
      it.CAB_Length = dto.length;
    }
    if (dto.hasOwnProperty('quantity')) {
      it.CAB_Quantity = dto.quantity;
    }
    if (dto.hasOwnProperty('connector_A')) {
      it.CON_Id_A = dto.connector_A.id;
    }
    if (dto.hasOwnProperty('connector_B')) {
      it.CON_Id_B = dto.connector_B.id;
    }
    if (dto.hasOwnProperty('category')) {
      it.CAT_Id = dto.category.id;
    }
    return it;
  }
}

I know that these three methods to convert in both directions DTOs and entites feels pretty dirty, that's why I'm here ..

My service for a simple create or get request know :

async create(dto: CableDto): Promise<CableDto> {
  const cable = await this.cablesRepository.save(CableDto.toEntity(dto));
  return await this.findById(cable.CAB_Id)
}

I'm convince that there is simpler solution to achieve that, or at least a proper way to do it.

Any idea ?

Thank you.

Upvotes: 13

Views: 33695

Answers (4)

DSabalsa
DSabalsa

Reputation: 116

For this kind of mapping you can do something like:

function ObjectToDTO(cable: Cable): CableDto {
  const cableDto = new CableDto();

  for (const key in cable) {
    if (cable[key] instanceof Object) {
      cableDto[key] = ObjectToDTO(cable[key]);
    } else {
      cableDto[key] = cable[key];
    }
  }

  return dto;
}

Also you can do this more reusable with generics:

function ObjectToDTO<T>(obj: Object): T

Upvotes: 0

Nairi Abgaryan
Nairi Abgaryan

Reputation: 666

You can review this boilerplate and take some working examples from there. It has a readable and easy solution for dto-s and entities.

Example for dto converting.
https://github.com/NarHakobyan/awesome-nest-boilerplate/blob/main/src/modules/user/dtos/user.dto.ts
Just you have to spend a little bit of effort for bringing that.

And here is another example of creating and using Mapper. Just here it uses for converting entity to dto, but you can use it visiversa
Mapper Usage:
https://github.com/nairi-abgaryan/analyzer/blob/master/src/modules/user/user.service.ts#L48

The Mapper:
https://github.com/nairi-abgaryan/analyzer/blob/master/src/providers/mapper.service.ts

Upvotes: 0

Julio Kriger
Julio Kriger

Reputation: 59

You could use Automapper to do all the conversion between Entity and DTO.

I use it in many projects with great result, especially on projects with many DTOs and Entities.

For example, you could do something like this:

    export class InitializeMapper {
        mapper: AutoMapper;
    
        constructor() {
            this.mapper = new AutoMapper();
    
            this.mapper.createMap(CableDto, CableEntitt)
                .forMember(dst => dst.length, mapFrom(s => s.length))
                .forMember(dst => dst.quantity, mapFrom(s => s.quantity))
                .forMember(dst => dst.connector_A, mapFrom(s => s.connector_A.id))
                .forMember(dst => dst.connector_B, mapFrom(s => s.connector_B.id))
                .forMember(dst => dst.connector_C, mapFrom(s => s.connector_C.id));
            });
        }
        
        map(a: any, b: any) {
            return this.mapper.map(a, b);
        }
    
        mapArray(a: Array<any>, b: any) {
            return this.mapper.mapArray(a, b);
        }
    }

So you could reuse the same mapping anywhere in your project.

Regards

Upvotes: 2

Fabio Formosa
Fabio Formosa

Reputation: 1084

For all type conversions (for example DTO > entity or entity > DTO), I've developed a library metamorphosis-nestjs to ease conversions of objects.
It adds to NestJS the missing concept of a injectable conversion service for all conversions provided by your converters, early registered into the conversion service (like conversion service provided by Spring Framework in Java app).

So in your case, with typerORM:

  1. npm install --save @fabio.formosa/metamorphosis-nest
    
  2. import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
    
    @Module({
      imports: [MetamorphosisModule.register()],
      ...
    }
    export class MyApp{ }
    
  3. import { Convert, Converter } from '@fabio.formosa/metamorphosis';
    
    @Injectable()
    @Convert(CableDto, Cable)
    export default class CableDtoToCableConverter implements Converter<CableDto, Promise<Cable>> {
    
    constructor(private readonly connection: Connection){}
    
    public async convert(source: CableDto): Promise<Cable> {
      const cableRepository: Repository<Cable> = this.connection.getRepository(Cable);
      const target: Product | undefined = await cableRepository.findOne(source.id);
      if(!target)
        throw new Error(`not found any cable by id ${source.id}`);
      target.CAB_Length = source.length;
      target.CAB_Quantity = source.quantity;
      ... and so on ...
      return target;
    }
    

}

Finally you can inject and use conversionService whatever you want:

 const cable = <Cable> await this.convertionService.convert(cableDto, Cable);

The converter from Cable to CableDto is simpler.

Get the README to find out examples and all benefits of metamorphosis-nestjs .

Upvotes: 3

Related Questions