Aborder une nouvelle route

Cette partie aborde la méthodologie de travail à adopter lors de l’utilisation du framework DuploJS. Tous les exemples présentés dans ce cours sont disponibles en entier ici.

  1. Outils Ă  ma disposition
  2. Comment procéder ?

Outils Ă  ma disposition

Pour rappel, dans DuploJS, une route est constituée d’étapes à franchir avant d’effectuer une action. Chaque étape est une vérification qui peut mettre fin à l’exécution de la route en renvoyant une réponse. Les différentes étapes abordées précédement sont :

  • les ExtractStep qui permettent d’extraire de la donnĂ©e de la requĂŞte courante.
  • les CheckerStep qui effectuent une vĂ©rification Ă  partir d’une valeur d’entrĂ©e.
  • les CutStep qui effectuent une vĂ©rification directement au sein de la route pour des cas uniques.

La HandlerStep fait exception car elle doit contenir l’action d’une route, elle sera donc la dernière étape et devra renvoyer une réponse positive.

En plus de jouer un role de garde, les étapes enrichissent de manière typées le floor qui est un accumulateur de données.

Pour finir, il existe les contrats de sortie qui permettent explicitement d’indiquer ce que l’on renvoie. C’est un aspect très important qui garantit un retour correct.

Comment procéder ?

Pour commencer, il vous faut établir un but. À quoi votre route va-t-elle servir :

  • Ă€ rĂ©cupĂ©rer des informations d’un utilisateur ?
  • Ă€ poster un message dans une conversation ?
  • Ă€ ajouter un utilisateur Ă  une organisation ?
  • Ă€ crĂ©er un utilisateur ?

Pour illustrer la méthodologie, le but choisi sera d’envoyer un message à un utilisateur.

Après avoir établi ce que nous voulons, nous pouvons commencer par définir le document que notre route renverra. Cela nous permettera de mettre en place le contrat de sortie.

import { zod } from "@duplojs/core";

export const messageSchema = zod.object({
    senderId: zod.number(),
    receiverId: zod.number(),
    content: zod.string(),
    postedAt: zod.date(),
});

Quand le body de votre contrat est un objet, il est préférable de le déclarer dans un autre fichier. Dans une architecture simple, créez un dossier src/schemas et enregistez vos schémas dans des fichiers différents suivant le document qu’ils représentent.

Ensuite nous pouvons commencer à déclarer notre route. Nous utiliserons la méthode POST et le chemin /users/{receiverId}/messages, car il s’agit d’un envoi de données dans les messages d’un utilisateur.

