Search code examples
node.jstypescriptnestjsclass-validator

NestJS custom validator do not inject services in constructor - cannot read properties on undefined


I'm trying to create custom validator for my input class in NestJS. This is what I have done:

// is-building-id-provided.validator.ts
import { Injectable } from "@nestjs/common";
import { AddToQueueInput } from "@warp-core/building-queue/input/add-to-queue.input";
import { BuildingZoneService } from "@warp-core/building-zone/building-zone.service";
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";

@ValidatorConstraint({ async: true })
@Injectable()
export class IsBuildingIdProvidedConstraint implements ValidatorConstraintInterface {
    constructor(
        protected readonly buildingZoneService: BuildingZoneService
    ) {}

    async validate(buildingId: number, args: ValidationArguments) {
        if (buildingId) {
            return true;
        }

        const addToQueue = args.object as AddToQueueInput;

        const buildingZone = await this.buildingZoneService.getSingleBuildingZone(
            addToQueue.counterPerHabitat
        );

        if (!buildingZone.buildingId) {
            return false;
        } 
        
        return true;
    }

    defaultMessage(args: ValidationArguments) {
        return `Building Id is required to create new building on existing building zone`;
    }
}

export function IsBuildingIdProvided(validationOptions?: ValidationOptions) {
    return function (object: Object, propertyName: string) {
        registerDecorator({
            target: object.constructor,
            propertyName: propertyName,
            options: validationOptions,
            constraints: [],
            validator: IsBuildingIdProvidedConstraint,
        });
    };
}
// add-to-queue.input.ts
import { Field, InputType, Int, PartialType } from "@nestjs/graphql";
import { IsBuildingIdProvided } from "@warp-core/building-queue/input/validator/is-building-id-provided.validator";
import { BuildingQueueElementModel } from "@warp-core/database/model/building-queue-element.model";

@InputType({description: "Creates new element in queue"})
export class AddToQueueInput extends PartialType(BuildingQueueElementModel)
{
    @Field(type => Int, {
        description: "Id of building type that will be constructed. If building is already placed, that field will be ignored"
    })
    @IsBuildingIdProvided()
    buildingId?: number;

    @Field(type => Int, {description: "How much levels will be build"})
    endLevel: number;
}

// building-queue.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AuthModule } from "@warp-core/auth/auth.module";
import { BuildingQueueAddService } from "@warp-core/building-queue/building-queue-add.service";
import { BuildingQueueResolver } from "@warp-core/building-queue/building-queue.resolver";
import { IsBuildingIdProvidedConstraint } from "@warp-core/building-queue/input/validator/is-building-id-provided.validator";
import { BuildingZoneModule } from "@warp-core/building-zone/building-zone.module";
import { BuildingModule } from "@warp-core/building/building.module";
import { DatabaseModule } from "@warp-core/database/database.module";
@Module({
    providers: [
        BuildingQueueAddService,
        BuildingQueueResolver,
        IsBuildingIdProvidedConstraint,
    ],
    imports: [
        BuildingZoneModule,
        BuildingModule,
        DatabaseModule,
        ConfigModule,
        AuthModule,
    ],
    exports: [
    ]
})
export class BuildingQueueModule {
}
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@warp-core/app.module';
import { useContainer } from 'class-validator';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      transform: true,
    }),
  );

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

  await app.listen(3000);
  console.log("App is loaded at " + (await app.getUrl()) + "/graphql URL");
}
bootstrap();

As you can see, my Constraint class have Injectable decorator, also all external modules are imported in module file and in main.ts file I call useContainer function from class-validator package. I think I have done everything mentioned in other stack questions and in NestJS github issue. Unfortunately every time when I'm trying to use injected functions, buildingZoneService is undefined. I have no idea what to do more with that issue, I'm out of ideas...


Solution

  • One of validator dependencies has a Request scope, and that is impossible to be injected into class-validator custom class. (https://github.com/nestjs/nest/issues/5566)

    Instead of that I have decided to use Validation pipe, as described in Nest.js documentation: https://docs.nestjs.com/pipes#binding-validation-pipes