From d01765b7454b44e0331e0a30b71f4f1dec210f23 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 7 Jul 2025 13:34:59 +0530 Subject: [PATCH 1/3] implement leaderboard feature --- internal/app/router.go | 4 +++ internal/app/user/domain.go | 8 +++++ internal/app/user/handler.go | 30 +++++++++++++++++ internal/app/user/service.go | 27 +++++++++++++++ internal/repository/domain.go | 8 +++++ internal/repository/user.go | 62 +++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+) diff --git a/internal/app/router.go b/internal/app/router.go index 612efd1..02f9c7e 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -26,5 +26,9 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.GetAllUsersRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index 471f0b1..cf0e527 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -45,3 +45,11 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type LeaderboardUser struct { + Id int `db:"id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + Rank int `db:"rank"` +} diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 00bcd51..3f085b8 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -15,6 +15,8 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) + GetAllUsersRank(w http.ResponseWriter, r *http.Request) + GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -44,3 +46,31 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } + +func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + leaderboard, err := h.userService.GetAllUsersRank(ctx) + if err != nil { + slog.Error("failed to get all users rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "leaderboard fetched successfully", leaderboard) +} + +func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx) + if err != nil { + slog.Error("failed to get current user rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 6dad743..0aed35e 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,6 +19,8 @@ type Service interface { CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error + GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) } func NewService(userRepository repository.UserRepository) Service { @@ -98,3 +100,28 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran return nil } + +func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { + leaderboard, err := s.userRepository.GetAllUsersRank(ctx, nil) + if err != nil { + slog.Error("error obtaining all users rank", "error", err) + return nil, err + } + + serviceLeaderboard := make([]LeaderboardUser, len(leaderboard)) + for i, l := range leaderboard { + serviceLeaderboard[i] = LeaderboardUser((l)) + } + + return serviceLeaderboard, nil +} + +func (s *service) GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil) + if err != nil { + slog.Error("error obtaining current user rank", "error", err) + return LeaderboardUser{}, err + } + + return LeaderboardUser(currentUserRank), nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 1a94621..d52494b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -77,3 +77,11 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type LeaderboardUser struct { + Id int `db:"id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + Rank int `db:"rank"` +} diff --git a/internal/repository/user.go b/internal/repository/user.go index a3a748a..82f660a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,6 +9,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type userRepository struct { @@ -23,6 +24,8 @@ type UserRepository interface { UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error + GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -51,6 +54,31 @@ const ( getAllUsersGithubIdQuery = "SELECT github_id from users" updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" + + getAllUsersRankQuery = ` + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() over (ORDER BY current_balance DESC) AS rank + FROM users + ORDER BY current_balance DESC` + + getCurrentUserRankQuery = ` + SELECT * + FROM + ( + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() OVER (ORDER BY current_balance DESC) AS rank + FROM users + ) + ranked_users + WHERE id = $1;` ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -142,3 +170,37 @@ func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx return nil } + +func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var leaderboard []LeaderboardUser + err := executer.SelectContext(ctx, &leaderboard, getAllUsersRankQuery) + if err != nil { + slog.Error("failed to get users rank", "error", err) + return nil, apperrors.ErrInternalServer + } + + return leaderboard, nil +} + +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return LeaderboardUser{}, apperrors.ErrInternalServer + } + + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var currentUserRank LeaderboardUser + err := executer.GetContext(ctx, ¤tUserRank, getCurrentUserRankQuery, userId) + if err != nil { + slog.Error("failed to get user rank", "error", err) + return LeaderboardUser{}, apperrors.ErrInternalServer + } + + return currentUserRank, nil +} From 6ee49c93e5ad06f3ff69f6b6c965e1b4a230be47 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:44:57 +0530 Subject: [PATCH 2/3] rename GetUserRanks to ListUserRank and get value from context in handler itself --- internal/app/router.go | 2 +- internal/app/user/handler.go | 17 ++++++++++++++--- internal/app/user/service.go | 16 ++++++++-------- internal/pkg/apperrors/errors.go | 3 ++- internal/repository/user.go | 12 ++---------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/internal/app/router.go b/internal/app/router.go index 02f9c7e..f7047ab 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,7 +27,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) - router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.GetAllUsersRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 3f085b8..e1000ad 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -15,7 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) - GetAllUsersRank(w http.ResponseWriter, r *http.Request) + ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } @@ -47,7 +48,7 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } -func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() leaderboard, err := h.userService.GetAllUsersRank(ctx) @@ -64,7 +65,17 @@ func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - currentUserRank, err := h.userService.GetCurrentUserRank(ctx) + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId) if err != nil { slog.Error("failed to get current user rank", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 0aed35e..24a0c2d 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -20,7 +20,7 @@ type Service interface { UpdateUserEmail(ctx context.Context, email string) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) } func NewService(userRepository repository.UserRepository) Service { @@ -102,22 +102,22 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran } func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { - leaderboard, err := s.userRepository.GetAllUsersRank(ctx, nil) + userRanks, err := s.userRepository.GetAllUsersRank(ctx, nil) if err != nil { slog.Error("error obtaining all users rank", "error", err) return nil, err } - serviceLeaderboard := make([]LeaderboardUser, len(leaderboard)) - for i, l := range leaderboard { - serviceLeaderboard[i] = LeaderboardUser((l)) + Leaderboard := make([]LeaderboardUser, len(userRanks)) + for i, l := range userRanks { + Leaderboard[i] = LeaderboardUser(l) } - return serviceLeaderboard, nil + return Leaderboard, nil } -func (s *service) GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) { - currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil) +func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil, userId) if err != nil { slog.Error("error obtaining current user rank", "error", err) return LeaderboardUser{}, err diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 31c9e49..1ebee57 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -6,6 +6,7 @@ import ( ) var ( + ErrContextValue = errors.New("error obtaining value from context") ErrInternalServer = errors.New("internal server error") ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body") @@ -52,7 +53,7 @@ var ( func MapError(err error) (statusCode int, errMessage string) { switch err { - case ErrInvalidRequestBody, ErrInvalidQueryParams: + case ErrInvalidRequestBody, ErrInvalidQueryParams, ErrContextValue: return http.StatusBadRequest, err.Error() case ErrUnauthorizedAccess: return http.StatusUnauthorized, err.Error() diff --git a/internal/repository/user.go b/internal/repository/user.go index 82f660a..0a80cbe 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,7 +9,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type userRepository struct { @@ -25,7 +24,7 @@ type UserRepository interface { GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -184,14 +183,7 @@ func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]L return leaderboard, nil } -func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return LeaderboardUser{}, apperrors.ErrInternalServer - } +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) From ecc55c15e30b69ca6ea32d0aecdeb95565346474 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:45:36 +0530 Subject: [PATCH 3/3] create index for users_current_balance --- .../1752476063_create-index-users-current-balance.down.sql | 1 + .../1752476063_create-index-users-current-balance.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.down.sql create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.up.sql diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql new file mode 100644 index 0000000..a987013 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql @@ -0,0 +1 @@ +drop index idx_users_current_balance \ No newline at end of file diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql new file mode 100644 index 0000000..a934bf1 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_users_current_balance ON users(current_balance DESC); \ No newline at end of file