Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Support class-level decorators #1788

Open
blended-bram opened this issue Nov 22, 2024 · 0 comments
Open

feature: Support class-level decorators #1788

blended-bram opened this issue Nov 22, 2024 · 0 comments
Labels
flag: needs discussion Issues which needs discussion before implementation. type: feature Issues related to new features.

Comments

@blended-bram
Copy link

blended-bram commented Nov 22, 2024

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;
}
@blended-bram blended-bram added flag: needs discussion Issues which needs discussion before implementation. type: feature Issues related to new features. labels Nov 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
flag: needs discussion Issues which needs discussion before implementation. type: feature Issues related to new features.
Development

No branches or pull requests

1 participant