diff options
author | Joe Chen <jc@unknwon.io> | 2023-02-08 13:55:54 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-08 13:55:54 +0800 |
commit | 8350daf505b837984397679f07ccc2324b4d2451 (patch) | |
tree | 19d3bd7b8dc8370a8973614a74537bddc5f618a6 /internal/db/users.go | |
parent | 133b9d90441008ee175e1f8e6369e06309e1392a (diff) |
refactor(db): merge relation stores into entity stores (#7341)
Diffstat (limited to 'internal/db/users.go')
-rw-r--r-- | internal/db/users.go | 176 |
1 files changed, 133 insertions, 43 deletions
diff --git a/internal/db/users.go b/internal/db/users.go index 51810dc7..b33772c0 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -32,8 +32,6 @@ import ( ) // UsersStore is the persistent interface for users. -// -// NOTE: All methods are sorted in alphabetical order. type UsersStore interface { // Authenticate validates username and password via given login source ID. It // returns ErrUserNotExist when the user was not found. @@ -47,29 +45,12 @@ type UsersStore interface { // When the "loginSourceID" is positive, it tries to authenticate via given // login source and creates a new user when not yet exists in the database. Authenticate(ctx context.Context, username, password string, loginSourceID int64) (*User, error) - // ChangeUsername changes the username of the given user and updates all - // references to the old username. It returns ErrNameNotAllowed if the given - // name or pattern of the name is not allowed as a username, or - // ErrUserAlreadyExist when another user with same name already exists. - ChangeUsername(ctx context.Context, userID int64, newUsername string) error - // Count returns the total number of users. - Count(ctx context.Context) int64 // Create creates a new user and persists to database. It returns // ErrNameNotAllowed if the given name or pattern of the name is not allowed as // a username, or ErrUserAlreadyExist when a user with same name already exists, // or ErrEmailAlreadyUsed if the email has been used by another user. Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error) - // DeleteCustomAvatar deletes the current user custom avatar and falls back to - // use look up avatar by email. - DeleteCustomAvatar(ctx context.Context, userID int64) error - // DeleteByID deletes the given user and all their resources. It returns - // ErrUserOwnRepos when the user still has repository ownership, or returns - // ErrUserHasOrgs when the user still has organization membership. It is more - // performant to skip rewriting the "authorized_keys" file for individual - // deletion in a batch operation. - DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error - // DeleteInactivated deletes all inactivated users. - DeleteInactivated() error + // GetByEmail returns the user (not organization) with given email. It ignores // records with unverified emails and returns ErrUserNotExist when not found. GetByEmail(ctx context.Context, email string) (*User, error) @@ -86,15 +67,45 @@ type UsersStore interface { // addresses (where email notifications are sent to) of users with given list of // usernames. Non-existing usernames are ignored. GetMailableEmailsByUsernames(ctx context.Context, usernames []string) ([]string, error) - // HasForkedRepository returns true if the user has forked given repository. - HasForkedRepository(ctx context.Context, userID, repoID int64) bool + // SearchByName returns a list of users whose username or full name matches the + // given keyword case-insensitively. Results are paginated by given page and + // page size, and sorted by the given order (e.g. "id DESC"). A total count of + // all results is also returned. If the order is not given, it's up to the + // database to decide. + SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*User, int64, error) + // IsUsernameUsed returns true if the given username has been used other than // the excluded user (a non-positive ID effectively meaning check against all // users). IsUsernameUsed(ctx context.Context, username string, excludeUserId int64) bool - // List returns a list of users. Results are paginated by given page and page - // size, and sorted by primary key (id) in ascending order. - List(ctx context.Context, page, pageSize int) ([]*User, error) + // ChangeUsername changes the username of the given user and updates all + // references to the old username. It returns ErrNameNotAllowed if the given + // name or pattern of the name is not allowed as a username, or + // ErrUserAlreadyExist when another user with same name already exists. + ChangeUsername(ctx context.Context, userID int64, newUsername string) error + // Update updates fields for the given user. + Update(ctx context.Context, userID int64, opts UpdateUserOptions) error + // UseCustomAvatar uses the given avatar as the user custom avatar. + UseCustomAvatar(ctx context.Context, userID int64, avatar []byte) error + + // DeleteCustomAvatar deletes the current user custom avatar and falls back to + // use look up avatar by email. + DeleteCustomAvatar(ctx context.Context, userID int64) error + // DeleteByID deletes the given user and all their resources. It returns + // ErrUserOwnRepos when the user still has repository ownership, or returns + // ErrUserHasOrgs when the user still has organization membership. It is more + // performant to skip rewriting the "authorized_keys" file for individual + // deletion in a batch operation. + DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error + // DeleteInactivated deletes all inactivated users. + DeleteInactivated() error + + // Follow marks the user to follow the other user. + Follow(ctx context.Context, userID, followID int64) error + // Unfollow removes the mark the user to follow the other user. + Unfollow(ctx context.Context, userID, followID int64) error + // IsFollowing returns true if the user is following the other user. + IsFollowing(ctx context.Context, userID, followID int64) bool // ListFollowers returns a list of users that are following the given user. // Results are paginated by given page and page size, and sorted by the time of // follow in descending order. @@ -103,16 +114,12 @@ type UsersStore interface { // Results are paginated by given page and page size, and sorted by the time of // follow in descending order. ListFollowings(ctx context.Context, userID int64, page, pageSize int) ([]*User, error) - // SearchByName returns a list of users whose username or full name matches the - // given keyword case-insensitively. Results are paginated by given page and - // page size, and sorted by the given order (e.g. "id DESC"). A total count of - // all results is also returned. If the order is not given, it's up to the - // database to decide. - SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*User, int64, error) - // Update updates fields for the given user. - Update(ctx context.Context, userID int64, opts UpdateUserOptions) error - // UseCustomAvatar uses the given avatar as the user custom avatar. - UseCustomAvatar(ctx context.Context, userID int64, avatar []byte) error + + // List returns a list of users. Results are paginated by given page and page + // size, and sorted by primary key (id) in ascending order. + List(ctx context.Context, page, pageSize int) ([]*User, error) + // Count returns the total number of users. + Count(ctx context.Context) int64 } var Users UsersStore @@ -650,6 +657,88 @@ func (db *users) DeleteInactivated() error { return nil } +func (*users) recountFollows(tx *gorm.DB, userID, followID int64) error { + /* + Equivalent SQL for PostgreSQL: + + UPDATE "user" + SET num_followers = ( + SELECT COUNT(*) FROM follow WHERE follow_id = @followID + ) + WHERE id = @followID + */ + err := tx.Model(&User{}). + Where("id = ?", followID). + Update( + "num_followers", + tx.Model(&Follow{}).Select("COUNT(*)").Where("follow_id = ?", followID), + ). + Error + if err != nil { + return errors.Wrap(err, `update "user.num_followers"`) + } + + /* + Equivalent SQL for PostgreSQL: + + UPDATE "user" + SET num_following = ( + SELECT COUNT(*) FROM follow WHERE user_id = @userID + ) + WHERE id = @userID + */ + err = tx.Model(&User{}). + Where("id = ?", userID). + Update( + "num_following", + tx.Model(&Follow{}).Select("COUNT(*)").Where("user_id = ?", userID), + ). + Error + if err != nil { + return errors.Wrap(err, `update "user.num_following"`) + } + return nil +} + +func (db *users) Follow(ctx context.Context, userID, followID int64) error { + if userID == followID { + return nil + } + + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + f := &Follow{ + UserID: userID, + FollowID: followID, + } + result := tx.FirstOrCreate(f, f) + if result.Error != nil { + return errors.Wrap(result.Error, "upsert") + } else if result.RowsAffected <= 0 { + return nil // Relation already exists + } + + return db.recountFollows(tx, userID, followID) + }) +} + +func (db *users) Unfollow(ctx context.Context, userID, followID int64) error { + if userID == followID { + return nil + } + + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Where("user_id = ? AND follow_id = ?", userID, followID).Delete(&Follow{}).Error + if err != nil { + return errors.Wrap(err, "delete") + } + return db.recountFollows(tx, userID, followID) + }) +} + +func (db *users) IsFollowing(ctx context.Context, userID, followID int64) bool { + return db.WithContext(ctx).Where("user_id = ? AND follow_id = ?", userID, followID).First(&Follow{}).Error == nil +} + var _ errutil.NotFound = (*ErrUserNotExist)(nil) type ErrUserNotExist struct { @@ -757,12 +846,6 @@ func (db *users) GetMailableEmailsByUsernames(ctx context.Context, usernames []s Find(&emails).Error } -func (db *users) HasForkedRepository(ctx context.Context, userID, repoID int64) bool { - var count int64 - db.WithContext(ctx).Model(new(Repository)).Where("owner_id = ? AND fork_id = ?", userID, repoID).Count(&count) - return count > 0 -} - func (db *users) IsUsernameUsed(ctx context.Context, username string, excludeUserId int64) bool { if username == "" { return false @@ -1181,7 +1264,7 @@ func (u *User) AvatarURL() string { // TODO(unknwon): This is also used in templates, which should be fixed by // having a dedicated type `template.User`. func (u *User) IsFollowing(followID int64) bool { - return Follows.IsFollowing(context.TODO(), u.ID, followID) + return Users.IsFollowing(context.TODO(), u.ID, followID) } // IsUserOrgOwner returns true if the user is in the owner team of the given @@ -1208,7 +1291,7 @@ func (u *User) IsPublicMember(orgId int64) bool { // TODO(unknwon): This is also used in templates, which should be fixed by // having a dedicated type `template.User`. func (u *User) GetOrganizationCount() (int64, error) { - return OrgUsers.CountByUser(context.TODO(), u.ID) + return Orgs.CountByUser(context.TODO(), u.ID) } // ShortName truncates and returns the username at most in given length. @@ -1336,3 +1419,10 @@ func isNameAllowed(names map[string]struct{}, patterns []string, name string) er func isUsernameAllowed(name string) error { return isNameAllowed(reservedUsernames, reservedUsernamePatterns, name) } + +// Follow represents relations of users and their followers. +type Follow struct { + ID int64 `gorm:"primaryKey"` + UserID int64 `xorm:"UNIQUE(follow)" gorm:"uniqueIndex:follow_user_follow_unique;not null"` + FollowID int64 `xorm:"UNIQUE(follow)" gorm:"uniqueIndex:follow_user_follow_unique;not null"` +} |