Skip to content

feature: Support class-level decorators #1788

Open
@blended-bram

Description

@blended-bram

Context: we're working with NestJS and using this library to mark up data-transfer-object (DTO) classes to control serialization for api responses. I'll refer a lot to DTOs, which will mean a use-case for a class-tranformer decorated class that is meant to parse a json response from some API using plainToInstance and at some point later use instanceToPlain to print out the type as a response for out own API.

Also, I'm willing to contribute this feature. Please provide feedback as our codebase would benefit from implementing this.

Description

There are cases, in particular for parsing with plainToInstance that there are transformations, and annotations that are universal to a class, whenever it is used as a property. What I'd like to do is take the decorators that we now need to repeat on all properties using that type, and place them on the class directly.

Detailed use-case, involving Sanity

Let me describe a real use-case where we are facing this.
We have some parts that query Sanity, a free-form json-encoded data-store.
You get to set-up the entire schema for the data yourself. Additionally Sanity has a small set of datatypes, one of which is asset. Asset is a file uploaded to the sanity store and you can reference that asset within your data.

This is what the data of an asset could look like:

const data = {
  _type: 'user', // _type indicates the type of the object in the data; here 'user' would be a custom datatype
  // imagine 'user' has an avatar field:
  avatar: {
    // The 'image' type is a built-in sanity type for an asset, specifically an image one.
    _type: 'image', // sanity built-in type for an image asset
    // ... snip, more fields ...
  },
  // ... snip, more fields ...
}

const parsed = plainToInstance(UserDto, data);

class UserDto {
  @Type(() => AssetDto)
  @InjectSanityAssetUrl()
  avatar!: AssetDto;
}

class AssetDto {
  _type!: string;
  _ref!: string;
  url!: string;
}

/** Injects the `url` property on the `AssetDto` property */
function InjectSanityAssetUrl(): PropertyDecorator {
  return Transform(({ value }) => {
    if (value instanceof AssetDto) {
      value.url = assetUrl(value);
    }
    return value as unknown;
  });
}

import { SanityImageSource } from '@sanity/image-url/lib/types/types';
import imageUrlBuilder from '@sanity/image-url';

export function assetUrl(image: SanityImageSource): string {
  // `imageUrlBuilder` is a function from `@sanity/image-url`
  // It basically uses `_ref` to build the public url for the asset.
  return imageUrlBuilder(useSanityClient()).image(image).url();
}

In this case, InjectSanityAssetUrl is a decorator that needs to be set on all DTOs that have a AssetDto property.

Proposed solution

Taking @Transform as example, but other decorators may qualify as well, allow it to be used on at the class level.
A decorator applied at the class level is equivalent to applying the decorator to the property the class is used in, when it shows up in a class that is being transformed.

In its simplest form, what would happen is that when the decorators of a property are collected, the @Type decorator is evaluated to find class-level decorators that merged onto the property the @Type applies to.

Before

class Asset {
  _ref!: string;
  url!: string;
}

class User {
  @Transform(/* impl */)
  @Type(() => Asset)
  avatar !: Asset;
}

class Product {
  @Transform(/* impl */)
  @Type(() => Asset)
  manual !: Asset;
}

After

@Transform(/* impl */)
class Asset {
  _ref!: string;
  url!: string;
}

class User {
  @Type(() => Asset)
  avatar !: Asset;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    flag: needs discussionIssues which needs discussion before implementation.type: featureIssues related to new features.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions