Créer le service transactionnel de gestion des relations #57

Closed
opened 2026-07-21 07:34:56 +02:00 by fy59 · 0 comments
Owner

Créer le service transactionnel de gestion des relations

Contexte

Les composants suivants existent désormais :

  • RelationRecord ;
  • RelationDao ;
  • RelationEvidenceDao ;
  • EvidenceDao ;
  • l'infrastructure SQLite et les transactions.

Une relation peut être insérée dans la table relations, puis associée à une
ou plusieurs preuves dans la table relation_preuves.

Ces opérations ne doivent pas être exécutées séparément par l'interface GTK
ou par le futur graphe interactif. Une erreur pendant l'association des
preuves ne doit jamais laisser une relation partiellement créée.

La création d'une relation et de toutes ses associations doit donc constituer
une seule opération métier atomique.

Objectif

Créer un service métier RelationService chargé de coordonner :

  1. la validation de la demande ;
  2. l'ouverture d'une transaction SQLite ;
  3. l'insertion de la relation ;
  4. l'association de toutes les preuves demandées ;
  5. la validation de la transaction ;
  6. l'annulation complète en cas d'erreur.

Le service doit rester indépendant de GTK.

Fichiers

Créer :

include/core/relation_service.h
src/core/relation_service.c
tests/test_relation_service.c

Type opaque

typedef struct RelationService RelationService;

Le service doit :

  • emprunter une connexion Database existante ;
  • créer et posséder ses DAO internes ;
  • ne jamais fermer la connexion empruntée ;
  • libérer ses DAO lors de sa destruction.

Domaine d'erreur

Créer :

typedef enum
{
    RELATION_SERVICE_ERROR_INVALID_ARGUMENT,
    RELATION_SERVICE_ERROR_MEMORY,
    RELATION_SERVICE_ERROR_TRANSACTION_BEGIN,
    RELATION_SERVICE_ERROR_RELATION_INSERT,
    RELATION_SERVICE_ERROR_EVIDENCE_LINK,
    RELATION_SERVICE_ERROR_TRANSACTION_COMMIT,
    RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK
} RelationServiceError;

Ajouter :

#define RELATION_SERVICE_ERROR \
    relation_service_error_quark()

GQuark relation_service_error_quark(void);

Les messages d'erreur doivent conserver un contexte métier compréhensible.

Lorsqu'une erreur provient d'un DAO, le message d'origine doit être inclus
dans le message remonté par le service.

Construction

RelationService *relation_service_new(
    Database *database,
    GError **error
);

La fonction doit :

  • refuser une connexion NULL ;
  • créer les DAO nécessaires ;
  • nettoyer toutes les allocations si une création intermédiaire échoue ;
  • emprunter la connexion ;
  • accepter un GError ** facultatif.

Destruction

void relation_service_free(
    RelationService *relation_service
);

La fonction doit :

  • accepter NULL ;
  • libérer les DAO internes ;
  • ne jamais fermer la connexion Database.

Création transactionnelle

gboolean relation_service_create(
    RelationService *relation_service,
    const RelationRecord *relation_record,
    const GPtrArray *evidence_identifiers,
    GError **error
);

Propriété des paramètres

Le service emprunte :

  • relation_record ;
  • evidence_identifiers ;
  • toutes les chaînes UUID contenues dans le tableau.

Il ne doit modifier ni libérer ces objets.

evidence_identifiers peut être NULL ou vide.

Validation avant transaction

Avant toute écriture, la fonction doit :

  • refuser un service invalide ;
  • refuser un RelationRecord absent ;
  • vérifier que les accesseurs obligatoires du modèle ne retournent pas
    NULL ;
  • vérifier que chaque élément du tableau est une chaîne non NULL ;
  • vérifier que chaque chaîne est un UUID valide ;
  • refuser deux fois le même UUID de preuve dans la demande.

Une erreur de validation ne doit ouvrir aucune transaction et ne doit produire
aucune écriture.

Transaction

L'opération doit suivre exactement ce déroulement :

