From f3620da240ab4fa2432f7c40b0f233e194b1c093 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:21:08 +0300 Subject: [PATCH] rewrite competition service: remove http related things --- go.mod | 1 - go.sum | 2 - internal/competition/config/config.go | 2 + internal/competition/domain/competition.go | 73 ------ internal/competition/domain/errors.go | 8 - .../competition/handler/grpc/competition.go | 201 ---------------- internal/competition/handler/grpc/handler.go | 50 ++++ .../competition/repository/competition.go | 18 +- .../repository/postgres/competition.go | 219 ++++++------------ internal/competition/server/server.go | 45 ++-- internal/competition/service/competition.go | 75 ------ internal/competition/service/service.go | 78 +++++++ 12 files changed, 229 insertions(+), 543 deletions(-) delete mode 100644 internal/competition/domain/competition.go delete mode 100644 internal/competition/domain/errors.go delete mode 100644 internal/competition/handler/grpc/competition.go create mode 100644 internal/competition/handler/grpc/handler.go delete mode 100644 internal/competition/service/competition.go create mode 100644 internal/competition/service/service.go diff --git a/go.mod b/go.mod index 9d96342..e7130d6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ toolchain go1.24.9 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.28.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-migrate/migrate/v4 v4.19.0 diff --git a/go.sum b/go.sum index af41754..aa22628 100644 --- a/go.sum +++ b/go.sum @@ -35,7 +35,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -52,7 +51,6 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go index 6388bf7..7245d9c 100644 --- a/internal/competition/config/config.go +++ b/internal/competition/config/config.go @@ -20,6 +20,7 @@ type Config struct { DBUser string DBPassword string DBName string + JWTSecret string } func Load() (*Config, error) { @@ -35,6 +36,7 @@ func Load() (*Config, error) { DBUser: getEnv("POSTGRES_USERNAME", "postgres"), DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), }, nil } diff --git a/internal/competition/domain/competition.go b/internal/competition/domain/competition.go deleted file mode 100644 index de9c1f3..0000000 --- a/internal/competition/domain/competition.go +++ /dev/null @@ -1,73 +0,0 @@ -package domain - -import ( - "fmt" - "time" - - "github.com/go-playground/validator/v10" - "github.com/google/uuid" -) - -type CompetitionState string - -const ( - CompetitionStateUnspecified CompetitionState = "UNSPECIFIED" - CompetitionStateDraft CompetitionState = "DRAFT" - CompetitionStateNotStarted CompetitionState = "NOT_STARTED" - CompetitionStateStarted CompetitionState = "STARTED" - CompetitionStateFinished CompetitionState = "FINISHED" - CompetitionStateArchived CompetitionState = "ARCHIVED" -) - -type ParticipationType string - -const ( - ParticipationTypeUnspecified ParticipationType = "UNSPECIFIED" - ParticipationTypeIndividual ParticipationType = "INDIVIDUAL" - ParticipationTypeTeam ParticipationType = "TEAM" -) - -type CompetitionType string - -const ( - CompetitionTypeUnspecified CompetitionType = "UNSPECIFIED" - CompetitionTypeEducative CompetitionType = "EDUCATIVE" - CompetitionTypeCompetitive CompetitionType = "COMPETITIVE" -) - -type Competition struct { - ID uuid.UUID - State CompetitionState - Title string - Description string - ImageURL *string - StartTime time.Time - EndTime time.Time - Type CompetitionType - ParticipationType ParticipationType - CreatedAt time.Time - UpdatedAt time.Time -} - - -func NewCompetition(id uuid.UUID, state CompetitionState, title string, description string, imageURL *string, startTime time.Time, endTime time.Time, tpe CompetitionType, participationType ParticipationType, createdAt time.Time, updatedAt time.Time) *Competition { - return &Competition{ - ID: id, - State: state, - Title: title, - Description: description, - ImageURL: imageURL, - StartTime: startTime, - EndTime: endTime, - Type: tpe, - ParticipationType: participationType, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } -} - -func (c *Competition) Validate() error { - validate := validator.New() - - return fmt.Errorf("%w: %w", ErrInvalidCompetitionData, validate.Struct(c)) -} diff --git a/internal/competition/domain/errors.go b/internal/competition/domain/errors.go deleted file mode 100644 index 86b9827..0000000 --- a/internal/competition/domain/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package domain - -import "errors" - -var ( - ErrCompetitionNotFound = errors.New("competition not found") - ErrInvalidCompetitionData = errors.New("invalid competition data") -) diff --git a/internal/competition/handler/grpc/competition.go b/internal/competition/handler/grpc/competition.go deleted file mode 100644 index 427276a..0000000 --- a/internal/competition/handler/grpc/competition.go +++ /dev/null @@ -1,201 +0,0 @@ -package grpc - -import ( - "context" - - "datarush/internal/competition/domain" - pb "datarush/pkg/api/competition" - "strings" - - "github.com/google/uuid" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// ICompetitionService defines the interface for business logic. -// It works with domain models. -type ICompetitionService interface { - CreateCompetition(ctx context.Context, comp *domain.Competition) (*domain.Competition, error) - GetCompetition(ctx context.Context, id uuid.UUID) (*domain.Competition, error) - EditCompetition(ctx context.Context, comp *domain.Competition) (*domain.Competition, error) - DeleteCompetition(ctx context.Context, id uuid.UUID) error - ListCompetitions(ctx context.Context, pageSize int32, pageToken int32, state *domain.CompetitionState, isParticipating *bool, searchQuery *string) (competitions []domain.Competition, totalCount int32, nextPageToken int32, err error) - ChangeCompetitionState(ctx context.Context, id uuid.UUID, state domain.CompetitionState) (*domain.Competition, error) -} - -type CompetitionHandler struct { - pb.UnimplementedCompetitionServiceServer - service ICompetitionService -} - -func NewCompetitionHandler(s ICompetitionService) *CompetitionHandler { - return &CompetitionHandler{service: s} -} - -func (h *CompetitionHandler) CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { - domainComp, err := toDomainCompetition(req) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "failed to map competition: %v", err) - } - - createdComp, err := h.service.CreateCompetition(ctx, domainComp) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to create competition: %v", err) - } - - return fromDomainCompetition(createdComp), nil -} - -func (h *CompetitionHandler) GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) { - id, err := uuid.Parse(req.GetCompetitionId()) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format") - } - comp, err := h.service.GetCompetition(ctx, id) - if err != nil { - return nil, status.Errorf(codes.NotFound, "failed to get competition: %v", err) - } - return fromDomainCompetition(comp), nil -} - -func (h *CompetitionHandler) EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { - domainComp, err := toDomainCompetition(req) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "failed to map competition: %v", err) - } - - updatedComp, err := h.service.EditCompetition(ctx, domainComp) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to edit competition: %v", err) - } - - return fromDomainCompetition(updatedComp), nil -} - -func (h *CompetitionHandler) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) { - id, err := uuid.Parse(req.GetCompetitionId()) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format") - } - if err := h.service.DeleteCompetition(ctx, id); err != nil { - return nil, status.Errorf(codes.Internal, "failed to delete competition: %v", err) - } - return &emptypb.Empty{}, nil -} - -func (h *CompetitionHandler) ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) { - var state *domain.CompetitionState - if req.State != nil { - s := toDomainCompetitionState(*req.State) - state = &s - } - - var searchQuery *string - if req.SearchQuery != nil { - searchQuery = req.SearchQuery - } - - competitions, totalCount, nextPageToken, err := h.service.ListCompetitions(ctx, req.PageSize, req.PageToken, state, req.IsParticipating, searchQuery) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to list competitions: %v", err) - } - - pbCompetitions := make([]*pb.Competition, len(competitions)) - for i, c := range competitions { - pbCompetitions[i] = fromDomainCompetition(&c) - } - - return &pb.ListCompetitionsResponse{ - Competitions: pbCompetitions, - TotalCount: totalCount, - NextPageToken: nextPageToken, - }, nil -} - -func (h *CompetitionHandler) ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) { - id, err := uuid.Parse(req.GetCompetitionId()) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format") - } - - comp, err := h.service.ChangeCompetitionState(ctx, id, toDomainCompetitionState(req.GetState())) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to change competition state: %v", err) - } - return fromDomainCompetition(comp), nil -} - - -func toDomainCompetition(c *pb.Competition) (*domain.Competition, error) { - if c == nil { - return nil, nil - } - - var id uuid.UUID - var err error - if c.Id != "" { - id, err = uuid.Parse(c.Id) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid id format") - } - } - - return &domain.Competition{ - ID: id, - State: toDomainCompetitionState(c.State), - Title: c.Title, - Description: c.Description, - ImageURL: c.ImageUrl, - StartTime: c.StartTime.AsTime(), - EndTime: c.EndTime.AsTime(), - Type: toDomainCompetitionType(c.Type), - ParticipationType: toDomainParticipationType(c.ParticipationType), - CreatedAt: c.CreatedAt.AsTime(), - UpdatedAt: c.UpdatedAt.AsTime(), - }, nil -} - -func fromDomainCompetition(c *domain.Competition) *pb.Competition { - if c == nil { - return nil - } - return &pb.Competition{ - Id: c.ID.String(), - State: fromDomainCompetitionState(c.State), - Title: c.Title, - Description: c.Description, - ImageUrl: c.ImageURL, - StartTime: timestamppb.New(c.StartTime), - EndTime: timestamppb.New(c.EndTime), - Type: fromDomainCompetitionType(c.Type), - ParticipationType: fromDomainParticipationType(c.ParticipationType), - CreatedAt: timestamppb.New(c.CreatedAt), - UpdatedAt: timestamppb.New(c.UpdatedAt), - } -} - -func toDomainCompetitionState(s pb.CompetitionState) domain.CompetitionState { - return domain.CompetitionState(strings.TrimPrefix(s.String(), "COMPETITION_STATE_")) -} - -func fromDomainCompetitionState(s domain.CompetitionState) pb.CompetitionState { - return pb.CompetitionState(pb.CompetitionState_value["COMPETITION_STATE_"+string(s)]) -} - -func toDomainParticipationType(pt pb.ParticipationType) domain.ParticipationType { - return domain.ParticipationType(strings.TrimPrefix(pt.String(), "PARTICIPATION_TYPE_")) -} - -func fromDomainParticipationType(pt domain.ParticipationType) pb.ParticipationType { - return pb.ParticipationType(pb.ParticipationType_value["PARTICIPATION_TYPE_"+string(pt)]) -} - -func toDomainCompetitionType(ct pb.CompetitionType) domain.CompetitionType { - return domain.CompetitionType(strings.TrimPrefix(ct.String(), "COMPETITION_TYPE_")) -} - -func fromDomainCompetitionType(ct domain.CompetitionType) pb.CompetitionType { - return pb.CompetitionType(pb.CompetitionType_value["COMPETITION_TYPE_"+string(ct)]) -} \ No newline at end of file diff --git a/internal/competition/handler/grpc/handler.go b/internal/competition/handler/grpc/handler.go new file mode 100644 index 0000000..5ee136b --- /dev/null +++ b/internal/competition/handler/grpc/handler.go @@ -0,0 +1,50 @@ +package grpc + +import ( + "context" + + pb "datarush/pkg/api/competition" + "google.golang.org/protobuf/types/known/emptypb" +) + +type CompetitionService interface { + CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) + GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) + EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) + DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) + ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) + ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) +} + +type CompetitionHandler struct { + pb.UnimplementedCompetitionServiceServer + service CompetitionService +} + +func NewCompetitionHandler(service CompetitionService) *CompetitionHandler { + return &CompetitionHandler{service: service} +} + +func (h *CompetitionHandler) CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return h.service.CreateCompetition(ctx, req) +} + +func (h *CompetitionHandler) GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) { + return h.service.GetCompetition(ctx, req) +} + +func (h *CompetitionHandler) EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return h.service.EditCompetition(ctx, req) +} + +func (h *CompetitionHandler) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) { + return h.service.DeleteCompetition(ctx, req) +} + +func (h *CompetitionHandler) ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) { + return h.service.ListCompetitions(ctx, req) +} + +func (h *CompetitionHandler) ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) { + return h.service.ChangeCompetitionState(ctx, req) +} \ No newline at end of file diff --git a/internal/competition/repository/competition.go b/internal/competition/repository/competition.go index f8d8498..e82bb23 100644 --- a/internal/competition/repository/competition.go +++ b/internal/competition/repository/competition.go @@ -2,25 +2,25 @@ package repository import ( "context" - "datarush/internal/competition/domain" "github.com/google/uuid" -) + pb "datarush/pkg/api/competition" +) type ListCompetitionsOptions struct { Page int PageSize int - State *domain.CompetitionState + State *pb.CompetitionState IsParticipating *bool SearchQuery *string } type CompetitionRepository interface { - Create(ctx context.Context, competition *domain.Competition) error - Get(ctx context.Context, id uuid.UUID) (*domain.Competition, error) - Update(ctx context.Context, competition *domain.Competition) error + Create(ctx context.Context, competition *pb.Competition) (*pb.Competition, error) + Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) + Update(ctx context.Context, competition *pb.Competition) (*pb.Competition, error) Delete(ctx context.Context, id uuid.UUID) error - List(ctx context.Context, opts ListCompetitionsOptions) ([]domain.Competition, int, error) - ChangeState(ctx context.Context, id uuid.UUID, state domain.CompetitionState) (*domain.Competition, error) -} + List(ctx context.Context, opts ListCompetitionsOptions) ([]*pb.Competition, int, error) + ChangeState(ctx context.Context, id uuid.UUID, state pb.CompetitionState) (*pb.Competition, error) +} \ No newline at end of file diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go index 5832a88..a2778a5 100644 --- a/internal/competition/repository/postgres/competition.go +++ b/internal/competition/repository/postgres/competition.go @@ -2,152 +2,91 @@ package postgres import ( "context" - "database/sql" - "datarush/internal/competition/domain" - "datarush/internal/competition/repository" - "encoding/json" "fmt" "strings" "time" - "github.com/google/uuid" - "github.com/redis/go-redis/v9" -) + "datarush/internal/competition/repository" + pb "datarush/pkg/api/competition" -const ( - competitionCachePrefix = "competition:" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" ) type CompetitionRepository struct { - db *sql.DB - redisClient *redis.Client - cacheEnabled bool + db *sqlx.DB } -func NewCompetitionRepository(db *sql.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { +func NewCompetitionRepository(db *sqlx.DB) repository.CompetitionRepository { return &CompetitionRepository{ - db: db, - redisClient: redisClient, - cacheEnabled: cacheEnabled, + db: db, } } -func (r *CompetitionRepository) cacheKey(id string) string { - return competitionCachePrefix + id -} +func (r *CompetitionRepository) Create(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { + query := `INSERT INTO competitions (state, title, description, image_url, start_time, end_time, type, participation_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, updated_at` -func (r *CompetitionRepository) Create(ctx context.Context, competition *domain.Competition) error { - query := `INSERT INTO competitions (id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)` - _, err := r.db.ExecContext(ctx, query, - competition.ID, - competition.State, - competition.Title, - competition.Description, - competition.ImageURL, - competition.StartTime, - competition.EndTime, - competition.Type, - competition.ParticipationType, - competition.CreatedAt, - competition.UpdatedAt, - ) - if err != nil { - return err - } - - if r.cacheEnabled { - data, err := json.Marshal(competition) - if err == nil { - r.redisClient.Set(ctx, r.cacheKey(competition.ID.String()), data, 10*time.Minute).Err() - } - } - - return nil -} - -func (r *CompetitionRepository) Get(ctx context.Context, id uuid.UUID) (*domain.Competition, error) { - if r.cacheEnabled { - val, err := r.redisClient.Get(ctx, r.cacheKey(id.String())).Result() - if err == nil { - var competition domain.Competition - if json.Unmarshal([]byte(val), &competition) == nil { - return &competition, nil - } - } - } - - query := `SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions WHERE id = $1` - row := r.db.QueryRowContext(ctx, query, id) - - var competition domain.Competition - err := row.Scan( - &competition.ID, - &competition.State, - &competition.Title, - &competition.Description, - &competition.ImageURL, - &competition.StartTime, - &competition.EndTime, - &competition.Type, - &competition.ParticipationType, - &competition.CreatedAt, - &competition.UpdatedAt, - ) + var createdCompetition pb.Competition + err := r.db.QueryRowxContext(ctx, query, + c.State, + c.Title, + c.Description, + c.ImageUrl, + c.StartTime.AsTime(), + c.EndTime.AsTime(), + c.Type, + c.ParticipationType, + ).StructScan(&createdCompetition) if err != nil { return nil, err } + c.Id = createdCompetition.Id + c.CreatedAt = createdCompetition.CreatedAt + c.UpdatedAt = createdCompetition.UpdatedAt - if r.cacheEnabled { - data, err := json.Marshal(&competition) - if err == nil { - r.redisClient.Set(ctx, r.cacheKey(id.String()), data, 10*time.Minute).Err() - } - } - - return &competition, nil + return c, nil } -func (r *CompetitionRepository) Update(ctx context.Context, competition *domain.Competition) error { +func (r *CompetitionRepository) Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) { + query := `SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions WHERE id = $1` + var competition pb.Competition + err := r.db.GetContext(ctx, &competition, query, id) + return &competition, err +} + +func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { query := `UPDATE competitions SET - state = $2, title = $3, description = $4, image_url = $5, start_time = $6, end_time = $7, type = $8, participation_type = $9, updated_at = $10 - WHERE id = $1` - _, err := r.db.ExecContext(ctx, query, - competition.ID, - competition.State, - competition.Title, - competition.Description, - competition.ImageURL, - competition.StartTime, - competition.EndTime, - competition.Type, - competition.ParticipationType, - competition.UpdatedAt, - ) + state = $2, title = $3, description = $4, image_url = $5, start_time = $6, end_time = $7, type = $8, participation_type = $9, updated_at = now() + WHERE id = $1 RETURNING updated_at` + + var updatedCompetition pb.Competition + err := r.db.QueryRowxContext(ctx, query, + c.Id, + c.State, + c.Title, + c.Description, + c.ImageUrl, + c.StartTime.AsTime(), + c.EndTime.AsTime(), + c.Type, + c.ParticipationType, + ).StructScan(&updatedCompetition) if err != nil { - return err + return nil, err } + c.UpdatedAt = updatedCompetition.UpdatedAt - if r.cacheEnabled { - r.redisClient.Del(ctx, r.cacheKey(competition.ID.String())).Err() - } - - return nil + return c, nil } + func (r *CompetitionRepository) Delete(ctx context.Context, id uuid.UUID) error { query := `DELETE FROM competitions WHERE id = $1` _, err := r.db.ExecContext(ctx, query, id) - if err != nil { - return err - } - - if r.cacheEnabled { - r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() - } - - return nil + return err } -func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]domain.Competition, int, error) { + +func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*pb.Competition, int, error) { var args []interface{} var whereClauses []string argId := 1 @@ -164,15 +103,15 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo } if opts.IsParticipating != nil { - userID, ok := ctx.Value("user_id").(uuid.UUID) + userID, ok := ctx.Value("user_id").(string) if !ok { return nil, 0, fmt.Errorf("user not authenticated or user_id not in context") } if *opts.IsParticipating { - whereClauses = append(whereClauses, fmt.Sprintf("id IN (SELECT competition_id FROM competition_participants WHERE user_id = $%d)", argId)) + whereClauses = append(whereClauses, fmt.Sprintf("id IN (SELECT competition_id FROM user_competitions WHERE user_id = $%d)", argId)) } else { - whereClauses = append(whereClauses, fmt.Sprintf("id NOT IN (SELECT competition_id FROM competition_participants WHERE user_id = $%d)", argId)) + whereClauses = append(whereClauses, fmt.Sprintf("id NOT IN (SELECT competition_id FROM user_competitions WHERE user_id = $%d)", argId)) } args = append(args, userID) argId++ @@ -185,54 +124,30 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo countQuery := "SELECT COUNT(*) FROM competitions " + where var total int - if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil { + if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil { return nil, 0, err } query := fmt.Sprintf(`SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`, where, argId, argId+1) - args = append(args, opts.PageSize, opts.Page*opts.PageSize) + args = append(args, opts.PageSize, (opts.Page-1)*opts.PageSize) - rows, err := r.db.QueryContext(ctx, query, args...) + var competitions []*pb.Competition + err := r.db.SelectContext(ctx, &competitions, query, args...) if err != nil { return nil, 0, err } - defer rows.Close() - - var competitions []domain.Competition - for rows.Next() { - var c domain.Competition - err := rows.Scan( - &c.ID, - &c.State, - &c.Title, - &c.Description, - &c.ImageURL, - &c.StartTime, - &c.EndTime, - &c.Type, - &c.ParticipationType, - &c.CreatedAt, - &c.UpdatedAt, - ) - if err != nil { - return nil, 0, err - } - competitions = append(competitions, c) - } return competitions, total, nil } -func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, state domain.CompetitionState) (*domain.Competition, error) { - query := `UPDATE competitions SET state = $1, updated_at = $2 WHERE id = $3` + +func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, state pb.CompetitionState) (*pb.Competition, error) { + query := `UPDATE competitions SET state = $1, updated_at = $2 WHERE id = $3 RETURNING updated_at` now := time.Now() - _, err := r.db.ExecContext(ctx, query, state, now, id) + var competition pb.Competition + err := r.db.QueryRowxContext(ctx, query, state, now, id).StructScan(&competition) if err != nil { return nil, err } - if r.cacheEnabled { - r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() - } - return r.Get(ctx, id) } \ No newline at end of file diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go index eb6f4b6..4325c6d 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -4,11 +4,11 @@ import ( "fmt" "log" "net" - "net/http" "time" "datarush/internal/competition/config" grpcHandlers "datarush/internal/competition/handler/grpc" + "datarush/internal/competition/middleware" "datarush/internal/competition/repository/postgres" "datarush/internal/competition/service" pb "datarush/pkg/api/competition" @@ -27,7 +27,6 @@ const ( type Server struct { grpcServer *grpc.Server - httpServer *http.Server config *config.Config db *sqlx.DB } @@ -45,32 +44,32 @@ func (s *Server) Start() error { } s.db = db + if err := s.registerGRPCServices(); err != nil { + return fmt.Errorf("failed to register gRPC services: %w", err) + } + go func() { - if err := s.startGRPCServer(); err != nil { - log.Fatalf("failed to start gRPC server: %v", err) + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.GRPCPort)) + if err != nil { + log.Fatalf("failed to listen on grpc port: %v", err) + } + + log.Printf("starting gRPC server on port %d", s.config.GRPCPort) + if err := s.grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) } }() - log.Println("competition service started") return nil } -func (s *Server) startGRPCServer() error { - lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.GRPCPort)) - if err != nil { - return fmt.Errorf("failed to listen on grpc port: %w", err) - } +func (s *Server) registerGRPCServices() error { + s.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(middleware.AuthInterceptor(s.config.JWTSecret)), + ) - s.grpcServer = grpc.NewServer() - s.registerGRPCServices() - - log.Printf("starting gRPC server on port %d", s.config.GRPCPort) - return s.grpcServer.Serve(lis) -} - -func (s *Server) registerGRPCServices() { - compRepo := postgres.NewCompetitionRepository(s.db.DB, nil, false) - compService := service.NewService(compRepo) + compRepo := postgres.NewCompetitionRepository(s.db) + compService := service.NewCompetitionService(compRepo) compHandler := grpcHandlers.NewCompetitionHandler(compService) pb.RegisterCompetitionServiceServer(s.grpcServer, compHandler) @@ -78,6 +77,8 @@ func (s *Server) registerGRPCServices() { if s.config.GRPCEnableReflection { reflection.Register(s.grpcServer) } + + return nil } func (s *Server) Stop() { @@ -89,9 +90,9 @@ func (s *Server) Stop() { if s.db != nil { if err := s.db.Close(); err != nil { - log.Printf("failed to close database connection: %v", err) + log.Printf("failed to close database: %v", err) } } log.Println("competition server stopped") -} +} \ No newline at end of file diff --git a/internal/competition/service/competition.go b/internal/competition/service/competition.go deleted file mode 100644 index 68699f7..0000000 --- a/internal/competition/service/competition.go +++ /dev/null @@ -1,75 +0,0 @@ -package service - -import ( - "context" - "time" - - "datarush/internal/competition/domain" - "datarush/internal/competition/repository" - - "github.com/google/uuid" -) - -type Service struct { - repo repository.CompetitionRepository -} - -func NewService(repo repository.CompetitionRepository) *Service { - return &Service{repo: repo} -} - -func (s *Service) CreateCompetition(ctx context.Context, comp *domain.Competition) (*domain.Competition, error) { - now := time.Now() - if comp.ID == uuid.Nil { - comp.ID = uuid.New() - } - comp.CreatedAt = now - comp.UpdatedAt = now - - if err := s.repo.Create(ctx, comp); err != nil { - return nil, err - } - return comp, nil -} - -func (s *Service) GetCompetition(ctx context.Context, id uuid.UUID) (*domain.Competition, error) { - return s.repo.Get(ctx, id) -} - -func (s *Service) EditCompetition(ctx context.Context, comp *domain.Competition) (*domain.Competition, error) { - comp.UpdatedAt = time.Now() - if err := s.repo.Update(ctx, comp); err != nil { - return nil, err - } - return s.repo.Get(ctx, comp.ID) -} - -func (s *Service) DeleteCompetition(ctx context.Context, id uuid.UUID) error { - return s.repo.Delete(ctx, id) -} - -func (s *Service) ListCompetitions(ctx context.Context, pageSize int32, pageToken int32, state *domain.CompetitionState, isParticipating *bool, searchQuery *string) ([]domain.Competition, int32, int32, error) { - opts := repository.ListCompetitionsOptions{ - Page: int(pageToken), - PageSize: int(pageSize), - State: state, - IsParticipating: isParticipating, - SearchQuery: searchQuery, - } - - competitions, total, err := s.repo.List(ctx, opts) - if err != nil { - return nil, 0, 0, err - } - - var nextPageToken int32 - if (opts.Page+1)*opts.PageSize < total { - nextPageToken = int32(opts.Page + 1) - } - - return competitions, int32(total), nextPageToken, nil -} - -func (s *Service) ChangeCompetitionState(ctx context.Context, id uuid.UUID, state domain.CompetitionState) (*domain.Competition, error) { - return s.repo.ChangeState(ctx, id, state) -} diff --git a/internal/competition/service/service.go b/internal/competition/service/service.go new file mode 100644 index 0000000..3f07da8 --- /dev/null +++ b/internal/competition/service/service.go @@ -0,0 +1,78 @@ +package service + +import ( + "context" + + "datarush/internal/competition/repository" + pb "datarush/pkg/api/competition" + + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/emptypb" +) + +type CompetitionService struct { + repo repository.CompetitionRepository +} + +func NewCompetitionService(repo repository.CompetitionRepository) *CompetitionService { + return &CompetitionService{repo: repo} +} + +func (s *CompetitionService) CreateCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return s.repo.Create(ctx, req) +} + +func (s *CompetitionService) GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + return s.repo.Get(ctx, id) +} + +func (s *CompetitionService) EditCompetition(ctx context.Context, req *pb.Competition) (*pb.Competition, error) { + return s.repo.Update(ctx, req) +} + +func (s *CompetitionService) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + err = s.repo.Delete(ctx, id) + return &emptypb.Empty{}, err +} + +func (s *CompetitionService) ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) { + opts := repository.ListCompetitionsOptions{ + Page: int(req.PageToken), + PageSize: int(req.PageSize), + State: req.State, + IsParticipating: req.IsParticipating, + SearchQuery: req.SearchQuery, + } + + competitions, total, err := s.repo.List(ctx, opts) + if err != nil { + return nil, err + } + + var nextPageToken int32 + if (opts.Page+1)*opts.PageSize < total { + nextPageToken = int32(opts.Page + 1) + } + + return &pb.ListCompetitionsResponse{ + Competitions: competitions, + TotalCount: int32(total), + NextPageToken: nextPageToken, + }, nil +} + +func (s *CompetitionService) ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) { + id, err := uuid.Parse(req.CompetitionId) + if err != nil { + return nil, err + } + return s.repo.ChangeState(ctx, id, req.State) +} \ No newline at end of file