video-surveillance-app/src/main/java/me/belkacemi/iotalarm/infrastructure/persistence/AlarmReadModelAdapter.java
Adam Belkacemi 63ead8a966 Systeme d'alarme IoT evenementiel : hexagonal, CQRS, Kafka
Backend d'un systeme de video-surveillance dont le capteur est un iPhone
recycle. Le probleme est simple, la structure est le sujet :

- domain/ sans aucun import de framework (verifie par 7 regles ArchUnit
  qui tournent aussi pendant le docker build)
- CQRS strict : write et read ne partagent ni modele ni port
- AlarmDecision en sealed interface -> switch exhaustif verifie a la
  compilation, plutot qu'une chaine de if
- Kafka en KRaft (Zookeeper est supprime depuis Kafka 4.0), cle de
  partition = deviceId pour garantir l'ordre par capteur
- ErrorHandlingDeserializer + DLQ : un message malforme ne bloque pas
  le flux des evenements suivants
- notification WebSocket differee apres commit, pour ne jamais afficher
  une intrusion qui partirait en rollback

31 tests au vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 17:32:45 +02:00

86 lines
3.5 KiB
Java

package me.belkacemi.iotalarm.infrastructure.persistence;
import me.belkacemi.iotalarm.application.query.port.AlarmReadModelPort;
import me.belkacemi.iotalarm.application.query.view.AlarmStatusView;
import me.belkacemi.iotalarm.application.query.view.AlertView;
import me.belkacemi.iotalarm.domain.model.AlarmPolicy;
import me.belkacemi.iotalarm.domain.model.AlarmState;
import me.belkacemi.iotalarm.infrastructure.persistence.jpa.AlarmStateJpaRepository;
import me.belkacemi.iotalarm.infrastructure.persistence.jpa.AlertJpaRepository;
import me.belkacemi.iotalarm.infrastructure.persistence.mapper.AlertMapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* ADAPTER SORTANT du cote LECTURE : implementation du {@link AlarmReadModelPort}.
*
* <p>Il ne charge jamais l'agregat. Il assemble une vue a partir de requetes
* ciblees - c'est la raison d'etre du CQRS : afficher un compteur d'alertes non
* acquittees via un {@code COUNT(*)} plutot qu'en rehydratant un objet metier
* complet dont on n'utiliserait aucune methode.</p>
*/
@Component
public class AlarmReadModelAdapter implements AlarmReadModelPort {
private final AlertJpaRepository alertRepository;
private final AlarmStateJpaRepository alarmStateRepository;
private final AlarmPolicy policy;
private final Clock clock;
public AlarmReadModelAdapter(AlertJpaRepository alertRepository,
AlarmStateJpaRepository alarmStateRepository,
AlarmPolicy policy,
Clock clock) {
this.alertRepository = alertRepository;
this.alarmStateRepository = alarmStateRepository;
this.policy = policy;
this.clock = clock;
}
@Override
public Optional<AlarmStatusView> alarmStatus(String zoneId) {
long unacknowledged = alertRepository.countByZoneIdAndAcknowledgedFalse(zoneId);
AlertView lastAlert = alertRepository.findFirstByZoneIdOrderByDetectedAtDesc(zoneId)
.map(AlertMapper::toView)
.orElse(null);
return Optional.of(alarmStateRepository.findById(zoneId)
.map(entity -> new AlarmStatusView(
entity.getZoneId(),
entity.getState().name(),
entity.getStateChangedAt(),
policy.exitDelay().toSeconds(),
entity.getLastAlertAt(),
unacknowledged,
lastAlert))
// Base neuve : l'alarme n'a simplement jamais ete armee. La
// console doit afficher DESARMEE, pas une erreur 404.
.orElseGet(() -> new AlarmStatusView(
zoneId,
AlarmState.DISARMED.name(),
clock.instant(),
policy.exitDelay().toSeconds(),
null,
unacknowledged,
lastAlert)));
}
@Override
public List<AlertView> recentAlerts(String zoneId, int limit) {
return alertRepository
.findByZoneIdOrderByDetectedAtDesc(zoneId, PageRequest.of(0, limit))
.stream()
.map(AlertMapper::toView)
.toList();
}
@Override
public Optional<AlertView> findAlert(UUID alertId) {
return alertRepository.findById(alertId).map(AlertMapper::toView);
}
}