BEGIN
  ├── insertion de la relation
  ├── association de la première preuve
  ├── association de la deuxième preuve
  └── ...
COMMIT

En cas d'échec :

erreur
  └── ROLLBACK

Le service doit utiliser l'infrastructure de transaction existante du projet.

Il ne doit pas exécuter directement de requête SQL métier : les écritures
doivent passer par RelationDao et RelationEvidenceDao.

Atomicité

Les comportements suivants sont obligatoires :

  • si l'insertion de la relation échoue, aucune association n'est créée ;
  • si la première association échoue, la relation est annulée ;
  • si une association intermédiaire échoue, les associations précédentes sont
    annulées ;
  • si le commit échoue, aucune création partielle ne doit être considérée comme
    réussie ;
  • un retour TRUE signifie que la relation et toutes les associations sont
    persistées ;
  • un retour FALSE signifie que l'opération complète a échoué.

Échec du rollback

Si l'opération métier échoue puis que le rollback échoue également :

  • la fonction doit retourner FALSE ;
  • l'erreur finale doit signaler
    RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK ;
  • le message doit mentionner l'erreur métier initiale et l'erreur du rollback.

Le service ne doit jamais masquer silencieusement l'échec d'un rollback.

Absence de preuves

Une relation sans preuve associée reste autorisée par ce ticket.

Cette décision évite d'introduire une règle que le schéma actuel ne peut pas
nuancer entre :

  • relation saisie manuellement ;
  • relation importée ;
  • relation proposée automatiquement ;
  • relation validée ;
  • hypothèse.

La gestion de ces catégories et l'obligation éventuelle d'une provenance
feront l'objet d'un ticket métier séparé.

Doublons dans la demande

Le tableau suivant est invalide :

preuve-A
preuve-B
preuve-A

Le service doit détecter ce doublon avant d'ouvrir la transaction.

Il ne doit pas attendre la contrainte SQLite pour découvrir cette erreur.

Tests

Créer :

tests/test_relation_service.c

Les tests doivent utiliser une base SQLite temporaire initialisée avec le
schéma réel.

Les entités et preuves nécessaires doivent être créées avec les DAO existants.

Scénarios minimaux

  1. refus d'une connexion NULL ;
  2. création et destruction du service ;
  3. relation_service_free(NULL) ;
  4. création d'une relation sans preuve ;
  5. création d'une relation avec une preuve ;
  6. création d'une relation avec plusieurs preuves ;
  7. vérification des associations créées ;
  8. refus d'un service NULL ;
  9. refus d'un modèle NULL ;
  10. refus d'un tableau contenant un élément NULL ;
  11. refus d'un UUID de preuve invalide ;
  12. refus d'un UUID de preuve dupliqué dans la demande ;
  13. absence d'écriture après une erreur de validation ;
  14. rollback lorsque l'identifiant de relation existe déjà ;
  15. rollback lorsqu'une preuve demandée n'existe pas ;
  16. rollback complet lorsque la deuxième preuve n'existe pas ;
  17. disparition de la première association après le rollback du scénario
    précédent ;
  18. absence de relation après un échec d'association ;
  19. conservation des preuves après un rollback ;
  20. création après un échec précédent afin de vérifier que la connexion reste
    utilisable ;
  21. fonctionnement avec evidence_identifiers == NULL ;
  22. fonctionnement avec un tableau vide ;
  23. GError ** facultatif ;
  24. la destruction du service ne ferme pas la connexion ;
  25. tous les anciens tests restent valides.

Vérification de l'atomicité

Le test principal d'atomicité doit utiliser :

  • une relation valide ;
  • une première preuve existante ;
  • une deuxième preuve inexistante.

Après l'échec attendu, le test doit vérifier directement que :

relations             : 0 ligne pour l'UUID demandé
relation_preuves      : 0 association pour l'UUID demandé
preuves existantes    : inchangées

Makefile

Ajouter :

RELATION_SERVICE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
TEST_RELATION_SERVICE := tests/test_relation_service