import { makeResponseContract, OkHttpResponse, useBuilder, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .handler(
        (pickup) => {
            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                postedAt: new Date(),
                /* ... */
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema)
    );

L’information décrit comment la route s’est arrêtée. Ici, si la réponse message.posted est reçue, cela signifie que la route s’est arrêtée après avoir posté le message.

Dans notre cas, pour envoyer un message, nous voulons être sûr que l’utilisateur qui le reçoit existe avant de stocker son message. Ici il sera nommé receiver et son id est présent dans les paramètres du path /users/{receiverId}/messages de notre route. La prochaine étape sera donc de l’extraire, afin d’avoir le receiverId indéxé dans le floor.

import { makeResponseContract, OkHttpResponse, useBuilder, zod, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .extract({
        params: {
            receiverId: zod.coerce.number(),
        },
    })
    .handler(
        (pickup) => {
            const { receiverId } = pickup(["receiverId"]);

            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                receiverId,
                postedAt: new Date(),
                /* ... */
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema)
    );

Les paramètres de path sont toujours des string. C’est pour cela que l’on utilise le coerce de zod pour le convertir en number.

Ensuite, il faut vérifier que notre receveur existe. Nous allons utiliser le checker userExist provenant de cette exemple et en faire un preset checker.

import { createPresetChecker, makeResponseContract, NotFoundHttpResponse } from "@duplojs/core";

export const iWantUserExist = createPresetChecker(
    userExistCheck,
    {
        result: "user.exist",
        catch: () => new NotFoundHttpResponse("user.notfound"),
        indexing: "user",
    },
    makeResponseContract(NotFoundHttpResponse, "user.notfound"),
);

Une fois devenu un preset checker, son implémentation sera beaucoup plus explicite et rapide.

import { makeResponseContract, OkHttpResponse, useBuilder, zod, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .extract({
        params: {
            receiverId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExist,
        (pickup) => pickup("receiverId"),
    )
    .handler(
        (pickup) => {
            const { user } = pickup(["user"]);

            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                receiverId: user.id,
                postedAt: new Date(),
                /* ... */
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema)
    );

Pour obtenir le contenu du message il nous faut également l’extraire.

import { makeResponseContract, OkHttpResponse, useBuilder, zod, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .extract({
        params: {
            receiverId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExist,
        (pickup) => pickup("receiverId"),
    )
    .extract({
        body: zod.object({
            content: zod.string(),
        }),
    })
    .handler(
        (pickup) => {
            const { user, body } = pickup(["user", "body"]);

            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                receiverId: user.id,
                content: body.content,
                postedAt: new Date(),
                /* ... */
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema)
    );

Il est tout à fait possible d’utiliser la première ExtractStep pour obtenir le body. Mais imaginons que, par soucis de performance, nous ne souhaitions pas extraire le contenu du body immédiatement.

N’oublions pas que si quelqu’un reçoit un message, c’est qu’une autre personne l’a envoyé. C’est moi, en temps qu’utilisateur qui ai appelé la route pour poster un message dans la pile d’un autre utilisateur. Pour cela, imaginons que notre userId (ou senderId) soit stocké dans un header userId. Habituellement, il aurait dû être obtenu via un token qu’il aurait fallu valider en amont, mais pour notre exemple, nous ferons plus simple.

import { makeResponseContract, OkHttpResponse, useBuilder, zod, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .extract({
        params: {
            receiverId: zod.coerce.number(),
        },
        headers: {
            userId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExist,
        (pickup) => pickup("receiverId"),
    )
    .extract({
        body: zod.object({
            content: zod.string(),
        }),
    })
    .handler(
        (pickup) => {
            const { user, body } = pickup(["user", "body"]);

            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                receiverId: user.id,
                content: body.content,
                postedAt: new Date(),
                /* ... */
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema)
    );

Nous rencontrons ici un petit problème. Il y a deux utilisateurs différents dans la route : le sender et le receiver. Dans le cas actuel, si je réutilise mon preset checker iWantUserExist en y envoyant mon userId à la place de receiverId, le preset checker va réindexer l’utilisateur trouvé à l’index user dans le floor. Cela écrasera la valeur indéxée du précédant preset checker. De plus, un second problème arrive. Si le preset checker est re-implémenté tel quel, la route peut renvoyer deux fois la même information user.notfound pour 2 raisons différentes. La solution à tous nos problèmes est de créer un second preset checker à partir du premier.

import { createPresetChecker, makeResponseContract, NotFoundHttpResponse } from "@duplojs/core";

export const iWantUserExist = createPresetChecker(
    userExistCheck,
    {
        result: "user.exist",
        catch: () => new NotFoundHttpResponse("user.notfound"),
        indexing: "user",
    },
    makeResponseContract(NotFoundHttpResponse, "user.notfound"),
);

export const iWantReceiverExist = iWantUserExist
    .rewriteIndexing("receiver")
    .redefineCatch(
        () => new NotFoundHttpResponse("receiver.notfound"),
        makeResponseContract(NotFoundHttpResponse, "receiver.notfound"),
    );

Avec cela, le preset checker iWantReceiverExist indexera la donnée à receiver dans le floor et, en cas d’échec, l’information receiver.notfound sera renvoyée. Il ne reste plus qu’à l’implémenter.

import { makeResponseContract, OkHttpResponse, useBuilder, zod, type ZodSpace } from "@duplojs/core";

useBuilder()
    .createRoute("POST", "/users/{receiverId}/messages")
    .extract({
        params: {
            receiverId: zod.coerce.number(),
        },
        headers: {
            userId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExist,
        (pickup) => pickup("userId"),
    )
    .presetCheck(
        iWantReceiverExist,
        (pickup) => pickup("receiverId"),
    )
    .extract({
        body: zod.object({
            content: zod.string(),
        }),
    })
    .handler(
        (pickup) => {
            const { user, receiver, body } = pickup(["user", "receiver", "body"]);

            const postedMessage: ZodSpace.infer<typeof messageSchema> = {
                senderId: user.id,
                receiverId: receiver.id,
                content: body.content,
                postedAt: new Date(),
            };

            return new OkHttpResponse("message.posted", postedMessage);
        },
        makeResponseContract(OkHttpResponse, "message.posted", messageSchema),
    );

La déclaration de la route s’arrête ici. Toutes vos vérifications sont explicites, votre code est robuste et sans erreur grâce au typage de bout en bout !


<< Définir une réponse Routine de vérification >>>