Entités TypeORM, mappers, repositories concrets et classe Build
Bootcode IWA-S04 — Semaine 21, Jour 3
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
Rappel : la structure adapters/
repositories postgres, mappers, gateways, Build
Tips : l'ordre d'implémentation
Entités TypeORM → mappers → repositories
Créer la classe Build
Enregistrer tout dans le container Inversify
Circuler et aider
C'est le jour le plus technique du projet
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é.
Les entités TypeORM
Schéma de persistance — colonnes, types, relations
Les mappers
Conversion Entity ↔ agrégat du domaine
Les repositories
Implémentent l'interface avec TypeORM (ou InMemory)
La classe Build
bind() de tout dans le container Inversify
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.
📦 BookEntity (adapters/)
🧠 Book (core/)
⚠️ Ne confondez pas les deux. L'Entity sert à persister, l'agrégat sert à raisonner. Le mapper fait le pont.
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
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.
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);
}
}
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 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/.
❌ 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.
✅ 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 🚀