Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/app/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.ListUserRanks, deps.AppCfg))
router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg))

return middleware.CorsMiddleware(router, deps.AppCfg)
}
8 changes: 8 additions & 0 deletions internal/app/user/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
41 changes: 41 additions & 0 deletions internal/app/user/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -15,6 +16,8 @@ type handler struct {

type Handler interface {
UpdateUserEmail(w http.ResponseWriter, r *http.Request)
ListUserRanks(w http.ResponseWriter, r *http.Request)
GetCurrentUserRank(w http.ResponseWriter, r *http.Request)
}

func NewHandler(userService Service) Handler {
Expand Down Expand Up @@ -44,3 +47,41 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) {

response.WriteJson(w, http.StatusOK, "email updated successfully", nil)
}

func (h *handler) ListUserRanks(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()

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)
response.WriteJson(w, status, errorMessage, nil)
return
}

response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank)
}
27 changes: 27 additions & 0 deletions internal/app/user/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, userId int) (LeaderboardUser, error)
}

func NewService(userRepository repository.UserRepository) Service {
Expand Down Expand Up @@ -98,3 +100,28 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran

return nil
}

func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) {
userRanks, err := s.userRepository.GetAllUsersRank(ctx, nil)
if err != nil {
slog.Error("error obtaining all users rank", "error", err)
return nil, err
}

Leaderboard := make([]LeaderboardUser, len(userRanks))
for i, l := range userRanks {
Leaderboard[i] = LeaderboardUser(l)
}

return Leaderboard, 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
}

return LeaderboardUser(currentUserRank), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
drop index idx_users_current_balance
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX idx_users_current_balance ON users(current_balance DESC);
3 changes: 2 additions & 1 deletion internal/pkg/apperrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions internal/repository/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
54 changes: 54 additions & 0 deletions internal/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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, userId int) (LeaderboardUser, error)
}

func NewUserRepository(db *sqlx.DB) UserRepository {
Expand Down Expand Up @@ -51,6 +53,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`
Comment on lines +57 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have index on current_balance?, would be good to add an index on it when using RANK()

CREATE INDEX idx_users_current_balance ON users(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) {
Expand Down Expand Up @@ -142,3 +169,30 @@ 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, userId int) (LeaderboardUser, error) {

executer := ur.BaseRepository.initiateQueryExecuter(tx)

var currentUserRank LeaderboardUser
err := executer.GetContext(ctx, &currentUserRank, getCurrentUserRankQuery, userId)
if err != nil {
slog.Error("failed to get user rank", "error", err)
return LeaderboardUser{}, apperrors.ErrInternalServer
}

return currentUserRank, nil
}