Files
Inventory/docs/superpowers/specs/2026-03-25-entity-versioning-design.md
Matthieu 476060cf7d WIP
2026-03-31 17:57:59 +02:00

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 create ou update sur une entite incremente automatiquement le compteur version de 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 avec action = "restore" et le diff contient restoredFromVersion: N
  • Important : le flush du restore declenche les AuditSubscribers, qui produiraient un update duplique. Pour eviter cela, l'entite porte un flag transitoire $skipAudit = true que 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_GESTIONNAIRE et 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 :

  1. Appeler $entity->incrementVersion()
  2. 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 AuditLog
  • getRestorePreview(string $entityType, string $entityId, int $version): array — controles + diff
  • restore(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 warnings
  • applyRestore(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].vue
  • pages/composants/[id].vue
  • pages/pieces/[id].vue
  • pages/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 par productSlots (tables normalisees). Les anciens AuditLogs conservent productIds dans leur snapshot mais les nouveaux ne l'auront plus. Le restore utilise productSlots exclusivement.

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)