From f59627d00ebf9715c4572643e858636998c22c52 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 12:08:10 +0530 Subject: [PATCH 01/28] implement fetch all contributed repositories of user (my contributions section) --- internal/app/dependencies.go | 28 +++-- internal/app/repository/domain.go | 41 +++++++ internal/app/repository/handler.go | 39 +++++++ internal/app/repository/service.go | 157 +++++++++++++++++++++++++ internal/app/router.go | 5 + internal/pkg/apperrors/errors.go | 15 ++- internal/repository/repository.go | 176 +++++++++++++++++++++++++++++ 7 files changed, 448 insertions(+), 13 deletions(-) create mode 100644 internal/app/repository/domain.go create mode 100644 internal/app/repository/handler.go create mode 100644 internal/app/repository/service.go create mode 100644 internal/repository/repository.go diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index fa49370..3f6448a 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -9,11 +9,14 @@ import ( ) type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig + AuthService auth.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + RepositoryHandler repoService.Handler + AppCfg config.AppConfig + Client config.Bigquery } func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { @@ -24,12 +27,17 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) + repositoryHandler := repoService.NewHandler(repositoryService) + contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, + AuthService: authService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + RepositoryHandler: repositoryHandler, + ContributionHandler: contributionHandler, + AppCfg: appCfg, + Client: client, } } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go new file mode 100644 index 0000000..fdb1204 --- /dev/null +++ b/internal/app/repository/domain.go @@ -0,0 +1,41 @@ +package repository + +import "time" + +type RepoOWner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + RepoName string + Description string + Languages []string + UpdateDate time.Time + TotalCoinsEarned int +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go new file mode 100644 index 0000000..6868bdb --- /dev/null +++ b/internal/app/repository/handler.go @@ -0,0 +1,39 @@ +package repository + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + repositoryService Service +} + +type Handler interface { + FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(repositoryService Service) Handler { + return &handler{ + repositoryService: repositoryService, + } +} + +func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) + if err != nil { + slog.Error("error fetching users conributed repos") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go new file mode 100644 index 0000000..988e5d7 --- /dev/null +++ b/internal/app/repository/service.go @@ -0,0 +1,157 @@ +package repository + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + appCfg config.AppConfig +} + +type Service interface { + GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) + FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { + return &service{ + repositoryRepository: repositoryRepository, + appCfg: appCfg, + } +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { + req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { + usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) + if err != nil { + slog.Error("error fetching users conributed repos") + return nil, err + } + + fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) + + for i, usersContributedRepo := range usersContributedRepos { + fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName + fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description + fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + + contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return nil, err + } + + for language := range contributedRepoLanguages { + fetchUsersContributedReposResponse[i].Languages = append(fetchUsersContributedReposResponse[i].Languages, language) + } + + userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return nil, err + } + + fetchUsersContributedReposResponse[i].TotalCoinsEarned = userRepoTotalCoins + } + + return fetchUsersContributedReposResponse, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..ad5d19b 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,10 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 5c7244d..878bbaf 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -28,8 +28,17 @@ var ( ErrUserNotFound = errors.New("user not found") ErrUserCreationFailed = errors.New("failed to create user") - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ) func MapError(err error) (statusCode int, errMessage string) { @@ -40,7 +49,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound: + case ErrUserNotFound, ErrRepoNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..21b5b10 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,176 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) + GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) + FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *` + + getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` + + fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + ).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return 0, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var totalCoins int + + err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return 0, apperrors.ErrCalculatingUserRepoTotalCoins + } + + return totalCoins, nil +} + +func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + if err != nil { + slog.Error("error fetching users contributed repositories") + return nil, apperrors.ErrFetchingUsersContributedRepos + } + defer rows.Close() + + var usersContributedRepos []Repository + for rows.Next() { + var usersContributedRepo Repository + if err = rows.Scan( + &usersContributedRepo.Id, + &usersContributedRepo.GithubRepoId, + &usersContributedRepo.RepoName, + &usersContributedRepo.Description, + &usersContributedRepo.LanguagesUrl, + &usersContributedRepo.RepoUrl, + &usersContributedRepo.OwnerName, + &usersContributedRepo.UpdateDate, + &usersContributedRepo.CreatedAt, + &usersContributedRepo.UpdatedAt); err != nil { + return nil, err + } + + usersContributedRepos = append(usersContributedRepos, usersContributedRepo) + } + + return usersContributedRepos, nil +} From 82afbddd58fb8db5f81ed543719e442aa0b571a3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:31:07 +0530 Subject: [PATCH 02/28] send all details for the repository --- internal/app/repository/domain.go | 5 +---- internal/app/repository/service.go | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index fdb1204..11dc558 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -29,13 +29,10 @@ type Repository struct { UpdatedAt time.Time } - type RepoLanguages map[string]int type FetchUsersContributedReposResponse struct { - RepoName string - Description string + Repository Languages []string - UpdateDate time.Time TotalCoinsEarned int } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 988e5d7..53ef3b8 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -130,9 +130,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) for i, usersContributedRepo := range usersContributedRepos { - fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName - fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description - fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) if err != nil { From c8da5f1295b12a9bde34970b1923c52cbd6861fb Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:50:12 +0530 Subject: [PATCH 03/28] fetch particular repository details --- internal/app/repository/handler.go | 24 ++++++++++++++++++++++++ internal/app/router.go | 2 ++ 2 files changed, 26 insertions(+) diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 6868bdb..dbfd979 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -3,6 +3,7 @@ package repository import ( "log/slog" "net/http" + "strconv" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" @@ -14,6 +15,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) + FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -37,3 +39,25 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) } + +func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) +} diff --git a/internal/app/router.go b/internal/app/router.go index ad5d19b..1117988 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -25,5 +25,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } From 121d59c07a50fec4a7b7a9d1eecea9285461c1f1 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 17:35:53 +0530 Subject: [PATCH 04/28] implement fetch particular repo contributors --- internal/app/repository/domain.go | 49 +++++++++++++++++++----------- internal/app/repository/handler.go | 33 ++++++++++++++++++++ internal/app/repository/service.go | 30 ++++++++++++++++++ internal/repository/domain.go | 23 +++++++------- 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 11dc558..1b6f91e 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -7,26 +7,28 @@ type RepoOWner struct { } type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` - RepoUrl string `json:"html_url"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` + ContributorsUrl string `json:"contributors_url"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type RepoLanguages map[string]int @@ -36,3 +38,16 @@ type FetchUsersContributedReposResponse struct { Languages []string TotalCoinsEarned int } + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index dbfd979..13bceb2 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -61,3 +61,36 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) } + +func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + if err != nil { + slog.Error("error fetching repo contributors") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 53ef3b8..b4c5d8c 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -22,6 +22,7 @@ type Service interface { CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -153,3 +154,32 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } + +func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + req, err := http.NewRequest("GET", getRepoContributorsURl, nil) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 13cedc5..4a6966b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -43,16 +43,17 @@ type Contribution struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type ContributionScore struct { @@ -62,4 +63,4 @@ type ContributionScore struct { Score int CreatedAt time.Time UpdatedAt time.Time -} \ No newline at end of file +} From 465fcd4666e1aff463c800678668b74f7b30333b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 18:27:14 +0530 Subject: [PATCH 05/28] implement fetch contributions of user in a particular repository --- internal/app/repository/domain.go | 12 +++++++++ internal/app/repository/handler.go | 24 +++++++++++++++++ internal/app/repository/service.go | 16 +++++++++++ internal/app/router.go | 2 +- internal/pkg/apperrors/errors.go | 9 ++++--- internal/repository/repository.go | 43 ++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 1b6f91e..0dcfa99 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -51,3 +51,15 @@ type FetchParticularRepoDetails struct { Repository Contributors []FetchRepoContributorsResponse } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 13bceb2..2567e8c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -16,6 +16,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) + FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -94,3 +95,26 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) } + +func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b4c5d8c..bb2bf17 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -23,6 +23,7 @@ type Service interface { FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -183,3 +184,18 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. return repoContributors, nil } + +func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { + userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, err + } + + serviceUserContributionsInRepo := make([]Contribution, len(userContributionsInRepo)) + for i, c := range userContributionsInRepo { + serviceUserContributionsInRepo[i] = Contribution(c) + } + + return serviceUserContributionsInRepo, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 1117988..6e77e6c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -26,6 +26,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) 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)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 8703f38..743e37a 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -31,10 +31,11 @@ var ( ErrJWTCreationFailed = errors.New("failed to create jwt token") ErrAuthorizationFailed = errors.New("failed to authorize user") - ErrRepoNotFound = errors.New("repository not found") - ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") - ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 21b5b10..3bf0d81 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -21,6 +21,7 @@ type RepositoryRepository interface { CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) + FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) } func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { @@ -48,6 +49,8 @@ const ( getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` + + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { @@ -174,3 +177,43 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t return usersContributedRepos, nil } + +func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, apperrors.ErrFetchingUserContributionsInRepo + } + defer rows.Close() + + var userContributionsInRepo []Contribution + for rows.Next() { + var userContributionInRepo Contribution + if err = rows.Scan( + &userContributionInRepo.Id, + &userContributionInRepo.UserId, + &userContributionInRepo.RepositoryId, + &userContributionInRepo.ContributionScoreId, + &userContributionInRepo.ContributionType, + &userContributionInRepo.BalanceChange, + &userContributionInRepo.ContributedAt, + &userContributionInRepo.CreatedAt, + &userContributionInRepo.UpdatedAt); err != nil { + return nil, err + } + + userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) + } + + return userContributionsInRepo, nil +} From f85a010a4f7ab3cfae652f7a9478c7384c56bcb8 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 13:39:23 +0530 Subject: [PATCH 06/28] fetch particular repository language percentage --- internal/app/repository/domain.go | 6 +++++ internal/app/repository/handler.go | 42 ++++++++++++++++++++++++++++++ internal/app/repository/service.go | 22 ++++++++++++++++ internal/app/router.go | 1 + 4 files changed, 71 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 0dcfa99..45305de 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -63,3 +63,9 @@ type Contribution struct { CreatedAt time.Time UpdatedAt time.Time } + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 2567e8c..e8f65f8 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -17,6 +17,7 @@ type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) + FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -118,3 +119,44 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) } + +func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "language percentages for repo fetched successfully", langPercent) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index bb2bf17..0eddc1e 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "log/slog" + "math" "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -24,6 +25,7 @@ type Service interface { FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) + CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -199,3 +201,23 @@ func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId return serviceUserContributionsInRepo, nil } + +func (s *service) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) { + var total int + for _, bytes := range repoLanguages { + total += bytes + } + + var langPercent []LanguagePercent + + for lang, bytes := range repoLanguages { + percentage := (float64(bytes) / float64(total)) * 100 + langPercent = append(langPercent, LanguagePercent{ + Name: lang, + Bytes: bytes, + Percentage: math.Round(percentage*10) / 10, + }) + } + + return langPercent, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 6e77e6c..3e3a1b5 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,5 +27,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) 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)) return middleware.CorsMiddleware(router, deps.AppCfg) } From 966d2a23d5b5292ab63445b2e6e272ba32fe0902 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:34:23 +0530 Subject: [PATCH 07/28] Fix: Restore correct merge changes for internal/app/repository/domain.go --- internal/app/repository/domain.go | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 90101b5..7788e8c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -30,3 +30,42 @@ type Repository struct { CreatedAt time.Time UpdatedAt time.Time } + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + Repository + Languages []string + TotalCoinsEarned int +} + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} \ No newline at end of file From 671c8ac8ad0762d90ec0bc35a0b99e70b123ee3e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:39:47 +0530 Subject: [PATCH 08/28] add contributors url when fetching users contributes repositories --- internal/repository/repository.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 084099a..75d61a8 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -172,7 +172,8 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t &usersContributedRepo.OwnerName, &usersContributedRepo.UpdateDate, &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt); err != nil { + &usersContributedRepo.UpdatedAt, + &usersContributedRepo.ContributorsUrl); err != nil { return nil, err } From 67ab318b49c635b0b5dc6e8486d765b775660fa3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:59:08 +0530 Subject: [PATCH 09/28] seperate logic for fetching repo by github id and repository table id --- internal/app/repository/service.go | 17 ++++++++++++--- internal/repository/repository.go | 34 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 23ab6f3..9a6774f 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -19,7 +19,8 @@ type service struct { } type Service interface { - GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) @@ -37,10 +38,20 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con } } -func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by github id") + slog.Error("failed to get repository by repo github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) + if err != nil { + slog.Error("failed to get repository by repo id") return Repository{}, err } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 75d61a8..453947b 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -18,6 +18,7 @@ type repositoryRepository struct { type RepositoryRepository interface { RepositoryTransaction GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) @@ -33,6 +34,8 @@ func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { const ( getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + getrepoByRepoIdQuery = `SELECT * from repositories where id=$1` + createRepositoryQuery = ` INSERT INTO repositories ( github_repo_id, @@ -76,7 +79,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. slog.Error("repository not found", "error", err) return Repository{}, apperrors.ErrRepoNotFound } - slog.Error("error occurred while getting repository by id", "error", err) + slog.Error("error occurred while getting repository by repo github id", "error", err) return Repository{}, apperrors.ErrInternalServer } @@ -84,6 +87,35 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. } +func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + &repository.ContributorsUrl, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil +} + func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { executer := rr.BaseRepository.initiateQueryExecuter(tx) From 778c359fc5361f2556316e34fa950753c3bd7d1e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 12:38:43 +0530 Subject: [PATCH 10/28] use repo id for fetching contributions of a particular repo of a user --- internal/repository/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 453947b..fcda6b2 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -54,7 +54,7 @@ const ( fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` - fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id=$1 and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { From cbafcc09738f627db01c9ce7f2222e54a8f95f69 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:06:35 +0530 Subject: [PATCH 11/28] use sqlx methods in user.go --- internal/repository/base.go | 2 + internal/repository/domain.go | 38 +++++++++---------- internal/repository/user.go | 69 ++++------------------------------- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/internal/repository/base.go b/internal/repository/base.go index a38e9ba..e4b0795 100644 --- a/internal/repository/base.go +++ b/internal/repository/base.go @@ -24,6 +24,8 @@ type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 29450f0..8febe1f 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -6,28 +6,28 @@ import ( ) type User struct { - Id int - GithubId int - GithubUsername string - Email string - AvatarUrl string - CurrentBalance int - CurrentActiveGoalId sql.NullInt64 - IsBlocked bool - IsAdmin bool - Password string - IsDeleted bool - DeletedAt sql.NullTime - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + Email string `db:"email"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + CurrentActiveGoalId sql.NullInt64 `db:"current_active_goal_id"` + IsBlocked bool `db:"is_blocked"` + IsAdmin bool `db:"is_admin"` + Password string `db:"password"` + IsDeleted bool `db:"is_deleted"` + DeletedAt sql.NullTime `db:"deleted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type CreateUserRequestBody struct { - GithubId int - GithubUsername string - AvatarUrl string - Email string - IsAdmin bool + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + Email string `db:"email"` + IsAdmin bool `db:"is_admin"` } type Contribution struct { diff --git a/internal/repository/user.go b/internal/repository/user.go index c504048..806124a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -54,22 +54,7 @@ func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId i executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByIdQuery, userId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByIdQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -86,22 +71,7 @@ func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, gi executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByGithubIdQuery, githubId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByGithubIdQuery, githubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -118,27 +88,12 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, createUserQuery, + err := executer.GetContext(ctx, &user, createUserQuery, userInfo.GithubId, userInfo.GithubUsername, userInfo.Email, - userInfo.AvatarUrl, - ).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + userInfo.AvatarUrl) + if err != nil { slog.Error("error occurred while creating user", "error", err) return User{}, apperrors.ErrUserCreationFailed @@ -162,21 +117,13 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) + + var githubUsernames []string + err := executer.SelectContext(ctx, &githubUsernames, getAllUsersGithubUsernamesQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) return nil, apperrors.ErrInternalServer } - defer rows.Close() - - var githubUsernames []string - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - return nil, err - } - githubUsernames = append(githubUsernames, username) - } return githubUsernames, nil } From f36069f9577c22510d74bb05e6aa8581719881ce Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:27:09 +0530 Subject: [PATCH 12/28] use sqlx methods in repository.go --- internal/repository/domain.go | 40 +++++++------- internal/repository/repository.go | 92 +++---------------------------- 2 files changed, 28 insertions(+), 104 deletions(-) diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 8febe1f..7a00a25 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -31,29 +31,29 @@ type CreateUserRequestBody struct { } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + UserId int `db:"user_id"` + RepositoryId int `db:"repository_id"` + ContributionScoreId int `db:"contribution_score_id"` + ContributionType string `db:"contribution_type"` + BalanceChange int `db:"balance_change"` + ContributedAt time.Time `db:"contributed_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - ContributorsUrl string - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubRepoId int `db:"github_repo_id"` + RepoName string `db:"repo_name"` + Description string `db:"description"` + LanguagesUrl string `db:"languages_url"` + RepoUrl string `db:"repo_url"` + OwnerName string `db:"owner_name"` + UpdateDate time.Time `db:"update_date"` + ContributorsUrl string `db:"contributors_url"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ContributionScore struct { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..c1b03ce 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -61,19 +61,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getRepoByGithubIdQuery, repoGithubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -91,19 +79,7 @@ func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getrepoByRepoIdQuery, repoId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -120,7 +96,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, createRepositoryQuery, + err := executer.GetContext(ctx, &repository, createRepositoryQuery, repositoryInfo.GithubRepoId, repositoryInfo.RepoName, repositoryInfo.Description, @@ -129,18 +105,6 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.OwnerName, repositoryInfo.UpdateDate, repositoryInfo.ContributorsUrl, - ).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, ) if err != nil { slog.Error("error occured while creating repository", "error", err) @@ -164,7 +128,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq var totalCoins int - err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + err := executer.GetContext(ctx, &totalCoins, getUserRepoTotalCoinsQuery, userId, repoId) if err != nil { slog.Error("error calculating total coins earned by user for the repository") return 0, apperrors.ErrCalculatingUserRepoTotalCoins @@ -184,33 +148,12 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + var usersContributedRepos []Repository + err := executer.SelectContext(ctx, &usersContributedRepos, fetchUsersContributedReposQuery, userId) if err != nil { slog.Error("error fetching users contributed repositories") return nil, apperrors.ErrFetchingUsersContributedRepos } - defer rows.Close() - - var usersContributedRepos []Repository - for rows.Next() { - var usersContributedRepo Repository - if err = rows.Scan( - &usersContributedRepo.Id, - &usersContributedRepo.GithubRepoId, - &usersContributedRepo.RepoName, - &usersContributedRepo.Description, - &usersContributedRepo.LanguagesUrl, - &usersContributedRepo.RepoUrl, - &usersContributedRepo.OwnerName, - &usersContributedRepo.UpdateDate, - &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt, - &usersContributedRepo.ContributorsUrl); err != nil { - return nil, err - } - - usersContributedRepos = append(usersContributedRepos, usersContributedRepo) - } return usersContributedRepos, nil } @@ -226,31 +169,12 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + var userContributionsInRepo []Contribution + err := executer.SelectContext(ctx, &userContributionsInRepo, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { slog.Error("error fetching users contribution in repository") return nil, apperrors.ErrFetchingUserContributionsInRepo } - defer rows.Close() - - var userContributionsInRepo []Contribution - for rows.Next() { - var userContributionInRepo Contribution - if err = rows.Scan( - &userContributionInRepo.Id, - &userContributionInRepo.UserId, - &userContributionInRepo.RepositoryId, - &userContributionInRepo.ContributionScoreId, - &userContributionInRepo.ContributionType, - &userContributionInRepo.BalanceChange, - &userContributionInRepo.ContributedAt, - &userContributionInRepo.CreatedAt, - &userContributionInRepo.UpdatedAt); err != nil { - return nil, err - } - - userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) - } return userContributionsInRepo, nil } From 050487e1b12dd66fbaa45f6d401347173a111767 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:38:26 +0530 Subject: [PATCH 13/28] use sqlx methods in contribution.go --- internal/repository/contribution.go | 43 +++-------------------------- internal/repository/domain.go | 12 ++++---- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..54081ab 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -48,23 +48,13 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq executer := cr.BaseRepository.initiateQueryExecuter(tx) var contribution Contribution - err := executer.QueryRowContext(ctx, createContributionQuery, + err := executer.GetContext(ctx, &contribution, createContributionQuery, contributionInfo.UserId, contributionInfo.RepositoryId, contributionInfo.ContributionScoreId, contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, - ).Scan( - &contribution.Id, - &contribution.UserId, - &contribution.RepositoryId, - &contribution.ContributionScoreId, - &contribution.ContributionType, - &contribution.BalanceChange, - &contribution.ContributedAt, - &contribution.CreatedAt, - &contribution.UpdatedAt, ) if err != nil { slog.Error("error occured while inserting contributions", "error", err) @@ -78,14 +68,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( executer := cr.BaseRepository.initiateQueryExecuter(tx) var contributionScoreDetails ContributionScore - err := executer.QueryRowContext(ctx, getContributionScoreDetailsByContributionTypeQuery, contributionType).Scan( - &contributionScoreDetails.Id, - &contributionScoreDetails.AdminId, - &contributionScoreDetails.ContributionType, - &contributionScoreDetails.Score, - &contributionScoreDetails.CreatedAt, - &contributionScoreDetails.UpdatedAt, - ) + err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err @@ -105,30 +88,12 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context executer := cr.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + var usersAllContributions []Contribution + err := executer.SelectContext(ctx, &usersAllContributions, fetchUsersAllContributionsQuery, userId) if err != nil { slog.Error("error fetching all contributions for user") return nil, apperrors.ErrFetchingAllContributions } - defer rows.Close() - - var usersAllContributions []Contribution - for rows.Next() { - var currentContribution Contribution - if err = rows.Scan( - ¤tContribution.Id, - ¤tContribution.UserId, - ¤tContribution.RepositoryId, - ¤tContribution.ContributionScoreId, - ¤tContribution.ContributionType, - ¤tContribution.BalanceChange, - ¤tContribution.ContributedAt, - ¤tContribution.CreatedAt, ¤tContribution.UpdatedAt); err != nil { - return nil, err - } - - usersAllContributions = append(usersAllContributions, currentContribution) - } return usersAllContributions, nil } diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 7a00a25..148c2e3 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -57,10 +57,10 @@ type Repository struct { } type ContributionScore struct { - Id int - AdminId int - ContributionType string - Score int - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + AdminId int `db:"admin_id"` + ContributionType string `db:"contribution_type"` + Score int `db:"score"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } From f5af0ac6f607a8923b24f971b1a1e6bc52af7987 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:44:08 +0530 Subject: [PATCH 14/28] log error in slog --- internal/app/contribution/handler.go | 4 ++-- internal/app/contribution/service.go | 4 ++-- internal/app/repository/handler.go | 24 ++++++++++++------------ internal/app/repository/service.go | 10 +++++----- internal/repository/contribution.go | 2 +- internal/repository/repository.go | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..b4b8a4e 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -28,7 +28,7 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re err := h.contributionService.ProcessFetchedContributions(ctx) if err != nil { - slog.Error("error fetching latest contributions") + slog.Error("error fetching latest contributions", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -42,7 +42,7 @@ func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Requ usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 046e617..085c269 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -56,7 +56,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { - slog.Error("error getting contribution type") + slog.Error("error getting contribution type", "error", err) return err } @@ -65,7 +65,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { if err != nil { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { - slog.Error("error fetching repository details") + slog.Error("error fetching repository details", "error", err) return err } diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index e8f65f8..083d14c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -33,7 +33,7 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -47,7 +47,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -55,7 +55,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -72,7 +72,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -80,7 +80,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -88,7 +88,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) if err != nil { - slog.Error("error fetching repo contributors") + slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -103,7 +103,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -111,7 +111,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -128,7 +128,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -136,7 +136,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -144,7 +144,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -152,7 +152,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 9a6774f..a7499f6 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -41,7 +41,7 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by repo github id") + slog.Error("failed to get repository by repo github id", "error", err) return Repository{}, err } @@ -51,7 +51,7 @@ func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repo func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) if err != nil { - slog.Error("failed to get repository by repo id") + slog.Error("failed to get repository by repo id", "error", err) return Repository{}, err } @@ -141,7 +141,7 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) return nil, err } @@ -162,7 +162,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return nil, err } @@ -204,7 +204,7 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, err } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..94b7e13 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -107,7 +107,7 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) return nil, apperrors.ErrFetchingAllContributions } defer rows.Close() diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..05e5cd9 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -166,7 +166,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return 0, apperrors.ErrCalculatingUserRepoTotalCoins } @@ -186,7 +186,7 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) if err != nil { - slog.Error("error fetching users contributed repositories") + slog.Error("error fetching users contributed repositories", "error", err) return nil, apperrors.ErrFetchingUsersContributedRepos } defer rows.Close() @@ -228,7 +228,7 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, apperrors.ErrFetchingUserContributionsInRepo } defer rows.Close() From 5293cb6dbfca5ad55ab66409941f8fe0627330e6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 11:47:14 +0530 Subject: [PATCH 15/28] make contribution fetch process from gh-archive dataset on bigquery, a cron job that is scheduled everyday at 1 am IST --- go.mod | 1 + go.sum | 2 ++ internal/app/cronJob/cronjob.go | 26 ++++++++++++++++++++++ internal/app/cronJob/dailyJob.go | 32 +++++++++++++++++++++++++++ internal/app/cronJob/init.go | 38 ++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 internal/app/cronJob/cronjob.go create mode 100644 internal/app/cronJob/dailyJob.go create mode 100644 internal/app/cronJob/init.go diff --git a/go.mod b/go.mod index f308cae..e73f2e2 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/robfig/cron/v3 v3.0.1 golang.org/x/oauth2 v0.29.0 google.golang.org/api v0.231.0 ) diff --git a/go.sum b/go.sum index d272ace..a599248 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= diff --git a/internal/app/cronJob/cronjob.go b/internal/app/cronJob/cronjob.go new file mode 100644 index 0000000..a567985 --- /dev/null +++ b/internal/app/cronJob/cronjob.go @@ -0,0 +1,26 @@ +package cronJob + +import ( + "context" + "log/slog" + "time" +) + +type Job interface { + Schedule(c *CronSchedular) error +} + +type CronJob struct { + Name string +} + +func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) func() { + return func() { + slog.Info("cron job started at", "time ", time.Now()) + defer func() { + slog.Info("cron job completed") + }() + + fn(ctx) + } +} diff --git a/internal/app/cronJob/dailyJob.go b/internal/app/cronJob/dailyJob.go new file mode 100644 index 0000000..30f225f --- /dev/null +++ b/internal/app/cronJob/dailyJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" +) + +type DailyJob struct { + CronJob + contributionService contribution.Service +} + +func NewDailyJob(contributionService contribution.Service) *DailyJob { + return &DailyJob{ + contributionService: contributionService, + CronJob: CronJob{Name: "Fetch Contributions Daily"}, + } +} + +func (d *DailyJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run)() }) + if err != nil { + return err + } + + return nil +} + +func (d *DailyJob) run(ctx context.Context) { + d.contributionService.ProcessFetchedContributions(ctx) +} diff --git a/internal/app/cronJob/init.go b/internal/app/cronJob/init.go new file mode 100644 index 0000000..7fba0f3 --- /dev/null +++ b/internal/app/cronJob/init.go @@ -0,0 +1,38 @@ +package cronJob + +import ( + "log/slog" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/robfig/cron/v3" +) + +type CronSchedular struct { + cron *cron.Cron +} + +func NewCronSchedular() *CronSchedular { + location, err := time.LoadLocation("Asia/Kolkata") + if err != nil { + slog.Error("failed to load IST timezone", "error", err) + } + + return &CronSchedular{ + cron: cron.New(cron.WithLocation(location)), + } +} + +func (c *CronSchedular) InitCronJobs(contributionService contribution.Service) { + jobs := []Job{ + NewDailyJob(contributionService), + } + + for _, job := range jobs { + if err := job.Schedule(c); err != nil { + slog.Error("failed to execute cron job") + } + } + + c.cron.Start() +} From adb7845dafd5935585dd76eb218fa067eec024c9 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 11:49:40 +0530 Subject: [PATCH 16/28] remove fetch users latest contribution handler, made for testing processfetchcontributions service. As this will be a cronjob --- internal/app/contribution/handler.go | 15 --------------- internal/app/router.go | 1 - 2 files changed, 16 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..df50396 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -13,7 +13,6 @@ type handler struct { } type Handler interface { - FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } @@ -23,20 +22,6 @@ func NewHandler(contributionService Service) Handler { } } -func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - err := h.contributionService.ProcessFetchedContributions(ctx) - if err != nil { - slog.Error("error fetching latest contributions") - status, errorMessage := apperrors.MapError(err) - response.WriteJson(w, status, errorMessage, nil) - return - } - - response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) -} - func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/app/router.go b/internal/app/router.go index 6ab919a..7c15d9b 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,7 +20,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } From 4a736ceba73320f16def079eb91d7ffd1f9f35d0 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 16:00:00 +0530 Subject: [PATCH 17/28] pass contribution service in dependencies for cron job and remove unnecessary auth and user service from dependencies --- cmd/main.go | 4 ++++ internal/app/dependencies.go | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 947cd10..1a5b091 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/joshsoftware/code-curiosity-2025/internal/app" + "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" ) @@ -43,6 +44,9 @@ func main() { router := app.NewRouter(dependencies) + newCronSchedular := cronJob.NewCronSchedular() + newCronSchedular.InitCronJobs(dependencies.ContributionService) + server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 616e377..e65fd39 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -15,8 +15,7 @@ import ( ) type Dependencies struct { - AuthService auth.Service - UserService user.Service + ContributionService contribution.Service AuthHandler auth.Handler UserHandler user.Handler ContributionHandler contribution.Handler @@ -40,8 +39,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, + ContributionService: contributionService, AuthHandler: authHandler, UserHandler: userHandler, ContributionHandler: contributionHandler, From 3ac004e4c2ad0653db3445a0063026cc968ca170 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 11:52:32 +0530 Subject: [PATCH 18/28] define contribution fetching query from bigquery as a constant --- internal/app/bigquery/domain.go | 29 +++++++++++++++++++++++++ internal/app/bigquery/service.go | 37 ++++---------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go index 3a8575a..7b15922 100644 --- a/internal/app/bigquery/domain.go +++ b/internal/app/bigquery/domain.go @@ -2,6 +2,35 @@ package bigquery import "time" +const DailyQuery = `SELECT + id, + type, + public, + actor.id AS actor_id, + actor.login AS actor_login, + actor.gravatar_id AS actor_gravatar_id, + actor.url AS actor_url, + actor.avatar_url AS actor_avatar_url, + repo.id AS repo_id, + repo.name AS repo_name, + repo.url AS repo_url, + payload, + created_at, + other +FROM + githubarchive.day.%s +WHERE + type IN ( + 'IssuesEvent', + 'PullRequestEvent', + 'PullRequestReviewEvent', + 'IssueCommentEvent', + 'PullRequestReviewCommentEvent' + ) + AND ( + actor.login IN (%s) + )` + type ContributionResponse struct { ID string `bigquery:"id"` Type string `bigquery:"type"` diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 7a15d7c..5f64ee1 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -30,9 +30,6 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User } func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { - YesterdayDate := time.Now().AddDate(0, 0, -1) - YesterdayYearMonthDay := YesterdayDate.Format("20060102") - usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) if err != nil { slog.Error("error fetching users github usernames") @@ -44,37 +41,11 @@ func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) } + YesterdayDate := time.Now().AddDate(0, 0, -1) + YesterdayYearMonthDay := YesterdayDate.Format("20060102") + githubUsernames := strings.Join(quotedUsernamesList, ",") - fetchDailyContributionsQuery := fmt.Sprintf(` -SELECT - id, - type, - public, - actor.id AS actor_id, - actor.login AS actor_login, - actor.gravatar_id AS actor_gravatar_id, - actor.url AS actor_url, - actor.avatar_url AS actor_avatar_url, - repo.id AS repo_id, - repo.name AS repo_name, - repo.url AS repo_url, - payload, - created_at, - other -FROM - githubarchive.day.%s -WHERE - type IN ( - 'IssuesEvent', - 'PullRequestEvent', - 'PullRequestReviewEvent', - 'IssueCommentEvent', - 'PullRequestReviewCommentEvent' - ) - AND ( - actor.login IN (%s) - ) -`, YesterdayYearMonthDay, githubUsernames) + fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, githubUsernames) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) From a3067768c603b5633ceb97188b7ab974260f6f6f Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 11:54:07 +0530 Subject: [PATCH 19/28] rename function FetchUsersAllContributions to FetchUserContributions, define constants for strings --- internal/app/contribution/handler.go | 10 +- internal/app/contribution/service.go | 148 ++++++++++++++++++--------- internal/app/repository/domain.go | 4 +- internal/app/router.go | 2 +- internal/pkg/apperrors/errors.go | 2 + internal/repository/contribution.go | 14 +-- 6 files changed, 117 insertions(+), 63 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..fe614f8 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) - FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) + FetchUserContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -37,16 +37,16 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } -func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { +func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) + userContributions, err := h.contributionService.FetchUserContributions(ctx) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching user contributions") status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "all contributions for user fetched successfully", usersAllContributions) + response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 046e617..7d0b3cf 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -9,10 +9,44 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" "google.golang.org/api/iterator" ) +// github event names +const ( + pullRequestEvent = "PullRequestEvent" + issuesEvent = "IssuesEvent" + pushEvent = "PushEvent" + issueCommentEvent = "IssueCommentEvent" +) + +// app contribution types +const ( + pullRequestMerged = "PullRequestMerged" + pullRequestOpened = "PullRequestOpened" + issueOpened = "IssueOpened" + issueClosed = "IssueClosed" + issueResolved = "IssueResolved" + pullRequestUpdated = "PullRequestUpdated" + issueComment = "IssueComment" + pullRequestComment = "PullRequestComment" +) + +// payload +const ( + payloadActionKey = "action" + payloadPullRequestKey = "pull_request" + PayloadMergedKey = "merged" + PayloadIssueKey = "issue" + PayloadStateReasonKey = "state_reason" + PayloadClosedKey = "closed" + PayloadOpenedKey = "opened" + PayloadNotPlannedKey = "not_planned" + PayloadCompletedKey = "completed" +) + type service struct { bigqueryService bigquery.Service contributionRepository repository.ContributionRepository @@ -25,7 +59,7 @@ type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) + FetchUserContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { @@ -33,8 +67,9 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, - userService: userService, - httpClient: httpClient, + + userService: userService, + httpClient: httpClient, } } @@ -42,18 +77,30 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) - return err + return apperrors.ErrFetchingFromBigquery } + var fetchedContributions []ContributionResponse + + //using a local copy here to copy contribution so that I can implement retry mechanism in future + //thinking of batch processing to be implemented later on, to handle memory overflow for { var contribution ContributionResponse - if err := contributions.Next(&contribution); err == iterator.Done { - break - } else if err != nil { + err := contributions.Next(&contribution) + if err != nil { + if err == iterator.Done { + break + } + slog.Error("error iterating contribution rows", "error", err) - break + return apperrors.ErrNextContribution } + fetchedContributions = append(fetchedContributions, contribution) + } + + for _, contribution := range fetchedContributions { + contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { slog.Error("error getting contribution type") @@ -61,21 +108,26 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) //err no rows + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) - if err != nil { - slog.Error("error fetching repository details") + if err == apperrors.ErrRepoNotFound { + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + if err != nil { + slog.Error("error fetching repository details") + return err + } + + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) + if err != nil { + slog.Error("error creating repository", "error", err) + return err + } + + repositoryId = repositoryCreated.Id + } else { + slog.Error("error fetching repo by repo id", "error", err) return err } - - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } - - repositoryId = repositoryCreated.Id } else { repositoryId = repoFetched.Id } @@ -105,50 +157,50 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu } var action string - if actionVal, ok := contributionPayload["action"]; ok { + if actionVal, ok := contributionPayload[action]; ok { action = actionVal.(string) } var pullRequest map[string]interface{} var isMerged bool - if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + if pullRequestPayload, ok := contributionPayload[payloadPullRequestKey]; ok { pullRequest = pullRequestPayload.(map[string]interface{}) - isMerged = pullRequest["merged"].(bool) + isMerged = pullRequest[PayloadMergedKey].(bool) } var issue map[string]interface{} var stateReason string - if issuePayload, ok := contributionPayload["issue"]; ok { + if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok { issue = issuePayload.(map[string]interface{}) - stateReason = issue["state_reason"].(string) + stateReason = issue[PayloadStateReasonKey].(string) } var contributionType string switch contribution.Type { - case "PullRequestEvent": - if action == "closed" && isMerged { - contributionType = "PullRequestMerged" - } else if action == "opened" { - contributionType = "PullRequestOpened" + case pullRequestEvent: + if action == PayloadClosedKey && isMerged { + contributionType = pullRequestMerged + } else if action == PayloadOpenedKey { + contributionType = pullRequestOpened } - case "IssuesEvent": - if action == "opened" { - contributionType = "IssueOpened" - } else if action == "closed" && stateReason == "not_planned" { - contributionType = "IssueClosed" - } else if action == "closed" && stateReason == "completed" { - contributionType = "IssueResolved" + case issuesEvent: + if action == PayloadOpenedKey { + contributionType = issueOpened + } else if action == PayloadClosedKey && stateReason == PayloadNotPlannedKey { + contributionType = issueClosed + } else if action == PayloadClosedKey && stateReason == PayloadCompletedKey { + contributionType = issueResolved } - case "PushEvent": - contributionType = "PullRequestUpdated" + case pushEvent: + contributionType = pullRequestUpdated - case "IssueCommentEvent": - contributionType = "IssueComment" + case issueCommentEvent: + contributionType = issueComment - case "PullRequestComment ": - contributionType = "PullRequestComment" + case pullRequestComment: + contributionType = pullRequestComment } return contributionType, nil @@ -191,15 +243,15 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { - usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) +func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, error) { + userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil) if err != nil { - slog.Error("error occured while fetching all contributions for user", "error", err) + slog.Error("error occured while fetching user contributions", "error", err) return nil, err } - serviceContributions := make([]Contribution, len(usersAllContributions)) - for i, c := range usersAllContributions { + serviceContributions := make([]Contribution, len(userContributions)) + for i, c := range userContributions { serviceContributions[i] = Contribution((c)) } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 90101b5..be5757c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -2,7 +2,7 @@ package repository import "time" -type RepoOWner struct { +type RepoOwner struct { Login string `json:"login"` } @@ -12,7 +12,7 @@ type FetchRepositoryDetailsResponse struct { Description string `json:"description"` LanguagesURL string `json:"languages_url"` UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` + RepoOwnerName RepoOwner `json:"owner"` ContributorsUrl string `json:"contributors_url"` RepoUrl string `json:"html_url"` } diff --git a/internal/app/router.go b/internal/app/router.go index 6ab919a..cdcf7c2 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,6 +21,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index c9c7f2d..a1a79d0 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -34,6 +34,8 @@ var ( ErrRepoNotFound = errors.New("repository not found") ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") + ErrNextContribution = errors.New("error while loading next bigquery contribution") ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..385b8c6 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -17,7 +17,7 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) - FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -41,7 +41,7 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` - fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` + fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -94,7 +94,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } -func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { +func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { userIdValue := ctx.Value(middleware.UserIdKey) userId, ok := userIdValue.(int) @@ -105,14 +105,14 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context executer := cr.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + rows, err := executer.QueryContext(ctx, fetchUserContributionsQuery, userId) if err != nil { slog.Error("error fetching all contributions for user") return nil, apperrors.ErrFetchingAllContributions } defer rows.Close() - var usersAllContributions []Contribution + var userContributions []Contribution for rows.Next() { var currentContribution Contribution if err = rows.Scan( @@ -127,8 +127,8 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context return nil, err } - usersAllContributions = append(usersAllContributions, currentContribution) + userContributions = append(userContributions, currentContribution) } - return usersAllContributions, nil + return userContributions, nil } From 187dc89eeae4d596417d7951e31633cc395440c6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 12:03:15 +0530 Subject: [PATCH 20/28] handle no row error in GetContributionScoreDetails function --- internal/pkg/apperrors/errors.go | 1 + internal/repository/contribution.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 743e37a..7c6a6e4 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -40,6 +40,7 @@ var ( ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 674dec6..3293992 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "github.com/jmoiron/sqlx" @@ -70,6 +72,11 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( var contributionScoreDetails ContributionScore err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Warn("no contribution score details found for contribution type", "contributionType", contributionType) + return ContributionScore{}, apperrors.ErrContributionScoreNotFound + } + slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err } From a73d29500237642c629ce13a3e27d871dfc22dd9 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 12:07:46 +0530 Subject: [PATCH 21/28] refactor Execute function --- internal/app/cronJob/cronjob.go | 14 ++++++-------- internal/app/cronJob/dailyJob.go | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/app/cronJob/cronjob.go b/internal/app/cronJob/cronjob.go index a567985..f8f3a21 100644 --- a/internal/app/cronJob/cronjob.go +++ b/internal/app/cronJob/cronjob.go @@ -14,13 +14,11 @@ type CronJob struct { Name string } -func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) func() { - return func() { - slog.Info("cron job started at", "time ", time.Now()) - defer func() { - slog.Info("cron job completed") - }() +func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) { + slog.Info("cron job started at", "time ", time.Now()) + defer func() { + slog.Info("cron job completed") + }() - fn(ctx) - } + fn(ctx) } diff --git a/internal/app/cronJob/dailyJob.go b/internal/app/cronJob/dailyJob.go index 30f225f..67f3e24 100644 --- a/internal/app/cronJob/dailyJob.go +++ b/internal/app/cronJob/dailyJob.go @@ -19,7 +19,7 @@ func NewDailyJob(contributionService contribution.Service) *DailyJob { } func (d *DailyJob) Schedule(s *CronSchedular) error { - _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run)() }) + _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run) }) if err != nil { return err } From 5397c3ebaeea07c077caf8a6d53054033360c1c7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 13:23:31 +0530 Subject: [PATCH 22/28] fix - use actionkey for action string --- internal/app/contribution/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index c9c74f4..17095d8 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -113,7 +113,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { if err == apperrors.ErrRepoNotFound { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { - slog.Error("error fetching repository details") + slog.Error("error fetching repository details", "error", err) return err } @@ -157,7 +157,7 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu } var action string - if actionVal, ok := contributionPayload[action]; ok { + if actionVal, ok := contributionPayload[payloadActionKey]; ok { action = actionVal.(string) } From 6cb482d8b73ec9aca99cb79f248aa87e30824ec4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 18:29:41 +0530 Subject: [PATCH 23/28] save github event id for each contribution in db --- internal/app/contribution/domain.go | 1 + internal/app/contribution/service.go | 1 + internal/app/repository/domain.go | 3 ++- internal/repository/contribution.go | 8 +++++--- internal/repository/domain.go | 1 + 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 5fecd14..872f452 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -36,6 +36,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time + GithubEventId int CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 17095d8..8273aef 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -213,6 +213,7 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin RepositoryId: repositoryId, ContributionType: contributionType, ContributedAt: contributionDetails.CreatedAt, + GithubEventId: contributionDetails.ActorID, } contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index d63c113..940a5f3 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -60,6 +60,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time + GithubEventId int CreatedAt time.Time UpdatedAt time.Time } @@ -68,4 +69,4 @@ type LanguagePercent struct { Name string Bytes int Percentage float64 -} \ No newline at end of file +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 5270983..74887a6 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -36,9 +36,10 @@ const ( contribution_score_id, contribution_type, balance_change, - contributed_at + contributed_at, + github_event_id ) - VALUES ($1, $2, $3, $4, $5, $6) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *` getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` @@ -57,6 +58,7 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, + contributionInfo.GithubEventId, ) if err != nil { slog.Error("error occured while inserting contributions", "error", err) @@ -101,6 +103,6 @@ func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx slog.Error("error fetching user contributions", "error", err) return nil, apperrors.ErrFetchingAllContributions } - + return userContributions, nil } diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 148c2e3..7d2066b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,6 +38,7 @@ type Contribution struct { ContributionType string `db:"contribution_type"` BalanceChange int `db:"balance_change"` ContributedAt time.Time `db:"contributed_at"` + GithubEventId int `db:"github_event_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } From 521be1adb57d17cbff334c766c70f6d216c38239 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 15:55:34 +0530 Subject: [PATCH 24/28] use user github id to fetch contributions from bigquery and refactor ProcessFetchedContributions function --- internal/app/bigquery/domain.go | 2 +- internal/app/bigquery/service.go | 13 ++++-------- internal/app/contribution/domain.go | 2 +- internal/app/contribution/service.go | 30 +++++++++++++++++++++++----- internal/app/repository/domain.go | 2 +- internal/pkg/apperrors/errors.go | 2 ++ internal/pkg/utils/helper.go | 15 ++++++++++++++ internal/repository/contribution.go | 21 +++++++++++++++++++ internal/repository/domain.go | 2 +- internal/repository/user.go | 12 +++++------ 10 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 internal/pkg/utils/helper.go diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go index 7b15922..bedd595 100644 --- a/internal/app/bigquery/domain.go +++ b/internal/app/bigquery/domain.go @@ -28,7 +28,7 @@ WHERE 'PullRequestReviewCommentEvent' ) AND ( - actor.login IN (%s) + actor.id IN (%s) )` type ContributionResponse struct { diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 5f64ee1..21a2064 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -4,12 +4,12 @@ import ( "context" "fmt" "log/slog" - "strings" "time" bq "cloud.google.com/go/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -30,22 +30,17 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User } func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { - usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) + usersGithubId, err := s.userRepository.GetAllUsersGithubId(ctx, nil) if err != nil { slog.Error("error fetching users github usernames") return nil, apperrors.ErrInternalServer } - var quotedUsernamesList []string - for _, username := range usersNamesList { - quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) - } + formattedGithubIds := utils.FormatIntSliceForQuery(usersGithubId) YesterdayDate := time.Now().AddDate(0, 0, -1) YesterdayYearMonthDay := YesterdayDate.Format("20060102") - - githubUsernames := strings.Join(quotedUsernamesList, ",") - fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, githubUsernames) + fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, formattedGithubIds) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 872f452..295c851 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -36,7 +36,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - GithubEventId int + GithubEventId string CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 8273aef..f54f41b 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -60,6 +60,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { @@ -100,15 +101,18 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } for _, contribution := range fetchedContributions { + _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err == nil { + continue + } - contributionType, err := s.GetContributionType(ctx, contribution) - if err != nil { - slog.Error("error getting contribution type", "error", err) + if err != apperrors.ErrContributionNotFound { + slog.Error("error fetching contribution by github event id", "error", err) return err } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) if err != nil { if err == apperrors.ErrRepoNotFound { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) @@ -138,6 +142,12 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return err } + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return err + } + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) @@ -213,7 +223,7 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin RepositoryId: repositoryId, ContributionType: contributionType, ContributedAt: contributionDetails.CreatedAt, - GithubEventId: contributionDetails.ActorID, + GithubEventId: contributionDetails.ID, } contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) @@ -258,3 +268,13 @@ func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, e return serviceContributions, nil } + +func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) { + contribution, err := s.contributionRepository.GetContributionByGithubEventId(ctx, nil, githubEventId) + if err != nil { + slog.Error("error fetching contribution by github event id") + return Contribution{}, err + } + + return Contribution(contribution), nil +} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 940a5f3..6d8303a 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -60,7 +60,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - GithubEventId int + GithubEventId string CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index a432f41..4efe8cf 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -43,6 +43,8 @@ var ( ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") + ErrFetchingContribution = errors.New("error fetching contribution by github repo id") + ErrContributionNotFound = errors.New("contribution not found") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go new file mode 100644 index 0000000..05cca78 --- /dev/null +++ b/internal/pkg/utils/helper.go @@ -0,0 +1,15 @@ +package utils + +import ( + "fmt" + "strings" +) + +func FormatIntSliceForQuery(ids []int) string { + strIDs := make([]string, len(ids)) + for i, id := range ids { + strIDs[i] = fmt.Sprintf("%d", id) + } + + return strings.Join(strIDs, ",") +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 74887a6..eb1d1a5 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -20,6 +20,7 @@ type ContributionRepository interface { CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -45,6 +46,8 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` + + GetContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -106,3 +109,21 @@ func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx return userContributions, nil } + +func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.GetContext(ctx, &contribution, GetContributionByGithubEventIdQuery, githubEventId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("contribution not found", "error", err) + return Contribution{}, apperrors.ErrContributionNotFound + } + slog.Error("error fetching contribution by github event id", "error", err) + return Contribution{}, apperrors.ErrFetchingContribution + } + + return contribution, nil + +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 7d2066b..eb4ba97 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,7 +38,7 @@ type Contribution struct { ContributionType string `db:"contribution_type"` BalanceChange int `db:"balance_change"` ContributedAt time.Time `db:"contributed_at"` - GithubEventId int `db:"github_event_id"` + GithubEventId string `db:"github_event_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } diff --git a/internal/repository/user.go b/internal/repository/user.go index 806124a..254da1b 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,7 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error - GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) + GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -47,7 +47,7 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" - getAllUsersGithubUsernamesQuery = "SELECT github_username from users" + getAllUsersGithubIdQuery = "SELECT github_id from users" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -115,15 +115,15 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } -func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { +func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - var githubUsernames []string - err := executer.SelectContext(ctx, &githubUsernames, getAllUsersGithubUsernamesQuery) + var githubIds []int + err := executer.SelectContext(ctx, &githubIds, getAllUsersGithubIdQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) return nil, apperrors.ErrInternalServer } - return githubUsernames, nil + return githubIds, nil } From 031ba382748b8494817cdd616661fafa2fe04418 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 17:18:32 +0530 Subject: [PATCH 25/28] make separate github service for github api calls --- internal/app/contribution/service.go | 37 ++++----- internal/app/dependencies.go | 4 +- internal/app/github/domain.go | 28 +++++++ internal/app/github/service.go | 118 +++++++++++++++++++++++++++ internal/app/repository/domain.go | 28 ------- internal/app/repository/handler.go | 8 +- internal/app/repository/service.go | 108 ++---------------------- 7 files changed, 177 insertions(+), 154 deletions(-) create mode 100644 internal/app/github/domain.go create mode 100644 internal/app/github/service.go diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index f54f41b..b4f31cb 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -68,9 +68,8 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, - - userService: userService, - httpClient: httpClient, + userService: userService, + httpClient: httpClient, } } @@ -81,10 +80,10 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return apperrors.ErrFetchingFromBigquery } - var fetchedContributions []ContributionResponse - //using a local copy here to copy contribution so that I can implement retry mechanism in future //thinking of batch processing to be implemented later on, to handle memory overflow + var fetchedContributions []ContributionResponse + for { var contribution ContributionResponse err := contributions.Next(&contribution) @@ -113,27 +112,19 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { var repositoryId int repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err != nil { - if err == apperrors.ErrRepoNotFound { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) - if err != nil { - slog.Error("error fetching repository details", "error", err) - return err - } - - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } - - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) + if err == nil { + repositoryId = repoFetched.Id + } else if err == apperrors.ErrRepoNotFound { + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) + if err != nil { + slog.Error("error creating repository", "error", err) return err } + + repositoryId = repositoryCreated.Id } else { - repositoryId = repoFetched.Id + slog.Error("error fetching repo by repo id", "error", err) + return err } user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 9789265..f390342 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -7,6 +7,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -32,7 +33,8 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) - repositoryService := repoService.NewService(repositoryRepository, appCfg, httpClient) + githubService := github.NewService(appCfg, httpClient) + repositoryService := repoService.NewService(repositoryRepository, githubService) contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) authHandler := auth.NewHandler(authService, appCfg) diff --git a/internal/app/github/domain.go b/internal/app/github/domain.go new file mode 100644 index 0000000..af7300f --- /dev/null +++ b/internal/app/github/domain.go @@ -0,0 +1,28 @@ +package github + +import "time" + +type RepoOwner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOwner `json:"owner"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` +} + +type RepoLanguages map[string]int + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} diff --git a/internal/app/github/service.go b/internal/app/github/service.go new file mode 100644 index 0000000..c4982c1 --- /dev/null +++ b/internal/app/github/service.go @@ -0,0 +1,118 @@ +package github + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" +) + +type service struct { + appCfg config.AppConfig + httpClient *http.Client +} + +type Service interface { + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) +} + +func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { + return &service{ + appCfg: appCfg, + httpClient: httpClient, + } +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + resp, err := s.httpClient.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { + req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + req, err := http.NewRequest("GET", getRepoContributorsURl, nil) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 6d8303a..60b7400 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -2,21 +2,6 @@ package repository import "time" -type RepoOwner struct { - Login string `json:"login"` -} - -type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOwner `json:"owner"` - ContributorsUrl string `json:"contributors_url"` - RepoUrl string `json:"html_url"` -} - type Repository struct { Id int GithubRepoId int @@ -39,19 +24,6 @@ type FetchUsersContributedReposResponse struct { TotalCoinsEarned int } -type FetchRepoContributorsResponse struct { - Id int `json:"id"` - Name string `json:"login"` - AvatarUrl string `json:"avatar_url"` - GithubUrl string `json:"html_url"` - Contributions int `json:"contributions"` -} - -type FetchParticularRepoDetails struct { - Repository - Contributors []FetchRepoContributorsResponse -} - type Contribution struct { Id int UserId int diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 083d14c..5edbb29 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -5,12 +5,14 @@ import ( "net/http" "strconv" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) type handler struct { repositoryService Service + githubService github.Service } type Handler interface { @@ -86,7 +88,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http return } - repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) if err != nil { slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) @@ -142,7 +144,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) @@ -150,7 +152,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, RepoLanguages(repoLanguages)) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index a7499f6..bbae42d 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -2,39 +2,32 @@ package repository import ( "context" - "encoding/json" - "io" "log/slog" "math" "net/http" - "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { repositoryRepository repository.RepositoryRepository - appCfg config.AppConfig - httpClient *http.Client + githubService github.Service } type Service interface { GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) - CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) - FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) - FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) } -func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig, httpClient *http.Client) Service { +func NewService(repositoryRepository repository.RepositoryRepository, githubService github.Service) Service { return &service{ repositoryRepository: repositoryRepository, - appCfg: appCfg, - httpClient: httpClient, + githubService: githubService, } } @@ -58,38 +51,13 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { - req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) { + repo, err := s.githubService.FetchRepositoryDetails(ctx, ContributionRepoDetailsUrl) if err != nil { slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - - resp, err := s.httpClient.Do(req) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - var repoDetails FetchRepositoryDetailsResponse - err = json.Unmarshal(body, &repoDetails) - if err != nil { - slog.Error("error unmarshalling fetch repository details body", "error", err) - return FetchRepositoryDetailsResponse{}, err + return Repository{}, err } - return repoDetails, nil -} - -func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { createRepo := Repository{ GithubRepoId: repoGithubId, RepoName: repo.Name, @@ -109,35 +77,6 @@ func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo F return Repository(repositoryCreated), nil } -func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { - req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return RepoLanguages{}, err - } - - var repoLanguages RepoLanguages - err = json.Unmarshal(body, &repoLanguages) - if err != nil { - slog.Error("error unmarshalling fetch repository languages body", "error", err) - return RepoLanguages{}, err - } - - return repoLanguages, nil -} - func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { @@ -150,7 +89,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C for i, usersContributedRepo := range usersContributedRepos { fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) - contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) if err != nil { slog.Error("error fetching languages for repository", "error", err) return nil, err @@ -172,35 +111,6 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } -func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { - req, err := http.NewRequest("GET", getRepoContributorsURl, nil) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return nil, err - } - - var repoContributors []FetchRepoContributorsResponse - err = json.Unmarshal(body, &repoContributors) - if err != nil { - slog.Error("error unmarshalling fetch contributors body", "error", err) - return nil, err - } - - return repoContributors, nil -} - func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { From 1e3e4a48b0106183d2fab910d6478a55fd3e3ca6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 17:34:39 +0530 Subject: [PATCH 26/28] reduce cognitive complexity of ProcessFetchedContributions Function by separating logic for ProcessEachContribution --- internal/app/contribution/service.go | 79 ++++++++++++++++------------ 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b4f31cb..5be08ec 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -57,6 +57,7 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error + ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) @@ -100,50 +101,60 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } for _, contribution := range fetchedContributions { - _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) - if err == nil { - continue - } - - if err != apperrors.ErrContributionNotFound { - slog.Error("error fetching contribution by github event id", "error", err) + err := s.ProcessEachContribution(ctx, contribution) + if err != nil { + slog.Error("error processing contribution with github event id", "github event id", "error", contribution.ID, err) return err } + } - var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err == nil { - repositoryId = repoFetched.Id - } else if err == apperrors.ErrRepoNotFound { - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } + return nil +} - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) - return err - } +func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error { + _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err == nil { + return nil + } - user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) - if err != nil { - slog.Error("error getting user id", "error", err) - return err - } + if err != apperrors.ErrContributionNotFound { + slog.Error("error fetching contribution by github event id", "error", err) + return err + } - contributionType, err := s.GetContributionType(ctx, contribution) + var repositoryId int + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + if err == nil { + repositoryId = repoFetched.Id + } else if err == apperrors.ErrRepoNotFound { + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) if err != nil { - slog.Error("error getting contribution type", "error", err) + slog.Error("error creating repository", "error", err) return err } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) - if err != nil { - slog.Error("error creating contribution", "error", err) - return err - } + repositoryId = repositoryCreated.Id + } else { + slog.Error("error fetching repo by repo id", "error", err) + return err + } + + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return err + } + + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return err + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) + if err != nil { + slog.Error("error creating contribution", "error", err) + return err } return nil From 76df1d2ca2397e113364a19fd4216aa5f3958ef4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 12:27:24 +0530 Subject: [PATCH 27/28] log error in GetContributionByGithubEventId --- internal/app/contribution/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 5be08ec..b8e832b 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -274,7 +274,7 @@ func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, e func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) { contribution, err := s.contributionRepository.GetContributionByGithubEventId(ctx, nil, githubEventId) if err != nil { - slog.Error("error fetching contribution by github event id") + slog.Error("error fetching contribution by github event id", "error", err) return Contribution{}, err } From f3c25f37edb9f33c89b961c9dc4b6ef659e6f62b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 12:27:59 +0530 Subject: [PATCH 28/28] make utility function for GET requests to github API's --- internal/app/github/domain.go | 2 ++ internal/app/github/service.go | 59 ++++++++++------------------------ internal/pkg/utils/helper.go | 30 +++++++++++++++++ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/internal/app/github/domain.go b/internal/app/github/domain.go index af7300f..efdd441 100644 --- a/internal/app/github/domain.go +++ b/internal/app/github/domain.go @@ -2,6 +2,8 @@ package github import "time" +const AuthorizationKey = "Authorization" + type RepoOwner struct { Login string `json:"login"` } diff --git a/internal/app/github/service.go b/internal/app/github/service.go index c4982c1..8b81912 100644 --- a/internal/app/github/service.go +++ b/internal/app/github/service.go @@ -3,11 +3,11 @@ package github import ( "context" "encoding/json" - "io" "log/slog" "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" ) type service struct { @@ -16,6 +16,7 @@ type service struct { } type Service interface { + configureGithubApiHeaders() map[string]string FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) @@ -28,24 +29,18 @@ func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { } } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { - req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err +func (s *service) configureGithubApiHeaders() map[string]string { + return map[string]string{ + AuthorizationKey: s.appCfg.GithubPersonalAccessToken, } +} - req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - - resp, err := s.httpClient.Do(req) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + headers := s.configureGithubApiHeaders() - body, err := io.ReadAll(resp.Body) + body, err := utils.DoGet(s.httpClient, getUserRepoDetailsUrl, headers) if err != nil { - slog.Error("error reading body", "error", err) + slog.Error("error making a GET request", "error", err) return FetchRepositoryDetailsResponse{}, err } @@ -60,21 +55,11 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails } func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { - req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } + headers := s.configureGithubApiHeaders() - resp, err := client.Do(req) + body, err := utils.DoGet(s.httpClient, getRepoLanguagesURL, headers) if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) + slog.Error("error making a GET request", "error", err) return RepoLanguages{}, err } @@ -89,22 +74,12 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli } func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { - req, err := http.NewRequest("GET", getRepoContributorsURl, nil) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } + headers := s.configureGithubApiHeaders() - body, err := io.ReadAll(resp.Body) + body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) if err != nil { - slog.Error("error reading body", "error", err) - return nil, err + slog.Error("error making a GET request", "error", err) + return []FetchRepoContributorsResponse{}, err } var repoContributors []FetchRepoContributorsResponse diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go index 05cca78..3fa0492 100644 --- a/internal/pkg/utils/helper.go +++ b/internal/pkg/utils/helper.go @@ -2,6 +2,9 @@ package utils import ( "fmt" + "io" + "log/slog" + "net/http" "strings" ) @@ -13,3 +16,30 @@ func FormatIntSliceForQuery(ids []int) string { return strings.Join(strIDs, ",") } + +func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + slog.Error("failed to create GET request", "error", err) + return nil, err + } + + for key, value := range headers { + req.Header.Add(key, value) + } + + resp, err := httpClient.Do(req) + if err != nil { + slog.Error("failed to send GET request", "error", err) + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + return body, nil +}