Bootcode · IWA-S04 · Semaine 14 · Jour 5

TypeORM Relations
& Mapper

Le climax architectural du programme

Aujourd'hui, tout ce qu'on a construit depuis la semaine 10 converge vers un seul moment : changer UNE ligne de code et voir toute l'architecture tenir.

Objectifs de la leçon

1. Implémenter Repository<Article> avec PostgreSQL

CrĂ©er un PostgresArticleStorage qui respecte la mĂȘme interface.

2. Écrire un Mapper

Convertir entre ArticleEntity (TypeORM) et Article (domaine).

3. Constater que la logique métier ne change pas

Changer d'implĂ©mentation sans toucher au cƓur de l'application.

4. Comprendre la Dependency Inversion

Le D de SOLID, par la pratique — pas juste en thĂ©orie.

Plan du cours

1

LE MOMENT

Résumé de tout ce qui a mené ici : S10 interfaces, S11 injection, S13 API, S14 SQL/TypeORM

2

Le Mapper

Convertir entre ArticleEntity et Article — toDomain / fromDomain

3

PostgresArticleStorage

Implémenter Repository<Article> avec TypeORM + Mapper

4

LA DÉMO EN LIVE

Changer UNE SEULE LIGNE dans main.ts — InMemory → PostgreSQL

5

Constater

Routes, service, logique — RIEN n'a changĂ©

6

Dependency Inversion

Nommer ce qu'on fait : le D de SOLID, Clean Architecture

Module 1

LE MOMENT

Tout ce qu'on a construit depuis 4 semaines converge ici

Prenez une seconde. Respirez. Aujourd'hui, vous allez comprendre pourquoi on a fait tout ça.

Le chemin qui mĂšne ici

S10

Interfaces & Contrats

On a dĂ©fini Repository<Article> — une promesse, pas une implĂ©mentation

S11

Injection de dépendances

On a injectĂ© l'interface, pas la classe — le service ne sait pas qui fait le travail

S13

API REST & Express

Les routes utilisent le service, qui utilise l'interface — la chaüne est complùte

S14

SQL, PostgreSQL & TypeORM

J1–J4 : on a appris Ă  parler Ă  une vraie base de donnĂ©es

Et maintenant


AUJOURD'HUI

On relie les deux bouts.

L'interface créée en S10 + la DB apprise en S14 = PostgresArticleStorage

📄

Interface

Le contrat

🔄

Mapper

Le traducteur

🐘

PostgreSQL

L'implémentation

-->

Rappel : l'interface Repository<Article>

C'est le contrat qu'on a écrit en S10. Il dit QUELLES méthodes existent, pas COMMENT elles fonctionnent.

export interface Repository<T> {

getAll(): Promise<T[]>;

getById(id: string): Promise<T | null>;

create(item: Omit<T, "id">): Promise<T>;

update(id: string, item: Partial<T>): Promise<T | null>;

delete(id: string): Promise<boolean>;

}

💡 Cette interface ne mentionne ni SQL, ni tableau, ni base de donnĂ©es. C'est volontaire.

Rappel : InMemoryArticleStorage

Notre implĂ©mentation actuelle — les donnĂ©es vivent dans un tableau en mĂ©moire.

export class InMemoryArticleStorage implements Repository<Article> {

private articles: Article[] = [];

async getAll(): Promise<Article[]> {

return this.articles;

}

async getById(id: string): Promise<Article | null> {

return this.articles.find(a => a.id === id) ?? null;

}

// ... create, update, delete

}

⚠ ProblĂšme : au redĂ©marrage du serveur, toutes les donnĂ©es disparaissent.

Module 2

Le Mapper

Le traducteur entre deux mondes

ArticleEntity

Le monde TypeORM

Décorateurs, colonnes SQL, relations

Conçu pour la base de données

Article

Le monde du domaine

Objet pur, pas de décorateur

Conçu pour la logique métier

Mapper : toDomain()

Convertir une ArticleEntity (TypeORM) en Article (domaine)

export class ArticleMapper {

static toDomain(entity: ArticleEntity): Article {

return new Article(

entity.id,

entity.title,

entity.content,

entity.authorId,

entity.createdAt,

entity.updatedAt,

);

}

}

💡 Le domaine reçoit un objet propre, sans aucune trace de TypeORM.

Mapper : fromDomain()

Convertir un Article (domaine) en ArticleEntity (TypeORM)

