Le pattern Mapper

Traducteur entre la BDD et le domaine

Bootcode IWA-S04 — Semaine 16, Jour 3

Objectifs de la leçon

1. Créer une entité TypeORM

Séparée de l'objet métier

2. Écrire un Mapper

Avec toDomain() et fromDomain()

3. Utiliser le Mapper

Dans le Repository PostgreSQL

4. Vérifier le round-trip

objet → entité → objet = identique

Plan du cours

1

Deux représentations

Objet métier vs entité BDD

2

Pourquoi séparer ?

Nommage, colonnes techniques, décorateurs

3

Le Mapper : toDomain() et fromDomain()

Traducteur bidirectionnel

4

Le Mapper vit dans l'infra

À côté des entités TypeORM

5

Seul le Repository PostgreSQL l'utilise

Le use case ne voit jamais l'entité

Module 1

Deux représentations

Objet métier ≠ entité BDD

Le mĂŞme concept, deux mondes

🧠 Objet métier (Domain)

  • • camelCase : createdAt
  • • Pas de dĂ©corateurs
  • • Types simples
  • • Logique mĂ©tier incluse

💾 Entité TypeORM (Infra)

  • • snake_case : created_at
  • • DĂ©corateurs @Column
  • • Colonnes techniques (timestamps)
  • • Liens vers d'autres tables

💡 Elles se ressemblent, mais servent des buts différents

Le domaine pense "métier", la BDD pense "stockage"

L'objet métier (Domain)

// domain/models/Article.ts

export interface Article {

id: string;

title: string;

content: string;

authorId: string;

createdAt: Date;

}

💡 Propre, simple, aucun décorateur

camelCase, types clairs — c'est ce que le use case manipule

L'entité TypeORM (Infra)

// infra/entities/ArticleEntity.ts

import { Entity, Column, PrimaryColumn } from "typeorm";

@Entity({ name: "articles" })

export class ArticleEntity {

@PrimaryColumn()

id: string;

@Column()

title: string;

@Column({ type: "text" })

content: string;

@Column({ name: "author_id" })

authorId: string;

@Column({ name: "created_at", type: "timestamp" })

createdAt: Date;

}

💡 Décorateurs, snake_case dans la BDD, types SQL

C'est ce que TypeORM utilise pour parler Ă  PostgreSQL

Module 2

Pourquoi séparer ?

Trois raisons concrètes

Raison 1 : Le nommage

đź§  Domaine (camelCase)

createdAt

authorId

isPublished

đź’ľ BDD (snake_case)

created_at

author_id

is_published

đź’ˇ Convention JavaScript vs convention SQL

Le Mapper fait la conversion automatiquement

Raison 2 : Colonnes techniques

L'entité TypeORM a des colonnes que le domaine ne veut PAS voir

// Entité TypeORM — colonnes techniques

@Column({ name: "updated_at" })

updatedAt: Date; // géré par la BDD

@Column({ name: "version" })

version: number; // optimistic locking

@Column({ name: "deleted_at", nullable: true })

deletedAt: Date | null; // soft delete

✅ Le Mapper cache ces détails au domaine

L'objet métier ne contient que ce qui compte pour la logique

Raison 3 : Pas de décorateurs dans le domaine

❌ Entité TypeORM dans le domaine

@Entity // ❌

@Column // ❌

import { Entity } from "typeorm" // ❌

Le domaine dépend de TypeORM !

✅ Objet métier pur

export interface Article {

id: string;

title: string;

}

Aucune dépendance technique !

Module 3

Le Mapper

toDomain() et fromDomain()

Le Mapper : un traducteur bidirectionnel

Deux méthodes, deux sens

toDomain()

BDD → Domaine (lecture)

Entité TypeORM → Objet métier

fromDomain()

Domaine → BDD (écriture)

Objet métier → Entité TypeORM

đź’ˇ "to" = vers quoi je convertis

toDomain = je convertis VERS le domaine

ArticleMapper en TypeScript

// infra/mappers/ArticleMapper.ts

import { ArticleEntity } from "../entities/ArticleEntity";

import { Article } from "../../domain/models/Article";

export class ArticleMapper {

// BDD → Domaine (lecture)

static toDomain(entity: ArticleEntity): Article {

return {

id: entity.id,

title: entity.title,

content: entity.content,

authorId: entity.authorId,

createdAt: entity.createdAt,

};

}

// Domaine → BDD (écriture)

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;

return entity;

}

}

Le Mapper dans le Repository

// infra/repositories/PostgresArticleRepository.ts

import { ArticleMapper } from "../mappers/ArticleMapper";

async save(article: Article): Promise<void> {

// Domaine → BDD

const entity = ArticleMapper.fromDomain(article);

await this.db.getRepository(ArticleEntity).save(entity);

}

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

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

// BDD → Domaine

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

}

đź’ˇ Le Repository est le SEUL Ă  utiliser le Mapper

Le use case reçoit/tire des objets métier — jamais d'entités

Module 4

Le round-trip

Vérifier que la conversion est sans perte

Tester le round-trip

// Test : objet → entité → objet = identique

test("round-trip Article", () => {

const original: Article = {

id: "123",

title: "Mon article",

content: "Contenu",

authorId: "auth-1",

createdAt: new Date(),

};

// Domaine → BDD → Domaine

const entity = ArticleMapper.fromDomain(original);

const result = ArticleMapper.toDomain(entity);

expect(result).toEqual(original); // âś…

});

💡 Si le round-trip échoue, vous perdez des données !

Vérifiez que chaque champ est converti dans les deux sens

Pièges courants à éviter

❌ Utiliser l'entité TypeORM directement dans le use case

✅ Le use case ne voit QUE l'objet métier — le Mapper convertit

❌ Oublier de convertir snake_case ↔ camelCase

✅ Le Mapper gère la conversion — created_at ↔ createdAt

❌ Confondre toDomain et fromDomain

âś… "to" = vers quoi je convertis. toDomain = VERS le domaine

L'entité TypeORM dans le use case ?

❌ L'erreur

// domain/usecases/GetArticle.ts

import { ArticleEntity } from

"typeorm" // ❌

async execute(id) {

const entity = await

repo.findById(id);

return entity.title; // ❌

}

Le domaine dépend de TypeORM !

âś… La solution

// domain/usecases/GetArticle.ts

import { Article } from

"../models/Article" // âś…

async execute(id) {

const article: Article =

await repo.findById(id);

return article.title; // âś…

}

Le domaine reste pur !

Points clés à retenir

📌 L'entité TypeORM ≠ l'objet métier

📌 toDomain = BDD → domaine, fromDomain = domaine → BDD

📌 Le Mapper vit dans infra/, à côté des entités

📌 Le use case ne voit JAMAIS l'entité TypeORM

Ă€ retenir !

🔄

Mapper = traducteur

Entre BDD et domaine

📥

toDomain = lecture

BDD → objet métier

📤

fromDomain = écriture

Objet métier → BDD

đźš«

Use case = jamais d'entité

Que des objets métier

Questions ?

Créez votre Mapper et testez le round-trip

Demain : Le Use Case — une classe par action