'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. NestJS has dependency injection, but the service is not injected.

The error is:

TypeError: Cannot read property 'findByEmail' of undefined

Any help on implementing a custom email validator?

user.entity.ts:

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

My CustomEmail validator is

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: true
})

but this throws a mysql error and the ExceptionsHandler that crashes my app, so I can't handle it myself...

Thankx!



Solution 1:[1]

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.

Solution 2:[2]

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. In 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:

Screenshot of error from Insomnia (MacOS App)

Global error filter solution

Or even create a global 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:

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. error message. Example:

enter image description here

Solution 3:[3]

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({ [u]: 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);
    }
  }
}

Solution 4:[4]

Try this solution.

UniqueValidation.ts

import { HttpException, HttpStatus } from '@nestjs/common';
import { Repository } from 'typeorm';
export async function emailUnique(
  newV: any,
  repo: Repository<any>,
  updateMode = false,
  oldV: any = null,
) {
  if (!updateMode) {
    const unique = await repo.find({
      where: [{ email: newV.email }],
    });
    if (unique.length !== 0) {
      throw new HttpException(`email must be unique!`, HttpStatus.BAD_REQUEST);
    }
    return false;
  } else {
    const subquery = await repo
      .createQueryBuilder('user')
      .select('user.email')
      .where('user.email != :oldEmail')
      .andWhere('user.user_id_pk != :id ')
      .setParameters({
        id: oldV.id,
        oldEmail: oldV.email,
      })
      .getMany();
    console.log(subquery);
    if (subquery.findIndex((item) => item.email === newV.email) > -1) {
      throw new HttpException(`email must be unique!`, HttpStatus.BAD_REQUEST);
    }
    return false;
  }
}

user_service.ts file:

 async createUser(createUserDto: CreateUserDto) {
        await emailUnique(createUserDto, this.userRepository);
        //code
      });
        this.userRepository.save(newUser);
        return true;
      }

 async updateUser(id: number, updateUserDto: UpdateUserDto) 
  {
    const user = await this.getUserById(id);
    await emailUnique(updateUserDto, this.userRepository, true, 
    user);
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 LuJaks
Solution 2
Solution 3
Solution 4