-
Notifications
You must be signed in to change notification settings - Fork 7
Le pattern Repository fr FR
Résumé : Pour accéder à la base de données, StartER adopte le pattern Repository. Ce modèle encapsule les requêtes SQL dans des classes dédiées et fournit une interface claire pour effectuer les opérations CRUD.
Le pattern Repository impose une stricte séparation des responsabilités (Separation of Concerns) en gardant la logique d'accès aux données isolée de la logique métier.
Sans un repository, les actions Express deviennent rapidement encombrées de requêtes SQL brutes. En encapsulant les interactions avec la base de données dans une classe dédiée :
- Vos gestionnaires de routes restent propres et concentrés uniquement sur les préoccupations HTTP.
- Vous évitez de disperser des requêtes SQL partout dans le code.
- Vous centralisez toutes les interactions avec une table spécifique au même endroit, ce qui rend la couche de données beaucoup plus facile à appréhender, à maintenir et à tester.
L'ensemble des communications avec une table spécifique doit passer par son Repository.
Un exemple d'implémentation est proposé dans src/express/modules/item/itemRepository.ts. StartER tire parti de l'API synchrone native de SQLite, ce qui simplifie grandement les interactions avec la base de données : vous n'avez pas besoin de async ou await lors de l'exécution de requêtes SQL.
Important
Bien que les appels à la base de données SQLite soient synchrones, vous pouvez utiliser du code asynchrone dans les repositories. Par exemple, vous pouvez marquer une méthode de repository comme async si vous avez besoin d'effectuer des opérations asynchrones en parallèle des appels à la base de données, comme appeler une API HTTP externe ou lire un fichier sur le disque.
import database from "../../../database";
class ItemRepository {
// Le C de CRUD - Opération Create
create(item: Omit<Item, "id">): number | bigint {
const query = database.prepare(
"insert into item (title, user_id) values (?, ?)",
);
const result = query.run(item.title, item.user_id);
return result.lastInsertRowid;
}
// Le R de CRUD - Opération Read
find(byId: number): Item | null {
const query = database.prepare(
"select id, title, user_id from item where id = ? and deleted_at is null",
);
const row = query.get(byId);
return row ? itemSchema.parse(row) : null;
}
// ...
}
export default new ItemRepository();Ainsi, chaque module Express peut disposer de son propre repository, ce qui garantit une organisation claire et extensible du code.
SQLite ne retourne pas des types TypeScript stricts, mais des valeurs SQL de base (string | number | bigint | null, etc.).
Au lieu d'utiliser des assertions de type TypeScript (comme as Item qui masquerait d'éventuelles erreurs au moment de la compilation sans protéger l'exécution), StartER utilise Zod pour la validation explicite des sorties au runtime :
import { z } from "zod";
const itemSchema: z.ZodType<Item> = z.object({
id: z.number(),
title: z.string(),
user_id: z.number()
});
// ...
return itemSchema.parse(row);En liant le schéma Zod au type TypeScript (z.ZodType<Item>), cette approche garantit que l'objet retourné correspond exactement au contrat attendu par le reste de l'application. Si la base de données retourne une donnée inattendue, Zod prévient des comportements imprévisibles (bugs silencieux) en levant une erreur claire.
Important
Le schéma de sortie dans le Repository a un objectif complètement différent de celui du schéma d'entrée dans le Validator. Le schéma du Repository ne fait que transtyper les primitives brutes pour correspondre au type TypeScript. Il n'applique PAS de contraintes métier (comme .min(1) ou .email()) qui, elles, ont leur place dans le Validator.
Une action Express gère le cycle requête/réponse et fait appel au repository pour la donnée. Quand l'action n'interagit qu'avec le repository SQLite synchrone, elle n'a pas besoin d'être async. Marquez une action comme async uniquement lorsqu'elle effectue des opérations réellement asynchrones (par exemple, l'envoi d'e-mails ou l'appel à des API externes).
import itemRepository from "./itemRepository";
const browse = (req, res) => {
const items = itemRepository.findAll(10, 0); // Appel direct et synchrone
res.json(items);
};
export default { browse };Toutes les actions des items et le repository complet (lié à la base de données) sont disponibles dans les fichiers suivants :
src/express/modules/item/itemActions.tssrc/express/modules/item/itemRepository.ts
Le repository propose une pagination intégrée via findAll(limit, offset) :
findAll(limit: number, offset: number): Item[] {
const query = database.prepare(
"select id, title, user_id from item where deleted_at is null limit ? offset ?",
);
const rows = query.all(limit, offset);
return rows.map((row) => itemSchema.parse(row));
}L'action browse dans itemActions.ts utilise un offset calculé depuis le paramètre de requête ?start= :
const offset = Number(req.query.start ?? "0");
const items = itemRepository.findAll(10, offset);Tip
Ce mécanisme de pagination basique est suffisant pour démarrer. Pour des cas plus avancés, vous pouvez ajouter un compteur total, un tri configurable, ou une pagination par curseur.
StartER utilise une stratégie de suppression douce (soft delete) : les enregistrements ne sont pas physiquement supprimés de la base, mais marqués avec un timestamp deleted_at.
Le repository propose trois méthodes complémentaires :
| Méthode | Comportement |
|---|---|
softDelete |
Marque l'enregistrement comme supprimé (deleted_at = datetime('now')) |
softUndelete |
Restaure un enregistrement supprimé (deleted_at = null) |
hardDelete |
Supprime définitivement l'enregistrement de la base |
softDelete(id: number): boolean {
const query = database.prepare(
"update item set deleted_at = datetime('now') where id = ?",
);
const result = query.run(id);
return result.changes > 0;
}Les requêtes de lecture (findAll, find) filtrent automatiquement les enregistrements supprimés grâce à la clause where deleted_at is null.
Important
Par défaut, l'action destroy dans itemActions.ts utilise softDelete. Pour supprimer définitivement un enregistrement, utilisez hardDelete en connaissance de cause.
-
Privilégier la validation de sortie avec Zod : évitez les assertions TypeScript aveugles (
as Type). Validez les sorties de votre base de données via un schéma Zod pour garantir la sécurité et l'intégrité de vos données en cours d'exécution. - Pagination par défaut : pensez à limiter vos requêtes SQL pour éviter de surcharger l'application sur de grandes tables.
-
Privilégier le Soft Delete : garder un historique avec
deleted_atsécurise la donnée et simplifie les restaurations en cas d'erreur.
Co-création IA
Bien démarrer
Explications
Guides
Référence
Aller plus loin