Routine de vérification

Dans cette partie, nous allons voir comment créer des routines de vérification. Tous les exemples présentés dans ce cours sont disponibles en entier ici.

  1. Les process
  2. Créer un process
  3. Enregistrer un proccess
  4. Implémentation d’un process
  5. Créer ses propres builders

Les process

Les processes font partie des objets complexes de DuploJS. Leur création passe aussi par le biais d’un builder. Les objets Route et Process sont tous deux des extensions de l’objet HttpDuplose. Un process n’est donc pas fondamentalement différent dans sa structure et sa déclaration. Cependant, le builder des processes ne propose pas de méthode handler, car ceux-ci ne doivent pas inclure d’étape HandlerStep.

Les HandlerStep, utilisés pour les Routes, ont pour rôle de contenir l’action associée à la route. Dans le cas des processes, ils ne doivent pas inclure d’actions, car leur but est uniquement de réaliser des vérifications routinières. Les processes sont particulièrement adaptés pour :

  • l’authentification,
  • la vérification des rôles,
  • la factorisation d’utilisations récurrentes de CheckerStep et/ou CutStep.

Créer un process

Pour créer un Process il faut appeler la fonction createProcess, qui donne ensuite accès au builder de l’objet Process.

La fonction createProcess prend comme premier argument une string correspondant au nom du Process. La création d’un process se clôture par l’appel de la méthode exportation du builder. La méthode exportation prend en argument un tableau de string, qui représente les index des données présentes dans le floor du Process. Chaque clé spécifiée pourra être utilisée pour importer des données du Process dans le floor des Routes ou des Processes qui l’implémentent.

Les Processes peuvent être créés avec des options pouvant être surchargées lors de leur implémentation. Pour cela, il suffit de définir la propriété options dans l’objet passé en deuxième argument de la fonction createProcess.

import { ForbiddenHttpResponse, makeResponseContract, useBuilder, zod } from "@duplojs/core";

interface MustBeConnectedOptions {
    role: "user" | "admin";
}

export const mustBeConnectedProcess = createProcess(
    "mustBeConnected",
    {
        options: <MustBeConnectedOptions>{
            role: "user",
        },
    },
)
    .extract(
        {
            headers: {
                authorization: zod.string(),
            },
        },
        () => new ForbiddenHttpResponse("authorization.missing"),
    )
    .check(
        valideTokenCheck,
        {
            input: (pickup) => pickup("authorization"),
            result: "token.valide",
            catch: () => new ForbiddenHttpResponse("authorization.invalide"),
            indexing: "contentAuthorization",
        },
        makeResponseContract(ForbiddenHttpResponse, "authorization.invalide"),
    )
    .cut(
        ({ pickup, dropper }) => {
            const { contentAuthorization, options } = pickup(["contentAuthorization", "options"]);

            if (contentAuthorization.role !== options.role) {
                return new ForbiddenHttpResponse("authorization.wrongRole");
            }

            return dropper(null);
        },
        [],
        makeResponseContract(ForbiddenHttpResponse, "authorization.wrongRole"),
    )
    .exportation(["contentAuthorization"]);

Dans cet exemple :

  • Un process nommé mustBeConnected a été créé.
  • Le process exporte la donnée indexée sous contentAuthorization, permettant aux routes/processes qui l’implémentent d’utiliser cette donnée.
  • Le process a été créé avec l’option role, dont la valeur par défaut est user.
  • En survolant le code, nous pouvons déduire que le process exige un header authorization contenant un token. Ce token inclut des informations sur l’utilisateur. Grâce à ces informations, l’accès est interdit à l’utilisateur si son rôle ne correspond pas au rôle spécifié dans les options.

Les processes ont les mêmes steps disponibles que les routes (sauf la HandlerStep). Il n’y a aucune différence d’utilisation.

Enregistrer un proccess

À l’instar des routes, les processes doivent être enregistrés dans une instance Duplo. Cet enregistrement peut se faire soit de manière unitaire, soit à la volée. L’enregistrement multiple d’un même process n’a aucune incidence car les processes sont uniques et ne seront pas dupliqués.

import { Duplo, useProcessBuilder } from "@duplojs/core";

const duplo = new Duplo({
    environment: "DEV",
});

