question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Question] How to "remove" inherit decorator ?

See original GitHub issue

There is an elegant way to ā€œremoveā€ inherit decorator like @IsOptional in child class ?

I’m using class-validator with Nest to validate my input data with some DTO class.

I need to have a parent class with all fields optional and a child class with some mandatory fields.

So i have a CreateAccountDto wich inherit from AccountDto

The idea behind is to have different inputs DTO like CreateAccountDto and avoid repetition.

Actually i have :

export class AccountDto {
  @IsUUID('4')
  @IsOptional()
  @ApiModelProperty()
  readonly account_id: string;

  @IsString()
  @IsNotEmpty()
  @MaxLength(200)
  @IsEmail()
  @IsOptional()
  @ApiModelProperty()
  readonly email: string;

  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  @IsOptional()
  @ApiModelProperty()
  readonly password: string;
}

and

export class CreateAccountDto extends AccountDto {
  @Equals(undefined)
  @RemoveApiModelProperty()
  readonly account_id: string;

  @RemoveIsOptional()
  readonly email: string;

  @RemoveIsOptional()
  readonly password: string;
}
const removeValidationPropertyDecorator = (metakey: string): PropertyDecorator => {
  return (target: object, propertyKey: string | symbol) => {
    const validationMetadatasKey = 'validationMetadatas';
    const validationMetadatas: ValidationMetadata[] = getFromContainer(MetadataStorage)[validationMetadatasKey];

    _.remove(validationMetadatas, (validationMetadata: ValidationMetadata) => {
      return validationMetadata.propertyName === propertyKey && validationMetadata.type === metakey;
    });
  };
};
export const RemoveIsOptional = (): PropertyDecorator => {
  return removeValidationPropertyDecorator(ValidationTypes.CONDITIONAL_VALIDATION);
};

My actual solution is to have a custom @RemoveIsOptional decorator, but i’m not sure is the best way to accomplish my goal !?

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:13
  • Comments:11 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
miramocommented, Dec 17, 2020

For NestJS users, have you seen this: https://trilon.io/blog/introducing-mapped-types-for-nestjs ?

I don’t know if this can satisfy all your needs, but this doesn’t exist when I created this issue 🤷 And I think that would have helped us a lot.

In the meantime we have implemented/used another solution on our side ;

import { getFromContainer, MetadataStorage } from 'class-validator';
import cloneDeep from 'lodash/cloneDeep';

/**
 * Allow copying validation metadatas set by `class-validator` from
 * a given Class property to an other. Copied `ValidationMetadata`s
 * will have their `target` and `propertyName` changed according to
 * the decorated class and property.
 *
 * @param fromClass    Class to inherit validation metadatas from.
 * @param fromProperty Name of the target property (default to decorated property).
 *
 * @return {PropertyDecorator} Responsible for copying and registering `ValidationMetada`s.
 *
 * @example
 * class SubDto {
 *   @InheritValidation(Dto)
 *   readonly id: number;
 *
 *   @InheritValidation(Dto, 'name')
 *   readonly firstName: string;
 *
 *   @InheritValidation(Dto, 'name')
 *   readonly lastName: string;
 * }
 */
export function InheritValidation<T>(fromClass: new (...args: any[]) => T, fromProperty?: keyof T): PropertyDecorator {
  const metadataStorage = getFromContainer(MetadataStorage);
  const validationMetadatas = metadataStorage.getTargetValidationMetadatas(fromClass, typeof fromClass);

  /**
   * Change the `target` and `propertyName` of each `ValidationMetaData`
   * and add it to `MetadataStorage`. Thus, `class-validator` uses it
   * during validation.
   *
   * @param toClass    Class owning the decorated property.
   * @param toProperty Name of the decorated property.
   */
  return (toClass: object, toProperty: any) => {
    const toPropertyName = toProperty as string;

    const sourceProperty = fromProperty || toProperty;

    const metadatasCopy = cloneDeep(validationMetadatas.filter(vm => vm.target === fromClass && vm.propertyName === sourceProperty));

    metadatasCopy.forEach(vm => {
      vm.target = toClass.constructor;
      vm.propertyName = toPropertyName;
      metadataStorage.addValidationMetadata(vm);
    });
  };
}

Related PR : https://github.com/typestack/class-validator/pull/161

0reactions
mopcwebcommented, Nov 21, 2020

Digging deeper i found next solution for my use case, maybe it still would be useful for others. Insipred by https://github.com/typestack/class-validator/issues/164#issuecomment-369874196

setupOptionalValidators.ts

