This commit is contained in:
Albert
2025-12-19 23:12:25 +04:00
22 changed files with 290 additions and 109 deletions
+2 -2
View File
@@ -30,8 +30,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=${CGO_ENABLED} \ CGO_ENABLED=${CGO_ENABLED} \
GOOS=${GOOS} \ GOOS=${GOOS} \
GOARCH=${GOARCH} \ GOARCH=${GOARCH} \
go build -trimpath \ go build -a -installsuffix cgo -trimpath \
-ldflags "-s -w -X main.BuildTime=${BUILD_TIME} -X main.Version=${VERSION}" \ -ldflags '-extldflags "-static" -s -w -X main.BuildTime=${BUILD_TIME} -X main.Version=${VERSION}' \
-o /out/${SERVICE} ./cmd/${SERVICE} && \ -o /out/${SERVICE} ./cmd/${SERVICE} && \
chmod +x /out/${SERVICE} chmod +x /out/${SERVICE}
+5 -5
View File
@@ -28,11 +28,11 @@ func Load() (*Config, error) {
_ = godotenv.Load() _ = godotenv.Load()
return &Config{ return &Config{
DBHost: getEnv("POSTGRES_HOST", "localhost"), DBHost: getEnv("POSTGRES_HOST", "localhost"),
DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive DBPort: mustGetInt("POSTGRES_PORT", 5432), //nolint:mnd // false-positive
DBUser: getEnv("POSTGRES_USERNAME", "postgres"), DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
DBName: getEnv("POSTGRES_DATABASE", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"),
}, nil }, nil
} }
+12 -1
View File
@@ -29,7 +29,18 @@ type Result struct {
UpdatedAt time.Time UpdatedAt time.Time
} }
func NewResult(id uuid.UUID, competitionID uuid.UUID, userID uuid.UUID, score float64, rank *int, status ResultStatus, submissionID *uuid.UUID, metadata map[string]interface{}, createdAt time.Time, updatedAt time.Time) *Result { func NewResult(
id uuid.UUID,
competitionID uuid.UUID,
userID uuid.UUID,
score float64,
rank *int,
status ResultStatus,
submissionID *uuid.UUID,
metadata map[string]interface{},
createdAt time.Time,
updatedAt time.Time,
) *Result {
return &Result{ return &Result{
ID: id, ID: id,
CompetitionID: competitionID, CompetitionID: competitionID,
+37 -7
View File
@@ -24,9 +24,23 @@ type IResultService interface {
GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error) GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error)
UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error) UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error)
DeleteResult(ctx context.Context, id uuid.UUID) error DeleteResult(ctx context.Context, id uuid.UUID) error
ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) (results []domain.Result, totalCount int32, nextPageToken int32, err error) ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) (results []domain.Result, totalCount int32, nextPageToken int32, err error)
GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error)
GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error)
RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error
} }
@@ -43,7 +57,10 @@ type ResultsHandler struct {
mu sync.RWMutex mu sync.RWMutex
} }
func NewResultsHandler(s IResultService, userServiceAddr, submissionServiceAddr, taskServiceAddr string) *ResultsHandler { func NewResultsHandler(
s IResultService,
userServiceAddr, submissionServiceAddr, taskServiceAddr string,
) *ResultsHandler {
return &ResultsHandler{ return &ResultsHandler{
service: s, service: s,
userServiceAddr: userServiceAddr, userServiceAddr: userServiceAddr,
@@ -210,7 +227,11 @@ func (h *ResultsHandler) fetchTaskStatuses(ctx context.Context, competitionID, u
return taskStatuses return taskStatuses
} }
func (h *ResultsHandler) enrichUserResult(ctx context.Context, result *domain.Result, competitionID string) *pb.UserResult { func (h *ResultsHandler) enrichUserResult(
ctx context.Context,
result *domain.Result,
competitionID string,
) *pb.UserResult {
userID := result.UserID.String() userID := result.UserID.String()
username := h.fetchUsername(ctx, userID) username := h.fetchUsername(ctx, userID)
taskStatuses := h.fetchTaskStatuses(ctx, competitionID, userID) taskStatuses := h.fetchTaskStatuses(ctx, competitionID, userID)
@@ -230,7 +251,10 @@ func (h *ResultsHandler) enrichUserResult(ctx context.Context, result *domain.Re
return pbResult return pbResult
} }
func (h *ResultsHandler) GetCompetitionResults(ctx context.Context, req *pb.GetCompetitionResultsRequest) (*pb.GetCompetitionResultsResponse, error) { func (h *ResultsHandler) GetCompetitionResults(
ctx context.Context,
req *pb.GetCompetitionResultsRequest,
) (*pb.GetCompetitionResultsResponse, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId()) competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
@@ -267,7 +291,10 @@ func (h *ResultsHandler) GetCompetitionResults(ctx context.Context, req *pb.GetC
}, nil }, nil
} }
func (h *ResultsHandler) GetUserCompetitionResults(ctx context.Context, req *pb.GetUserCompetitionResultsRequest) (*pb.GetUserCompetitionResultsResponse, error) { func (h *ResultsHandler) GetUserCompetitionResults(
ctx context.Context,
req *pb.GetUserCompetitionResultsRequest,
) (*pb.GetUserCompetitionResultsResponse, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId()) competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
@@ -294,7 +321,10 @@ func (h *ResultsHandler) GetUserCompetitionResults(ctx context.Context, req *pb.
}, nil }, nil
} }
func (h *ResultsHandler) RecalculateResults(ctx context.Context, req *pb.RecalculateResultsRequest) (*emptypb.Empty, error) { func (h *ResultsHandler) RecalculateResults(
ctx context.Context,
req *pb.RecalculateResultsRequest,
) (*emptypb.Empty, error) {
competitionID, err := uuid.Parse(req.GetCompetitionId()) competitionID, err := uuid.Parse(req.GetCompetitionId())
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err)
+26 -4
View File
@@ -53,28 +53,50 @@ func (m *MockResultService) DeleteResult(ctx context.Context, id uuid.UUID) erro
return errors.New("not implemented") return errors.New("not implemented")
} }
func (m *MockResultService) ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) (results []domain.Result, totalCount int32, nextPageToken int32, err error) { func (m *MockResultService) ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) (results []domain.Result, totalCount int32, nextPageToken int32, err error) {
if m.ListResultsFunc != nil { if m.ListResultsFunc != nil {
return m.ListResultsFunc(ctx, pageSize, pageToken, competitionID, userID, status, minScore, maxScore) return m.ListResultsFunc(ctx, pageSize, pageToken, competitionID, userID, status, minScore, maxScore)
} }
return nil, 0, 0, errors.New("not implemented") return nil, 0, 0, errors.New("not implemented")
} }
func (m *MockResultService) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) { func (m *MockResultService) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
if m.GetByCompetitionAndUserFunc != nil { if m.GetByCompetitionAndUserFunc != nil {
return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID) return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID)
} }
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
func (m *MockResultService) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) { func (m *MockResultService) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error) {
if m.GetLeaderboardFunc != nil { if m.GetLeaderboardFunc != nil {
return m.GetLeaderboardFunc(ctx, competitionID, limit, offset) return m.GetLeaderboardFunc(ctx, competitionID, limit, offset)
} }
return nil, 0, errors.New("not implemented") return nil, 0, errors.New("not implemented")
} }
func (m *MockResultService) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) { func (m *MockResultService) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
if m.UpdateStatusFunc != nil { if m.UpdateStatusFunc != nil {
return m.UpdateStatusFunc(ctx, id, status) return m.UpdateStatusFunc(ctx, id, status)
} }
+26 -7
View File
@@ -3,13 +3,14 @@ package postgres
import ( import (
"context" "context"
"database/sql" "database/sql"
"datarush/internal/results/domain"
"datarush/internal/results/repository"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"datarush/internal/results/domain"
"datarush/internal/results/repository"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@@ -257,8 +258,13 @@ func (r *ResultRepository) List(ctx context.Context, opts repository.ListResults
return nil, 0, err return nil, 0, err
} }
query := fmt.Sprintf(`SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at query := fmt.Sprintf(
FROM results %s ORDER BY score DESC, created_at ASC LIMIT $%d OFFSET $%d`, where, argId, argId+1) `SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at
FROM results %s ORDER BY score DESC, created_at ASC LIMIT $%d OFFSET $%d`,
where,
argId,
argId+1,
)
args = append(args, opts.PageSize, opts.Page*opts.PageSize) args = append(args, opts.PageSize, opts.Page*opts.PageSize)
rows, err := r.db.QueryContext(ctx, query, args...) rows, err := r.db.QueryContext(ctx, query, args...)
@@ -299,7 +305,11 @@ func (r *ResultRepository) List(ctx context.Context, opts repository.ListResults
return results, total, nil return results, total, nil
} }
func (r *ResultRepository) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) { func (r *ResultRepository) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
query := `SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at query := `SELECT id, competition_id, user_id, score, rank, status, submission_id, metadata, created_at, updated_at
FROM results WHERE competition_id = $1 AND user_id = $2` FROM results WHERE competition_id = $1 AND user_id = $2`
row := r.db.QueryRowContext(ctx, query, competitionID, userID) row := r.db.QueryRowContext(ctx, query, competitionID, userID)
@@ -334,7 +344,12 @@ func (r *ResultRepository) GetByCompetitionAndUser(ctx context.Context, competit
return &result, nil return &result, nil
} }
func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) { func (r *ResultRepository) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int,
offset int,
) ([]domain.Result, int, error) {
if r.cacheEnabled { if r.cacheEnabled {
key := r.leaderboardCacheKey(competitionID, limit, offset) key := r.leaderboardCacheKey(competitionID, limit, offset)
val, err := r.redisClient.Get(ctx, key).Result() val, err := r.redisClient.Get(ctx, key).Result()
@@ -404,7 +419,11 @@ func (r *ResultRepository) GetLeaderboard(ctx context.Context, competitionID uui
return results, total, nil return results, total, nil
} }
func (r *ResultRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) { func (r *ResultRepository) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
query := `UPDATE results SET status = $1, updated_at = $2 WHERE id = $3` query := `UPDATE results SET status = $1, updated_at = $2 WHERE id = $3`
now := time.Now() now := time.Now()
res, err := r.db.ExecContext(ctx, query, status, now, id) res, err := r.db.ExecContext(ctx, query, status, now, id)
+1
View File
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"datarush/internal/results/domain" "datarush/internal/results/domain"
"github.com/google/uuid" "github.com/google/uuid"
+21 -3
View File
@@ -52,7 +52,16 @@ func (s *Service) DeleteResult(ctx context.Context, id uuid.UUID) error {
return s.repo.Delete(ctx, id) return s.repo.Delete(ctx, id)
} }
func (s *Service) ListResults(ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64) ([]domain.Result, int32, int32, error) { func (s *Service) ListResults(
ctx context.Context,
pageSize int32,
pageToken int32,
competitionID *uuid.UUID,
userID *uuid.UUID,
status *domain.ResultStatus,
minScore *float64,
maxScore *float64,
) ([]domain.Result, int32, int32, error) {
opts := repository.ListResultsOptions{ opts := repository.ListResultsOptions{
Page: int(pageToken), Page: int(pageToken),
PageSize: int(pageSize), PageSize: int(pageSize),
@@ -76,11 +85,20 @@ func (s *Service) ListResults(ctx context.Context, pageSize int32, pageToken int
return results, int32(total), nextPageToken, nil return results, int32(total), nextPageToken, nil
} }
func (s *Service) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) { func (s *Service) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
return s.repo.GetByCompetitionAndUser(ctx, competitionID, userID) return s.repo.GetByCompetitionAndUser(ctx, competitionID, userID)
} }
func (s *Service) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int32, offset int32) ([]domain.Result, int32, error) { func (s *Service) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int32,
offset int32,
) ([]domain.Result, int32, error) {
results, total, err := s.repo.GetLeaderboard(ctx, competitionID, int(limit), int(offset)) results, total, err := s.repo.GetLeaderboard(ctx, competitionID, int(limit), int(offset))
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
+30 -17
View File
@@ -59,21 +59,34 @@ func (m *MockRepository) List(ctx context.Context, opts repository.ListResultsOp
return nil, 0, errors.New("not implemented") return nil, 0, errors.New("not implemented")
} }
func (m *MockRepository) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) { func (m *MockRepository) GetByCompetitionAndUser(
ctx context.Context,
competitionID uuid.UUID,
userID uuid.UUID,
) (*domain.Result, error) {
if m.GetByCompetitionAndUserFunc != nil { if m.GetByCompetitionAndUserFunc != nil {
return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID) return m.GetByCompetitionAndUserFunc(ctx, competitionID, userID)
} }
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
func (m *MockRepository) GetLeaderboard(ctx context.Context, competitionID uuid.UUID, limit int, offset int) ([]domain.Result, int, error) { func (m *MockRepository) GetLeaderboard(
ctx context.Context,
competitionID uuid.UUID,
limit int,
offset int,
) ([]domain.Result, int, error) {
if m.GetLeaderboardFunc != nil { if m.GetLeaderboardFunc != nil {
return m.GetLeaderboardFunc(ctx, competitionID, limit, offset) return m.GetLeaderboardFunc(ctx, competitionID, limit, offset)
} }
return nil, 0, errors.New("not implemented") return nil, 0, errors.New("not implemented")
} }
func (m *MockRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) { func (m *MockRepository) UpdateStatus(
ctx context.Context,
id uuid.UUID,
status domain.ResultStatus,
) (*domain.Result, error) {
if m.UpdateStatusFunc != nil { if m.UpdateStatusFunc != nil {
return m.UpdateStatusFunc(ctx, id, status) return m.UpdateStatusFunc(ctx, id, status)
} }
@@ -423,20 +436,20 @@ func TestListResults(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
pageSize int32 pageSize int32
pageToken int32 pageToken int32
competitionID *uuid.UUID competitionID *uuid.UUID
userID *uuid.UUID userID *uuid.UUID
status *domain.ResultStatus status *domain.ResultStatus
minScore *float64 minScore *float64
maxScore *float64 maxScore *float64
repoResults []domain.Result repoResults []domain.Result
repoTotal int repoTotal int
repoError error repoError error
expectError bool expectError bool
expectedTotal int32 expectedTotal int32
expectedNext int32 expectedNext int32
}{ }{
{ {
name: "successful list with filters", name: "successful list with filters",
+14 -4
View File
@@ -3,8 +3,9 @@ package grpc
import ( import (
"context" "context"
pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
pb "datarush/pkg/api/task"
) )
type TaskService interface { type TaskService interface {
@@ -12,7 +13,10 @@ type TaskService interface {
GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error)
EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error)
DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error)
ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error)
GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error)
} }
@@ -41,10 +45,16 @@ func (h *TaskHandler) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest)
return h.service.DeleteTask(ctx, req) return h.service.DeleteTask(ctx, req)
} }
func (h *TaskHandler) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) { func (h *TaskHandler) ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error) {
return h.service.ListCompetitionTasks(ctx, req) return h.service.ListCompetitionTasks(ctx, req)
} }
func (h *TaskHandler) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) { func (h *TaskHandler) GetTaskAttachments(
ctx context.Context,
req *pb.GetTaskAttachmentsRequest,
) (*pb.GetTaskAttachmentsResponse, error) {
return h.service.GetTaskAttachments(ctx, req) return h.service.GetTaskAttachments(ctx, req)
} }
@@ -21,7 +21,8 @@ func (r *TaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Ta
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at` VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at`
var createdTask task.Task var createdTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).StructScan(&createdTask) err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).
StructScan(&createdTask)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -46,7 +47,8 @@ func (r *TaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task
WHERE id = $7 RETURNING updated_at` WHERE id = $7 RETURNING updated_at`
var updatedTask task.Task var updatedTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).StructScan(&updatedTask) err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).
StructScan(&updatedTask)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -65,7 +67,11 @@ func (r *TaskRepository) ListCompetitionTasks(ctx context.Context, competitionID
return tasks, err return tasks, err
} }
func (r *TaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) { func (r *TaskRepository) GetTaskAttachments(
ctx context.Context,
taskID string,
showPrivate bool,
) ([]*task.TaskAttachment, error) {
var attachments []*task.TaskAttachment var attachments []*task.TaskAttachment
query := "SELECT * FROM task_attachments WHERE task_id = $1" query := "SELECT * FROM task_attachments WHERE task_id = $1"
args := []interface{}{taskID} args := []interface{}{taskID}
+9 -2
View File
@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
pb "datarush/pkg/api/task" pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
@@ -41,7 +42,10 @@ func (s *TaskService) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest)
return &emptypb.Empty{}, err return &emptypb.Empty{}, err
} }
func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) { func (s *TaskService) ListCompetitionTasks(
ctx context.Context,
req *pb.ListCompetitionTasksRequest,
) (*pb.ListCompetitionTasksResponse, error) {
tasks, err := s.repo.ListCompetitionTasks(ctx, req.CompetitionId) tasks, err := s.repo.ListCompetitionTasks(ctx, req.CompetitionId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -49,7 +53,10 @@ func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListComp
return &pb.ListCompetitionTasksResponse{Tasks: tasks}, nil return &pb.ListCompetitionTasksResponse{Tasks: tasks}, nil
} }
func (s *TaskService) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) { func (s *TaskService) GetTaskAttachments(
ctx context.Context,
req *pb.GetTaskAttachmentsRequest,
) (*pb.GetTaskAttachmentsResponse, error) {
showPrivate := false showPrivate := false
if req.ShowPrivate != nil { if req.ShowPrivate != nil {
showPrivate = *req.ShowPrivate showPrivate = *req.ShowPrivate
+23 -23
View File
@@ -7,8 +7,8 @@ import (
"datarush/internal/task/service/mocks" "datarush/internal/task/service/mocks"
pb "datarush/pkg/api/task" pb "datarush/pkg/api/task"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
@@ -24,13 +24,13 @@ func TestTaskService(t *testing.T) {
t.Run("CreateTask", func(t *testing.T) { t.Run("CreateTask", func(t *testing.T) {
task := &pb.Task{ task := &pb.Task{
CompetitionId: "comp1", CompetitionId: "comp1",
Title: "Test Task", Title: "Test Task",
Description: "This is a test task", Description: "This is a test task",
InCompetitionPosition: 1, InCompetitionPosition: 1,
MaxPoints: 100, MaxPoints: 100,
MaxAttempts: 10, MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT, Type: pb.TaskType_TASK_TYPE_INPUT,
} }
mockRepo.EXPECT().CreateTask(ctx, task).Return(task, nil) mockRepo.EXPECT().CreateTask(ctx, task).Return(task, nil)
@@ -44,16 +44,16 @@ func TestTaskService(t *testing.T) {
taskID := "task1" taskID := "task1"
req := &pb.GetTaskRequest{TaskId: taskID} req := &pb.GetTaskRequest{TaskId: taskID}
expectedTask := &pb.Task{ expectedTask := &pb.Task{
Id: taskID, Id: taskID,
CompetitionId: "comp1", CompetitionId: "comp1",
Title: "Test Task", Title: "Test Task",
Description: "This is a test task", Description: "This is a test task",
InCompetitionPosition: 1, InCompetitionPosition: 1,
MaxPoints: 100, MaxPoints: 100,
MaxAttempts: 10, MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT, Type: pb.TaskType_TASK_TYPE_INPUT,
CreatedAt: timestamppb.Now(), CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(), UpdatedAt: timestamppb.Now(),
} }
mockRepo.EXPECT().GetTask(ctx, taskID).Return(expectedTask, nil) mockRepo.EXPECT().GetTask(ctx, taskID).Return(expectedTask, nil)
@@ -65,14 +65,14 @@ func TestTaskService(t *testing.T) {
t.Run("EditTask", func(t *testing.T) { t.Run("EditTask", func(t *testing.T) {
task := &pb.Task{ task := &pb.Task{
Id: "task1", Id: "task1",
CompetitionId: "comp1", CompetitionId: "comp1",
Title: "Updated Test Task", Title: "Updated Test Task",
Description: "This is an updated test task", Description: "This is an updated test task",
InCompetitionPosition: 1, InCompetitionPosition: 1,
MaxPoints: 150, MaxPoints: 150,
MaxAttempts: 5, MaxAttempts: 5,
Type: pb.TaskType_TASK_TYPE_CHECKER, Type: pb.TaskType_TASK_TYPE_CHECKER,
} }
mockRepo.EXPECT().EditTask(ctx, task).Return(task, nil) mockRepo.EXPECT().EditTask(ctx, task).Return(task, nil)
+18 -5
View File
@@ -3,15 +3,19 @@ package grpc
import ( import (
"context" "context"
pb "datarush/pkg/api/user"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
pb "datarush/pkg/api/user"
) )
type UserService interface { type UserService interface {
GetProfile(ctx context.Context, req *pb.GetProfileRequest) (*pb.User, error) GetProfile(ctx context.Context, req *pb.GetProfileRequest) (*pb.User, error)
RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error) RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error)
UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error) UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error)
ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error) ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error)
} }
type UserHandler struct { type UserHandler struct {
@@ -27,14 +31,23 @@ func (h *UserHandler) GetProfile(ctx context.Context, req *pb.GetProfileRequest)
return h.service.GetProfile(ctx, req) return h.service.GetProfile(ctx, req)
} }
func (h *UserHandler) RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error) { func (h *UserHandler) RegisterForCompetition(
ctx context.Context,
req *pb.RegisterForCompetitionRequest,
) (*emptypb.Empty, error) {
return h.service.RegisterForCompetition(ctx, req) return h.service.RegisterForCompetition(ctx, req)
} }
func (h *UserHandler) UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error) { func (h *UserHandler) UnregisterFromCompetition(
ctx context.Context,
req *pb.UnregisterFromCompetitionRequest,
) (*emptypb.Empty, error) {
return h.service.UnregisterFromCompetition(ctx, req) return h.service.UnregisterFromCompetition(ctx, req)
} }
func (h *UserHandler) ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error) { func (h *UserHandler) ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error) {
return h.service.ListUserCompetitions(ctx, req) return h.service.ListUserCompetitions(ctx, req)
} }
@@ -3,8 +3,9 @@ package postgres
import ( import (
"context" "context"
"datarush/pkg/api/user"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"datarush/pkg/api/user"
) )
type UserRepository struct { type UserRepository struct {
@@ -17,22 +18,42 @@ func NewUserRepository(db *sqlx.DB) *UserRepository {
func (r *UserRepository) GetProfile(ctx context.Context, userID string) (*user.User, error) { func (r *UserRepository) GetProfile(ctx context.Context, userID string) (*user.User, error) {
var u user.User var u user.User
err := r.db.GetContext(ctx, &u, "SELECT id, username, email, full_name, avatar_url FROM users WHERE id = $1", userID) err := r.db.GetContext(
ctx,
&u,
"SELECT id, username, email, full_name, avatar_url FROM users WHERE id = $1",
userID,
)
return &u, err return &u, err
} }
func (r *UserRepository) RegisterForCompetition(ctx context.Context, userID, competitionID string) error { func (r *UserRepository) RegisterForCompetition(ctx context.Context, userID, competitionID string) error {
_, err := r.db.ExecContext(ctx, "INSERT INTO user_competitions (user_id, competition_id) VALUES ($1, $2)", userID, competitionID) _, err := r.db.ExecContext(
ctx,
"INSERT INTO user_competitions (user_id, competition_id) VALUES ($1, $2)",
userID,
competitionID,
)
return err return err
} }
func (r *UserRepository) UnregisterFromCompetition(ctx context.Context, userID, competitionID string) error { func (r *UserRepository) UnregisterFromCompetition(ctx context.Context, userID, competitionID string) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM user_competitions WHERE user_id = $1 AND competition_id = $2", userID, competitionID) _, err := r.db.ExecContext(
ctx,
"DELETE FROM user_competitions WHERE user_id = $1 AND competition_id = $2",
userID,
competitionID,
)
return err return err
} }
func (r *UserRepository) ListUserCompetitions(ctx context.Context, userID string) ([]string, error) { func (r *UserRepository) ListUserCompetitions(ctx context.Context, userID string) ([]string, error) {
var competitionIDs []string var competitionIDs []string
err := r.db.SelectContext(ctx, &competitionIDs, "SELECT competition_id FROM user_competitions WHERE user_id = $1", userID) err := r.db.SelectContext(
ctx,
&competitionIDs,
"SELECT competition_id FROM user_competitions WHERE user_id = $1",
userID,
)
return competitionIDs, err return competitionIDs, err
} }
+14 -4
View File
@@ -4,9 +4,10 @@ import (
"context" "context"
"errors" "errors"
"google.golang.org/protobuf/types/known/emptypb"
"datarush/internal/user/middleware" "datarush/internal/user/middleware"
pb "datarush/pkg/api/user" pb "datarush/pkg/api/user"
"google.golang.org/protobuf/types/known/emptypb"
) )
type UserRepository interface { type UserRepository interface {
@@ -28,7 +29,10 @@ func (s *UserService) GetProfile(ctx context.Context, req *pb.GetProfileRequest)
return s.repo.GetProfile(ctx, req.UserId) return s.repo.GetProfile(ctx, req.UserId)
} }
func (s *UserService) RegisterForCompetition(ctx context.Context, req *pb.RegisterForCompetitionRequest) (*emptypb.Empty, error) { func (s *UserService) RegisterForCompetition(
ctx context.Context,
req *pb.RegisterForCompetitionRequest,
) (*emptypb.Empty, error) {
userID, ok := ctx.Value(middleware.UserIDKey{}).(string) userID, ok := ctx.Value(middleware.UserIDKey{}).(string)
if !ok { if !ok {
return nil, errors.New("user ID not found in context") return nil, errors.New("user ID not found in context")
@@ -38,7 +42,10 @@ func (s *UserService) RegisterForCompetition(ctx context.Context, req *pb.Regist
return &emptypb.Empty{}, err return &emptypb.Empty{}, err
} }
func (s *UserService) UnregisterFromCompetition(ctx context.Context, req *pb.UnregisterFromCompetitionRequest) (*emptypb.Empty, error) { func (s *UserService) UnregisterFromCompetition(
ctx context.Context,
req *pb.UnregisterFromCompetitionRequest,
) (*emptypb.Empty, error) {
userID, ok := ctx.Value(middleware.UserIDKey{}).(string) userID, ok := ctx.Value(middleware.UserIDKey{}).(string)
if !ok { if !ok {
return nil, errors.New("user ID not found in context") return nil, errors.New("user ID not found in context")
@@ -48,7 +55,10 @@ func (s *UserService) UnregisterFromCompetition(ctx context.Context, req *pb.Unr
return &emptypb.Empty{}, err return &emptypb.Empty{}, err
} }
func (s *UserService) ListUserCompetitions(ctx context.Context, req *pb.ListUserCompetitionsRequest) (*pb.ListUserCompetitionsResponse, error) { func (s *UserService) ListUserCompetitions(
ctx context.Context,
req *pb.ListUserCompetitionsRequest,
) (*pb.ListUserCompetitionsResponse, error) {
competitionIDs, err := s.repo.ListUserCompetitions(ctx, req.UserId) competitionIDs, err := s.repo.ListUserCompetitions(ctx, req.UserId)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -9,8 +9,8 @@ import (
"datarush/internal/user/service/mocks" "datarush/internal/user/service/mocks"
pb "datarush/pkg/api/user" pb "datarush/pkg/api/user"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
) )
func TestUserService(t *testing.T) { func TestUserService(t *testing.T) {