Last active
February 5, 2024 14:43
-
-
Save dehsilvadeveloper/4b70f643bfc42b4267a2d5e82614e9ba to your computer and use it in GitHub Desktop.
Exists validator example for NestJS. Inspired on Laravel validation rule "exists:table,column".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/character/dtos/create-character.dto.ts | |
import { IsNotEmpty, MaxLength, MinLength } from 'class-validator'; | |
import { ExistsOnDatabase } from '@modules/common/decorators/exists-on-database.decorator'; | |
export class CreateCharacterDto { | |
@IsNotEmpty() | |
@MinLength(2) | |
@MaxLength(100) | |
name: string; | |
@IsNotEmpty() | |
@ExistsOnDatabase({ model: 'alignment', column: 'id' }) | |
alignmentId: number; | |
} | |
// The decorator @ExistsOnDatabase will work similar to the Laravel validation rule "exists:table,column". | |
// The "model" serves to find which repository class will be used to search the information on the database. | |
// The "column" serves as filter of the information that will be searched on the database. | |
// If you want to override the default validation message, you can do something like this: | |
// @ExistsOnDatabase({ model: 'alignment', column: 'id' }, { message: 'Alignment does not exists.' }) | |
// The documentation of the Laravel validation rule can be found here: | |
// https://laravel.com/docs/10.x/validation#rule-exists |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/common/rules/exists-on-database.rule.ts | |
import { Injectable } from '@nestjs/common'; | |
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'; | |
import { RepositoryService } from '@modules/common/services/repository.service'; | |
@Injectable() | |
@ValidatorConstraint({ name: 'existsOnDatabase', async: true }) | |
export class ExistsOnDatabaseValidator implements ValidatorConstraintInterface { | |
constructor(private readonly repositoryService: RepositoryService) {} | |
async validate(value: any, args: ValidationArguments): Promise<boolean> { | |
if (isNaN(value)) { | |
return false; | |
} | |
const [property] = args.constraints; | |
const { model: modelName, column: columnName } = property; | |
const repository = await this.repositoryService.getRepositoryByModelName(modelName); | |
const filter = { [columnName]: value }; | |
const entity = await repository.firstWhere(filter); | |
return !!entity; | |
} | |
defaultMessage(args: ValidationArguments): string { | |
const [property] = args.constraints; | |
const { model: modelName, column: columnName } = property; | |
return `model ${modelName} with the ${columnName} provided does not exist on the database.`; | |
} | |
} | |
// This rule class will search the data using the repository related to the model name provided (along with the column name as well) | |
// The result of the method validate is a boolean based on the existence or not of the record. | |
// The method defaultMessage provides a default message for when the validation fails. This message can be override on the decorator call. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/common/decorators/exists-on-database.decorator.ts | |
import { registerDecorator, ValidationOptions } from 'class-validator'; | |
import { ExistsOnDatabaseValidator } from '../rules/exists-on-database.rule'; | |
interface existsOnDatabaseOptions { | |
model: string; | |
column: string; | |
} | |
/** | |
* Check if the value already exists for a entity on database. | |
*/ | |
export function ExistsOnDatabase( | |
existsOnDatabaseOptions: existsOnDatabaseOptions, | |
validationOptions?: ValidationOptions, | |
) { | |
return function (object: Object, propertyName: string) { | |
registerDecorator({ | |
target: object.constructor, | |
propertyName: propertyName, | |
options: validationOptions, | |
constraints: [existsOnDatabaseOptions], | |
validator: ExistsOnDatabaseValidator, | |
}); | |
}; | |
} | |
// Defining a decorator to use the custom rule |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/common/services/repository.service.ts | |
import { Injectable } from '@nestjs/common'; | |
import { AlignmentRepositoryInterface } from '@modules/character/repositories/alignment.repository.interface'; | |
import { CharacterRepositoryInterface } from '@modules/character/repositories/character.repository.interface'; | |
import { TeamRepositoryInterface } from '@modules/team/repositories/team.repository.interface'; | |
@Injectable() | |
export class RepositoryService { | |
constructor( | |
private readonly alignmentRepository: AlignmentRepositoryInterface, | |
private readonly characterRepository: CharacterRepositoryInterface, | |
private readonly teamRepository: TeamRepositoryInterface, | |
) {} | |
getRepositoryByModelName(modelName: string) { | |
switch (modelName) { | |
case 'alignment': | |
return this.alignmentRepository; | |
case 'character': | |
return this.characterRepository; | |
case 'team': | |
return this.teamRepository; | |
default: | |
throw new Error(`Could not found a repository related to the model name ${modelName}.`); | |
} | |
} | |
} | |
// Here I locate and return the repository class associated to model name provided. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/common/common.module.ts | |
import { Module } from '@nestjs/common'; | |
import { DatabaseModule } from '../database/database.module'; | |
import { RepositoryService } from './services/repository.service'; | |
import { ExistsOnDatabaseValidator } from '@modules/common/rules/exists-on-database.rule'; | |
@Module({ | |
providers: [ExistsOnDatabaseValidator, RepositoryService], | |
imports: [DatabaseModule], | |
exports: [ExistsOnDatabaseValidator, RepositoryService], | |
}) | |
export class CommonModule {} | |
// Here I exposed the rule class ExistsOnDatabaseValidator so it can be used by other modules via decorator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/modules/app/app.module.ts | |
import { Module } from '@nestjs/common'; | |
import { ConfigModule } from '@nestjs/config'; | |
import { config } from '@config/config'; | |
import { AppController } from './controllers/app.controller'; | |
import { DatabaseModule } from '@modules/database/database.module'; | |
import { CommonModule } from '@modules/common/common.module'; | |
import { CharacterModule } from '@modules/character/character.module'; | |
@Module({ | |
controllers: [AppController], | |
providers: [], | |
imports: [ | |
ConfigModule.forRoot({ | |
isGlobal: true, | |
load: [config], | |
}), | |
DatabaseModule, | |
CommonModule, | |
CharacterModule, | |
], | |
}) | |
export class AppModule {} | |
// Here I imported the CommonModule to be able to use the custom rule and its decorator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Path: src/main.ts | |
import { NestFactory } from '@nestjs/core'; | |
import { ValidationPipe } from '@nestjs/common'; | |
import { useContainer } from 'class-validator'; | |
import { AppModule } from '@modules/app/app.module'; | |
async function bootstrap() { | |
// Create application instance | |
const app = await NestFactory.create(AppModule); | |
// Enabling service container for custom validator constraint classes (class-validator) | |
useContainer(app.select(AppModule), { fallbackOnErrors: true }); | |
// Validation pipeline | |
app.useGlobalPipes(new ValidationPipe()); | |
// Application port | |
await app.listen(3000, () => { | |
console.info(`Server ready`); | |
}); | |
} | |
bootstrap(); | |
// Here I enabled service container for custom validator constraint classes, like the one been implemented in this example. | |
// The implementation of useContainer() is necessary so that the RepositoryService can be used inside of ExistsOnDatabaseValidator. | |
// This topic has more information aabout the subject: https://github.com/nestjs/nest/issues/528 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment