package persistence import ( "strings" "RenewCMS/internal/domain/role" "RenewCMS/internal/infrastructure/persistence/mappers" entity "RenewCMS/internal/infrastructure/persistence/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) type RoleRepository struct { db *gorm.DB } func NewRoleRepository(db *gorm.DB) *RoleRepository { return &RoleRepository{db} } func (r *RoleRepository) grantRows(roleID uint32) ([]entity.RolePermission, error) { var rows []entity.RolePermission if err := r.db.Where("role_id = ?", roleID).Order("id asc").Find(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *RoleRepository) withGrants(found entity.Role) (role.Role, error) { rows, err := r.grantRows(found.ID) if err != nil { return role.Role{}, err } return mappers.RoleToDomain(found, rows), nil } func (r *RoleRepository) List() ([]role.Role, error) { var found []entity.Role if err := r.db.Order("id asc").Find(&found).Error; err != nil { return nil, err } var rows []entity.RolePermission if err := r.db.Order("id asc").Find(&rows).Error; err != nil { return nil, err } rowsByRole := make(map[uint32][]entity.RolePermission, len(found)) for _, row := range rows { rowsByRole[row.RoleID] = append(rowsByRole[row.RoleID], row) } roles := make([]role.Role, 0, len(found)) for _, row := range found { roles = append(roles, mappers.RoleToDomain(row, rowsByRole[row.ID])) } return roles, nil } func (r *RoleRepository) Get(id uint32) (role.Role, error) { var found entity.Role if err := r.db.First(&found, id).Error; err != nil { return role.Role{}, mapNotFoundErr(err, role.ErrRoleNotFound) } return r.withGrants(found) } func (r *RoleRepository) GetByName(name string) (role.Role, error) { var found entity.Role if err := r.db.Where("name = ?", name).Order("built_in DESC, id ASC").First(&found).Error; err != nil { return role.Role{}, mapNotFoundErr(err, role.ErrRoleNotFound) } return r.withGrants(found) } func ensureNameUnused(tx *gorm.DB, name string, excludedID uint32) error { var taken int64 query := tx.Model(&entity.Role{}).Where("LOWER(name) = LOWER(?)", strings.TrimSpace(name)) if excludedID != 0 { query = query.Where("id <> ?", excludedID) } if err := query.Count(&taken).Error; err != nil { return err } if taken > 0 { return role.ErrRoleNameTaken } return nil } func (r *RoleRepository) Create(created role.Role) (role.Role, error) { model := mappers.RoleToModel(created) model.ID = 0 err := r.db.Transaction(func(tx *gorm.DB) error { if err := ensureNameUnused(tx, created.Name, 0); err != nil { return err } if err := tx.Create(&model).Error; err != nil { return err } rows := mappers.GrantsToModel(model.ID, created.Grants) if len(rows) == 0 { return nil } return tx.Create(&rows).Error }) if err != nil { return role.Role{}, err } return r.withGrants(model) } func (r *RoleRepository) Save(saved role.Role) (role.Role, error) { err := r.db.Transaction(func(tx *gorm.DB) error { if err := tx.First(&entity.Role{}, saved.ID).Error; err != nil { return mapNotFoundErr(err, role.ErrRoleNotFound) } if err := ensureNameUnused(tx, saved.Name, saved.ID); err != nil { return err } update := tx.Model(&entity.Role{}). Where("id = ? AND version = ?", saved.ID, saved.Version). Updates(map[string]any{"name": saved.Name, "description": saved.Description, "version": saved.Version + 1, "updated_at": mappers.RoleToModel(saved).EditedAt, "updated_by": saved.UpdatedBy}) if update.Error != nil { return update.Error } if update.RowsAffected == 0 { return role.ErrRoleConflict } if err := tx.Where("role_id = ?", saved.ID).Delete(&entity.RolePermission{}).Error; err != nil { return err } rows := mappers.GrantsToModel(saved.ID, saved.Grants) if len(rows) == 0 { return nil } return tx.Create(&rows).Error }) if err != nil { return role.Role{}, err } return r.Get(saved.ID) } func (r *RoleRepository) Delete(id uint32) error { return r.db.Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&entity.Role{}, id).Error; err != nil { return mapNotFoundErr(err, role.ErrRoleNotFound) } members, err := countMembers(tx, id) if err != nil { return err } if members > 0 { return role.ErrRoleHasMembers } if err := tx.Where("role_id = ?", id).Delete(&entity.RolePermission{}).Error; err != nil { return err } return tx.Delete(&entity.Role{}, id).Error }) } func countMembers(tx *gorm.DB, id uint32) (int64, error) { var users, invites int64 if err := tx.Model(&entity.User{}).Where("role_id = ?", id).Count(&users).Error; err != nil { return 0, err } if err := tx.Model(&entity.Invite{}).Where("role_id = ? AND accepted_at IS NULL", id).Count(&invites).Error; err != nil { return 0, err } return users + invites, nil } type memberRow struct { RoleID uint32 Members int } func (r *RoleRepository) MemberCounts() (map[uint32]int, error) { var users, invites []memberRow if err := r.db.Model(&entity.User{}).Select("role_id, COUNT(*) AS members").Where("role_id IS NOT NULL").Group("role_id").Scan(&users).Error; err != nil { return nil, err } if err := r.db.Model(&entity.Invite{}).Select("role_id, COUNT(*) AS members").Where("role_id IS NOT NULL AND accepted_at IS NULL").Group("role_id").Scan(&invites).Error; err != nil { return nil, err } counts := map[uint32]int{} for _, row := range append(users, invites...) { if row.RoleID != 0 { counts[row.RoleID] += row.Members } } return counts, nil } func (r *RoleRepository) ReassignMembers(from uint32, to uint32) (int, error) { moved := 0 err := r.db.Transaction(func(tx *gorm.DB) error { target := "EXISTS (SELECT 1 FROM roles WHERE id = ?)" users := tx.Model(&entity.User{}).Where("role_id = ? AND "+target, from, to).UpdateColumn("role_id", to) if users.Error != nil { return users.Error } invites := tx.Model(&entity.Invite{}).Where("role_id = ? AND accepted_at IS NULL AND "+target, from, to).UpdateColumn("role_id", to) if invites.Error != nil { return invites.Error } moved = int(users.RowsAffected + invites.RowsAffected) if moved > 0 { return nil } var targets int64 if err := tx.Model(&entity.Role{}).Where("id = ?", to).Count(&targets).Error; err != nil { return err } if targets == 0 { return role.ErrInvalidReassign } return nil }) if err != nil { return 0, err } return moved, nil } func (r *RoleRepository) RemoveTypedGrants(typeKey string) (int, error) { affected := 0 err := r.db.Transaction(func(tx *gorm.DB) error { var roleIDs []uint32 doomed := role.TypedKeys(typeKey) if err := tx.Model(&entity.RolePermission{}).Where("permission_key IN ?", doomed).Distinct().Pluck("role_id", &roleIDs).Error; err != nil { return err } if len(roleIDs) == 0 { return nil } if err := tx.Where("permission_key IN ?", doomed).Delete(&entity.RolePermission{}).Error; err != nil { return err } affected = len(roleIDs) return tx.Model(&entity.Role{}).Where("id IN ?", roleIDs).UpdateColumn("version", gorm.Expr("version + 1")).Error }) if err != nil { return 0, err } return affected, nil } var _ role.Repository = &RoleRepository{}