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 } from '@fougere/core';
import type { EntityOrm } from '@fougere/core';
import Post from '../entities/Post.js';
import Author from '../entities/Author.js';
/** Alias typé — la DI résout par nom de TYPE, et `AuthorOrm` est la clé du container. */
type AuthorOrm = EntityOrm<Author>;
export default class PostPresenter extends Presenter(Post) {
constructor(private authorOrm: AuthorOrm) { 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 ids = [...new Set(posts.map((p) => p.authorId))];
const authors = await Promise.all(ids.map((id) => this.authorOrm.findById(id)));
const byId = new Map(authors.filter(Boolean).map((a) => [a!.id, a!.name]));
return posts.map((post) => byId.get(post.authorId) ?? '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 ORM, puissent être injectées.
Application aux différentes surfaces
Un champ calculé est ajouté à la sortie de l'entité sur les quatre surfaces : l'enveloppe
(useQuery, useCommand, invoke), le catch-all REST, un hôte REST 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.