import { getMetadataStorage } from 'class-validator';
import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata';

export function setupOptionalValidators(): void {
  const validationMetadatasKey = 'validationMetadatas';
  const validationMetadatas: ValidationMetadata[] = getMetadataStorage()[validationMetadatasKey];

  const forUpdate = validationMetadatas.filter((item) => !item.groups?.length);
  forUpdate.forEach((_item, i) => { forUpdate[i].always = true; });
}
// setupOptionalValidators just iterates over all ValidationMetadata(s) and addes `{ always: true }` for thos without `groups` option
// So basically it is not necessary to use it, you could add manually `{ always: true }` to all `class-validator` decorators without groups
// Because without `{ always: true }` those decorators will not work properly (

This one should be called after importing all DTOs. In my case i’m using TsED v5, so i call it if $afterRoutesInit.

Now i can write my models in convenient way: (@Property and @Required are TsED v5 decorators used here for swagger schema)

import { SomeEnum } from 'path/to/SomeEnum';
import { SomeEntity } from 'path/to/SomeEntity';

export class UpdateDto implements Partial<SomeEntity> {
  @Property()
  @IsOptional({ groups: ['update'] })
  @IsString()
  public propertyOne?: string;

  @Property()
  @IsOptional({ groups: ['update'] })
  @IsEnum(SomeEnum)
  public propertyTwo?: SomeEnum;

  @Property()
  @IsOptional({ groups: ['update'] })
  @IsString()
  public propertyThree?: string;

  @Property()
  @IsOptional()
  @IsString()
  public propertyFour?: string;

  @Property()
  @IsOptional()
  @IsBoolean()
  public propertyFive?: boolean;
}

export class CreateDto extends UpdateDto implements SomeEntity {
  @Required() public propertyOne: string;
  @Required() public propertyTwo: SomeEnum;
  @Required() public propertyThree: string;
}

End when i validating my requests - i just provide [ā€˜create’] or [ā€˜update’] groups for validate();

P.S. For validation inside TsED v5 i am overriding default ValidationPipe:

@OverrideProvider(ValidationPipe)
export class ClassValidationPipe extends ValidationPipe implements IPipe {
  private _validateOptions: ValidatorOptions = { whitelist: true, forbidNonWhitelisted: true };

  public async transform(value: any, metadata: ParamMetadata): Promise<any> {
    if (!this.shouldValidate(metadata)) return value;
    const options: ValidatorOptions = { ...this._validateOptions, groups: metadata.required ? ['create'] : ['update'] };

    const dataToValidate = plainToClass(metadata.type, value);
    const result = Array.isArray(dataToValidate)
      ? await this.validateList(dataToValidate, options)
      : await validate(dataToValidate, options);

    if (result.length > 0) throwHttpError.badRequest(this.removeRestrictedProperties(result));
    return value;
  }

  protected async validateList<T>(list: T[], options: ValidatorOptions = { }): Promise<ValidationError[]> {
    let result: ValidationError[] = [];
    const promises = list.map((item) => validate(item, options).then((errors) => { result = result.concat(errors); }));
    await Promise.all(promises);
    return result;
  }

  protected shouldValidate(metadata: ParamMetadata): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];

    return !super.shouldValidate(metadata) || !types.includes(metadata.type);
  }

  // This one is just for convenient errors in my architechure
  protected removeRestrictedProperties(errors: ValidationError[]): GenericObject[] {
    if (!errors || !errors.length) return;
    const result = errors.map((item) => ({
      property: item.property,
      value: item.value,
      errors: item.constraints,
    }));
    return result;
  }
}

So now when calling @BodyParams() inside Controller ValidationPipe will add [ā€˜update’] group, while calling @Required() @BodyParams() or @BodyParams({ required: true }) will add [ā€˜create’] group.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Over-riding methods remove decorators inherited from base ...
Not really. When you over-ride a method, you replace it completely. Of course, you can use super to access the original parent method,...
Read more >
Decorator - Refactoring.Guru
Inheritance is static. You can't alter the behavior of an existing object at runtime. You can only replace the whole object with another...
Read more >
Multiple Inheritance in Python - GeeksforGeeks
When a class is derived from more than one base class it is called multiple Inheritance. The derived class inherits all the features...
Read more >
text-decoration - CSS: Cascading Style Sheets - MDN Web Docs
This means that if an element specifies a text decoration, then a child element can't remove the decoration. For example, in the markup...
Read more >
The Composition Over Inheritance Principle
A crucial weakness of inheritance as a design strategy is that a class often needs ... it might seem a clear win for...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found