typestack / class-transformer

Decorator-based transformation, serialization, and deserialization between objects and classes.
MIT License
6.78k stars 496 forks source link

question: How to transform response DTO? #555

Closed Tam2 closed 3 years ago

Tam2 commented 3 years ago

I have created a response DTO for my controller (have done this because i want different columns in different responses so i haven't added to the entity)

export class InventorySearchResponseDTO {
  vehicles: UsedVehicleSearchResponse[];
  cursor: Cursor;
}

export class UsedVehicleSearchResponse {
  id: number;
  make: string;

  @Exclude()
  clientId: number;

  @Type(() => Model)
  @Transform((model) => model.name)
  model: Model;
}

Then within my service, I'm setting the response type to InventorySearchResponseDTO

  async search(
    clientId: number,
    query: InventorySearchRequestDTO,
  ): Promise<InventorySearchResponseDTO> {
    const { data, cursor } = await this.usedVehicleRepository.searchInventory(
      clientId,
      query,
    );

    return {
      vehicles: data,
      cursor: cursor,
    };
  }

I have the ClassSerializerInterceptor enabled globally

app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

The problem:

When the request is sent back to the user it doesn't appear to be transforming the response, based on the above DTO, i would expect it to remove the clientId and transform the model to just model.name

{
  "vehicles": [
    {
      "id": 1220,
      "clientId": 12,
      "modelId": 17,
      "variant": "2 ISG",
      "price": 18999,
      "model": {
        "id": 17,
        "name": "Sportage",
        "makeId": 5,
        "vehicleType": 1,
        "slug": "sportage",
        "make": {
          "id": 5,
          "name": "Kia",
          "slug": "kia",
          "franchise": false,
        }
      },
      "make": "Kia"
    }
  ],
  "cursor": {
    "afterCursor": "aWQ6MTk1MQ==",
    "beforeCursor": null
  }
}
Tam2 commented 3 years ago

Tried adding @Type but it still doesn't work

export class InventorySearchResponseDTO {
  @Type(() => UsedVehicleSearchResponse)
  vehicles: UsedVehicleSearchResponse[];
  cursor: Cursor;
}
Tam2 commented 3 years ago

I've figured out a way to get it to work, however not sure if it's the correct solution

My service has been updated to this:

  async search(
    clientId: number,
    query: InventorySearchRequestDTO,
  ): Promise<InventorySearchResponseDTO> {
    const { data, cursor } = await this.usedVehicleRepository.searchInventory(
      clientId,
      query,
    );
    const vehicles = [];

    data.forEach((vehicle) => {
      const classProto = Object.getPrototypeOf(new UsedVehicleSearchResponse());
      Object.setPrototypeOf(vehicle, classProto);
      vehicles.push(vehicle);
    });

    return new InventorySearchResponseDTO(vehicles, cursor);
  }

Then within the DTO i manually expose the props i want

export class InventorySearchResponseDTO {
  @Type(() => UsedVehicleSearchResponse)
  vehicles: UsedVehicleSearchResponse[];
  cursor: Cursor;

  constructor(vehicles, cursor) {
    this.vehicles = vehicles;
    this.cursor = cursor;
  }
}

@Exclude()
export class UsedVehicleSearchResponse {
  @Expose()
  id: number;

  @Expose()
  make: string;

  @Expose()
  @Type(() => Model)
  @Transform(({ value }) => value.name)
  model: Model;
}
AckerApple commented 3 years ago

Your original code was correct but breaking changes were introduced in version 0.3.2

Please review top issues here that many of us our pitching in to alert others about

NoNameProvided commented 3 years ago

Your first example doesn't work because I assume the passed response is an object matching the interface of your class but for class-transformer to work, the object must be an instance of the class. (I cannot say for sure because the implementation of this.usedVehicleRepository.searchInventory is not included.)

In your second example you make your object a class instance without calling the constructor. I would just suggest to call plainToClass on the objects when you request them in the searchInventory function.

Also probably you want to update your model to simplify the model in to plain transformations only:

@Type(() => Model)
@Transform(({ value }) => value.name, { toPlainOnly: true })
model: Model;
github-actions[bot] commented 3 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.