Persistence (PostgreSQL via Prisma)¶
Schéma de données + conventions Prisma. Référence autoritative : apps/api/prisma/schema.prisma.
5.P.1 Schéma (vue d'ensemble)¶
erDiagram
CATCHMENT ||--o{ STATION : contains
STATION ||--o{ SENSOR : has
SENSOR ||--o{ MEASUREMENT : produces
STATION ||--o{ THRESHOLD : defines
STATION ||--o{ ALERT : triggers
STATION }o--o{ GLACIER : fed_by
WITHDRAWAL }o--|| STATION : impacts
INGESTION_RUN ||--o{ MEASUREMENT : traces
Tables du schéma :
Catchment— bassin versant (aujourd'huiborgneuniquement, extensible v2).Station— 7 stations seedées : 4 LIVE (BAFU via LINDAS) + 3 RESEARCH (CREALP Borgne). Champs clés :id,ofevCode(unique),name,riverName,latitude,longitude,altitudeM,flowType(NATURAL|RESIDUAL|DOTATION),operatorName,dataSource(LIVE|RESEARCH|SEED),sourcingStatus(CONFIRMED|ILLUSTRATIVE, cf. ADR-008),isActive.Sensor— un par combinaison{stationId, parameter}. Paramètres :DISCHARGE,WATER_LEVEL,TEMPERATURE(non-ingested v1),TURBIDITY(non-ingested v1).Measurement— séries temporelles. Contrainte unique(sensorId, recordedAt)— socle de l'idempotence de l'upsert. Alimenté exclusivement par le cron LINDAS.IngestionRun— trace chaque tick du cron. Champs :source,status(SUCCESS|FAILURE|PARTIAL),startedAt,completedAt,stationsSeenCount,measurementsCreatedCount,measurementsSkippedCount,errorMessage?,httpStatus?,payloadBytes?,payloadHash?,durationMs. Lu par/api/v1/status.Threshold— seuilsVIGILANCE/ALERTpar station + paramètre (seedés en dur v1, pas d'admin UI).Alert— ouvertures/fermetures d'alertes (schéma en place, évaluation hors scope v1).Glacier,StationGlacier— glaciers du bassin (Ferpècle, Mont Miné), jonction station ↔ glacier.Withdrawal— captages Grande Dixence (Ferpècle 1896 m, Arolla 2009 m).User,ThresholdAudit— schéma présent mais non utilisé v1 (admin JWT hors scope).
5.P.2 Conventions¶
- Cuids comme PK (
id: String @id @default(cuid())). Pas d'UUID, pas d'auto-increment. - Timestamps systématiques —
createdAt: DateTime @default(now())etupdatedAt: DateTime @updatedAtsur les entités à cycle de vie. - Enums en SCREAMING_SNAKE_CASE — verbatim DTO front/back, pas de conversion casing.
dataSource: 'LIVE',sourcingStatus: 'CONFIRMED',Parameter: 'DISCHARGE'cohabitent sans singularité à défendre. - Index composites — sur
Measurement(sensorId, recordedAt DESC)pour les requêtes de série temporelle. SurIngestionRun(startedAt DESC)pour/status. - Pas de soft delete —
isActive: booleansurStationsi besoin de masquer sans supprimer. Pas dedeletedAtpartout.
5.P.3 Migrations¶
- Strategy additive uniquement en prod — jamais de
DROP COLUMN,DROP TABLE,ALTER TYPEdestructif en production. Les évolutions se font par migrations additives (ADD COLUMN NOT NULL DEFAULT) puis backfill puis nettoyage différé si nécessaire. - Nommage —
YYYYMMDDHHMMSS_description_snake_casegénéré parprisma migrate dev --name description-kebab-case. Exemple :20260422141207_add_station_sourcing_status. - Historique versionné — toutes les migrations vivent dans
apps/api/prisma/migrations/et sont committées. Prisma les replay viaprisma migrate deploydansentrypoint.shau boot. - Pas de
prisma migrate reseten prod — destructeur, effacerait toutes les données. Interdit par convention d'ops (cf. post-mortem 2026-04-21 pour le contexte).
5.P.4 Seed idempotent¶
apps/api/prisma/seed.ts — alimenté via upsert sur clés naturelles stables :
Catchmentupserted surid.Stationupserted surofevCode(unique).Sensorupserted sur(stationId, parameter).Thresholdupserted sur(stationId, parameter).Glacierupserted surname.StationGlacierupserted sur(stationId, glacierId).Withdrawalupserted surname.
pruneStaleStations(currentOfevCodes) supprime les Station dont ofevCode n'est pas dans la liste seed actuelle — cascade Measurement, Alert, Threshold, Sensor, StationGlacier. C'est l'opération qui aurait pu causer l'incident 2026-04-21 si un seed stale avait été lancé contre prod (non prouvé, cf. post-mortem).
Le seed ne touche jamais Measurement, Alert, IngestionRun — tables opérationnelles, exclusivement écrites par le cron.
5.P.5 Observabilité côté DB¶
/api/v1/healthexécute unSELECT 1via Prisma. Retourdatabase: 'ok'ou'error'./api/v1/statuslit le dernierIngestionRun(tristartedAt DESC LIMIT 1) et calculelastSuccessAt(tristatus = 'SUCCESS' ORDER BY startedAt DESC LIMIT 1). Compteurs journée via agrégatCOUNT+SUMsurIngestionRundepuis minuit UTC.- Pas de métrique Prometheus exposée — reportée post-candidature. Pino JSON stdout suffit pour agréger à la main si besoin.