Recettes
Cette section contient des recettes prêtes à copier-coller pour les tâches backend les plus courantes. Toutes les recettes suivent le contrat d’architecture de _validation/ARCHITECTURE.md.
Ajouter une nouvelle route REST
Section intitulée « Ajouter une nouvelle route REST »- Ajouter le DTO dans
<module>.dto.ts. - Ajouter la méthode de contrôleur dans
<module>.controller.ts. - Ajouter l’orchestration service dans
<module>.service.ts. - Ajouter la méthode de repository dans
<module>.repository.ts. - Ajouter
@RequirePermissionsou@Public. - Caster les chaînes du DTO vers les types DB brandés dans le service.
- Écrire
pnpm db:slice:<module>pour vérifier la requête réelle. - Exécuter
pnpm db:verifyavant de commiter.
Exemple de méthode de contrôleur
Section intitulée « Exemple de méthode de contrôleur »@Post('clients/:id/notes')@RequirePermissions('CLIENT.MODIFIER')async ajouterNote( @Param('id') id: string, @Body() dto: AjouterNoteDto, @CurrentUser() user: AuthUser,) { return this.service.ajouterNote(id as BigIntStr, dto, user.id);}Exemple d’écriture service
Section intitulée « Exemple d’écriture service »async ajouterNote( idClient: BigIntStr, dto: AjouterNoteDto, idUtilisateur: BigIntStr,) { return this.scope.audit(idUtilisateur, async (a) => { return this.repo.ajouterNote(a, { id_client: idClient, contenu: dto.contenu, date_creation: new Date().toISOString() as TimestampStr, }); });}Ajouter une requête de liste au repository
Section intitulée « Ajouter une requête de liste au repository »async listerNotes( a: TxAccess, filtres: ListerNotesFiltres = {},) { let q = a.acc.selectFrom('client.note').selectAll(); q = applyEquality(q, filtres, { id_client: 'id_client' }); q = applySearch(q, filtres.recherche, ['contenu']); q = applySort(q, filtres.sort, filtres.order, ['date_creation'], 'date_creation'); q = applyPagination(q, filtres); return q.orderBy('date_creation', 'desc').execute();}Utiliser TxAccess en sécurité
Section intitulée « Utiliser TxAccess en sécurité »Toujours passer a comme premier argument. Ne jamais créer TxAccess à la main.
// bienasync majClient(a: TxAccess, id: BigIntStr, patch: Partial<client_clientUpdate>) { ... }
// malconst a = new DbAccess(kysely); // casse la propagation de transactionRégénérer generated.ts
Section intitulée « Régénérer generated.ts »cd sim-databasemake migratecd ../sim-backendpnpm db:gengit diff src/db/generated.tsLe diff doit correspondre exactement au delta de schéma. Sinon, commiter le diff généré avec la migration.
Caster les valeurs réseau vers les types DB brandés
Section intitulée « Caster les valeurs réseau vers les types DB brandés »const insert: client_clientInsert = { nom: dto.nom, prenoms: dto.prenoms ?? null, tel_principal: dto.tel_principal, date_naissance: (dto.date_naissance ?? null) as DateStr | null, date_enregistrement: new Date().toISOString() as TimestampStr,};Patrons courants :
| Type réseau | Type DB | Cast |
|---|---|---|
chaîne id |
bigint |
id as BigIntStr |
| chaîne montant | NUMERIC(12,2) |
montant as Money |
| chaîne quantité | NUMERIC(10,2) |
quantite as DecimalQuantity |
| chaîne date | date |
date as DateStr |
| timestamp ISO | timestamp |
new Date().toISOString() as TimestampStr |
Gérer les erreurs PostgreSQL
Section intitulée « Gérer les erreurs PostgreSQL »Laisser le filtre global traduire SQLSTATE en HTTP. Ne pas catcher et relancer d’erreurs génériques sauf si le domaine l’exige.
Mappages courants :
23505violation unique → 409CONFLICT23503violation de clé étrangère → 422FK_VIOLATION23502/23514check / not-null → 400DATA_ERROR22P02texte invalide → 400DATA_ERROR
Tester un repository avec une tranche sur base réelle
Section intitulée « Tester un repository avec une tranche sur base réelle »Créer src/<module>/<module>.e2e.ts :
describe = 'mon-module';
async function main() { const mod = await Test.createTestingModule({ imports: [DatabaseModule, MonModuleModule], }).compile();
const app = mod.createNestApplication(); configureApp(app); await app.init();
const repo = app.get(MonRepository); const scope = app.get(AccessScope);
try { await scope.audit(ACTEUR_ID, async (a) => { const created = await repo.creer(a, { ... }); ok('created', created != null);
const found = await repo.findById(a, created.id); ok('found', found != null);
throw new Error('rollback-marker'); }); } catch (e: any) { if (e.message !== 'rollback-marker') throw e; }
const outside = await repo.findById(scope.root, id); ok('rolled back', outside == null);
await app.close();}
main();L’exécuter avec pnpm db:slice:mon-module.
Anti-patrons à éviter
Section intitulée « Anti-patrons à éviter »- Ne pas ajouter de classe de base repository.
- Ne pas mettre en cache les résultats DB dans Redis.
- Ne pas écrire dans les vues ;
DbAccessgarantitTableKeys. - Ne pas utiliser
sql.rawpour l’entrée utilisateur. - Ne pas faire confiance aux unions d’enums de DTO au runtime ; utiliser des tableaux
@IsInécrits à la main. - Ne pas construire
TxAccessen dehors d’AccessScope. - Ne pas ajouter d’auto-migrations ; l’évolution du schéma appartient à
sim-database.
