Files
seadoc/service/version.go
Vladimir V Maksimov 00b12cdf7d upd store
2025-11-06 16:50:12 +03:00

213 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}