toonvanstrijp / nestjs-i18n

The i18n module for nestjs.
https://nestjs-i18n.com
Other
656 stars 113 forks source link

TypeError at formatI18nErrors when validating nested array of objects #633

Closed heken84 closed 2 weeks ago

heken84 commented 6 months ago

Hi there,

I always get a TypeError: Cannot convert undefined or null to object\n when using validation method "i18n.validate" from the "I18nService" while validating nested array of objects.

This error does not occur, when the normal "validate" function of the "class-validator" library is used.

Thank you, Heiko


My Dto (Parent)

export class RestrictionCodeDto {

  @IsNotEmpty({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isNotEmptyError') })
  @Validate(hasRestrictionCodeValidLength, { message: i18nValidationMessage<I18nTranslations>('messages.validation.rcLengthError') })
  id: string;

  @IsNotEmpty({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isNotEmptyError') })
  @IsString({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isStringError') })
  @Length(4, 4, { each: true, message: i18nValidationMessage<I18nTranslations>('messages.validation.lengthError') })
  salesOrganization: string;

  @Type(() => Boolean)
  @IsBoolean({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isBooleanError') })
  active: boolean

  @Type(() => Boolean)
  @IsBoolean({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isBooleanError') })
  defectMaingroupRequired: boolean;

  @IsNotEmpty({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isNotEmptyError') })
  @IsString({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isStringError') })
  @Length(7, 7, { message: i18nValidationMessage<I18nTranslations>('messages.validation.lengthError') })
  changedBy: string;

  @IsOptional()
  @Type(() => Date)
  @IsDate({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isDateError') })
  lastChangeOn: Date | null;

  @ValidateNested({each: true})
  @Type(() => RestrictionCodeTextDto)
  texts: RestrictionCodeTextDto [];
}

My Dto (Nested)

export class RestrictionCodeTextDto {

  @IsString({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isStringError') })
  @Length(2, 2, { message: i18nValidationMessage<I18nTranslations>('messages.validation.lengthError') })
  language: string;

  @IsString({ message: i18nValidationMessage<I18nTranslations>('messages.validation.isStringError') })
  @MaxLength(200, { message: i18nValidationMessage<I18nTranslations>('messages.validation.maxLengthError') })
  text: string;
}

My Pipe

@Injectable()
export class RestrictionCodePipe implements PipeTransform {

  constructor(private i18n: I18nService) { }

  async transform(value: any, { metatype }: ArgumentMetadata) {
    const appException: AppException = new AppException(HttpStatus.BAD_REQUEST, []);

    const restrictionCodeDto: RestrictionCodeDto = plainToInstance(metatype, value);

    // this method throws the stack trace
    const errors: I18nValidationError[] = await this.i18n.validate( 
      restrictionCodeDto, { lang: I18nContext.current().lang }
    );

   // processing of errors
  }
}

Stack Trace

{"TypeError: Cannot convert undefined or null to object\n    at Function.keys (<anonymous>)\n    at ...node_modules\\nestjs-i18n\\src\\utils\\util.ts:79:32\n    at Array.map (<anonymous>)\n    at formatI18nErrors (...node_modules\\nestjs-i18n\\src\\utils\\util.ts:64:17)\n    at ...\\node_modules\\nestjs-i18n\\src\\utils\\util.ts:78:22\n    at Array.map (<anonymous>)\n    at formatI18nErrors (...\\node_modules\\nestjs-i18n\\src\\utils\\util.ts:64:17)\n    at I18nService.validate (...\\node_modules\\nestjs-i18n\\src\\services\\i18n.service.ts:361:28)\n    at processTicksAndRejections (node:internal/process/task_queues:95:5)\n    at RestrictionCodePipe.transform (...\\src\\rc-maintenance\\pipes\\restriction-code.pipe.ts:19:43)"}

Originally posted by @heken84 in https://github.com/toonvanstrijp/nestjs-i18n/discussions/630

heken84 commented 4 months ago

Hi there,

is there someone who can help me with this issue?

Best regards, Heiko

velvet-lynx commented 2 weeks ago

Hello there !

I also had this issue. By following the stack trace and inspecting the code I found the source of the problem in this function:

// src/utils/util.ts, function definition starts at line 55

// ...
export function formatI18nErrors<K = Record<string, unknown>>(
  errors: I18nValidationError[],
  i18n: I18nService<K>,
  options?: TranslateOptions,
): I18nValidationError[] {
  return errors.map((error) => {
    error.children = formatI18nErrors(error.children ?? [], i18n, options);
    // the error is in the next line
    error.constraints = Object.keys(error.constraints).reduce((result, key) => {
      const [translationKey, argsString] = error.constraints[key].split('|');
      const args = !!argsString ? JSON.parse(argsString) : {};
      const constraints = args.constraints
        ? args.constraints.reduce((acc: object, cur: any, index: number) => {
            acc[index.toString()] = cur;
            return acc;
          }, {})
        : error.constraints;
      result[key] = i18n.translate(translationKey as Path<K>, {
        ...options,
        args: {
          property: error.property,
          value: error.value,
          target: error.target,
          contexts: error.contexts,
          ...args,
          constraints,
        },
      });
      return result;
    }, {});
    return error;
  });
}
// ...

Object.keys(...) will throw if error.constraints === undefined, which happens when the validation errors originate in the children and no validation error occurs in the parent.

The solution I found is to replace Object.keys(error.constraints) by Object.keys(error.constraints ?? {})

But obviously without a fix in the repo nothing will change when fetching from npm