Add configuration revisions system and project variant deletion
Features: - Configuration versioning: immutable snapshots in local_configuration_versions - Revisions UI: /configs/:uuid/revisions page to view version history - Clone from version: ability to clone configuration from specific revision - Project variant deletion: DELETE /api/projects/:uuid endpoint - Updated CLAUDE.md with new architecture details and endpoints Architecture updates: - local_configuration_versions table for immutable snapshots - Version tracking on each configuration save - Rollback capability to previous versions - Variant deletion with main variant protection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -442,14 +442,14 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
||||
|
||||
// GetByUUIDNoAuth returns configuration without ownership check
|
||||
func (s *LocalConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) {
|
||||
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
||||
if err != nil {
|
||||
var localCfg localdb.LocalConfiguration
|
||||
if err := s.localDB.DB().Preload("CurrentVersion").Where("uuid = ?", uuid).First(&localCfg).Error; err != nil {
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
if !localCfg.IsActive {
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
return localdb.LocalToConfiguration(localCfg), nil
|
||||
return localdb.LocalToConfiguration(&localCfg), nil
|
||||
}
|
||||
|
||||
// UpdateNoAuth updates configuration without ownership check
|
||||
@@ -571,10 +571,30 @@ func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName strin
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) CloneNoAuthToProject(configUUID string, newName string, ownerUsername string, projectUUID *string) (*models.Configuration, error) {
|
||||
return s.CloneNoAuthToProjectFromVersion(configUUID, newName, ownerUsername, projectUUID, 0)
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) CloneNoAuthToProjectFromVersion(configUUID string, newName string, ownerUsername string, projectUUID *string, fromVersion int) (*models.Configuration, error) {
|
||||
original, err := s.GetByUUIDNoAuth(configUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If fromVersion specified, use snapshot from that version
|
||||
if fromVersion > 0 {
|
||||
version, vErr := s.GetVersion(configUUID, fromVersion)
|
||||
if vErr != nil {
|
||||
return nil, vErr
|
||||
}
|
||||
snapshot, decErr := s.decodeConfigurationSnapshot(version.Data)
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("decode version snapshot for clone: %w", decErr)
|
||||
}
|
||||
snapshotCfg := localdb.LocalToConfiguration(snapshot)
|
||||
original = snapshotCfg
|
||||
original.UUID = configUUID // preserve original UUID for project resolution
|
||||
}
|
||||
|
||||
resolvedProjectUUID := original.ProjectUUID
|
||||
if projectUUID != nil {
|
||||
resolvedProjectUUID, err = s.resolveProjectUUID(ownerUsername, projectUUID)
|
||||
@@ -896,6 +916,7 @@ func (s *LocalConfigurationService) createWithVersion(localCfg *localdb.LocalCon
|
||||
return fmt.Errorf("set current version id: %w", err)
|
||||
}
|
||||
localCfg.CurrentVersionID = &version.ID
|
||||
localCfg.CurrentVersion = version
|
||||
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "create", version, createdBy); err != nil {
|
||||
return fmt.Errorf("enqueue create pending change: %w", err)
|
||||
@@ -937,6 +958,7 @@ func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.
|
||||
return fmt.Errorf("update current version id: %w", err)
|
||||
}
|
||||
localCfg.CurrentVersionID = &version.ID
|
||||
localCfg.CurrentVersion = version
|
||||
|
||||
cfg = localdb.LocalToConfiguration(localCfg)
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, operation, version, createdBy); err != nil {
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProjectNotFound = errors.New("project not found")
|
||||
ErrProjectForbidden = errors.New("access to project forbidden")
|
||||
ErrProjectCodeExists = errors.New("project code and variant already exist")
|
||||
ErrProjectNotFound = errors.New("project not found")
|
||||
ErrProjectForbidden = errors.New("access to project forbidden")
|
||||
ErrProjectCodeExists = errors.New("project code and variant already exist")
|
||||
ErrCannotDeleteMainVariant = errors.New("cannot delete main variant")
|
||||
)
|
||||
|
||||
type ProjectService struct {
|
||||
@@ -173,6 +174,17 @@ func (s *ProjectService) Reactivate(projectUUID, ownerUsername string) error {
|
||||
return s.setProjectActive(projectUUID, ownerUsername, true)
|
||||
}
|
||||
|
||||
func (s *ProjectService) DeleteVariant(projectUUID, ownerUsername string) error {
|
||||
localProject, err := s.localDB.GetProjectByUUID(projectUUID)
|
||||
if err != nil {
|
||||
return ErrProjectNotFound
|
||||
}
|
||||
if strings.TrimSpace(localProject.Variant) == "" {
|
||||
return ErrCannotDeleteMainVariant
|
||||
}
|
||||
return s.setProjectActive(projectUUID, ownerUsername, false)
|
||||
}
|
||||
|
||||
func (s *ProjectService) setProjectActive(projectUUID, ownerUsername string, isActive bool) error {
|
||||
return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
|
||||
var project localdb.LocalProject
|
||||
@@ -263,8 +275,8 @@ func (s *ProjectService) ListConfigurations(projectUUID, ownerUsername, status s
|
||||
}, nil
|
||||
}
|
||||
|
||||
localConfigs, err := s.localDB.GetConfigurations()
|
||||
if err != nil {
|
||||
var localConfigs []localdb.LocalConfiguration
|
||||
if err := s.localDB.DB().Preload("CurrentVersion").Order("created_at DESC").Find(&localConfigs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user