Aller au contenu

Accès aux données

Les services accèdent à la base via des accesseurs Sequelize typés exportés depuis database/sequelize/models.ts. Chaque entité de domaine possède un fromRecord(attrs) statique qui mappe un record Sequelize vers une entité de domaine.

Accesseurs typés et fromRecord

import { Workspaces } from '../../database/sequelize/models';
import { Workspace } from '../../domain/workspaces/workspace.model';

const record = await Workspaces.findByPk(id);
if (!record) throw new NotFoundError();
return Workspace.fromRecord(record.get());
  • findByPk / findOne renvoient null en cas d'absence → le service vérifie explicitement et lève NotFoundError (voir Erreurs & i18n).
  • L'interface d'attributs est exportée à côté de la classe de domaine.

Jamais de .catch(() => null)

Cela masquerait des pannes d'infrastructure. On ne capture que les erreurs de domaine attendues, et on relance les erreurs inattendues.

Lecture relationnelle optimisée avec include

Préférez include à un N+1 dès qu'un chemin de lecture fait « requête A, puis recherche B par les IDs de A » :

const memberships = await WorkspaceMemberships.findAll({
  where: { userId: user.id },
  include: [{ model: Workspaces }],
});

const workspaces = memberships
  .map((m) => m.get('Workspace') as WorkspaceModel | undefined)
  .filter((w): w is WorkspaceModel => w != null)
  .map((w) => Workspace.fromRecord(w.get()));

Cast d'include

Le cast de record.get('User') est as Entity pour un INNER JOIN, ou as Entity | null pour un LEFT JOIN sur une FK nullable. Jamais | undefined.

Chaque FK d'un modèle doit avoir une paire belongsTo / hasMany correspondante (voir Associations), sinon include lève une EagerLoadingError.

Invariants de colonnes

  • Ne pas écrire ?? default sur une lecture Sequelize quand la colonne est allowNull: false : on fait confiance à l'invariant de la base.
  • Les colonnes texte optionnelles sont allowNull: false, defaultValue: '' ; le type du champ de domaine est string, jamais string | null.

Modèles, migrations, associations

Élément Emplacement
Modèles database/sequelize/models/<entity>.model.ts (factories auto-chargées au démarrage).
Accesseurs typés database/sequelize/models.ts — ajoutez-y une entrée à chaque nouveau modèle.
Associations database/sequelize/sequelize.ts, dans buildAssociations() (jamais dans les fichiers de modèles).
Migrations database/sequelize/migrations/ — scaffolder avec bash scripts/makemigrations.sh <name>.
  • Les IDs sont des ULID (26 caractères, triables) stockés en CHAR(26).
  • En dev/test la base est SQLite ; en prod, PostgreSQL.

Migrations en vol (v2)

Tant que v2 est en cours, les changements de schéma de tables existantes vont dans le fichier de migration d'origine, en place — pas dans une migration de suivi. Les tables entièrement nouvelles obtiennent leur propre migration. Voir Guidelines Git.

Pour aller plus loin

La compétence ikloze-define-model du dépôt détaille le scaffolding de la couche de persistance (définition de modèle, migration, entité de domaine avec fromRecord + interface d'attributs, accesseur typé, associations).