Presenters
Un presenter ajoute des champs calculés à la sortie d'une entité. Chaque méthode définit un champ. Elle reçoit toutes les lignes de la réponse et renvoie une valeur par ligne.
import { Presenter, type RepositoryOf } from '@fougere/core';
import Post from '../entities/Post.js';
import Author from '../entities/Author.js';
export default class PostPresenter extends Presenter(Post) {
constructor(private authors: RepositoryOf<Author>) { super(); }
excerpt(posts: Post[]): string[] {
return posts.map((post) => post.body.slice(0, 200));
}
/** Une lecture pour la page, pas une par ligne — c'est pourquoi la page est l'argument. */
async authorName(posts: Post[]): Promise<string[]> {
const authors = await this.authors.findByKeys(posts.map((post) => post.authorId));
return posts.map((post) => authors.get(post.authorId)?.name ?? 'Anonymous');
}
}
Placez la classe dans le dossier presenters/ de la Frond. Le scan l'enregistre sous le
nom PostPresenter. Les méthodes peuvent être synchrones ou asynchrones, et les
dépendances du constructeur sont résolues par type.
Pourquoi ce n'est pas un champ de l'entité
excerpt est calculé depuis body et n'a pas besoin d'être stocké. authorName vient
d'une autre entité ; le calculer à la lecture évite de dupliquer cette valeur lors d'un
renommage d'auteur.
Utilisez un presenter lorsque le calcul demande des I/O, une autre entité ou les deux. Il
s'agit d'une classe afin que ses dépendances, notamment un repository, puissent être
injectées. L'injection directe d'Storage est refusée au boot, comme pour les handlers et
les collectors.
Application aux différentes surfaces
Un champ calculé est ajouté à la sortie de l'entité sur chaque porte : l'enveloppe
(useQuery, useCommand, invoke), REST — monté en catch-all ou en hôte standalone — et
GraphQL. Aucune configuration supplémentaire n'est nécessaire.
const { items } = await useQuery<Post>(Post, 'list');
items[0].excerpt; // ← présent, exactement comme dans une requête GraphQL
L'enrichissement est maintenant appliqué dans la façade commune. Les versions précédentes l'appliquaient séparément dans les projections REST et GraphQL, et pas dans
useQuery.
Où ça s'arrête : une vue nommée
Lorsqu'une opération nomme une vue de sortie, seuls les champs de cette vue sont renvoyés. Un champ calculé qui n'y figure pas est exclu :
Crud(Post, { list: PostCard }) // list émet PostCard, et rien que PostCard
Sans vue nommée, les champs calculés s'ajoutent à la sortie de l'entité. L'axe boundary continue de s'appliquer aux champs de l'entité.
Ce que ce n'est pas
- Pas un sérialiseur. Ce qui peut sortir du tout est l'axe
boundary; ce qu'une audience donnée voit est une vue. Un presenter ne fait qu'ajouter. - Pas un endroit pour les règles métier. Un champ calculé est une lecture. Une
transition (
publish) est une opération, pas une écriture de champ. - Pas automatiquement optimisé. Une méthode reçoit toute la page, ce qui permet de
regrouper les lectures. Un
findByIddans chaque itération produira toujours N lectures. - Pas d'échec silencieux. Une erreur dans un champ calculé produit
INTERNAL_ERRORen indiquant le champ concerné.
Suite : Collectors — résoudre les paramètres par type.