简体   繁体   English

如何处理 NestJS 中的 TypeORM 实体字段唯一验证错误?

[英]How to handle TypeORM entity field unique validation error in NestJS?

I've set a custom unique validator decorator on my TypeORM entity field email.我在我的 TypeORM 实体字段 email 上设置了一个自定义的唯一验证器装饰器。 NestJS has dependency injection, but the service is not injected. NestJS 有依赖注入,但服务没有注入。

The error is:错误是:

TypeError: Cannot read property 'findByEmail' of undefined

Any help on implementing a custom email validator?对实现自定义 email 验证器有任何帮助吗?

user.entity.ts : user.entity.ts

@Column()
@Validate(CustomEmail, {
    message: "Title is too short or long!"
})
@IsEmail()
email: string;

My CustomEmail validator is我的CustomEmail验证器是

import {ValidatorConstraint, ValidatorConstraintInterface, 
ValidationArguments} from "class-validator";
import {UserService} from "./user.service";

@ValidatorConstraint({ name: "customText", async: true })
export class CustomEmail implements ValidatorConstraintInterface {

  constructor(private userService: UserService) {}
  async validate(text: string, args: ValidationArguments) {

    const user = await this.userService.findByEmail(text);
    return !user; 
  }

  defaultMessage(args: ValidationArguments) { 
    return "Text ($value) is too short or too long!";
  }
}

I know I could set unique in the Column options我知道我可以在Column选项中设置unique

@Column({
  unique: true
})

but this throws a mysql error and the ExceptionsHandler that crashes my app, so I can't handle it myself...但这会引发 mysql 错误和ExceptionsHandler导致我的应用程序崩溃,所以我自己无法处理......

Thankx!谢谢!

I can propose 2 different approaches here, the first one catches the constraint violation error locally without additional request, and the second one uses a global error filter, catching such errors in the entire application.我可以在这里提出两种不同的方法,第一种在没有额外请求的情况下在本地捕获违反约束的错误,第二种使用全局错误过滤器,在整个应用程序中捕获此类错误。 I personally use the latter.我个人使用后者。

Local no-db request solution本地无数据库请求解决方案

No need to make additional database request.无需进行额外的数据库请求。 You can catch the error violating the unique constraint and throw any HttpException you want to the client.您可以捕获违反唯一约束的错误并向客户端抛出您想要的任何HttpException In users.service.ts :users.service.ts

  public create(newUser: Partial<UserEntity>): Promise<UserEntity> {
    return this.usersRepository.save(newUser).catch((e) => {
      if (/(email)[\s\S]+(already exists)/.test(e.detail)) {
        throw new BadRequestException(
          'Account with this email already exists.',
        );
      }
      return e;
    });
  }

Which will return:哪个将返回:

Insomnia (MacOS App) 错误截图

Global error filter solution全局错误过滤解决方案

Or even create a global QueryErrorFilter:或者甚至创建一个全局的 QueryErrorFilter:

@Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter {
  public catch(exception: any, host: ArgumentsHost): any {
    const detail = exception.detail;
    if (typeof detail === 'string' && detail.includes('already exists')) {
      const messageStart = exception.table.split('_').join(' ') + ' with';
      throw new BadRequestException(
        exception.detail.replace('Key', messageStart),
      );
    }
    return super.catch(exception, host);
  }
}

Then in main.ts :然后在main.ts

async function bootstrap() {
  const app = await NestFactory.create(/**/);
  /* ... */
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
  /* ... */
  await app.listen(3000);
}
bootstrap();

This will give generic $table entity with ($field)=($value) already exists.这将给出$table entity with ($field)=($value) already exists.通用$table entity with ($field)=($value) already exists. error message.错误信息。 Example:示例:

在此处输入图片说明

I have modified my code.我已经修改了我的代码。 I am checking the uniqueness of username/email in the user service (instead of a custom validator) and return an HttpExcetion in case the user is already inserted in the DB.我正在检查用户服务(而不是自定义验证器)中用户名/电子邮件的唯一性,并在用户已插入数据库的情况下返回 HttpExcetion。

The easiest solution!最简单的解决方案!

@Entity()
export class MyEntity extends BaseEntity{ 
 @Column({unique:true}) name:string; 
}

export abstract class BaseDataService<T> {

  constructor(protected readonly repo: Repository<T>) {}

  private  async isUnique(t: any) {
    const uniqueColumns = this.repo.metadata.uniques.map(
      (e) => e.givenColumnNames[0]
    );

    for (const u of uniqueColumns) {
      const count = await this.repo.count({ where: { [u]: ILike(t[u]) } });
      if (count > 0) {
        throw new UnprocessableEntityException(`${u} must be unique!`);
      }
    }
  }

  async save(body: DeepPartial<T>) {
    await this.isUnique(body);
    try {
      return await this.repo.save(body);
    } catch (err) {
      throw new UnprocessableEntityException(err.message);
    }
  }



  async update(id: number, updated: QueryDeepPartialEntity<T>) {
    await this.isUnique(updated)
    try {
      return await this.repo.update(id, updated);
    } catch (err) {
      throw new UnprocessableEntityException(err.message);
    }
  }
}

An approach that works for modern version of NestJS which is based in Daniel Kucal's answer and actually returns the error to the frontend when calling the JSON API is the following:适用于现代版 NestJS 的方法基于 Daniel Kucal 的回答,并且在调用 JSON API 时实际上将错误返回到前端:

import {
  Catch,
  ArgumentsHost,
  BadRequestException,
  HttpException,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { QueryFailedError } from 'typeorm';

type ExceptionType = { detail: string; table: string };

@Catch(QueryFailedError)
export class QueryErrorFilter extends BaseExceptionFilter<
  HttpException | ExceptionType
> {
  public catch(exception: ExceptionType, host: ArgumentsHost): void {
    const { detail = null } = exception || {};

    if (
      !detail ||
      typeof detail !== 'string' ||
      // deepcode ignore AttrAccessOnNull: <False positive>
      !detail.includes('already exists')
    ) {
      return super.catch(exception, host);
    } // else

    /**
     * this regex transform the message `(phone)=(123)` to a more intuitive `with phone: "123"` one,
     * the regex is long to prevent mistakes if the value itself is ()=(), for example, (phone)=(()=())
     */

    const extractMessageRegex =
      /\((.*?)(?:(?:\)=\()(?!.*(\))(?!.*\))=\()(.*?)\)(?!.*\)))(?!.*(?:\)=\()(?!.*\)=\()((.*?)\))(?!.*\)))/;

    const messageStart = `${exception.table.split('_').join(' ')} with`;

    /** prevent Regex DoS, doesn't treat messages longer than 200 characters */
    const exceptionDetail =
      exception.detail.length <= 200
        ? exception.detail.replace(extractMessageRegex, 'with $1: "$3"')
        : exception.detail;

    super.catch(
      new BadRequestException(exceptionDetail.replace('Key', messageStart)),
      host,
    );
  }
}

Also, not forgetting main.ts:另外,不要忘记 main.ts:

async function bootstrap() {
  const app = await NestFactory.create(/**/);
  /* ... */
  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new QueryErrorFilter(httpAdapter));
  /* ... */
  await app.listen(3000);
}
bootstrap();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM