Search code examples
javascripttypescriptnestjstypeormclass-transformer

How can I define same response output field name in NestJS using the @Expose decorator from class-transformer?


Currently, I have my Entity defined as so:

export class ItemEntity implements Item {

  @PrimaryColumn()
  @IsIn(['product', 'productVariant', 'category'])
  @IsNotEmpty()
  itemType: string;

  @PrimaryColumn()
  @IsUUID()
  @IsNotEmpty()
  itemId: string;

  @OneToOne(() => ProductEntity, product => product.id, { nullable: true })
  @JoinColumn()
  @Expose({ name: 'bundleItem' })
  producttItem: ProductEntity;

  @OneToOne(() => ProductVariantEntity, variant => variant.id, {
    nullable: true,
  })
  @JoinColumn()
  @Expose({ name: 'bundleItem' })
  variantItem: ProductVariantEntity;

  @OneToOne(() => CategoryEntity, category => category.id, { nullable: true })
  @JoinColumn()
  @Expose({ name: 'bundleItem' })
  categoryItem: CategoryEntity;
}

I added in the @Expose() decorator because I want to be able to return one of productItem, variantItem, or categoryItem as just a single bundleItem field in the response. Any single one of them can have a value, but NOT two or three of them.

But when I do a GET on the ItemEntity's controller, my desired effect is only applied on the first item, not the others:

[
    {
        "itemType": "category",
        "itemId": ""
        "bundleItem": {
            "categoryType": "Custom",
            "description": "First custom category",
            "id": "e00ad76c-95d3-4215-84b1-de17c7f1f82c",
            "name": "Category A",
            "updatedAt": "2023-02-24T08:49:22.913Z"
        }
    },
    {
        "itemType": "variant",
        "itemId": "",
        "bundletem": null
    }
]

I want the effect to extend to the other items in the returned array response. But currently, they are null. Essentially, I want the response to return a bundleItem field, regardless of what type the itemType is, be it productItem, variantItem, or categoryItem. Can I achieve that using `class-transformer?

Thanks.


Solution

  • You can probably use the @Transform for this. The @Transform decorator contains a few arguments that you can use in the transform process, one of them is the object itself.

    @Transform(({ value, key, obj, type }) => value)
    

    The following example contains an object with two properties, if one of them is null or undefined we return the other

    import {Exclude, Expose, Transform} from "class-transformer";
    
    export class ExampleDto {
      constructor(partial: Partial<ExampleDto>) {
        Object.assign(this, partial);
      }
    
      @Expose()
      name: string;
    
      @Expose({ name: "new_property" })
      @Transform(({ value, key, obj, type }) => {
        return obj.property1 ?? obj.property2;
      })
      property1: string | null;
    
      @Exclude()
      property2: string | null;
    }
    
    

    Controller

      @Get("example")
      @UseInterceptors(ClassSerializerInterceptor)
      getExample(): ExampleDto {
        return new ExampleDto({
          name: "Name",
          property2: "value 2"
        });
      }
    

    The result is

    {
        "name": "Name",
        "new_property": "value 2"
    }
    

    Although this might work, I highly recommend you to not use the same class for your database entity and the DTOs. This can cause all sorts of issues; right now your ItemEntity is representing two different concepts at the same time, a entity in the database and also a contract with the external world. Any changes in your model would also reflect in the API (and also the other way around).