All Articles

NestJS: Custom validator for checking relationship with Mongo model

Published Jun 26, 2022

Intro

Let’s assume we want to build a pretty basic NestJS To-Do application with MongoDB as a database. We’d want to have a few models that might be connected using relationships as one-to-one, one-to-many etc.

For example, we have a DTO for updating a To-Do item. It might look like this:

import { IsMongoId } from "class-validator";

class UpdateToDo {
  @IsMongoId()
  todoId: string;
}

With the help of using IsMongoId decorator we are able to ensure that provided ID string is a really Mongo ObjectId and not just a random formatted string. It’s cool, but with a one major minus - we cannot be sure that To-Do item with provided ID really exists in our database. Let’s fix that!

Implementation

Using class-validator we can build custom validators (classes or functions). Firstly, let’s define a provider that implements ValidatorConstraintInterface

import { Injectable, Type } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import {
  registerDecorator,
  ValidationArguments,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from 'class-validator';
import { Connection } from 'mongoose';

@ValidatorConstraint({ async: true })
@Injectable()
export class IsRelationshipProvider implements ValidatorConstraintInterface {
  constructor(@InjectConnection() private readonly connection: Connection) {}
  async validate(value: any, args: ValidationArguments): Promise<boolean> {
    try {
      const result = await this.connection.models[args.constraints[0].name].findById(value);

      return !!result;
    } catch (e) {
      return false;
    }
  }

  defaultMessage(args?: ValidationArguments): string {
    return `${args.property} field must refer to existing ${args.constraints[0].name} document`;
  }
}

Secondly, we need to define our decorator.

export const IsRelationShipWith =
  <TModel extends Object>(ModelClass: Type<TModel>, options?: ValidationOptions) =>
  (object: Object, propertyName: string) =>
    registerDecorator({
      name: `IsRelationShip`,
      target: object.constructor,
      propertyName,
      options,
      constraints: [ModelClass],
      validator: IsRelationshipProvider,
    });

Add the next line to bootstrap method in main.ts file.

import { useContainer } from 'class-validator';

useContainer(app.select(AppModule), { fallbackOnErrors: true });

The last step we have to do is to register our custom provider in Nest application. Just add IsRelationshipProvider to providers array of the AppModule.

Example

Now, we can leverage the help of the decorator.

import { IsMongoId } from "class-validator";

// mongoose entity
class ToDoEntity {}

class UpdateToDo {
  @IsMongoId()
  @IsRelationShipWith(ToDoEntity)
  todoId: string;
}

This decorator will check if provided ID really exists in the database.