package service import ( "fmt" "time" "git.gm6.ru/icewind/seadoc/models" "git.gm6.ru/icewind/seadoc/store" jsonpatch "github.com/evanphx/json-patch/v5" "github.com/google/uuid" ) // VersionService — слой логики версионирования, использующий интерфейс VersionStorage. type VersionService struct { storage store.IVersionStorage // snapshotInterval (опционально) — через сколько версий делать full snapshot snapshotInterval int } // NewVersionService создаёт новый сервис func NewVersionService(storage store.IVersionStorage, snapshotInterval int) *VersionService { return &VersionService{storage: storage, snapshotInterval: snapshotInterval} } // SetSnapshotInterval задаёт через сколько версий сохранять snapshot (если >0) func (s *VersionService) SetSnapshotInterval(n int) { s.snapshotInterval = n } func (s *VersionService) CreateDocument(name string) (*models.Document, error) { doc := models.Document{ ID: uuid.New(), Name: name, } if err := s.storage.CreateDocument(&doc); err != nil { return nil, err } return &doc, nil } // SaveNewVersion создаёт новую версию документа (merge-patch или snapshot) func (s *VersionService) SaveNewVersion(docID uuid.UUID, newJSON []byte) (*models.Version, error) { lastVer, err := s.storage.GetLatestVersion(docID) if err != nil { return nil, err } // Если нет предыдущих версий — первая версия всегда snapshot if lastVer == nil { newVer := models.Version{ ID: uuid.New(), DocumentID: docID, ParentID: nil, IsSnapshot: true, Snapshot: newJSON, CreatedAt: time.Now(), } if err := s.storage.CreateVersion(&newVer); err != nil { return nil, err } _ = s.storage.UpdateLatestVersion(docID, newVer.ID) return &newVer, nil } // Если включён интервал снапшота — проверяем глубину до создания патча if s.snapshotInterval > 0 { depth, err := s.storage.CountSinceLastSnapshot(docID) if err != nil { return nil, err } // если глубина достигла порога, сохраняем snapshot ВМЕСТО патча if depth >= s.snapshotInterval { snapVer := models.Version{ ID: uuid.New(), DocumentID: docID, ParentID: &lastVer.ID, IsSnapshot: true, Snapshot: newJSON, CreatedAt: time.Now(), } if err := s.storage.CreateVersion(&snapVer); err != nil { return nil, err } _ = s.storage.UpdateLatestVersion(docID, snapVer.ID) return &snapVer, nil } } // Иначе: создаём merge-patch относительно старой версии var old []byte if lastVer.IsSnapshot { old = lastVer.Snapshot } else { old, err = s.ReconstructVersion(lastVer.ID) if err != nil { return nil, err } } patch, err := jsonpatch.CreateMergePatch(old, newJSON) if err != nil { return nil, err } newVer := models.Version{ ID: uuid.New(), DocumentID: docID, ParentID: &lastVer.ID, IsSnapshot: false, Patch: patch, CreatedAt: time.Now(), } if err := s.storage.CreateVersion(&newVer); err != nil { return nil, err } _ = s.storage.UpdateLatestVersion(docID, newVer.ID) return &newVer, nil } // ReconstructVersion восстанавливает полную JSON-версию по ID // Патчи — в формате JSON Merge Patch (RFC 7386) применяются последовательно к ближайшему snapshot. func (s *VersionService) ReconstructVersion(versionID uuid.UUID) ([]byte, error) { v, err := s.storage.GetVersionByID(versionID) if err != nil { return nil, err } // Если это snapshot — возвращаем сразу if v.IsSnapshot { return v.Snapshot, nil } // Собираем цепочку версий от ближайшего snapshot (включая snapshot) до целевой версии chain := []models.Version{} curr := v for { // prepend chain = append([]models.Version{*curr}, chain...) if curr.ParentID == nil { // дошли до корня break } parent, err := s.storage.GetParentVersion(curr) if err != nil { return nil, err } if parent == nil { break } curr = parent // Если parent — snapshot, добавим его и прервём цикл, т.к. от него будем применять if curr.IsSnapshot { chain = append([]models.Version{*curr}, chain...) break } } // Найдём snapshot в цепочке startIndex := -1 for i, ver := range chain { if ver.IsSnapshot { startIndex = i break } } if startIndex == -1 { return nil, fmt.Errorf("snapshot not found in version chain for %s", versionID) } // Начинаем с snapshot data := chain[startIndex].Snapshot // Применяем последовательно merge-patch'и от snapshot+1 до конца chain for i := startIndex + 1; i < len(chain); i++ { if len(chain[i].Patch) == 0 { continue } // MergePatch принимает исходный документ и merge-patch и возвращает новый документ newData, err := jsonpatch.MergePatch(data, chain[i].Patch) if err != nil { return nil, fmt.Errorf("apply merge patch at version %s: %w", chain[i].ID, err) } data = newData } return data, nil } // DiffVersions создаёт JSON Merge Patch между двумя версиями. // Возвращает JSON-патч (diff), показывающий, какие поля изменились. func (s *VersionService) DiffVersions(v1ID, v2ID uuid.UUID) ([]byte, error) { // Восстанавливаем обе версии полностью data1, err := s.ReconstructVersion(v1ID) if err != nil { return nil, err } data2, err := s.ReconstructVersion(v2ID) if err != nil { return nil, err } // Вычисляем разницу patch, err := jsonpatch.CreateMergePatch(data1, data2) if err != nil { return nil, err } return patch, nil }