export class ArticleMapper {

// ... toDomain()

static fromDomain(article: Article): ArticleEntity {

const entity = new ArticleEntity();

entity.id = article.id;

entity.title = article.title;

entity.content = article.content;

entity.authorId = article.authorId;

entity.createdAt = article.createdAt;

entity.updatedAt = article.updatedAt;

return entity;

}

}

🔄 Deux directions, une seule classe. Le Mapper est le gardien de la frontiùre.

Module 3

PostgresArticleStorage

Implémenter Repository<Article> avec TypeORM

La mĂȘme interface. La mĂȘme signature. Mais cette fois, les donnĂ©es persistent dans PostgreSQL.

PostgresArticleStorage : getAll & getById

export class PostgresArticleStorage implements Repository<Article> {

constructor(private repo: Repository<ArticleEntity>) {}

async getAll(): Promise<Article[]> {

const entities = await this.repo.find();

return entities.map(e => ArticleMapper.toDomain(e));

}

async getById(id: string): Promise<Article | null> {

const entity = await this.repo.findOneBy({ id });

return entity ? ArticleMapper.toDomain(entity) : null;

}

}

🔑 On rĂ©cupĂšre des ArticleEntity, on renvoie des Article. Le Mapper fait la traduction.

PostgresArticleStorage : create, update, delete

async create(data: Omit<Article, "id">): Promise<Article> {

const entity = ArticleMapper.fromDomain(new Article(crypto.uuid(), ...data));

const saved = await this.repo.save(entity);

return ArticleMapper.toDomain(saved);

}

async update(id: string, data: Partial<Article>): Promise<Article | null> {

const entity = await this.repo.findOneBy({ id });

if (!entity) return null;

Object.assign(entity, data);

const saved = await this.repo.save(entity);

return ArticleMapper.toDomain(saved);

}

async delete(id: string): Promise<boolean> {

const result = await this.repo.delete(id);

return result.affected > 0;

}

⚡ Pattern : Entity → Mapper.fromDomain() → save → Mapper.toDomain() → Article

Module 4

LA DÉMO
EN LIVE

Le moment oĂč tout bascule

Ouvrez votre éditeur. On va changer UNE SEULE LIGNE.

main.ts — AVANT (InMemory)

import { InMemoryArticleStorage } from "./adapters/InMemoryArticleStorage";

import { ArticleService } from "./core/ArticleService";

// Composition root

const storage = new InMemoryArticleStorage();

const service = new ArticleService(storage);

// Routes

app.get("/articles", (req, res) => {

const articles = await service.getAll();

res.json(articles);

});

❌ Les donnĂ©es disparaissent au redĂ©marrage

main.ts — APRÈS (PostgreSQL)

// ❌ import { InMemoryArticleStorage } from "./adapters/InMemoryArticleStorage";

import { PostgresArticleStorage } from "./adapters/PostgresArticleStorage";

import { ArticleService } from "./core/ArticleService";

// Composition root

const storage = new PostgresArticleStorage(articleRepo);

const service = new ArticleService(storage);

// Routes — IDENTIQUES

app.get("/articles", (req, res) => {

const articles = await service.getAll();

res.json(articles);

});

UNE SEULE LIGNE A CHANGÉ

InMemoryArticleStorage → PostgresArticleStorage

Et maintenant, les donnĂ©es persistent. 🎉

Module 5

Constater

VĂ©rifions ce qui a changé  et ce qui n'a PAS changĂ©

Qu'est-ce qui n'a PAS changé ?

đŸ›Łïž

Routes

articleRoutes.ts

✅ Identique

⚙

Service

ArticleService.ts

✅ Identique

📩

Domaine

Article.ts

✅ Identique

📄

Interface

Repository<Article>

✅ Identique

đŸ§Ș

Tests

ArticleService.test.ts

✅ Identique

🔌

main.ts

Composition root

⚡ 1 ligne changĂ©e

La seule chose qui change : l'adaptateur

❌ Avant

const storage =

new InMemoryArticleStorage();

DonnĂ©es en mĂ©moire → perdues au redĂ©marrage

✅ Aprùs

const storage =

new PostgresArticleStorage(repo);

DonnĂ©es en PostgreSQL → persistent toujours

Le service, les routes, le domaine, les tests — RIEN n'a bougĂ©.

C'est la preuve vivante que l'architecture tient.

Module 6

