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

FormeCe que vous écrivezExemples
une classe dont vous héritezextends Crud(Post)entity, Crud, Presenter, Collector, Repository, Mirror
une valeur que vous recevezconstructor(posts: RepositoryOf<Post>)RepositoryOf<E>, Emit<F>, Fact<F>, Facade<H>
un appel que vous écrivezuseQuery(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 tiennentCe dont vous héritezCe que vous écrivez
entity({…})les champsle type, le juge, les métadonnéesla carte des champs
Crud(Post)une entitécinq opérationsrien, ou celles que vous redéfinissez
Presenter(Post)une entitéla cible que le scan litune méthode par champ calculé
Collector(User)n'importe quelle classela cible que le scan litcollect(ctx)
Repository(Reading)une entité, ou les plusieurs qu'il possèdetous les gestes du port gardé — ou aucun, à partir de deuxles requêtes que vous nommez
Mirror(BookCard)une formela boucle, l'âge, le juge, l'écriturepull()

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.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.