197 lines
5.4 KiB
Go
197 lines
5.4 KiB
Go
package repository
|
|
|
|
import (
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type ProjectRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewProjectRepository(db *gorm.DB) *ProjectRepository {
|
|
return &ProjectRepository{db: db}
|
|
}
|
|
|
|
func (r *ProjectRepository) Create(project *models.Project) error {
|
|
return r.db.Create(project).Error
|
|
}
|
|
|
|
func (r *ProjectRepository) Update(project *models.Project) error {
|
|
return r.db.Save(project).Error
|
|
}
|
|
|
|
func (r *ProjectRepository) UpsertByUUID(project *models.Project) error {
|
|
if err := r.db.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "uuid"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"owner_username",
|
|
"code",
|
|
"variant",
|
|
"name",
|
|
"tracker_url",
|
|
"is_active",
|
|
"is_system",
|
|
"updated_at",
|
|
}),
|
|
}).Create(project).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure caller always gets canonical server ID.
|
|
var persisted models.Project
|
|
if err := r.db.Where("uuid = ?", project.UUID).First(&persisted).Error; err != nil {
|
|
return err
|
|
}
|
|
project.ID = persisted.ID
|
|
return nil
|
|
}
|
|
|
|
func (r *ProjectRepository) GetByUUID(uuid string) (*models.Project, error) {
|
|
var project models.Project
|
|
if err := r.db.Where("uuid = ?", uuid).First(&project).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &project, nil
|
|
}
|
|
|
|
func (r *ProjectRepository) GetSystemByOwner(ownerUsername string) (*models.Project, error) {
|
|
var project models.Project
|
|
if err := r.db.Where("owner_username = ? AND is_system = ? AND name = ?", ownerUsername, true, "Без проекта").
|
|
First(&project).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &project, nil
|
|
}
|
|
|
|
func (r *ProjectRepository) List(offset, limit int, includeArchived bool) ([]models.Project, int64, error) {
|
|
var projects []models.Project
|
|
var total int64
|
|
|
|
query := r.db.Model(&models.Project{})
|
|
if !includeArchived {
|
|
query = query.Where("is_active = ?", true)
|
|
}
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&projects).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return projects, total, nil
|
|
}
|
|
|
|
func (r *ProjectRepository) ListByOwner(ownerUsername string, includeArchived bool) ([]models.Project, error) {
|
|
var projects []models.Project
|
|
|
|
query := r.db.Where("owner_username = ?", ownerUsername)
|
|
if !includeArchived {
|
|
query = query.Where("is_active = ?", true)
|
|
}
|
|
|
|
if err := query.Order("created_at DESC").Find(&projects).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return projects, nil
|
|
}
|
|
|
|
func (r *ProjectRepository) Archive(uuid string) error {
|
|
return r.db.Model(&models.Project{}).Where("uuid = ?", uuid).Update("is_active", false).Error
|
|
}
|
|
|
|
func (r *ProjectRepository) Reactivate(uuid string) error {
|
|
return r.db.Model(&models.Project{}).Where("uuid = ?", uuid).Update("is_active", true).Error
|
|
}
|
|
|
|
// PurgeEmptyNamelessProjects removes service-trash projects that have no configurations attached:
|
|
// 1) projects with empty names;
|
|
// 2) duplicate "Без проекта" rows without configurations (case-insensitive, trimmed).
|
|
func (r *ProjectRepository) PurgeEmptyNamelessProjects() (int64, error) {
|
|
tx := r.db.Exec(`
|
|
DELETE p
|
|
FROM qt_projects p
|
|
WHERE (
|
|
TRIM(COALESCE(p.name, '')) = ''
|
|
OR LOWER(TRIM(COALESCE(p.name, ''))) = LOWER('Без проекта')
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM qt_configurations c
|
|
WHERE c.project_uuid = p.uuid
|
|
)`)
|
|
return tx.RowsAffected, tx.Error
|
|
}
|
|
|
|
// EnsureSystemProjectsAndBackfillConfigurations ensures there is a single shared system project
|
|
// named "Без проекта", reassigns orphan/legacy links to it and removes duplicates.
|
|
func (r *ProjectRepository) EnsureSystemProjectsAndBackfillConfigurations() error {
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
type row struct {
|
|
UUID string `gorm:"column:uuid"`
|
|
}
|
|
var canonical row
|
|
err := tx.Raw(`
|
|
SELECT uuid
|
|
FROM qt_projects
|
|
WHERE LOWER(TRIM(COALESCE(name, ''))) = LOWER('Без проекта')
|
|
AND is_system = TRUE
|
|
ORDER BY CASE WHEN TRIM(COALESCE(owner_username, '')) = '' THEN 0 ELSE 1 END, created_at ASC, id ASC
|
|
LIMIT 1`).Scan(&canonical).Error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if canonical.UUID == "" {
|
|
if err := tx.Exec(`
|
|
INSERT INTO qt_projects (uuid, owner_username, name, is_active, is_system, created_at, updated_at)
|
|
VALUES (UUID(), '', 'Без проекта', TRUE, TRUE, NOW(), NOW())`).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Raw(`
|
|
SELECT uuid
|
|
FROM qt_projects
|
|
WHERE LOWER(TRIM(COALESCE(name, ''))) = LOWER('Без проекта')
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT 1`).Scan(&canonical).Error; err != nil {
|
|
return err
|
|
}
|
|
if canonical.UUID == "" {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
}
|
|
|
|
if err := tx.Exec(`
|
|
UPDATE qt_projects
|
|
SET name = 'Без проекта',
|
|
is_active = TRUE,
|
|
is_system = TRUE
|
|
WHERE uuid = ?`, canonical.UUID).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Exec(`
|
|
UPDATE qt_configurations
|
|
SET project_uuid = ?
|
|
WHERE project_uuid IS NULL OR project_uuid = ''`, canonical.UUID).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Exec(`
|
|
UPDATE qt_configurations c
|
|
JOIN qt_projects p ON p.uuid = c.project_uuid
|
|
SET c.project_uuid = ?
|
|
WHERE LOWER(TRIM(COALESCE(p.name, ''))) = LOWER('Без проекта')
|
|
AND p.uuid <> ?`, canonical.UUID, canonical.UUID).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Exec(`
|
|
DELETE FROM qt_projects
|
|
WHERE LOWER(TRIM(COALESCE(name, ''))) = LOWER('Без проекта')
|
|
AND uuid <> ?`, canonical.UUID).Error
|
|
})
|
|
}
|