Dependency Inversion

Nommer ce qu'on vient de faire

Ce n'est pas juste un pattern. C'est un principe fondateur. Le D de SOLID.

SOLID — Le D est pour Dependency Inversion

S

Single Responsibility

O

Open/Closed

L

Liskov Substitution

I

Interface Segregation

D

Dependency Inversion

❌ Sans Inversion

Le service dépend d'une implémentation concrÚte (InMemoryArticleStorage)

→ Changer de stockage = réécrire le service

✅ Avec Inversion

Le service dépend d'une abstraction (Repository<Article>)

→ Changer de stockage = changer 1 ligne

💡 Les modules de haut niveau ne doivent pas dĂ©pendre des modules de bas niveau. Les deux doivent dĂ©pendre d'abstractions.

Clean Architecture : core vs adapters

📩 core/

Le cƓur de l'application

Article.ts — domaine

Repository.ts — interface

ArticleService.ts — logique

Ne connaßt PAS la base de données

🔌 adapters/

Les implémentations

InMemoryArticleStorage.ts

PostgresArticleStorage.ts

ArticleMapper.ts

ArticleEntity.ts

ConnaĂźt le core ET la DB

📁 C'est exactement la structure de packages/iac/core/ vs packages/iac/adapters/ dans le codebase Bootcode.

⚠ PiĂšge : rater le moment

❌ Passer trop vite

« Bon, on change l'implémentation, next. »

→ Les Ă©tudiants ne rĂ©alisent pas l'importance de ce qu'ils viennent de faire

✅ Prendre le temps

« Regardez. UNE ligne. Et TOUT fonctionne encore. C'est CA, l'architecture. »

→ Laisser le silence s'installer. Laisser la rĂ©vĂ©lation faire son effet.

C'est LE jour le plus important du programme. Prenez le temps.

⚠ PiĂšge : interfaces mal construites

❌ Interface couplĂ©e

interface ArticleStorage {

query(sql: string): any;

}

L'interface fuit l'implĂ©mentation SQL → impossible de changer

✅ Interface propre

interface Repository<T> {

getAll(): Promise<T[]>;

}

L'interface exprime le QUOI, pas le COMMENT → interchangeable

💡 Les Ă©tudiants qui ont mal fait les interfaces en S10 auront du mal ici — aider individuellement.

⚠ PiĂšge : « le Mapper, c'est relou »

❌ Raccourci dangereux

Utiliser ArticleEntity partout dans le service et les routes

→ Le domaine est polluĂ© par les dĂ©corateurs TypeORM

→ Impossible de changer de DB sans tout réécrire

✅ L'investissement Mapper

Un fichier de plus, mais une frontiĂšre propre

→ Le domaine reste pur et testable

→ Changer d'ORM devient possible

Le Mapper est un investissement qui paie. Aujourd'hui vous le voyez.

À retenir !

Le jour le plus important du programme

1. Interface = contrat

Repository<Article> dit QUOI, pas COMMENT

2. Mapper = frontiĂšre

toDomain / fromDomain séparent les mondes

3. Adapter = bouchon

InMemory ou PostgreSQL, le service s'en fiche

4. 1 ligne = tout change

La composition root est le seul point de branchement

5. Dependency Inversion = le D de SOLID

DĂ©pendre d'abstractions, pas d'implĂ©mentations — vous l'avez VU aujourd'hui

La phrase du jour

« Changer d'implĂ©mentation sans toucher Ă  la logique mĂ©tier, c'est pas de la magie — c'est de l'architecture. »

📄

Interface

Le contrat

🔄

Mapper

Le traducteur

🔌

Adapter

L'implémentation

Questions ?

Est-ce que tout est clair ? C'est normal si c'est dense — on en reparle.

N'hésitez pas à revoir le diff en live : seule l'instanciation change, tout le reste est identique.

Exercices

1. Implémenter PostgresArticleStorage

Créer le fichier, implémenter les 5 méthodes de Repository<Article> avec TypeORM + Mapper.

2. Basculez en live

Changer la ligne dans main.ts. Redémarrer le serveur. Vérifier que les données persistent.

3. Vérifiez le diff

Comparer les fichiers modifiés. Seul main.ts a changé ? Si non, pourquoi ?

4. Bonus : ajouter une relation

Ajouter un AuthorEntity avec une relation OneToMany vers ArticleEntity. Mettre Ă  jour le Mapper.