Aller au contenu

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.

  1. Ajouter le DTO dans <module>.dto.ts.
  2. Ajouter la méthode de contrôleur dans <module>.controller.ts.
  3. Ajouter l’orchestration service dans <module>.service.ts.
  4. Ajouter la méthode de repository dans <module>.repository.ts.
  5. Ajouter @RequirePermissions ou @Public.
  6. Caster les chaînes du DTO vers les types DB brandés dans le service.
  7. Écrire pnpm db:slice:<module> pour vérifier la requête réelle.
  8. Exécuter pnpm db:verify avant de commiter.
@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);
}
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,
});
});
}
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();
}

Toujours passer a comme premier argument. Ne jamais créer TxAccess à la main.

// bien
async majClient(a: TxAccess, id: BigIntStr, patch: Partial<client_clientUpdate>) { ... }
// mal
const a = new DbAccess(kysely); // casse la propagation de transaction
Fenêtre de terminal
cd sim-database
make migrate
cd ../sim-backend
pnpm db:gen
git diff src/db/generated.ts

Le 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

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 :

  • 23505 violation unique → 409 CONFLICT
  • 23503 violation de clé étrangère → 422 FK_VIOLATION
  • 23502 / 23514 check / not-null → 400 DATA_ERROR
  • 22P02 texte invalide → 400 DATA_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.

  • 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 ; DbAccess garantit TableKeys.
  • Ne pas utiliser sql.raw pour 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 TxAccess en dehors d’AccessScope.
  • Ne pas ajouter d’auto-migrations ; l’évolution du schéma appartient à sim-database.