Ajouter une cible dédiée compilant uniquement :

  • tests/test_relation_service.c ;
  • src/core/relation_service.c ;
  • src/dao/relation_dao.c ;
  • src/dao/relation_evidence_dao.c ;
  • les DAO utilisés pour préparer les données ;
  • les modèles nécessaires ;
  • les modules de base de données et de transaction nécessaires.

La cible ne doit inclure aucun autre fichier tests/test_*.c.

Ajouter $(TEST_RELATION_SERVICE) :

  • aux dépendances de make test ;
  • aux exécutables lancés par make test ;
  • aux fichiers supprimés par make clean.

La compilation doit conserver :

-std=c17 -Wall -Wextra -Wpedantic -Werror

Hors périmètre

Ce ticket ne couvre pas :

  • GTK ;
  • un formulaire de création ;
  • le graphe interactif ;
  • le déplacement des nœuds ;
  • la modification d'une relation ;
  • la suppression d'une relation ;
  • la validation ou le rejet d'une proposition automatique ;
  • les hypothèses ;
  • les observations ;
  • les relations issues d'un outil OSINT ;
  • une migration SQLite ;
  • les positions graphiques ;
  • l'historique d'audit.

Critères d'acceptation

  • RelationService est opaque.
  • Le service emprunte la connexion Database.
  • Les DAO internes sont possédés et libérés par le service.
  • La connexion n'est jamais fermée par le service.
  • Une relation sans preuve peut être créée.
  • Une relation peut être créée avec plusieurs preuves.
  • La création et les associations utilisent une seule transaction.
  • Aucune écriture partielle ne subsiste après un échec.
  • Les UUID de preuves sont validés avant la transaction.
  • Les doublons de la demande sont refusés avant la transaction.
  • Les écritures passent uniquement par les DAO existants.
  • Les erreurs des DAO sont contextualisées.
  • Un échec de rollback est signalé explicitement.
  • Tous les objets empruntés restent la propriété de l'appelant.
  • Aucun changement GTK n'est introduit.
  • Aucune migration SQLite n'est ajoutée.
  • Les tests ciblés passent.
  • make clean && make && make test réussit.
  • git diff --check ne retourne aucune erreur.
