簡體   English   中英

如何處理 NestJS 中的 TypeORM 實體字段唯一驗證錯誤?

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

我在我的 TypeORM 實體字段 email 上設置了一個自定義的唯一驗證器裝飾器。 NestJS 有依賴注入,但服務沒有注入。

錯誤是:

TypeError: Cannot read property 'findByEmail' of undefined

對實現自定義 email 驗證器有任何幫助嗎?

user.entity.ts

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

我的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!";
  }
}

我知道我可以在Column選項中設置unique

@Column({
  unique: true
})

但這會引發 mysql 錯誤和ExceptionsHandler導致我的應用程序崩潰,所以我自己無法處理......

謝謝!

我可以在這里提出兩種不同的方法,第一種在沒有額外請求的情況下在本地捕獲違反約束的錯誤,第二種使用全局錯誤過濾器,在整個應用程序中捕獲此類錯誤。 我個人使用后者。

本地無數據庫請求解決方案

無需進行額外的數據庫請求。 您可以捕獲違反唯一約束的錯誤並向客戶端拋出您想要的任何HttpException 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;
    });
  }

哪個將返回:

Insomnia (MacOS App) 錯誤截圖

全局錯誤過濾解決方案

或者甚至創建一個全局的 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);
  }
}

然后在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();

這將給出$table entity with ($field)=($value) already exists.通用$table entity with ($field)=($value) already exists. 錯誤信息。 示例:

在此處輸入圖片說明

我已經修改了我的代碼。 我正在檢查用戶服務(而不是自定義驗證器)中用戶名/電子郵件的唯一性,並在用戶已插入數據庫的情況下返回 HttpExcetion。

最簡單的解決方案!

@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);
    }
  }
}

適用於現代版 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,
    );
  }
}

另外,不要忘記 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