213 lines
6.0 KiB
Go
213 lines
6.0 KiB
Go
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
|
||
}
|