Le socle
Le socle d'une application Fougere n'est pas ce que le framework expédie. C'est l'entité que vous avez écrite.
Tout ce que Fougere vous rend est indexé sur cette déclaration, et cela prend trois formes. Les connaître suffit à prédire où va le schéma dans une API que vous n'avez jamais vue : entre les parenthèses d'une classe dont vous héritez, dans le type d'une valeur que vous recevez, ou en premier argument d'un appel que vous écrivez.
Trois formes, un seul paramètre
| Forme | Ce que vous écrivez | Exemples |
|---|---|---|
| une classe dont vous héritez | extends Crud(Post) | entity, Crud, Presenter, Collector, Repository, Mirror |
| une valeur que vous recevez | constructor(posts: RepositoryOf<Post>) | RepositoryOf<E>, Emit<F>, Fact<F>, Facade<H> |
| un appel que vous écrivez | useQuery(Post, 'list') | useQuery, useCommand, useFormFor |
Le paramètre est toujours quelque chose que vous avez déclaré vous-même — le plus souvent une entité, parfois un fait ou un handler. Rien dans cette colonne n'est une chaîne, un nom ou une clé enregistrée ailleurs : renommez la classe et le compilateur suit chacun de ses lecteurs.
Une classe dont vous héritez
Elles sont six. Ce qui varie n'est pas leur nature mais la quantité que votre déclaration
détermine — Collector(User) ne peut presque rien dériver de « je résous un User », là où
Crud(Post) dérive cinq opérations entières de la table de Post.
| Les parenthèses tiennent | Ce dont vous héritez | Ce que vous écrivez | |
|---|---|---|---|
entity({…}) | les champs | le type, le juge, les métadonnées | la carte des champs |
Crud(Post) | une entité | cinq opérations | rien, ou celles que vous redéfinissez |
Presenter(Post) | une entité | la cible que le scan lit | une méthode par champ calculé |
Collector(User) | n'importe quelle classe | la cible que le scan lit | collect(ctx) |
Repository(Reading) | une entité, ou les plusieurs qu'il possède | tous les gestes du port gardé — ou aucun, à partir de deux | les requêtes que vous nommez |
Mirror(BookCard) | une forme | la boucle, l'âge, le juge, l'écriture | pull() |
Seule la première est obligatoire. Un handler qui n'étend rien est un handler ordinaire ; un repository que vous n'écrivez jamais se résout quand même. Là où hériter est la déclaration — presenters, collectors — la classe de base porte une cible et aucun comportement.
Chacune est documentée là où elle sert, et les liens ci-dessus y mènent. Une seule n'a pas de page à elle, parce qu'elle n'appartient à aucune étape en particulier : c'est le socle lui-même.
Mirror — une copie locale de lignes qu'on ne peut pas interroger
Une vraie base de données s'interroge là où elle est. Mirror(Shape) est la réponse pour
une source sans algèbre : une API HTTP, le catalogue d'un partenaire, une Frond derrière un
fil. La sous-classe fournit une seule chose, le tirage :
// services/PartnerCatalog.ts
export default class PartnerCatalog extends Mirror(BookCard) {
async *pull(since?: Date) {
for (let page = 0; page !== null; ) {
const body = await fetch(`${api}?page=${page}&since=${since?.toISOString() ?? ''}`);
const { items, next } = await body.json();
yield items.map(toCard);
page = next;
}
}
}
Tout ce qu'il y a autour est identique pour tous les miroirs et vit dans la classe de
base : refresh() lit la ligne de flottaison, tire à partir de là, juge chaque page et
l'écrit. La forme doit porter un champ updated() — une copie
incapable de dire quand elle a été tirée se lit exactement comme des lignes vivantes — et
Mirror refuse à la construction sinon.
Une page est une entrée client : elle rencontre le même juge que n'importe quelle autre, et
strictement — sans quoi un partenaire qui renomme label écrit des null en silence. Une
page refusée arrête la passe ; ce que les pages précédentes ont écrit reste, parce qu'un
upsert est idempotent et que la passe suivante repart de la ligne de flottaison.
Il ne se planifie pas lui-même. Un hook de démarrage, un cron, une opération derrière une porte sont tous légitimes et aucun n'est l'affaire de cette classe.
Mirror n'a pas encore de dossier à lui dans le vocabulaire d'une Frond — contrairement à
presenters/ et collectors/, un dossier mirrors/ n'est pas scanné. Placez-le là où les
providers sont lus (services/ ou repositories/). En tant que détenteur du stockage, il
reçoit l'Storage sur lequel son prefab a été construit ; handlers, presenters et
collectors n'ont pas cette exception.Une valeur que vous recevez
La deuxième forme est un paramètre de constructeur, résolu par type — jamais par nom de paramètre, jamais par une clé en chaîne :
export default class PostHandler {
constructor(
private posts: RepositoryOf<Post>,
private announce: Emit<PostPublished>,
) {}
}
RepositoryOf<E> est la forme du repository par défaut : il transmet le port gardé et se
résout même sans fichier repository. Injecter directement Storage<E> dans un handler,
un presenter ou un collector est refusé au boot ; l'accès au stockage se nomme par un
repository. Emit<F> annonce
un fait, et accepter un Fact<F> est l'abonnement — pas de topic, pas d'appel
d'enregistrement. Facade<H> est
la seule dont le paramètre n'est pas une entité : elle nomme le handler d'une autre Frond,
qui reste une déclaration de vous.
Un appel que vous écrivez
La troisième forme est côté client, et l'entité est le premier argument :
const { items } = await useQuery(Post, 'list');
const publish = useCommand(Post, 'publish');
const form = useFormFor(Post, { op: 'update', params: { id } });
Même désignation que côté serveur : le juge local est le juge distant, et un contrat de formulaire n'est pas une seconde déclaration de l'entité.
Une exception mérite d'être dite : useCurrentUser() ne prend
aucun argument. Il lit son entité dans la session plutôt que sur le site d'appel.
Pourquoi une classe, et jamais un décorateur
Un décorateur devrait modifier le runtime pour accrocher le storage, la cible ou les cinq
opérations. extends énonce la même chose dans le langage : le type suit sans seconde
déclaration, getFields() est un statique ordinaire, et rien n'est enregistré dans votre
dos.
Cela survit aussi là où un scan ne va pas. Crud déclare ses opérations au runtime, sur la
classe : une Frond installée dont aucun parseur ne peut ouvrir les sources publie quand
même son contrat. Des métadonnées de décorateur demanderaient un drapeau de compilation et
une bibliothèque de réflexion pour en dire autant, et ne le diraient que là où les deux
sont configurés.
Suite : Les entités.