docs(versioning) : add entity versioning design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
303
docs/superpowers/specs/2026-03-25-entity-versioning-design.md
Normal file
303
docs/superpowers/specs/2026-03-25-entity-versioning-design.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# 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
|
||||
- L'AuditLog de la restauration a `action = "restore"` et le diff contient `restoredFromVersion: N`
|
||||
|
||||
### 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)
|
||||
|
||||
### Machines
|
||||
- Pas de controle de squelette (pas de ModelType) : restauration toujours complete
|
||||
- Controle d'integrite sur le site et les liens machine
|
||||
|
||||
### Permissions
|
||||
- Consulter les versions : `ROLE_VIEWER`
|
||||
- Restaurer une version : `ROLE_GESTIONNAIRE` et au-dessus
|
||||
|
||||
---
|
||||
|
||||
## Modifications backend
|
||||
|
||||
### 1. Colonne `version` sur AuditLog
|
||||
|
||||
```sql
|
||||
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
|
||||
|
||||
```sql
|
||||
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 :**
|
||||
```json
|
||||
{
|
||||
"id": "cl...",
|
||||
"name": "...",
|
||||
"reference": "...",
|
||||
"description": "...",
|
||||
"prix": 100.00,
|
||||
"typeComposant": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"product": { "id": "cl...", "name": "..." },
|
||||
"constructeurs": [{ "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 :**
|
||||
```json
|
||||
{
|
||||
"id": "cl...",
|
||||
"name": "...",
|
||||
"reference": "...",
|
||||
"description": "...",
|
||||
"prix": 50.00,
|
||||
"typePiece": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"product": { "id": "cl...", "name": "..." },
|
||||
"constructeurs": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"productSlots": [
|
||||
{ "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 }
|
||||
],
|
||||
"version": 2
|
||||
}
|
||||
```
|
||||
|
||||
**Produit :**
|
||||
```json
|
||||
{
|
||||
"id": "cl...",
|
||||
"name": "...",
|
||||
"reference": "...",
|
||||
"supplierPrice": 25.00,
|
||||
"typeProduct": { "id": "cl...", "name": "...", "code": "..." },
|
||||
"constructeurs": [{ "id": "cl...", "name": "..." }],
|
||||
"customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Machine :**
|
||||
```json
|
||||
{
|
||||
"id": "cl...",
|
||||
"name": "...",
|
||||
"reference": "...",
|
||||
"description": "...",
|
||||
"site": { "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 :**
|
||||
```json
|
||||
{
|
||||
"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 :**
|
||||
```json
|
||||
{
|
||||
"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 :**
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
```typescript
|
||||
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 :
|
||||
|
||||
```sql
|
||||
-- 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 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)
|
||||
Reference in New Issue
Block a user