Nest js POST Request Not Recognizing DTO method
Asked Answered
H

3

3

I'm having some trouble hitting a POST endpoint that triggers a typeorm repository.save() method to my postgres DB.

Here's my DTO object:

import { ApiProperty } from '@nestjs/swagger/';
import { IsString, IsUUID} from 'class-validator';

import { Client } from '../../../models';
import { User } from '../../../user.decorator';


export class ClientDTO implements Readonly<ClientDTO> {
    @ApiProperty({ required: true })
    @IsUUID()
    id: string;


    @ApiProperty({ required: true })
    @IsString()
    name: string;

    public static from(dto: Partial<ClientDTO>) {
      const cl = new ClientDTO();
      cl.id = dto.id;
      cl.name = dto.name;
      return cl;
    }

    public static fromEntity(entity: Client) {
      return this.from({
        id: entity.id,
        name: entity.name,
      });
    }

    public toEntity = (user: User | null) => {
      const cl = new Client();
      cl.id = this.id;
      cl.name = this.name;
      cl.createDateTime = new Date();
      cl.createdBy = user ? user.id : null;
      cl.lastChangedBy = user ? user.id : null;
      return cl;
    }
  }

My controller at POST - /client:

import { 
    Body,
    Controller, 
    Get, Post 
} from '@nestjs/common';

import { ClientDTO } from './dto/client.dto';
import { ClientService } from './client.service';
import { User } from 'src/user.decorator';

@Controller('client')
export class ClientController {
    constructor(
        private clientService: ClientService
    ) { }

    @Get()
    public async getAllClients(): Promise<ClientDTO[]> {
        return this.clientService.getAllClients();
    }

    @Post()
    public async createClient(@User() user: User, @Body() dto: ClientDTO): Promise<ClientDTO> {
        return this.clientService.createClient(dto, user);
    }
}

And my service:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Client } from '../../models';
import { ClientDTO } from './dto/client.dto';
import { User } from '../../user.decorator';


@Injectable()
export class ClientService {
    constructor(
        @InjectRepository(Client) private readonly clientRepository: Repository<Client>
    ) {}

    public async getAllClients(): Promise<ClientDTO[]> {
        return await this.clientRepository.find()
            .then(clients => clients.map(e => ClientDTO.fromEntity(e)));
    }

    public async createClient(dto: ClientDTO, user: User): Promise<ClientDTO> {
        return this.clientRepository.save(dto.toEntity(user))
            .then(e => ClientDTO.fromEntity(e));
    }
}

I get a 500 internal server error with log message stating that my ClientDTO.toEntity is not a function.

TypeError: dto.toEntity is not a function
    at ClientService.createClient (C:\...\nest-backend\dist\features\client\client.service.js:29:47)
    at ClientController.createClient (C:\...\nest-backend\dist\features\client\client.controller.js:27:35)
    at C:\...\nest-backend\node_modules\@nestjs\core\router\router-execution-context.js:37:29
    at process._tickCallback (internal/process/next_tick.js:68:7)

I'm confused because this only happens via http request. I have a script that seed my dev database after I launch it fresh in a docker container called seed.ts:

import * as _ from 'lodash';

import { Client } from '../models';
import { ClientDTO } from '../features/client/dto/client.dto';
import { ClientService } from '../features/client/client.service';
import { configService } from '../config/config.service';
import { createConnection, ConnectionOptions } from 'typeorm';
import { User } from '../user.decorator';

async function run() {

  const seedUser: User = { id: 'seed-user' };

  const seedId = Date.now()
    .toString()
    .split('')
    .reverse()
    .reduce((s, it, x) => (x > 3 ? s : (s += it)), '');

  const opt = {
    ...configService.getTypeOrmConfig(),
    debug: true
  };

  const connection = await createConnection(opt as ConnectionOptions);
  const clientService = new ClientService(connection.getRepository(Client));

  const work = _.range(1, 10).map(n => ClientDTO.from({
      name: `seed${seedId}-${n}`,
    }))
######################## my service calls ClientDTO.toEntity() without issue ###########################
    .map(dto => clientService.createClient(dto, seedUser) 
      .then(r => (console.log('done ->', r.name), r)))

  return await Promise.all(work);
}

run()
  .then(_ => console.log('...wait for script to exit'))
  .catch(error => console.error('seed error', error));

It makes me think I am missing something simple/obvious.

Thanks!

Hear answered 20/3, 2020 at 4:32 Comment(0)
O
4

The fact that the dto is declared like this dto: ClientDTO in the controller is not enough to create instances of the class. This is just an indication for you and the other developers on the project, to prevent misuses of the dto object in the rest of the application.

In order to have instances of classes, and use the methods from the class, you have to explicitly set a mapping like this:

@Post()
public async createClient(@User() user: User, @Body() dto: ClientDTO): Promise<ClientDTO> {
    const client = ClientDTO.from(dto);
    return this.clientService.createClient(client, user);
}

Assuming ClientDTO.from is the right function to use for the data contained in dto. If not, adapt it, create a new one, or add a constructor.

Otha answered 21/3, 2020 at 15:54 Comment(2)
your solution will not work because the object coming in to the route from the post is still just a generic object. In order for the "from" method to work, a new object has to be created from the class. Then the "from" method as it exists can work as long as it takes the plain object as a param.Busterbustle
this is just JavaScript behind, no types involved, you could write constructor(obj?: any) { Object.assign(this, obj); } in the DTO class, and then const client = new ClientDTO(dto);Otha
L
6

Looks like you are using ValidationPipe. The solution is mentioned here https://github.com/nestjs/nest/issues/552

when setting your validation pipe you need to tell it to transform for example

app.useGlobalPipes(new ValidationPipe({
  transform: true
}));
Lit answered 18/3, 2021 at 1:27 Comment(1)
The Ops post is based on a tutorial in Medium. This answer fixes it. Just add the above code to main.ts, and "npm install class-transformer" and you are good to go (at least on this issue)Contract
O
4

The fact that the dto is declared like this dto: ClientDTO in the controller is not enough to create instances of the class. This is just an indication for you and the other developers on the project, to prevent misuses of the dto object in the rest of the application.

In order to have instances of classes, and use the methods from the class, you have to explicitly set a mapping like this:

@Post()
public async createClient(@User() user: User, @Body() dto: ClientDTO): Promise<ClientDTO> {
    const client = ClientDTO.from(dto);
    return this.clientService.createClient(client, user);
}

Assuming ClientDTO.from is the right function to use for the data contained in dto. If not, adapt it, create a new one, or add a constructor.

Otha answered 21/3, 2020 at 15:54 Comment(2)
your solution will not work because the object coming in to the route from the post is still just a generic object. In order for the "from" method to work, a new object has to be created from the class. Then the "from" method as it exists can work as long as it takes the plain object as a param.Busterbustle
this is just JavaScript behind, no types involved, you could write constructor(obj?: any) { Object.assign(this, obj); } in the DTO class, and then const client = new ClientDTO(dto);Otha
B
1

Your dto was not a class-based object when coming in through your api call-- it's just a generic object. Therefore it can't have methods and so your toEntity method won't work. The error message you get is a red herring that doesn't tell you the true cause of the failure.

You can fix this by creating a new object based on your class and then calling a method on the new object to copy the properties in from your plain object dto, or by using the class-transformer library, or by whatever you want that achieves the same result.

Busterbustle answered 21/3, 2020 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.