Effectuer une validation asynchrone de données avec des décorateurs personnalisés dans Nest JS
Depuis peu, j'utilise Nest JS au maximum pour des projets backend; et une chose qui devient récurrent est la validation des données avant leurs traitements. Nest JS recommande l'utilisation des Data Transfert Object communément appelé DTO pour faire valider les entrées utilisateurs.
export class SignupDto {
@IsNotEmpty()
@IsDefined()
@IsString()
firstName!: string;
@IsNotEmpty()
@IsDefined()
@IsString()
lastName!: string;
@IsNotEmpty()
@IsDefined()
@IsEmail()
email!: string;
@IsNotEmpty()
@IsDefined()
@IsString()
password!: string;
}
Dans une classe DTO, on peut faire appel à des décorateurs fournit par class-validator pour faire valider les propriétés de la classe. J'avoue que les décorateurs fournit par class-validator sont très utiles. Mais un besoin auquel je suis souvent le plus confronté est lorsque la validation fait appel à des services externes ce qui rend la validation asynchrone.
Prenons le cas ci comme exemple: vérifier si le nom d'utilisateur ou l'adresse email fournit par l'utilisateur est déjà lié à un compte existant en base de données. Au début, la méthode que je trouvais était d'effectuer cette vérification dans mon controller qui lui ferait appel à un service pour effectuer la vérification.
Mais, j'ai vite trouvé cette technique très contraignant; et non seulement ça, le code source devient très vite un peu pollué; car il a un mélange de vérification et de logique métier.
Il m'était devenu plus logique de séparer la logique de vérification asynchrone et de l'ajouter à l'étape de validation qui s'exécute avant tout appel à la logique d'un controller donné. Pour cela, class-validator propose les "Custom validation decorators" et c'est vraiment une solution pratique.
Dans ce articles, je présenterai avec des exemples de codes à l'appui 2 cas d'usage de validation asynchrone.
- Vérifier si une adresse email ou un username existe déjà dans la base de donnée
- Vérifier si par exemple des id de catégories fournit par l'utilisateur existe dans la base de données.
Vérifier si une adresse email ou un username existe déjà dans la base de donnée
Faire usage des custom validation decorators pour vérifier si un élément existe dans une base de donnée est un cas très pratique. Voici un exemple de code:
@ValidatorConstraint({ name: "email", async: true })
export default class IsUsedEmail implements ValidatorConstraintInterface {
async validate(value: any, args: ValidationArguments): Promise<boolean> {
return validateUserEmail(value);
}
defaultMessage(args?: ValidationArguments): string {
return `${args.property} is already used`;
}
}
async function validateUserEmail(email: string) {
const dataSource = [
"bob@email.com",
"sponge@email.com",
"tom@email.com",
"jerry@email.com",
]; // Use a real datasource linked to a database
return new Promise<boolean>((resolve) => {
// Asynchrone process verification
setTimeout(() => {
resolve(!dataSource.includes(email));
}, 1000);
});
}
Pour faire usage de ce décorateur, il suffit juste de lui faire appel dans votre DTO avec le décorateur @Validate
.
@IsNotEmpty()
@IsEmail()
@Validate(IsUsedEmail)
email: string;
Comme vous l'avez vu, on peut aussi le coupler avec d'autres décorateurs natifs de class-validator. La validation donne l'erreur suivant:
Passons maintenant au deuxième cas d'usage qui est un peu atypique.
Vérifier si les ids de catégories fournit par l'utilisateur existe nt dans la base de données.
Ce cas d'usage peut être pratique dans un contexte ou l'utilisateur souhaite lier des catégories à un produits qu'il ajoute en base de donnés ou si par exemple il souhaite lier des tags à un article qu'il souhaite publier. Voici 2 contraintes que le validateur doit s'assurer de gérer:
- les ids peuvent être un tableau de chaîne de caractère ou de chiffre ou si possible les deux:
- ["1", "3", "5"]
- [1, 3, 5]
- ["1", 3, 7]
La docs de class-validator propose un décorateur natif pour faire valider un nombre ecris sous forme de chaine de caractère: isNumberString
. Mais ce décorateur contient une limite et ne peux pas gérer le cas definit ci-dessous. Alors on peut décider de faire la vérification manuellement en utilisant des méthodes proposés par class-validator à savoir: isNumber && isNumberString
- la deuxième contrainte qui a la principale est de s'assurer que tout les ids fournit sont disponibles en base de données.
Voici un extrait de code:
@ValidatorConstraint({ name: "productCategoriesIds", async: true })
export default class IsValidProductCategoryIdArray
implements ValidatorConstraintInterface
{
async validate(value: any, args: ValidationArguments): Promise<boolean> {
return validateCategoriesIds(value);
}
defaultMessage(args?: ValidationArguments): string {
if (!isValidCategoriesIdsFormat(args.value)) {
return `${args.property} must be an array of numbers or array of string number`;
}
return `${args.property} contains invalid category ids `;
}
}
async function validateCategoriesIds(categoriesIds: string[] | number[]) {
const dataSource = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
]; // Use a real datasource linked to a database
return new Promise<boolean>((resolve) => {
// Asynchrone process verification
// You can also extract invalids ids and return to user.
setTimeout(() => {
resolve(
isArray(categoriesIds) &&
categoriesIds.every((categoryId) =>
dataSource.includes(toNumberValue(categoryId))
)
);
}, 1000);
});
}
function isValidCategoriesIdsFormat(categoriesIds: any[]) {
return (
isArray(categoriesIds) &&
categoriesIds.every(
(categoryId) => isNumber(categoryId) || isNumberString(categoryId)
)
);
}
Comme indiquer plus haut, la fonction isValidCategoriesIdsFormat
s'assure de vérifier si chaque valeur du tableau est soit une chaîne de caractère ou un chiffre.
On peut bien aussi pousser les cas d'usage un peu plus loin pour par exemple vérifier si le mot de passe fournit par utilisateur a déjà fait l'objet d'un data leak.
Liens utiles
- Code complet et article en anglais: https://github.com/nahtandev/nestjs-dto-custom-decorator
- Docs de class-validator: https://github.com/typestack/class-validator
Crédits
- Image de couverture par rawpixel.com sur Freepik