Skip to content

Base de données ​

L'API garde toutes ses données dans PostgreSQL 17, à travers Ecto, la bibliothèque d'accès aux bases d'Elixir. La structure des tables est créée et modifiée par les migrations de timemanager-api/priv/repo/migrations/, appliquées à chaque démarrage du conteneur (voir Docker).

Le schéma ​

Source : docs/database.mmd, inclus tel quel ci-dessous. Chaque colonne y a un commentaire qui dit son rôle. Une version en ligne est aussi disponible sur mermaid.ai.

Lire un schéma entité-relation

Chaque boîte est une table, chaque ligne une colonne (PK clé primaire, FK clé étrangère, UK valeur unique). Les traits relient les tables : ||--o{ veut dire « un » d'un côté et « zéro ou plusieurs » de l'autre. Par exemple, un utilisateur a zéro ou plusieurs pointages. Clique sur le schéma pour l'agrandir.

Les tables en bref ​

TableContenu
usersLes comptes : email, nom, rôle, statut (invité ou actif), empreinte du mot de passe
password_tokensLes liens d'invitation, de réinitialisation et de changement d'email. Seule l'empreinte du lien est gardée.
refresh_tokensLes sessions de 30 jours, pour le web ou pour le mobile (client)
teams, team_membersLes équipes, leur manager et leurs membres
clocksChaque pointage brut : arrivée ou départ, à quelle heure, et quand le serveur l'a reçu
workingtimeUne journée de travail, de l'arrivée au départ
breaksLes pauses, à l'intérieur d'une journée
correction_requestsLes demandes de correction et leur traitement. Un client_id évite d'enregistrer deux fois une demande renvoyée par le téléphone.
working_time_changesL'historique de chaque modification manuelle des heures (avant, après, par qui)
absencesUn jour d'absence : congé, maladie ou autre, sans motif
week_validationsLes semaines validées par un manager, et leur réouverture
notificationsCe qui est montré dans la cloche et envoyé par email

Toutes les dates sont en UTC, au format AAAA-MM-JJ hh:mm:ss (règle BR-09).

Deux sortes de dates ​

Type EctoFormat renvoyéOù
naive_datetime2026-09-30T06:00:00, sans fuseaules heures des journées et des pauses (start, end), au format demandé par le sujet
utc_datetime2026-09-30T06:00:00Z, avec le Z d'UTCtout le reste : un pointage, quand quelque chose a été enregistré, reçu, validé…

Dans les deux cas, l'heure est en UTC dans la base. C'est l'affichage qui la convertit : le site et l'application dans le fuseau de l'appareil, l'API dans celui de la mairie (Europe/Paris) quand elle doit savoir à quel jour appartient une heure (une semaine validée, une absence).

Ce que la base garantit elle-même ​

Le code vérifie déjà ces règles, mais la base les garantit aussi, pour toute écriture, quel que soit son chemin : c'est la défense en profondeur. Si un bug laissait passer un doublon, PostgreSQL refuserait l'écriture. Quand le changeset déclare la contrainte (unique_constraint), Ecto transforme ce refus en erreur 422 (code taken) ; sinon, la requête échoue sans rien écrire.

ContrainteTableCe qu'elle garantit
users_email_lower_indexusersun e-mail par compte, sans tenir compte de la casse (Bruce@… = bruce@…)
teams_name_lower_indexteamsun nom d'équipe unique, sans tenir compte de la casse
workingtime_one_open_per_userworkingtimeune seule journée en cours par personne
breaks_one_open_per_working_timebreaksune seule pause en cours par journée
absences_user_id_date_indexabsencesune seule absence par personne et par jour
week_validations_in_force_indexweek_validationsune seule validation en vigueur par semaine et par personne
clocks_user_id_client_id_indexclocksun pointage envoyé deux fois par le téléphone n'est enregistré qu'une fois
breaks_start_client_id_index, breaks_end_client_id_indexbreaksla même chose pour le début et la fin d'une pause
correction_requests_user_id_client_id_indexcorrection_requestsla même chose pour une demande de correction
password_tokens_token_hash_index, refresh_tokens_token_hash_indexchaque jeton est unique

Les index partiels ​

Plusieurs de ces index sont partiels : ils ne portent que sur certaines lignes, grâce à une clause WHERE. Par exemple :

sql
CREATE UNIQUE INDEX workingtime_one_open_per_user ON workingtime (user_id) WHERE "end" IS NULL;

Une personne a des centaines de journées terminées (avec une fin), mais au plus une sans fin. De même, client_id IS NOT NULL laisse libres les pointages du site web, qui n'ont pas d'identifiant d'appareil.

Les index sur une expression ​

lower(email) : l'index porte sur l'e-mail en minuscules. Deux adresses qui ne diffèrent que par la casse tombent sur la même entrée, et la seconde est refusée.

Quand un compte est supprimé ​

Une clé étrangère dit ce que devient une ligne quand la ligne qu'elle désigne disparaît. Deux comportements sont utilisés, selon ce qu'on veut garder :

ComportementTablesPourquoi
CASCADE : la ligne est supprimée avec le compteclocks, password_tokens, refresh_tokens, team_members, absences, week_validations, notificationsces données n'ont plus de sens sans la personne
SET NULL : la ligne reste, le lien devient videworkingtime, working_time_changes, correction_requests (et les champs « par qui » : recorded_by_id, validated_by_id, reviewed_by_id…)les heures et leur historique sont gardés : les totaux des équipes et le journal des modifications restent justes
RESTRICT : la suppression est refuséeteams.manager_idune équipe ne peut pas rester sans manager : il faut d'abord la confier à quelqu'un d'autre

Quand le lien devient vide, qui a fait quoi reste lisible grâce aux copies : changed_by_name, reviewed_by_name, user_email… sont recopiés au moment de l'action, justement pour survivre à une suppression.

Les migrations ​

Une migration est un fichier qui décrit un changement de structure : créer une table, ajouter une colonne, un index. Les migrations sont appliquées dans l'ordre de leur date (le début du nom de fichier), et la table schema_migrations retient celles qui sont déjà passées.

bash
mix ecto.gen.migration add_reason_to_absences   # crée le fichier, à remplir
mix ecto.migrate                                 # applique les nouvelles
mix ecto.rollback                                # annule la dernière (en dev)

En production, l'image Docker les applique au démarrage, avec Timemanager.Release.migrate/0 (lib/timemanager/release.ex).

Les règles d'une bonne migration

  • Additive autant que possible : ajouter une colonne ou une table, plutôt que renommer ou supprimer. L'ancienne version de l'API, encore en service pendant le déploiement, continue de fonctionner.
  • Jamais modifier une migration déjà fusionnée : elle a déjà tourné sur d'autres bases. On en écrit une nouvelle.
  • Mettre à jour docs/database.mmd dans la même PR, avec un commentaire pour chaque nouvelle colonne.