Architecture
Le backend SIM est l’API REST NestJS de GLOBAL SIM GROUP. Il expose une surface HTTP versionnée unique (/api/v1) pour tous les modules métier tout en gardant la couche d’accès aux données volontairement petite, typée et générée depuis PostgreSQL.
Idée centrale
Section intitulée « Idée centrale »Une frontière d’accès PostgreSQL typée, générée depuis PostgreSQL lui-même.
La chaîne source est :
Migrations Flyway → PostgreSQL → pg_catalog → dbgen → src/db/generated.tssrc/db/generated.ts est commité car une PR de migration embarque son diff généré : les reviewers voient exactement ce que TypeScript croit avoir changé.
| Couche | Choix | Notes |
|---|---|---|
| Runtime | Node.js 22 LTS, TypeScript 5.7 | package.json |
| Framework | NestJS 11 sur Express 5 | package.json |
| Versioning API | URI /api/v1/<module> |
src/app.setup.ts |
| Accès base | Kysely 0.29 + pg |
query builder typé |
| Base | PostgreSQL 18 | les migrations vivent dans sim-database |
| Auth | JWT access + rotation refresh, permissions RBAC | src/auth |
| Redis | révocation de jetons, throttling distribué, files BullMQ | src/redis |
| Stockage objet | MinIO / S3 (@aws-sdk/client-s3) |
src/s3, src/uploads |
| Logs | pino (nestjs-pino) | src/main.ts, src/app.module.ts |
| Métriques | prom-client | /metrics |
| Docs API | Swagger (/docs) |
src/app.setup.ts |
| PDF / impression | Puppeteer 25.8.2 | src/pdf, Dockerfile |
| Files de tâches | BullMQ 6 | src/jobs |
| Validation | class-validator / class-transformer | ValidationPipe global |
Stack en couches
Section intitulée « Stack en couches »Contrôleur Nest │ ▼Service de domaine │ ▼Repository │ ▼DbAccess (surface CRUD figée) │ ▼Kysely → pg → PostgreSQLIl n’y a pas d’EntityManager, de UnitOfWork, d’IdentityMap ni de graphe d’entités façon ORM.
Liste des anti-fonctionnalités
Section intitulée « Liste des anti-fonctionnalités »Le contrat d’architecture évite explicitement :
- les relations / graphes d’entités à chargement paresseux
- l’identity map / le change tracking
- un DSL de requêtes générique au-delà du CRUD de base
- les jointures automatiques
- le cache au niveau des résultats DB
- les migrations automatiques
- l’abstraction de moteur de base
- la « magie de repository » / les repositories auto-générés
Ce qu’il conserve :
- les types dérivés de la base et les vues typées
- un CRUD sûr avec protection à la compilation
- des transactions explicites
- le SQL PostgreSQL et les colonnes possédées par les triggers
- la génération de code consciente du schéma
- la détection de dérive (
pnpm db:check)
Organisation des modules
Section intitulée « Organisation des modules »Chaque module vit dans src/<module>/ avec une forme canonique :
src/<module>/ <module>.module.ts # câblage du module Nest <module>.controller.ts # routage + Swagger + décorateurs d'auth <module>.service.ts # orchestration, casts DTO → marques <module>.repository.ts # SQL de domaine <module>.dto.ts # classes de transport + class-validator <module>.e2e.ts # tranche sur base réelle (transaction rollback)Il n’y a pas de classe de base repository. Chaque repository est explicite et façonné par le domaine.
Propagation des transactions
Section intitulée « Propagation des transactions »Les lectures utilisent scope.root. Les écritures utilisent scope.audit(idUtilisateur, (a, tx) => …).
@Injectable()export class AccessScope { readonly root: TxAccess;
audit<T>( idUtilisateur: bigint | string | null, work: (a: TxAccess, tx: Kysely<Database>) => Promise<T>, ): Promise<T> { return withAudit(this.db, idUtilisateur, (tx) => work(txAccess(tx), tx)); }}withAudit positionne le GUC PostgreSQL sim.id_utilisateur dans une transaction :
export async function withAudit<T>( db: Kysely<Database>, idUtilisateur: bigint | string | null, work: (tx: Kysely<Database>) => Promise<T>,): Promise<T> { return db.transaction().execute(async (tx) => { if (idUtilisateur != null) { await sql`SELECT set_config('sim.id_utilisateur', ${String(idUtilisateur)}, true)`.execute(tx); } return work(tx); });}Cela rend l’identifiant de l’acteur disponible aux triggers audit.journal_audit sans fuiter entre connexions.
Gouvernance du CRUD
Section intitulée « Gouvernance du CRUD »DbAccess fournit une surface figée :
findById/findOne/findMany/exists/countinsert/insertManyupdate/delete
findMany n’accepte qu’une map d’égalité + orderBy + limit + offset. Au-delà, on écrit un tag sql ponctuel ou une méthode de repository de domaine.
insertInto, updateTable et deleteFrom sont typés contre TableKeys, pas keyof Database. Les vues sont en lecture seule à la compilation.
Types générés
Section intitulée « Types générés »src/db/generated.ts expose :
| Type | Signification | Exemple |
|---|---|---|
BigIntStr |
bigint en chaîne exacte |
id as BigIntStr |
Money |
montants FCFA NUMERIC(12,2) |
montant as Money |
DecimalQuantity |
quantités NUMERIC(10,2) |
qty as DecimalQuantity |
DateStr |
date naïve en chaîne |
d('2026-10-31') |
TimestampStr |
timestamp naïf en chaîne |
new Date().toISOString() as TimestampStr |
TimeStr |
time naïf en chaîne |
— |
Json |
JSON / JSONB |
unknown |
Database mappe les noms schema.table vers des interfaces de tables et de vues. ViewKeys liste les vues en lecture seule ; TableKeys les exclut.
Échappatoires
Section intitulée « Échappatoires »- Les valeurs sont toujours liées (
sql${value}`). - Les identifiants utilisent
sql.ref. sql.rawest réservé au générateur (dbgen/dbgen.mjs).
Colonnes possédées par les triggers
Section intitulée « Colonnes possédées par les triggers »dbgen analyse statiquement les corps de triggers et marque les colonnes affectées ins: never / upd: never. Si un trigger est trop opaque, il exige une déclaration explicite dans dbgen/dbgen.metadata.json.
Invariant db:check
Section intitulée « Invariant db:check »Le generated.ts commité doit correspondre au catalogue PostgreSQL migré :
PostgreSQL frais → Flyway migrate → pnpm db:gen → git diff --exit-code → tsc --noEmit → testspnpm db:verify est l’application complète de cet invariant. À exécuter systématiquement après un changement de schéma.
