10 KiB
Entity Versioning — Design Spec
Date : 2026-03-25 Entites concernees : Machine, Composant, Piece, Produit Approche : Extension du systeme AuditLog existant
Objectif
Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des entites principales et de restaurer n'importe quelle version anterieure, afin de ne jamais perdre de donnees.
Regles metier
Creation de version
- Chaque
createouupdatesur une entite incremente automatiquement le compteurversionde l'entite - Le numero de version est enregistre dans l'AuditLog correspondant (nouvelle colonne
version)
Restauration
- La restauration cree une nouvelle version (v+1) — on ne supprime jamais d'historique
- Le service
EntityVersionService::restore()cree manuellement un AuditLog avecaction = "restore"et le diff contientrestoredFromVersion: N - Important : le flush du restore declenche les AuditSubscribers, qui produiraient un
updateduplique. Pour eviter cela, l'entite porte un flag transitoire$skipAudit = trueque les subscribers verifient
Controle de squelette (Composant, Piece, Produit uniquement)
- Avant restauration, on compare le ModelType actuel avec celui du snapshot
- Meme squelette (ModelType) : restore complet — champs de base + slots + custom fields
- Squelette different : restore partiel — uniquement les champs de base (nom, description, reference, constructeurs, prix)
Controle d'integrite
- Avant restauration, on verifie que toutes les entites liees dans le snapshot existent encore en base :
- Composant : pieces selectionnees dans les slots, produits, sous-composants, constructeurs
- Piece : produits selectionnes dans les slots, constructeurs
- Produit : constructeurs
- Machine : site, liens composants/pieces/produits (MachineComponentLink, MachinePieceLink, MachineProductLink)
- Les entites manquantes generent des warnings affiches a l'utilisateur
- Les slots avec des entites supprimees sont restaures vides (sans selection)
- Pour les custom field values : restauration par
fieldId+ entite parente (pas par ID de la CustomFieldValue elle-meme, car un sync ModelType peut recreer les CFV avec des IDs differents) - Les controles d'integrite utilisent des requetes batch (
findBy(['id' => $ids])) plutot que des requetes individuelles par slot
Machines
- Pas de controle de squelette (pas de ModelType) : restauration toujours complete
- Controle d'integrite sur le site et les liens machine
- Machine n'a pas de champ
description(contrairement aux autres entites)
Permissions
- Consulter les versions :
ROLE_VIEWER - Restaurer une version :
ROLE_GESTIONNAIREet au-dessus
Modifications backend
1. Colonne version sur AuditLog
ALTER TABLE audit_logs ADD COLUMN version INT DEFAULT NULL;
Nullable car les AuditLogs existants n'ont pas de version.
2. Colonne version sur Machine
ALTER TABLE machine ADD COLUMN version INT NOT NULL DEFAULT 1;
Les entites Composant, Piece, Produit ont deja cette colonne.
3. Enrichissement des snapshots
Les Audit Subscribers doivent inclure dans le snapshot :
Composant :
{
"id": "cl...",
"name": "...",
"reference": "...",
"description": "...",
"prix": 100.00,
"typeComposant": { "id": "cl...", "name": "...", "code": "..." },
"product": { "id": "cl...", "name": "..." },
"constructeurIds": [{ "id": "cl...", "name": "..." }],
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
"pieceSlots": [
{ "id": "cl...", "typePieceId": "cl...", "selectedPieceId": "cl...", "quantity": 1, "position": 0 }
],
"subcomponentSlots": [
{ "id": "cl...", "alias": "...", "familyCode": "...", "typeComposantId": "cl...", "selectedComposantId": "cl...", "position": 0 }
],
"productSlots": [
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
],
"version": 3
}
Piece :
{
"id": "cl...",
"name": "...",
"reference": "...",
"description": "...",
"prix": 50.00,
"typePiece": { "id": "cl...", "name": "...", "code": "..." },
"product": { "id": "cl...", "name": "..." },
"constructeurIds": [{ "id": "cl...", "name": "..." }],
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
"productSlots": [
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
],
"version": 2
}
Produit :
{
"id": "cl...",
"name": "...",
"reference": "...",
"supplierPrice": 25.00,
"typeProduct": { "id": "cl...", "name": "...", "code": "..." },
"constructeurIds": [{ "id": "cl...", "name": "..." }],
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
"version": 1
}
Machine :
{
"id": "cl...",
"name": "...",
"reference": "...",
"prix": 1500.00,
"site": { "id": "cl...", "name": "..." },
"constructeurIds": [{ "id": "cl...", "name": "..." }],
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
"version": 4
}
4. Incrementation automatique de la version
Dans chaque Audit Subscriber, a chaque create/update :
- Appeler
$entity->incrementVersion() - Ecrire
$auditLog->setVersion($entity->getVersion())
Pour Machine, ajouter la methode incrementVersion() et la propriete version a l'entite.
5. Nouveaux endpoints — EntityVersionController
| Methode | Route | Description | Role |
|---|---|---|---|
| GET | /api/{entity}/{id}/versions |
Liste des versions | ROLE_VIEWER |
| GET | /api/{entity}/{id}/versions/{version}/preview |
Preview + controles avant restore | ROLE_GESTIONNAIRE |
| POST | /api/{entity}/{id}/versions/{version}/restore |
Execute la restauration | ROLE_GESTIONNAIRE |
{entity} = machines, composants, pieces, products
GET versions — Response :
{
"items": [
{
"version": 3,
"action": "update",
"createdAt": "2026-03-25T14:30:00+00:00",
"actor": { "id": "cl...", "label": "Jean Dupont" },
"diff": { "name": { "from": "Ancien", "to": "Nouveau" } }
}
],
"total": 3
}
GET preview — Response :
{
"version": 2,
"restoreMode": "full",
"diff": {
"name": { "current": "Nouveau", "restored": "Ancien" },
"reference": { "current": "REF-002", "restored": "REF-001" }
},
"warnings": [
{
"field": "pieceSlots[0].selectedPieceId",
"message": "La piece 'Roulement XY' (cl...) n'existe plus. Le slot sera restaure vide.",
"missingEntityId": "cl...",
"missingEntityName": "Roulement XY"
}
],
"snapshot": { }
}
restoreMode : "full" (meme squelette) ou "partial" (squelette different, champs de base uniquement).
POST restore — Response :
{
"success": true,
"newVersion": 6,
"restoredFromVersion": 2,
"restoreMode": "full",
"warnings": []
}
6. Service EntityVersionService
Service centralise pour la logique de versioning :
getVersions(string $entityType, string $entityId): array— liste des versions depuis AuditLoggetRestorePreview(string $entityType, string $entityId, int $version): array— controles + diffrestore(string $entityType, string $entityId, int $version): array— execution du restore
Methodes internes :
checkSkeletonCompatibility(object $entity, array $snapshot): string— retourne"full"ou"partial"checkIntegrity(string $entityType, array $snapshot): array— retourne les warningsapplyRestore(object $entity, array $snapshot, string $mode): void— applique les changements
Modifications frontend
1. Composant EntityVersionList.vue
Composant reutilisable affiche dans un onglet "Versions" sur les pages de detail.
Props :
entityType: 'machines' | 'composants' | 'pieces' | 'products'entityId: string
Affichage :
- Tableau : version, date, auteur, action, diff resume
- Badge "Actuelle" sur la version la plus recente
- Bouton "Restaurer" sur chaque ligne (sauf version actuelle), visible uniquement pour ROLE_GESTIONNAIRE+
2. Composant VersionRestoreModal.vue
Modal de confirmation avec preview.
Props :
entityType,entityId,version(cible)previewData(resultat du GET preview)
Affichage :
- Indicateur de mode : "Restauration complete" ou "Restauration partielle"
- Diff visuel : champs qui changent (valeur actuelle -> valeur restauree)
- Warnings en alerte orange pour les entites manquantes
- Boutons "Confirmer la restauration" / "Annuler"
3. Composable useEntityVersions.ts
interface Deps {
entityType: MaybeRef<string>
entityId: MaybeRef<string>
}
export function useEntityVersions(deps: Deps) {
// fetchVersions() — GET /api/{entity}/{id}/versions
// fetchPreview(version) — GET /api/{entity}/{id}/versions/{version}/preview
// restore(version) — POST /api/{entity}/{id}/versions/{version}/restore
}
4. Integration dans les pages de detail
Ajouter un onglet "Versions" dans les pages :
pages/machines/[id].vuepages/composants/[id].vuepages/pieces/[id].vuepages/products/[id].vue
L'onglet affiche EntityVersionList qui gere l'ouverture de VersionRestoreModal.
Migration
Une seule migration PostgreSQL :
-- Colonne version sur audit_logs
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS version INT DEFAULT NULL;
-- Colonne version sur machine
ALTER TABLE machine ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1;
-- Index pour requetes par version
CREATE INDEX IF NOT EXISTS idx_audit_entity_version ON audit_logs (entity_type, entity_id, version);
Ce qui change (breaking)
- Piece snapshot : le champ legacy
productIds(ancien JSON) est remplace parproductSlots(tables normalisees). Les anciens AuditLogs conserventproductIdsdans leur snapshot mais les nouveaux ne l'auront plus. Le restore utiliseproductSlotsexclusivement.
Ce qui ne change PAS
- L'onglet/page d'historique existant (
EntityHistoryController) reste inchange - Les AuditLogs existants (sans version) continuent de fonctionner
- Le mecanisme d'audit automatique via les Subscribers reste identique, juste enrichi
- Les documents ne sont pas versionnes (hors scope)