duplo.register(mustBeConnectedProcess);
// or
duplo.register(...useProcessBuilder.getAllCreatedProcess());

Dans cet exemple :

  • Le process mustBeConnectedProcess a été enregistré unitairement dans une instance Duplo.
  • L’alternative permet d’enregistrer tous les processes créés en une seule fois dans l’instance Duplo.

Implémentation d’un process

Les processes peuvent être implémentés dans des routes, dans d’autres processes, mais aussi avant des routes.

Implémentation basic

Pour implémenter un process dans une route ou un autre process, il faut utiliser la méthode execute des builders. Cette méthode prend en premier argument un process et en second les paramètres d’implémentation.

Deux propriétés importantes sont à retenir dans les paramètres d’implémentation :

  • options : permet de surcharger les options par défaut du process
  • pickup : permet de récupérer dans la route les données exportées depuis le floor du process
import { makeResponseContract, OkHttpResponse, useBuilder } from "@duplojs/core";

useBuilder()
    .createRoute("GET", "/user")
    .execute(
        mustBeConnectedProcess,
        {
            pickup: ["contentAuthorization"],
        },
    )
    .presetCheck(
        iWantUserExistById,
        (pickup) => pickup("contentAuthorization").id,
    )
    .handler(
        (pickup) => {
            const { user } = pickup(["user"]);

            return new OkHttpResponse("user.getSelf", user);
        },
        makeResponseContract(OkHttpResponse, "user.getSelf", userSchema),
    );

Dans cet exemple :

  • Le process mustBeConnected a été implémenté dans une route.
  • L’option role du process est définie sur user.
  • Le paramètre pickup rapatrie la donnée contentAuthorization dans le floor de la route.

Implémentation preflight

Il est possible d’implémenter un process avant la création d’une route/d’un process. Le process devient un preflight. Les preflights s’éxécutent avant l’interprétation du body. Il est conseillé de les utiliser pour faire des routines d’autentification.

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

useBuilder()
    .preflight(
        mustBeConnectedProcess,
        {
            options: { role: "admin" },
        },
    )
    .createRoute("GET", "/users/{userId}")
    .extract({
        params: {
            userId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExistById,
        (pickup) => pickup("userId"),
    )
    .handler(
        (pickup) => {
            const { user } = pickup(["user"]);

            return new OkHttpResponse("user.get", user);
        },
        makeResponseContract(OkHttpResponse, "user.get", userSchema),
    );

Dans cet exemple :

  • Le process mustBeConnected a été implémenté en tant que preflight.
  • L’option role du process est définie sur admin.

Il est possible d’implémenter autant de preflights que vous le souhaitez. Vous pouvez très bien ajouter localement un preflight avant la déclaration d’une route, sans effets de bord.

Créer ses propres builders

Comme vu précédemment, un process implémenté en preflight est complètement indépendant. Cela nous permet de créer nos propres builders avec des preflights déjà intégrés.

import { makeResponseContract, NoContentHttpResponse, useBuilder, zod } from "@duplojs/core";

export function mustBeConnectedBuilder(options: MustBeConnectedOptions) {
    return useBuilder()
        .preflight(
            mustBeConnectedProcess,
            {
                options,
                pickup: ["contentAuthorization"],
            },
        );
}

mustBeConnectedBuilder({ role: "admin" })
    .createRoute("DELETE", "/users/{userId}")
    .extract({
        params: {
            userId: zod.coerce.number(),
        },
    })
    .presetCheck(
        iWantUserExistById,
        (pickup) => pickup("userId"),
    )
    .handler(
        (pickup) => {
            const { user } = pickup(["user"]);

            // action

            return new NoContentHttpResponse("user.delete");
        },
        makeResponseContract(NoContentHttpResponse, "user.delete"),
    );

Dans cet exemple :

  • Un builder nommé mustBeConnectedBuilder a été créé.
  • Le builder prend les options du process mustBeConnected en argument, ce qui permet d’être flexible sur le rôle exigé pour la connexion.
  • Il est possible d’utiliser mustBeConnectedBuilder pour déclarer autant de routes/processes que nous le souhaitons.
  • Une route DELETE : /users/{userId} a été créée avec mustBeConnectedBuilder.


<< Aborder une nouvelle route