Implementation adapters/

Entités TypeORM, mappers, repositories concrets et classe Build

Bootcode IWA-S04 — Semaine 21, Jour 3

Objectifs du jour

1. Créer les entités TypeORM pour chaque agrégat

Classes avec décorateurs @Entity, @Column — la persistance

2. Implémenter les mappers toDomain() et toEntity()

Conversion bidirectionnelle domaine ↔ persistance

3. Implémenter les repositories InMemory fonctionnels

Pour tester sans PostgreSQL

4. Créer la classe Build qui câble tout le domaine

Enregistre les bind() dans le container Inversify

Plan du cours

1

Rappel : la structure adapters/

repositories postgres, mappers, gateways, Build

2

Tips : l'ordre d'implémentation

Entités TypeORM → mappers → repositories

3

Créer la classe Build

Enregistrer tout dans le container Inversify

4

Circuler et aider

C'est le jour le plus technique du projet

La structure adapters/

La couche infrastructure — implémente les interfaces de core/

# src/adapters/

entities/

BookEntity.ts # @Entity, @Column

LoanEntity.ts

mappers/

BookMapper.ts # toDomain() / toEntity()

repositories/

PostgresBookRepository.ts # implémente BookRepository

InMemoryBookRepository.ts

Build.ts # câble le container Inversify

💡 adapters/ implémente les interfaces définies dans core/. Le domaine ne sait pas comment il est persisté.

L'ordre d'implémentation

1️⃣

Les entités TypeORM

Schéma de persistance — colonnes, types, relations

2️⃣

Les mappers

Conversion Entity ↔ agrégat du domaine

3️⃣

Les repositories

Implémentent l'interface avec TypeORM (ou InMemory)

4️⃣

La classe Build

bind() de tout dans le container Inversify

Une entité TypeORM

Simple classe avec décorateurs — c'est juste le schéma de la table

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

@Entity({ name: 'books' })

export class BookEntity {

@PrimaryColumn()

id: string;

@Column()

isbn: string;

@Column()

title: string;

@Column({ default: false })

isBorrowed: boolean;

}

💡 L'entité TypeORM n'a aucune logique métier. Pas de méthode borrow() ici — juste des colonnes.

Entity TypeORM vs agrégat du domaine

📦 BookEntity (adapters/)

  • Schéma de persistance
  • Décorateurs TypeORM
  • Pas de logique métier
  • Colonnes = types SQL
  • Représente une ligne de table

🧠 Book (core/)

  • Logique métier
  • init/restore, borrow(), return()
  • Produit des événements
  • Value objects typés
  • Protège les invariants

⚠️ Ne confondez pas les deux. L'Entity sert à persister, l'agrégat sert à raisonner. Le mapper fait le pont.

Le mapper bidirectionnel

Convertit dans les deux sens : Entity → agrégat et agrégat → Entity

export class BookMapper {

// Persistance → Domaine

static toDomain(entity: BookEntity): Book {

const events = [new BookAdded(...)];

if (entity.isBorrowed) events.push(new BookBorrowed(...));

return Book.restore(events);

}

// Domaine → Persistance

static toEntity(book: Book): BookEntity {

const e = new BookEntity();

e.id = book.id.value;

e.isbn = book.isbn.value;

e.isBorrowed = book.isBorrowed;

return e;

}

}

Mapper incomplet vs complet

❌ Mapper incomplet

static toEntity(book) {

e.id = book.id;

e.isbn = book.isbn;

// isBorrowed oublié !

}

Au prochain load, le livre apparaît comme disponible alors qu'il est emprunté. Bug silencieux.

✅ Mapper complet

static toEntity(book) {

e.id = book.id.value;

e.isbn = book.isbn.value;

e.title = book.title;

e.isBorrowed = book.isBorrowed;

}

Tous les champs sont mappés. L'état est fidèle après un cycle save/load.

⚠️ Test systématique : save → load → vérifier que l'agrégat reconstruit a le même état que l'original.

PostgresBookRepository

Implémente l'interface de core/ avec TypeORM + mapper

@injectable()

export class PostgresBookRepository implements BookRepository {

constructor(@inject('DataSource') private ds: DataSource) {}

async findById(id: BookId): Promise<Book | null> {

const entity = await this.ds

.getRepository(BookEntity)

.findOne({ where: { id: id.value } });

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

}

async save(book: Book): Promise<void> {

const entity = BookMapper.toEntity(book);

await this.ds.getRepository(BookEntity).save(entity);

}

}

La classe Build

Câble tout le domaine dans le container Inversify — un Build par bounded context

import { Container } from 'inversify';

export class LibraryBuild {

static build(container: Container): void {

// Repositories

container.bind('BookRepository').to(InMemoryBookRepository);

container.bind('LoanRepository').to(InMemoryLoanRepository);

// Use cases

container.bind('AddBook').to(AddBook);

container.bind('BorrowBook').to(BorrowBook);

container.bind('ReturnBook').to(ReturnBook);

container.bind('ListOverdueLoans').to(ListOverdueLoans);

}

}

💡 Pour passer en PostgreSQL demain, changez InMemoryBookRepository en PostgresBookRepository — une seule ligne.

Build qui oublie des use cases

❌ Build incomplet

container.bind('BookRepository')

.to(InMemoryBookRepository);

container.bind('AddBook').to(AddBook);

container.bind('BorrowBook').to(BorrowBook);

// ReturnBook oublié !

Au runtime : ContainerNotFoundError quand le controller demande ReturnBook.

✅ Build complet

// Tous les use cases sont bindés

container.bind('AddBook').to(AddBook);

container.bind('BorrowBook').to(BorrowBook);

container.bind('ReturnBook').to(ReturnBook);

container.bind('ListOverdueLoans').to(ListOverdueLoans);

Checklist : un bind() par use case défini dans core/usecases/.

Pièges courants

❌ Se perdre dans la configuration TypeORM

Gardez les entités simples. Pas de relations complexes pour ce projet — un @Column par champ suffit.

⚠️ Mapper incomplet

Oublier de mapper un champ = état corrompu après reload. Testez le cycle save/load.

❌ Build qui oublie des use cases

Vérifiez que chaque use case de core/ a son bind() dans Build. Sinon : erreur au runtime.

⚠️ Mettre de la logique métier dans l'Entity

L'Entity TypeORM ne contient que des colonnes. La logique reste dans l'agrégat du domaine.

À retenir !

✅ Les entités TypeORM sont de simples classes avec @Entity, @Column — aucune logique métier.

✅ Le Mapper convertit dans les deux sens : toDomain() et toEntity().

✅ Le Build reçoit le container et fait les bind()un Build par bounded context.

✅ Les repositories in-memory suffisent pour tester — pas besoin de PostgreSQL pour le projet.

Demain : controllers REST + tests + intégration 🚀