package mysql import ( "context" "gorm.io/gorm" "wucher/internal/domain/auth" "wucher/internal/domain/user" ) type userRoleJoinRow struct { UserID []byte `gorm:"column:user_id"` RoleID []byte `gorm:"column:role_id"` RoleName string `gorm:"column:role_name"` RoleDescription string `gorm:"column:role_description"` } func normalizeAssignedRoleIDs(primaryRoleID []byte, roleIDs [][]byte) [][]byte { out := make([][]byte, 0, len(roleIDs)+1) seen := make(map[string]struct{}, len(roleIDs)+1) appendRole := func(roleID []byte) { if len(roleID) != 16 { return } key := string(roleID) if _, exists := seen[key]; exists { return } seen[key] = struct{}{} out = append(out, append([]byte(nil), roleID...)) } appendRole(primaryRoleID) for i := range roleIDs { appendRole(roleIDs[i]) } return out } func replaceUserRolesTx(ctx context.Context, tx *gorm.DB, userID, primaryRoleID []byte, roleIDs [][]byte) error { normalized := normalizeAssignedRoleIDs(primaryRoleID, roleIDs) if len(userID) != 16 { return nil } if err := tx.Where("user_id = ?", userID).Delete(&auth.UserRole{}).Error; err != nil { return err } if len(normalized) == 0 { return nil } actor := actorUserIDFromContext(ctx) items := make([]auth.UserRole, 0, len(normalized)) for i := range normalized { item := auth.UserRole{ UserID: userID, RoleID: normalized[i], } if len(actor) > 0 { item.CreatedBy = actor item.UpdatedBy = actor } items = append(items, item) } return tx.Create(&items).Error } func listUserRoleIDsTx(tx *gorm.DB, userID []byte) ([][]byte, error) { var rows []struct { RoleID []byte `gorm:"column:role_id"` } if err := tx.Table("user_roles"). Select("role_id"). Where("user_id = ?", userID). Find(&rows).Error; err != nil { return nil, err } roleIDs := make([][]byte, 0, len(rows)) for i := range rows { if len(rows[i].RoleID) != 16 { continue } roleIDs = append(roleIDs, append([]byte(nil), rows[i].RoleID...)) } return roleIDs, nil } func replacePrimaryUserRoleTx(ctx context.Context, tx *gorm.DB, userID, currentPrimaryRoleID, newPrimaryRoleID []byte) error { roleIDs, err := listUserRoleIDsTx(tx, userID) if err != nil { return err } filtered := make([][]byte, 0, len(roleIDs)) currentPrimaryKey := string(currentPrimaryRoleID) newPrimaryKey := string(newPrimaryRoleID) for i := range roleIDs { key := string(roleIDs[i]) if key == currentPrimaryKey || key == newPrimaryKey { continue } filtered = append(filtered, roleIDs[i]) } return replaceUserRolesTx(ctx, tx, userID, newPrimaryRoleID, filtered) } func loadUserRoleRows(ctx context.Context, db *gorm.DB, userIDs [][]byte) (map[string][]userRoleJoinRow, error) { rowsByUserID := make(map[string][]userRoleJoinRow, len(userIDs)) if len(userIDs) == 0 { return rowsByUserID, nil } var rows []userRoleJoinRow if err := db.WithContext(ctx). Table("user_roles ur"). Select("ur.user_id, roles.id AS role_id, roles.name AS role_name, roles.description AS role_description"). Joins("JOIN roles ON roles.id = ur.role_id"). Where("ur.user_id IN ?", userIDs). Order("roles.name ASC"). Find(&rows).Error; err != nil { return nil, err } for i := range rows { key := string(rows[i].UserID) rowsByUserID[key] = append(rowsByUserID[key], userRoleJoinRow{ UserID: append([]byte(nil), rows[i].UserID...), RoleID: append([]byte(nil), rows[i].RoleID...), RoleName: rows[i].RoleName, RoleDescription: rows[i].RoleDescription, }) } return rowsByUserID, nil } func orderUserRoleRows(primaryRoleID []byte, rows []userRoleJoinRow) []userRoleJoinRow { if len(rows) < 2 || len(primaryRoleID) != 16 { return rows } primaryKey := string(primaryRoleID) ordered := make([]userRoleJoinRow, 0, len(rows)) for i := range rows { if string(rows[i].RoleID) == primaryKey { ordered = append(ordered, rows[i]) break } } for i := range rows { if string(rows[i].RoleID) == primaryKey { continue } ordered = append(ordered, rows[i]) } return ordered } func loadRoleRowsByID(ctx context.Context, db *gorm.DB, roleIDs [][]byte) (map[string]userRoleJoinRow, error) { out := make(map[string]userRoleJoinRow, len(roleIDs)) if len(roleIDs) == 0 { return out, nil } var rows []struct { ID []byte `gorm:"column:id"` Name string `gorm:"column:name"` Description string `gorm:"column:description"` } if err := db.WithContext(ctx). Table("roles"). Select("id, name, description"). Where("id IN ?", roleIDs). Find(&rows).Error; err != nil { return nil, err } for i := range rows { out[string(rows[i].ID)] = userRoleJoinRow{ RoleID: append([]byte(nil), rows[i].ID...), RoleName: rows[i].Name, RoleDescription: rows[i].Description, } } return out, nil } func attachAuthUserRoles(ctx context.Context, db *gorm.DB, users ...*auth.User) error { userIDs := make([][]byte, 0, len(users)) missingPrimaryRoleIDs := make([][]byte, 0, len(users)) for i := range users { if users[i] == nil || len(users[i].ID) != 16 { continue } userIDs = append(userIDs, users[i].ID) if len(users[i].RoleID) == 16 { missingPrimaryRoleIDs = append(missingPrimaryRoleIDs, users[i].RoleID) } } rowsByUserID, err := loadUserRoleRows(ctx, db, userIDs) if err != nil { return err } roleRowsByID, err := loadRoleRowsByID(ctx, db, missingPrimaryRoleIDs) if err != nil { return err } for i := range users { if users[i] == nil { continue } rows := orderUserRoleRows(users[i].RoleID, rowsByUserID[string(users[i].ID)]) if len(rows) == 0 && len(users[i].RoleID) == 16 { if roleRow, ok := roleRowsByID[string(users[i].RoleID)]; ok { rows = []userRoleJoinRow{roleRow} } } users[i].RoleIDs = make([][]byte, 0, len(rows)) users[i].Roles = make([]auth.Role, 0, len(rows)) for j := range rows { users[i].RoleIDs = append(users[i].RoleIDs, append([]byte(nil), rows[j].RoleID...)) users[i].Roles = append(users[i].Roles, auth.Role{ ID: append([]byte(nil), rows[j].RoleID...), Name: rows[j].RoleName, Description: rows[j].RoleDescription, }) } } return nil } func attachUserDomainRoles(ctx context.Context, db *gorm.DB, users ...*user.User) error { userIDs := make([][]byte, 0, len(users)) for i := range users { if users[i] == nil || len(users[i].ID) != 16 { continue } userIDs = append(userIDs, users[i].ID) } rowsByUserID, err := loadUserRoleRows(ctx, db, userIDs) if err != nil { return err } for i := range users { if users[i] == nil { continue } rows := orderUserRoleRows(users[i].RoleID, rowsByUserID[string(users[i].ID)]) if len(rows) == 0 && len(users[i].RoleID) == 16 { rows = []userRoleJoinRow{{ RoleID: append([]byte(nil), users[i].RoleID...), RoleName: users[i].RoleName, RoleDescription: users[i].RoleDescription, }} } users[i].RoleIDs = make([][]byte, 0, len(rows)) users[i].Roles = make([]user.RoleSummary, 0, len(rows)) for j := range rows { users[i].RoleIDs = append(users[i].RoleIDs, append([]byte(nil), rows[j].RoleID...)) users[i].Roles = append(users[i].Roles, user.RoleSummary{ ID: append([]byte(nil), rows[j].RoleID...), Name: rows[j].RoleName, Description: rows[j].RoleDescription, }) } } return nil }