Files
fm_be/internal/repository/mysql/user_repo.go
2026-07-16 22:16:45 +07:00

227 lines
6.6 KiB
Go

package mysql
import (
"context"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"wucher/internal/domain/auth"
"wucher/internal/domain/user"
"wucher/internal/shared/pkg/uuidv7"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, u *user.User) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
u.CreatedBy = actor
u.UpdatedBy = actor
}
if err := tx.Create(u).Error; err != nil {
return err
}
return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs)
})
}
func (r *UserRepository) Update(ctx context.Context, u *user.User) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
u.UpdatedBy = actor
}
// Persist only user columns and avoid relation side-effects from preloaded structs.
if err := tx.Omit(clause.Associations).Save(u).Error; err != nil {
return err
}
// Keep legacy and current image columns in sync when image patch is requested.
if u.ProfileAttachmentPatchSet {
updates := map[string]any{
"profile_attachment_id": nil,
"image_attachment_id": nil,
}
if len(u.ProfileAttachmentID) == 16 {
updates["profile_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...)
updates["image_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...)
}
if err := tx.Model(&user.User{}).Where("id = ?", u.ID).Updates(updates).Error; err != nil {
return err
}
}
if u.RoleIDs == nil {
return nil
}
return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs)
})
}
func (r *UserRepository) Delete(ctx context.Context, id []byte) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user.User{}).
Where("id = ?", id).
Updates(map[string]any{
"image_attachment_id": nil,
"profile_attachment_id": nil,
}).Error; err != nil {
return err
}
if idStr, err := uuidv7.BytesToString(id); err == nil && idStr != "" {
if err := tx.Exec("DELETE FROM attachments WHERE ref_type = ? AND ref_id = ?", "user_profile", idStr).Error; err != nil {
return err
}
}
if err := tx.Where("user_id = ?", id).Delete(&auth.UserRole{}).Error; err != nil {
return err
}
return tx.Delete(&user.User{}, "id = ?", id).Error
})
}
func (r *UserRepository) GetByID(ctx context.Context, id []byte) (*user.User, error) {
var u user.User
err := r.db.WithContext(ctx).
Model(&user.User{}).
Select("users.*, roles.name AS role_name, roles.description AS role_description").
Joins("LEFT JOIN roles ON roles.id = users.role_id").
Preload("ProfileAttachment").
Preload("ProfileAttachment.File").
Where("users.id = ?", id).
First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
return nil, err
}
return &u, nil
}
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*user.User, error) {
var u user.User
err := r.db.WithContext(ctx).
Model(&user.User{}).
Select("users.*, roles.name AS role_name, roles.description AS role_description").
Joins("LEFT JOIN roles ON roles.id = users.role_id").
Preload("ProfileAttachment").
Preload("ProfileAttachment.File").
Where("users.email = ?", email).
First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
return nil, err
}
return &u, nil
}
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*user.User, error) {
var u user.User
err := r.db.WithContext(ctx).
Model(&user.User{}).
Select("users.*, roles.name AS role_name, roles.description AS role_description").
Joins("LEFT JOIN roles ON roles.id = users.role_id").
Preload("ProfileAttachment").
Preload("ProfileAttachment.File").
Where("users.username = ?", username).
First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
return nil, err
}
return &u, nil
}
func (r *UserRepository) GetLastNameByID(ctx context.Context, id []byte) (string, error) {
var u user.User
err := r.db.WithContext(ctx).
Table("users").
Select("last_name").
Where("id = ?", id).
First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
if err != nil {
return "", err
}
return u.LastName, nil
}
// GetShortNameByID returns the user's profile short name (pilot or technician
// 3-letter code, e.g. "GAN"), or an empty string when the user has no profile
// short name set. Callers apply their own fallback (e.g. initials).
func (r *UserRepository) GetShortNameByID(ctx context.Context, id []byte) (string, error) {
var out struct {
ShortName string `gorm:"column:short_name"`
}
err := r.db.WithContext(ctx).
Table("users u").
Select("COALESCE(NULLIF(pp.short_name, ''), NULLIF(tp.short_name, ''), '') AS short_name").
Joins("LEFT JOIN pilot_profiles pp ON pp.user_id = u.id").
Joins("LEFT JOIN technician_profiles tp ON tp.user_id = u.id").
Where("u.id = ?", id).
Take(&out).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
if err != nil {
return "", err
}
return out.ShortName, nil
}
func (r *UserRepository) List(ctx context.Context, filter string, sort string, limit, offset int) ([]user.User, int64, error) {
var users []user.User
var total int64
base := r.db.WithContext(ctx).
Model(&user.User{}).
Joins("LEFT JOIN roles ON roles.id = users.role_id")
if filter != "" {
like := "%" + filter + "%"
base = base.Where("users.email LIKE ? OR users.username LIKE ? OR users.first_name LIKE ? OR users.last_name LIKE ?", like, like, like, like)
}
query := base.Select("users.*, roles.name AS role_name, roles.description AS role_description")
query = query.Preload("ProfileAttachment").Preload("ProfileAttachment.File")
if sort != "" {
query = query.Order(sort)
}
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
if limit > 0 {
query = query.Limit(limit).Offset(offset)
}
if err := query.Find(&users).Error; err != nil {
return nil, 0, err
}
ptrs := make([]*user.User, 0, len(users))
for i := range users {
ptrs = append(ptrs, &users[i])
}
if err := attachUserDomainRoles(ctx, r.db, ptrs...); err != nil {
return nil, 0, err
}
return users, total, nil
}