# Créer le service transactionnel de gestion des relations ## Contexte Les composants suivants existent désormais : - `RelationRecord` ; - `RelationDao` ; - `RelationEvidenceDao` ; - `EvidenceDao` ; - l'infrastructure SQLite et les transactions. Une relation peut être insérée dans la table `relations`, puis associée à une ou plusieurs preuves dans la table `relation_preuves`. Ces opérations ne doivent pas être exécutées séparément par l'interface GTK ou par le futur graphe interactif. Une erreur pendant l'association des preuves ne doit jamais laisser une relation partiellement créée. La création d'une relation et de toutes ses associations doit donc constituer une seule opération métier atomique. ## Objectif Créer un service métier `RelationService` chargé de coordonner : 1. la validation de la demande ; 2. l'ouverture d'une transaction SQLite ; 3. l'insertion de la relation ; 4. l'association de toutes les preuves demandées ; 5. la validation de la transaction ; 6. l'annulation complète en cas d'erreur. Le service doit rester indépendant de GTK. ## Fichiers Créer : ```text include/core/relation_service.h src/core/relation_service.c tests/test_relation_service.c ``` ## Type opaque ```c typedef struct RelationService RelationService; ``` Le service doit : - emprunter une connexion `Database` existante ; - créer et posséder ses DAO internes ; - ne jamais fermer la connexion empruntée ; - libérer ses DAO lors de sa destruction. ## Domaine d'erreur Créer : ```c typedef enum { RELATION_SERVICE_ERROR_INVALID_ARGUMENT, RELATION_SERVICE_ERROR_MEMORY, RELATION_SERVICE_ERROR_TRANSACTION_BEGIN, RELATION_SERVICE_ERROR_RELATION_INSERT, RELATION_SERVICE_ERROR_EVIDENCE_LINK, RELATION_SERVICE_ERROR_TRANSACTION_COMMIT, RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK } RelationServiceError; ``` Ajouter : ```c #define RELATION_SERVICE_ERROR \ relation_service_error_quark() GQuark relation_service_error_quark(void); ``` Les messages d'erreur doivent conserver un contexte métier compréhensible. Lorsqu'une erreur provient d'un DAO, le message d'origine doit être inclus dans le message remonté par le service. ## Construction ```c RelationService *relation_service_new( Database *database, GError **error ); ``` La fonction doit : - refuser une connexion `NULL` ; - créer les DAO nécessaires ; - nettoyer toutes les allocations si une création intermédiaire échoue ; - emprunter la connexion ; - accepter un `GError **` facultatif. ## Destruction ```c void relation_service_free( RelationService *relation_service ); ``` La fonction doit : - accepter `NULL` ; - libérer les DAO internes ; - ne jamais fermer la connexion `Database`. ## Création transactionnelle ```c gboolean relation_service_create( RelationService *relation_service, const RelationRecord *relation_record, const GPtrArray *evidence_identifiers, GError **error ); ``` ### Propriété des paramètres Le service emprunte : - `relation_record` ; - `evidence_identifiers` ; - toutes les chaînes UUID contenues dans le tableau. Il ne doit modifier ni libérer ces objets. `evidence_identifiers` peut être `NULL` ou vide. ### Validation avant transaction Avant toute écriture, la fonction doit : - refuser un service invalide ; - refuser un `RelationRecord` absent ; - vérifier que les accesseurs obligatoires du modèle ne retournent pas `NULL` ; - vérifier que chaque élément du tableau est une chaîne non `NULL` ; - vérifier que chaque chaîne est un UUID valide ; - refuser deux fois le même UUID de preuve dans la demande. Une erreur de validation ne doit ouvrir aucune transaction et ne doit produire aucune écriture. ### Transaction L'opération doit suivre exactement ce déroulement : ```text BEGIN ├── insertion de la relation ├── association de la première preuve ├── association de la deuxième preuve └── ... COMMIT ``` En cas d'échec : ```text erreur └── ROLLBACK ``` Le service doit utiliser l'infrastructure de transaction existante du projet. Il ne doit pas exécuter directement de requête SQL métier : les écritures doivent passer par `RelationDao` et `RelationEvidenceDao`. ### Atomicité Les comportements suivants sont obligatoires : - si l'insertion de la relation échoue, aucune association n'est créée ; - si la première association échoue, la relation est annulée ; - si une association intermédiaire échoue, les associations précédentes sont annulées ; - si le commit échoue, aucune création partielle ne doit être considérée comme réussie ; - un retour `TRUE` signifie que la relation et toutes les associations sont persistées ; - un retour `FALSE` signifie que l'opération complète a échoué. ### Échec du rollback Si l'opération métier échoue puis que le rollback échoue également : - la fonction doit retourner `FALSE` ; - l'erreur finale doit signaler `RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK` ; - le message doit mentionner l'erreur métier initiale et l'erreur du rollback. Le service ne doit jamais masquer silencieusement l'échec d'un rollback. ## Absence de preuves Une relation sans preuve associée reste autorisée par ce ticket. Cette décision évite d'introduire une règle que le schéma actuel ne peut pas nuancer entre : - relation saisie manuellement ; - relation importée ; - relation proposée automatiquement ; - relation validée ; - hypothèse. La gestion de ces catégories et l'obligation éventuelle d'une provenance feront l'objet d'un ticket métier séparé. ## Doublons dans la demande Le tableau suivant est invalide : ```text preuve-A preuve-B preuve-A ``` Le service doit détecter ce doublon avant d'ouvrir la transaction. Il ne doit pas attendre la contrainte SQLite pour découvrir cette erreur. ## Tests Créer : ```text tests/test_relation_service.c ``` Les tests doivent utiliser une base SQLite temporaire initialisée avec le schéma réel. Les entités et preuves nécessaires doivent être créées avec les DAO existants. ### Scénarios minimaux 1. refus d'une connexion `NULL` ; 2. création et destruction du service ; 3. `relation_service_free(NULL)` ; 4. création d'une relation sans preuve ; 5. création d'une relation avec une preuve ; 6. création d'une relation avec plusieurs preuves ; 7. vérification des associations créées ; 8. refus d'un service `NULL` ; 9. refus d'un modèle `NULL` ; 10. refus d'un tableau contenant un élément `NULL` ; 11. refus d'un UUID de preuve invalide ; 12. refus d'un UUID de preuve dupliqué dans la demande ; 13. absence d'écriture après une erreur de validation ; 14. rollback lorsque l'identifiant de relation existe déjà ; 15. rollback lorsqu'une preuve demandée n'existe pas ; 16. rollback complet lorsque la deuxième preuve n'existe pas ; 17. disparition de la première association après le rollback du scénario précédent ; 18. absence de relation après un échec d'association ; 19. conservation des preuves après un rollback ; 20. création après un échec précédent afin de vérifier que la connexion reste utilisable ; 21. fonctionnement avec `evidence_identifiers == NULL` ; 22. fonctionnement avec un tableau vide ; 23. `GError **` facultatif ; 24. la destruction du service ne ferme pas la connexion ; 25. tous les anciens tests restent valides. ### Vérification de l'atomicité Le test principal d'atomicité doit utiliser : - une relation valide ; - une première preuve existante ; - une deuxième preuve inexistante. Après l'échec attendu, le test doit vérifier directement que : ```text relations : 0 ligne pour l'UUID demandé relation_preuves : 0 association pour l'UUID demandé preuves existantes : inchangées ``` ## Makefile Ajouter : ```make RELATION_SERVICE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic TEST_RELATION_SERVICE := tests/test_relation_service ``` Ajouter une cible dédiée compilant uniquement : - `tests/test_relation_service.c` ; - `src/core/relation_service.c` ; - `src/dao/relation_dao.c` ; - `src/dao/relation_evidence_dao.c` ; - les DAO utilisés pour préparer les données ; - les modèles nécessaires ; - les modules de base de données et de transaction nécessaires. La cible ne doit inclure aucun autre fichier `tests/test_*.c`. Ajouter `$(TEST_RELATION_SERVICE)` : - aux dépendances de `make test` ; - aux exécutables lancés par `make test` ; - aux fichiers supprimés par `make clean`. La compilation doit conserver : ```text -std=c17 -Wall -Wextra -Wpedantic -Werror ``` ## Hors périmètre Ce ticket ne couvre pas : - GTK ; - un formulaire de création ; - le graphe interactif ; - le déplacement des nœuds ; - la modification d'une relation ; - la suppression d'une relation ; - la validation ou le rejet d'une proposition automatique ; - les hypothèses ; - les observations ; - les relations issues d'un outil OSINT ; - une migration SQLite ; - les positions graphiques ; - l'historique d'audit. ## Critères d'acceptation - [x] `RelationService` est opaque. - [x] Le service emprunte la connexion `Database`. - [x] Les DAO internes sont possédés et libérés par le service. - [x] La connexion n'est jamais fermée par le service. - [x] Une relation sans preuve peut être créée. - [x] Une relation peut être créée avec plusieurs preuves. - [x] La création et les associations utilisent une seule transaction. - [x] Aucune écriture partielle ne subsiste après un échec. - [x] Les UUID de preuves sont validés avant la transaction. - [x] Les doublons de la demande sont refusés avant la transaction. - [x] Les écritures passent uniquement par les DAO existants. - [x] Les erreurs des DAO sont contextualisées. - [x] Un échec de rollback est signalé explicitement. - [x] Tous les objets empruntés restent la propriété de l'appelant. - [x] Aucun changement GTK n'est introduit. - [x] Aucune migration SQLite n'est ajoutée. - [x] Les tests ciblés passent. - [x] `make clean && make && make test` réussit. - [x] `git diff --check` ne retourne aucune erreur.
fy59 closed this issue 2026-07-21 08:02:03 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: fy59/labfy-investigation#57
No description provided.