Skip to content

Instantly share code, notes, and snippets.

@dehsilvadeveloper
Last active February 5, 2024 14:43
Show Gist options
  • Save dehsilvadeveloper/4b70f643bfc42b4267a2d5e82614e9ba to your computer and use it in GitHub Desktop.
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".
// 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
// 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.
// 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
// 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.
// 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
// 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
// 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