Aller au contenu
NestJS OpenPartie 5, section b
Changer de langue : lire en anglais

b · Le repository

  • Projet : Loup-Garou
  • Environ 55 minutes
  • 4 exercices

Le repository d’une entité offre tout ce qu’il faut pour lire et écrire ses lignes. Toutes ses méthodes sont asynchrones : elles parlent à la base, ce qui prend du temps, donc elles renvoient une promesse — la partie 1, section b, te sert à nouveau.

Ajoute d’abord un DTO pour créer un utilisateur (partie 4 : @IsString() et @IsNotEmpty()), puis complète le service :

src/utilisateurs/utilisateurs.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Utilisateur } from './utilisateur.entity.js';
import { CreerUtilisateurDto } from './dto/creer-utilisateur.dto.js';
@Injectable()
export class UtilisateursService {
constructor(@InjectRepository(Utilisateur) private readonly utilisateurs: Repository<Utilisateur>) {}
trouverTous() {
return this.utilisateurs.find();
}
async trouverUn(id: number) {
const utilisateur = await this.utilisateurs.findOneBy({ id });
if (!utilisateur) throw new NotFoundException(`Utilisateur ${id} introuvable`);
return utilisateur;
}
creer(dto: CreerUtilisateurDto) {
const utilisateur = this.utilisateurs.create(dto);
return this.utilisateurs.save(utilisateur);
}
async modifierPseudo(id: number, pseudo: string) {
const utilisateur = await this.trouverUn(id);
utilisateur.pseudo = pseudo;
return this.utilisateurs.save(utilisateur);
}
async supprimer(id: number) {
const resultat = await this.utilisateurs.delete(id);
if (resultat.affected === 0) throw new NotFoundException(`Utilisateur ${id} introuvable`);
}
}

Chaque méthode surlignée mérite un mot :

  • findOneBy({ id }) renvoie la ligne, ou null si elle n’existe pas — c’est à toi de transformer ce null en 404, comme avec le tableau en mémoire de la partie 3.
  • create(dto) fabrique une instance de l’entité sans toucher à la base. save(...) l’écrit, et te la rend avec son id tout juste attribué.
  • Appelé sur une instance qui a déjà un id, save ne crée pas une nouvelle ligne : il met à jour la ligne existante. C’est ce que fait modifierPseudo.
  • delete(id) renvoie un objet dont affected compte les lignes supprimées : 0 veut dire qu’aucune n’avait cet id.

Le controller n’apporte rien de neuf par rapport à la partie 3 : @Get(':id'), @Post(), @Patch(':id') et @Delete(':id') appellent ces méthodes, avec ParseIntPipe sur l’id. Pour DELETE, ajoute @HttpCode(204) : « supprimé, rien à renvoyer ».

src/utilisateurs/utilisateurs.controller.ts
@Delete(':id')
@HttpCode(204)
async supprimer(@Param('id', ParseIntPipe) id: number) {
await this.utilisateursService.supprimer(id);
}

Remarque ce que ne change pas ce passage à la base : le controller n’a pas bougé. trouverTous() renvoie maintenant une promesse au lieu d’un tableau, et NestJS l’attend tout seul avant de répondre. C’est l’intérêt de la séparation controller/service de la partie 3 : seul le service sait d’où viennent les données.

find accepte des options pour ne récupérer que ce dont tu as besoin :

this.utilisateurs.find({
where: { pseudo: 'LouveGrise' },
order: { id: 'ASC' },
take: 10,
});

where filtre, order trie, take limite le nombre de lignes (le limite du DefaultValuePipe de la partie 4, section c, y trouve enfin sa place). Et pour ne lire que certaines colonnes, select: { pseudo: true }.

@Column() accepte un objet d’options. Tu as déjà croisé le cas simple ; en voici deux autres :

@Column({ default: true })
vivant: boolean;
@Column({ type: 'numeric', precision: 10, scale: 2 })
prix: number;

default donne une valeur aux lignes créées sans elle. type: 'numeric' demande un décimal exact, avec precision chiffres au total dont scale après la virgule — le bon type pour de l’argent, à la différence d’un nombre à virgule flottante qui arrondit. Une surprise t’attend : PostgreSQL renvoie ce type sous forme de chaîne ("129.90"), pas de nombre, pour ne jamais perdre en précision. Ce n’est pas un bug.

Exercices 5.5 à 5.8

Projet : marketplace

Ajoute GET /api/vendeurs/:id, avec findOneBy et ParseIntPipe. Renvoie une NotFoundException si le vendeur n’existe pas. Vérifie qu’un identifiant inconnu donne un 404, et qu’un identifiant qui n’est pas un nombre donne un 400.

Ajoute PATCH /api/vendeurs/:id, qui change le nom : retrouve le vendeur (avec un 404 s’il n’existe pas), modifie sa propriété, puis appelle save. Vérifie avec un GET que le nouveau nom est bien en base.

Ajoute DELETE /api/vendeurs/:id avec @HttpCode(204), en t’appuyant sur affected pour renvoyer un 404 si rien n’a été supprimé. Vérifie qu’un deuxième DELETE sur le même id donne bien un 404.

Crée l’entité Produit (id, nom, prix en numeric, categorie) et remplace le tableau en mémoire de ProduitsService par son repository. Toutes les routes des parties 3 et 4 doivent continuer à fonctionner : la liste avec ?limite= (take), GET /api/produits/:id, GET /api/produits/categorie/:categorie (where), la liste des catégories sans doublon (exercice 3.11 : find avec select, puis Set), et la création validée avec sa limite NOMBRE_MAX_PRODUITS (indice : count()). Retire variantes du DTO : elles reviendront en 5.11, sous la forme d’une vraie table (les vérifications des exercices 4.2, 4.5 et 4.7 sur ce champ ne s’appliquent plus d’ici là). Nomme la table produits (@Entity('produits')) et place l’entité dans un fichier *.entity.ts ; fais de même pour variantes (5.11) et lignes_commande (5.12). Vérifie que prix revient sous forme de chaîne, et note pourquoi.

Pour vérifier ton travail : npm run test:partie-5 (dépôt d'exercices)