From 69d99e08ea8bc1aba449a0cd07ca2f1f5e1eed2b Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Tue, 16 Dec 2025 23:14:01 +0300 Subject: [PATCH 01/11] add competition service (it`s broken, I will fix it later) --- cmd/competition/main.go | 32 +++ cmd/server/main.go | 26 ++ internal/competition/config/config.go | 74 ++++++ internal/competition/domain/competition.go | 73 ++++++ internal/competition/domain/errors.go | 8 + .../competition/handler/grpc/competition.go | 199 +++++++++++++++ .../competition/repository/competition.go | 26 ++ .../repository/postgres/competition.go | 240 ++++++++++++++++++ internal/competition/server/server.go | 143 +++++++++++ internal/competition/service/competition.go | 75 ++++++ internal/lms/config/config.go | 2 + internal/lms/server/server.go | 2 - pkg/api/competition/competition.pb.go | 221 +++++++++++++++- 13 files changed, 1116 insertions(+), 5 deletions(-) create mode 100644 cmd/competition/main.go create mode 100644 internal/competition/config/config.go create mode 100644 internal/competition/domain/competition.go create mode 100644 internal/competition/domain/errors.go create mode 100644 internal/competition/handler/grpc/competition.go create mode 100644 internal/competition/repository/competition.go create mode 100644 internal/competition/repository/postgres/competition.go create mode 100644 internal/competition/server/server.go create mode 100644 internal/competition/service/competition.go diff --git a/cmd/competition/main.go b/cmd/competition/main.go new file mode 100644 index 0000000..f10a1ed --- /dev/null +++ b/cmd/competition/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "datarush/internal/competition/config" + "datarush/internal/competition/server" +) + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + srv := server.New(cfg) + + if err := srv.Start(); err != nil { + log.Fatalf("failed to start server: %v", err) + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("shutting down competition server...") + srv.Stop() + log.Println("competition server stopped") +} \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 37ff221..d452c70 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,6 +1,32 @@ package main +import ( + "log" + "os" + "os/signal" + "syscall" + + "datarush/internal/lms/config" + "datarush/internal/lms/server" +) func main() { + cfg, err := config.Load() + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + srv := server.New(cfg) + + if err := srv.Start(); err != nil { + log.Fatalf("failed to start server: %v", err) + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("shutting down auth server...") + srv.Stop() + log.Println("core server stopped") } diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go new file mode 100644 index 0000000..6388bf7 --- /dev/null +++ b/internal/competition/config/config.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "log" + "net" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Config struct { + GRPCPort int + GRPCEnableReflection bool + HTTPPort int + LogLevel string + DBHost string + DBPort int + DBUser string + DBPassword string + DBName string +} + +func Load() (*Config, error) { + _ = godotenv.Load() + + return &Config{ + GRPCPort: mustGetInt("COMPETITION_GRPC_PORT", 50053), + GRPCEnableReflection: mustGetBool("COMPETITION_GRPC_ENABLE_REFLECTION", false), + HTTPPort: mustGetInt("COMPETITION_HTTP_PORT", 8082), + LogLevel: getEnv("LOG_LEVEL", "info"), + DBHost: getEnv("POSTGRES_HOST", "localhost"), + DBPort: mustGetInt("POSTGRES_PORT", 5432), + DBUser: getEnv("POSTGRES_USERNAME", "postgres"), + DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), + DBName: getEnv("POSTGRES_DATABASE", "postgres"), + }, nil +} + +func getEnv(key, def string) string { + if val := os.Getenv(key); val != "" { + return val + } + return def +} + +func mustGetInt(key string, def int) int { + val := getEnv(key, strconv.Itoa(def)) + n, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("invalid int for %s: %v", key, err) + } + return n +} + +func mustGetBool(key string, def bool) bool { + val := getEnv(key, strconv.FormatBool(def)) + b, err := strconv.ParseBool(val) + if err != nil { + log.Fatalf("invalid bool for %s: %v", key, err) + } + return b +} + +func (c Config) BuildPostgresConnStr() string { + return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName) +} + +func (c Config) BuildPostgresDSN() string { + return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", + c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName) +} \ No newline at end of file diff --git a/internal/competition/domain/competition.go b/internal/competition/domain/competition.go new file mode 100644 index 0000000..de9c1f3 --- /dev/null +++ b/internal/competition/domain/competition.go @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..86b9827 --- /dev/null +++ b/internal/competition/domain/errors.go @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..5869479 --- /dev/null +++ b/internal/competition/handler/grpc/competition.go @@ -0,0 +1,199 @@ +package grpc + +import ( + "context" + + "datarush/internal/competition/domain" + pb "datarush/pkg/api/competition" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "strings" +) + +// 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) (*pb.DeleteCompetitionResponse, 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 &pb.DeleteCompetitionResponse{Success: true}, 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)]) +} diff --git a/internal/competition/repository/competition.go b/internal/competition/repository/competition.go new file mode 100644 index 0000000..f8d8498 --- /dev/null +++ b/internal/competition/repository/competition.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + "datarush/internal/competition/domain" + + "github.com/google/uuid" +) + + +type ListCompetitionsOptions struct { + Page int + PageSize int + State *domain.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 + 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) +} diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go new file mode 100644 index 0000000..f33e6b8 --- /dev/null +++ b/internal/competition/repository/postgres/competition.go @@ -0,0 +1,240 @@ +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" +) + +const ( + competitionCachePrefix = "competition:" +) + + +type CompetitionRepository struct { + db *sql.DB + redisClient *redis.Client + cacheEnabled bool +} + + +func NewCompetitionRepository(db *sql.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { + return &CompetitionRepository{ + db: db, + redisClient: redisClient, + cacheEnabled: cacheEnabled, + } +} + +func (r *CompetitionRepository) cacheKey(id string) string { + return competitionCachePrefix + id +} + +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, + ) + if err != nil { + return nil, err + } + + 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 +} + +func (r *CompetitionRepository) Update(ctx context.Context, competition *domain.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, + ) + if err != nil { + return err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(competition.ID.String())).Err() + } + + return 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 +} +func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*domain.Competition, int, error) { + var args []interface{} + var whereClauses []string + argId := 1 + + if opts.State != nil { + whereClauses = append(whereClauses, fmt.Sprintf("state = $%d", argId)) + args = append(args, *opts.State) + argId++ + } + if opts.SearchQuery != nil { + whereClauses = append(whereClauses, fmt.Sprintf("title ILIKE $%d", argId)) + args = append(args, "%"+*opts.SearchQuery+"%") + argId++ + } + + if opts.IsParticipating != nil { + userID, ok := ctx.Value("user_id").(uuid.UUID) + 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)) + } else { + whereClauses = append(whereClauses, fmt.Sprintf("id NOT IN (SELECT competition_id FROM competition_participants WHERE user_id = $%d)", argId)) + } + args = append(args, userID) + argId++ + } + + where := "" + if len(whereClauses) > 0 { + where = "WHERE " + strings.Join(whereClauses, " AND ") + } + + countQuery := "SELECT COUNT(*) FROM competitions " + where + var total int + if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); 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) + + rows, err := r.db.QueryContext(ctx, 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` + now := time.Now() + _, err := r.db.ExecContext(ctx, query, state, now, id) + if err != nil { + return nil, err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + + return r.Get(ctx, id) +} diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go new file mode 100644 index 0000000..4eed2c3 --- /dev/null +++ b/internal/competition/server/server.go @@ -0,0 +1,143 @@ +package server + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + "time" + + "datarush/internal/competition/config" + grpcHandlers "datarush/internal/competition/handler/grpc" + "datarush/internal/competition/repository/postgres" + "datarush/internal/competition/service" + pb "datarush/pkg/api/competition" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" +) + +const ( + httpReadTimeout = 10 * time.Second + httpWriteTimeout = 10 * time.Second + httpIdleTimeout = 60 * time.Second +) + +type Server struct { + grpcServer *grpc.Server + httpServer *http.Server + config *config.Config + db *sqlx.DB +} + +func New(cfg *config.Config) *Server { + return &Server{ + config: cfg, + } +} + +func (s *Server) Start() error { + db, err := sqlx.Connect("postgres", s.config.BuildPostgresConnStr()) + if err != nil { + return fmt.Errorf("failed to connect to postgres: %w", err) + } + s.db = db + + go func() { + if err := s.startGRPCServer(); err != nil { + log.Fatalf("failed to start gRPC server: %v", err) + } + }() + + go func() { + if err := s.startHTTPServer(); err != nil { + log.Fatalf("failed to start HTTP server: %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) + } + + 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) + compHandler := grpcHandlers.NewCompetitionHandler(compService) + + pb.RegisterCompetitionServiceServer(s.grpcServer, compHandler) + + if s.config.GRPCEnableReflection { + reflection.Register(s.grpcServer) + } +} + +func (s *Server) startHTTPServer() error { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + grpcEndpoint := fmt.Sprintf("localhost:%d", s.config.GRPCPort) + + err := pb.RegisterCompetitionServiceHandlerFromEndpoint(ctx, mux, grpcEndpoint, opts) + if err != nil { + return fmt.Errorf("failed to register http handlers: %w", err) + } + + s.httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", s.config.HTTPPort), + Handler: mux, + ReadTimeout: httpReadTimeout, + WriteTimeout: httpWriteTimeout, + IdleTimeout: httpIdleTimeout, + } + + log.Printf("starting HTTP server on port %d", s.config.HTTPPort) + return s.httpServer.ListenAndServe() +} + +func (s *Server) Stop() { + log.Println("shutting down competition server...") + + // Shutdown HTTP server + if s.httpServer != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.httpServer.Shutdown(ctx); err != nil { + log.Printf("failed to shutdown HTTP server gracefully: %v", err) + } + } + + // Shutdown gRPC server + if s.grpcServer != nil { + s.grpcServer.GracefulStop() + } + + // Close database connection + if s.db != nil { + if err := s.db.Close(); err != nil { + log.Printf("failed to close database connection: %v", err) + } + } + + log.Println("competition server stopped") +} diff --git a/internal/competition/service/competition.go b/internal/competition/service/competition.go new file mode 100644 index 0000000..68699f7 --- /dev/null +++ b/internal/competition/service/competition.go @@ -0,0 +1,75 @@ +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/lms/config/config.go b/internal/lms/config/config.go index 8a27c43..47280f9 100644 --- a/internal/lms/config/config.go +++ b/internal/lms/config/config.go @@ -23,6 +23,7 @@ type Config struct { DBName string RedisURI string AuthGRPCAddr string + CacheEnabled bool } func Load() (*Config, error) { @@ -41,6 +42,7 @@ func Load() (*Config, error) { DBName: getEnv("POSTGRES_DATABASE", "postgres"), RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"), AuthGRPCAddr: getEnv("AUTH_GRPC_ADDR", "localhost:50052"), + CacheEnabled: mustGetBool("CACHE_ENABLED", true), }, nil } diff --git a/internal/lms/server/server.go b/internal/lms/server/server.go index 19880bb..da4c891 100644 --- a/internal/lms/server/server.go +++ b/internal/lms/server/server.go @@ -94,8 +94,6 @@ func registerAuthService(ctx context.Context, gwmux *runtime.ServeMux, authAddr } func registerAuthHandlerFromEndpoint(ctx context.Context, gwmux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error { - // We'll import the proto and register it here - // For now, this is a placeholder that will be called from gateway integration return nil } func getDatabase(cfg config.Config) (*sqlx.DB, error) { diff --git a/pkg/api/competition/competition.pb.go b/pkg/api/competition/competition.pb.go index fddc813..dc178f6 100644 --- a/pkg/api/competition/competition.pb.go +++ b/pkg/api/competition/competition.pb.go @@ -7,12 +7,23 @@ package competition import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" + "reflect" + "sync" ) const ( @@ -775,3 +786,207 @@ func file_api_proto_competition_proto_init() { file_api_proto_competition_proto_goTypes = nil file_api_proto_competition_proto_depIdxs = nil } + +func RegisterCompetitionServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterCompetitionServiceHandlerClient(ctx, mux, NewCompetitionServiceClient(conn)) +} + + +func RegisterCompetitionServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client CompetitionServiceClient) error { + var err error + err = mux.Handle("POST", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "competitions"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/CreateCompetition", runtime.WithHTTPPathPattern("/v1/competitions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_CreateCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_CreateCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + err = mux.Handle("GET", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "competition_id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/GetCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_GetCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_GetCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + err = mux.Handle("PUT", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/EditCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_EditCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_EditCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + err = mux.Handle("DELETE", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "competition_id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/DeleteCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_DeleteCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_DeleteCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + err = mux.Handle("GET", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "competitions"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/ListCompetitions", runtime.WithHTTPPathPattern("/v1/competitions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_ListCompetitions_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_ListCompetitions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + err = mux.Handle("POST", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"v1", "competitions", "competition_id", "state"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/ChangeCompetitionState", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}/state")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_CompetitionService_ChangeCompetitionState_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_CompetitionService_ChangeCompetitionState_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + if err != nil { + return err + } + + return nil +} + +func RegisterCompetitionServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterCompetitionServiceHandler(ctx, mux, conn) +} + +func request_CompetitionService_CreateCompetition_0(ctx context.Context, marshaler runtime.Marshaler, client CompetitionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Competition + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateCompetition(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func forward_CompetitionService_CreateCompetition_0(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message)) { + runtime.ForwardResponseMessage(ctx, mux, marshaler, w, req, resp, opts...) +} From 1f2e197dc430facb68a608edeab68fda4c6d24cd Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 09:38:23 +0300 Subject: [PATCH 02/11] fix issues --- .../competition/handler/grpc/competition.go | 10 +- .../repository/postgres/competition.go | 12 +- internal/competition/server/server.go | 46 ----- pkg/api/competition/competition.pb.go | 184 ------------------ 4 files changed, 11 insertions(+), 241 deletions(-) diff --git a/internal/competition/handler/grpc/competition.go b/internal/competition/handler/grpc/competition.go index 5869479..427276a 100644 --- a/internal/competition/handler/grpc/competition.go +++ b/internal/competition/handler/grpc/competition.go @@ -5,11 +5,13 @@ import ( "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" - "strings" ) // ICompetitionService defines the interface for business logic. @@ -72,7 +74,7 @@ func (h *CompetitionHandler) EditCompetition(ctx context.Context, req *pb.Compet return fromDomainCompetition(updatedComp), nil } -func (h *CompetitionHandler) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*pb.DeleteCompetitionResponse, error) { +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") @@ -80,7 +82,7 @@ func (h *CompetitionHandler) DeleteCompetition(ctx context.Context, req *pb.Dele if err := h.service.DeleteCompetition(ctx, id); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete competition: %v", err) } - return &pb.DeleteCompetitionResponse{Success: true}, nil + return &emptypb.Empty{}, nil } func (h *CompetitionHandler) ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) { @@ -196,4 +198,4 @@ func toDomainCompetitionType(ct pb.CompetitionType) domain.CompetitionType { 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/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go index f33e6b8..5832a88 100644 --- a/internal/competition/repository/postgres/competition.go +++ b/internal/competition/repository/postgres/competition.go @@ -18,14 +18,12 @@ const ( competitionCachePrefix = "competition:" ) - type CompetitionRepository struct { - db *sql.DB + db *sql.DB redisClient *redis.Client cacheEnabled bool } - func NewCompetitionRepository(db *sql.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { return &CompetitionRepository{ db: db, @@ -149,7 +147,7 @@ func (r *CompetitionRepository) Delete(ctx context.Context, id uuid.UUID) error return nil } -func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*domain.Competition, int, error) { +func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]domain.Competition, int, error) { var args []interface{} var whereClauses []string argId := 1 @@ -200,7 +198,7 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo } defer rows.Close() - var competitions []*domain.Competition + var competitions []domain.Competition for rows.Next() { var c domain.Competition err := rows.Scan( @@ -219,7 +217,7 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo if err != nil { return nil, 0, err } - competitions = append(competitions, &c) + competitions = append(competitions, c) } return competitions, total, nil @@ -237,4 +235,4 @@ func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, s } 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 4eed2c3..eb6f4b6 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -1,7 +1,6 @@ package server import ( - "context" "fmt" "log" "net" @@ -14,11 +13,9 @@ import ( "datarush/internal/competition/service" pb "datarush/pkg/api/competition" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" ) @@ -54,12 +51,6 @@ func (s *Server) Start() error { } }() - go func() { - if err := s.startHTTPServer(); err != nil { - log.Fatalf("failed to start HTTP server: %v", err) - } - }() - log.Println("competition service started") return nil } @@ -89,50 +80,13 @@ func (s *Server) registerGRPCServices() { } } -func (s *Server) startHTTPServer() error { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - mux := runtime.NewServeMux() - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - grpcEndpoint := fmt.Sprintf("localhost:%d", s.config.GRPCPort) - - err := pb.RegisterCompetitionServiceHandlerFromEndpoint(ctx, mux, grpcEndpoint, opts) - if err != nil { - return fmt.Errorf("failed to register http handlers: %w", err) - } - - s.httpServer = &http.Server{ - Addr: fmt.Sprintf(":%d", s.config.HTTPPort), - Handler: mux, - ReadTimeout: httpReadTimeout, - WriteTimeout: httpWriteTimeout, - IdleTimeout: httpIdleTimeout, - } - - log.Printf("starting HTTP server on port %d", s.config.HTTPPort) - return s.httpServer.ListenAndServe() -} - func (s *Server) Stop() { log.Println("shutting down competition server...") - // Shutdown HTTP server - if s.httpServer != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := s.httpServer.Shutdown(ctx); err != nil { - log.Printf("failed to shutdown HTTP server gracefully: %v", err) - } - } - - // Shutdown gRPC server if s.grpcServer != nil { s.grpcServer.GracefulStop() } - // Close database connection if s.db != nil { if err := s.db.Close(); err != nil { log.Printf("failed to close database connection: %v", err) diff --git a/pkg/api/competition/competition.pb.go b/pkg/api/competition/competition.pb.go index a23b474..778c5c9 100644 --- a/pkg/api/competition/competition.pb.go +++ b/pkg/api/competition/competition.pb.go @@ -16,7 +16,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -742,189 +741,6 @@ func file_api_proto_competition_proto_init() { file_api_proto_competition_proto_depIdxs = nil } -func RegisterCompetitionServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterCompetitionServiceHandlerClient(ctx, mux, NewCompetitionServiceClient(conn)) -} - - -func RegisterCompetitionServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client CompetitionServiceClient) error { - var err error - err = mux.Handle("POST", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "competitions"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/CreateCompetition", runtime.WithHTTPPathPattern("/v1/competitions")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_CreateCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_CreateCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - err = mux.Handle("GET", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "competition_id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/GetCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_GetCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_GetCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - err = mux.Handle("PUT", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/EditCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{id}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_EditCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_EditCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - err = mux.Handle("DELETE", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "competitions", "competition_id"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/DeleteCompetition", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_DeleteCompetition_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_DeleteCompetition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - err = mux.Handle("GET", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "competitions"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/ListCompetitions", runtime.WithHTTPPathPattern("/v1/competitions")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_ListCompetitions_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_ListCompetitions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - err = mux.Handle("POST", runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"v1", "competitions", "competition_id", "state"}, "")), func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/competition.CompetitionService/ChangeCompetitionState", runtime.WithHTTPPathPattern("/v1/competitions/{competition_id}/state")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_CompetitionService_ChangeCompetitionState_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_CompetitionService_ChangeCompetitionState_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - if err != nil { - return err - } - - return nil -} - -func RegisterCompetitionServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterCompetitionServiceHandler(ctx, mux, conn) -} - func request_CompetitionService_CreateCompetition_0(ctx context.Context, marshaler runtime.Marshaler, client CompetitionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Competition var metadata runtime.ServerMetadata From e18489c96cc7c505e5583833735e25657e295a7d Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 10:31:14 +0300 Subject: [PATCH 03/11] fix proto files --- pkg/api/competition/competition.pb.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/api/competition/competition.pb.go b/pkg/api/competition/competition.pb.go index 778c5c9..0c980b9 100644 --- a/pkg/api/competition/competition.pb.go +++ b/pkg/api/competition/competition.pb.go @@ -15,7 +15,6 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" From f3620da240ab4fa2432f7c40b0f233e194b1c093 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:21:08 +0300 Subject: [PATCH 04/11] 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 From 8ded3cc6abd59ed67c59014e46cc12682107540a Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:21:22 +0300 Subject: [PATCH 05/11] rewrite competition service: remove http related things --- internal/competition/server/server.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go index 4325c6d..7b72d62 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -8,7 +8,6 @@ import ( "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" @@ -64,10 +63,6 @@ func (s *Server) Start() error { } func (s *Server) registerGRPCServices() error { - s.grpcServer = grpc.NewServer( - grpc.UnaryInterceptor(middleware.AuthInterceptor(s.config.JWTSecret)), - ) - compRepo := postgres.NewCompetitionRepository(s.db) compService := service.NewCompetitionService(compRepo) compHandler := grpcHandlers.NewCompetitionHandler(compService) @@ -95,4 +90,4 @@ func (s *Server) Stop() { } log.Println("competition server stopped") -} \ No newline at end of file +} From 6a9376505d24a090fd605dfa8d08c29180f48107 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:24:07 +0300 Subject: [PATCH 06/11] add redis cache to competitions --- go.mod | 2 +- go.sum | 2 + internal/competition/config/config.go | 8 +++ .../repository/postgres/competition.go | 68 +++++++++++++++++-- internal/competition/server/server.go | 20 +++++- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index e7130d6..6c82cea 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 - github.com/redis/go-redis/v9 v9.16.0 + github.com/redis/go-redis/v9 v9.17.2 golang.org/x/crypto v0.42.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 diff --git a/go.sum b/go.sum index aa22628..c4226ce 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go index 7245d9c..3748ffd 100644 --- a/internal/competition/config/config.go +++ b/internal/competition/config/config.go @@ -21,6 +21,10 @@ type Config struct { DBPassword string DBName string JWTSecret string + RedisAddr string + RedisPassword string + RedisDB int + CacheEnabled bool } func Load() (*Config, error) { @@ -37,6 +41,10 @@ func Load() (*Config, error) { DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), + RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", ""), + RedisDB: mustGetInt("REDIS_DB", 0), + CacheEnabled: mustGetBool("CACHE_ENABLED", true), }, nil } diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go index a2778a5..45dc692 100644 --- a/internal/competition/repository/postgres/competition.go +++ b/internal/competition/repository/postgres/competition.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "encoding/json" "fmt" "strings" "time" @@ -11,18 +12,31 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" +) + +const ( + competitionCachePrefix = "competition:" ) type CompetitionRepository struct { - db *sqlx.DB + db *sqlx.DB + redisClient *redis.Client + cacheEnabled bool } -func NewCompetitionRepository(db *sqlx.DB) repository.CompetitionRepository { +func NewCompetitionRepository(db *sqlx.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { return &CompetitionRepository{ - db: db, + db: db, + redisClient: redisClient, + cacheEnabled: cacheEnabled, } } +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` @@ -45,14 +59,42 @@ func (r *CompetitionRepository) Create(ctx context.Context, c *pb.Competition) ( c.CreatedAt = createdCompetition.CreatedAt c.UpdatedAt = createdCompetition.UpdatedAt + if r.cacheEnabled { + data, err := json.Marshal(c) + if err == nil { + r.redisClient.Set(ctx, r.cacheKey(c.Id), data, 10*time.Minute).Err() + } + } + return c, nil } func (r *CompetitionRepository) Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) { + if r.cacheEnabled { + val, err := r.redisClient.Get(ctx, r.cacheKey(id.String())).Result() + if err == nil { + var competition pb.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` var competition pb.Competition err := r.db.GetContext(ctx, &competition, query, id) - return &competition, err + if err != nil { + return nil, err + } + + 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 } func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { @@ -77,13 +119,25 @@ func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) ( } c.UpdatedAt = updatedCompetition.UpdatedAt + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(c.Id)).Err() + } + 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) - return err + if err != nil { + return err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + + return nil } func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*pb.Competition, int, error) { @@ -149,5 +203,9 @@ func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, s 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 7b72d62..a63c278 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/jmoiron/sqlx" _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) @@ -26,8 +27,9 @@ const ( type Server struct { grpcServer *grpc.Server - config *config.Config - db *sqlx.DB + config *config.Config + db *sqlx.DB + redisClient *redis.Client } func New(cfg *config.Config) *Server { @@ -43,6 +45,12 @@ func (s *Server) Start() error { } s.db = db + s.redisClient = redis.NewClient(&redis.Options{ + Addr: s.config.RedisAddr, + Password: s.config.RedisPassword, + DB: s.config.RedisDB, + }) + if err := s.registerGRPCServices(); err != nil { return fmt.Errorf("failed to register gRPC services: %w", err) } @@ -63,7 +71,7 @@ func (s *Server) Start() error { } func (s *Server) registerGRPCServices() error { - compRepo := postgres.NewCompetitionRepository(s.db) + compRepo := postgres.NewCompetitionRepository(s.db, s.redisClient, s.config.CacheEnabled) compService := service.NewCompetitionService(compRepo) compHandler := grpcHandlers.NewCompetitionHandler(compService) @@ -89,5 +97,11 @@ func (s *Server) Stop() { } } + if s.redisClient != nil { + if err := s.redisClient.Close(); err != nil { + log.Printf("failed to close redis client: %v", err) + } + } + log.Println("competition server stopped") } From 87cf677d1dffb179ce9ad24315e5f36cedf989eb Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:31:40 +0300 Subject: [PATCH 07/11] use auth service for validating user in competitions service --- internal/competition/config/config.go | 4 +- internal/competition/middleware/auth.go | 67 +++++++++++++++++++++++++ internal/competition/server/server.go | 23 +++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 internal/competition/middleware/auth.go diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go index 3748ffd..6ef7be1 100644 --- a/internal/competition/config/config.go +++ b/internal/competition/config/config.go @@ -20,7 +20,7 @@ type Config struct { DBUser string DBPassword string DBName string - JWTSecret string + AuthSvcAddr string RedisAddr string RedisPassword string RedisDB int @@ -40,7 +40,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"), + AuthSvcAddr: getEnv("AUTH_SVC_ADDR", "localhost:50051"), RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), RedisPassword: getEnv("REDIS_PASSWORD", ""), RedisDB: mustGetInt("REDIS_DB", 0), diff --git a/internal/competition/middleware/auth.go b/internal/competition/middleware/auth.go new file mode 100644 index 0000000..148f462 --- /dev/null +++ b/internal/competition/middleware/auth.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "context" + "strings" + + authpb "datarush/pkg/api/auth" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type contextKey string + +const ( + UserIDKey contextKey = "user_id" + authHeader = "authorization" + bearerScheme = "bearer" +) + +type AuthInterceptor struct { + authClient authpb.AuthServiceClient +} + +func NewAuthInterceptor(authClient authpb.AuthServiceClient) *AuthInterceptor { + return &AuthInterceptor{authClient: authClient} +} + +func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "metadata is not provided") + } + + authHeaders := md.Get(authHeader) + if len(authHeaders) == 0 { + return nil, status.Error(codes.Unauthenticated, "authorization token is not provided") + } + + header := authHeaders[0] + parts := strings.Split(header, " ") + if len(parts) != 2 || !strings.EqualFold(parts[0], bearerScheme) { + return nil, status.Errorf(codes.Unauthenticated, "invalid authorization header format") + } + + token := parts[1] + + validateResp, err := i.authClient.ValidateToken(ctx, &authpb.ValidateTokenRequest{ + Token: token, + }) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "failed to validate token: %v", err) + } + + newCtx := context.WithValue(ctx, UserIDKey, validateResp.GetUserId()) + + return handler(newCtx, req) + } +} \ No newline at end of file diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go index a63c278..4e0e82f 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -8,14 +8,17 @@ import ( "datarush/internal/competition/config" grpcHandlers "datarush/internal/competition/handler/grpc" + "datarush/internal/competition/middleware" "datarush/internal/competition/repository/postgres" "datarush/internal/competition/service" + authpb "datarush/pkg/api/auth" pb "datarush/pkg/api/competition" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/redis/go-redis/v9" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" ) @@ -30,6 +33,7 @@ type Server struct { config *config.Config db *sqlx.DB redisClient *redis.Client + authConn *grpc.ClientConn } func New(cfg *config.Config) *Server { @@ -51,6 +55,12 @@ func (s *Server) Start() error { DB: s.config.RedisDB, }) + authConn, err := grpc.Dial(s.config.AuthSvcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("failed to connect to auth service: %w", err) + } + s.authConn = authConn + if err := s.registerGRPCServices(); err != nil { return fmt.Errorf("failed to register gRPC services: %w", err) } @@ -71,6 +81,13 @@ func (s *Server) Start() error { } func (s *Server) registerGRPCServices() error { + authClient := authpb.NewAuthServiceClient(s.authConn) + authInterceptor := middleware.NewAuthInterceptor(authClient) + + s.grpcServer = grpc.NewServer( + grpc.UnaryInterceptor(authInterceptor.Unary()), + ) + compRepo := postgres.NewCompetitionRepository(s.db, s.redisClient, s.config.CacheEnabled) compService := service.NewCompetitionService(compRepo) compHandler := grpcHandlers.NewCompetitionHandler(compService) @@ -91,6 +108,12 @@ func (s *Server) Stop() { s.grpcServer.GracefulStop() } + if s.authConn != nil { + if err := s.authConn.Close(); err != nil { + log.Printf("failed to close auth service connection: %v", err) + } + } + if s.db != nil { if err := s.db.Close(); err != nil { log.Printf("failed to close database: %v", err) From 4c7e35dafd06462908f562505e43ee7325a8f4d0 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:32:27 +0300 Subject: [PATCH 08/11] use auth service for validating user in competitions service --- internal/competition/service/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/competition/service/service.go b/internal/competition/service/service.go index 3f07da8..d339888 100644 --- a/internal/competition/service/service.go +++ b/internal/competition/service/service.go @@ -75,4 +75,4 @@ func (s *CompetitionService) ChangeCompetitionState(ctx context.Context, req *pb return nil, err } return s.repo.ChangeState(ctx, id, req.State) -} \ No newline at end of file +} From 39b5f05d39f841369717999aaaec2047bddf3d4f Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:41:03 +0300 Subject: [PATCH 09/11] lint competition service --- internal/competition/config/config.go | 2 +- internal/competition/handler/grpc/handler.go | 25 +++++++++++---- internal/competition/middleware/auth.go | 2 +- .../competition/repository/competition.go | 2 +- .../repository/postgres/competition.go | 31 +++++++++++++++---- internal/competition/server/server.go | 2 +- internal/competition/service/service.go | 20 +++++++++--- 7 files changed, 64 insertions(+), 20 deletions(-) diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go index 6ef7be1..20e1d75 100644 --- a/internal/competition/config/config.go +++ b/internal/competition/config/config.go @@ -81,4 +81,4 @@ func (c Config) BuildPostgresConnStr() string { func (c Config) BuildPostgresDSN() string { return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName) -} \ No newline at end of file +} diff --git a/internal/competition/handler/grpc/handler.go b/internal/competition/handler/grpc/handler.go index 5ee136b..71a8ad9 100644 --- a/internal/competition/handler/grpc/handler.go +++ b/internal/competition/handler/grpc/handler.go @@ -3,8 +3,9 @@ package grpc import ( "context" - pb "datarush/pkg/api/competition" "google.golang.org/protobuf/types/known/emptypb" + + pb "datarush/pkg/api/competition" ) type CompetitionService interface { @@ -29,7 +30,10 @@ func (h *CompetitionHandler) CreateCompetition(ctx context.Context, req *pb.Comp return h.service.CreateCompetition(ctx, req) } -func (h *CompetitionHandler) GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) { +func (h *CompetitionHandler) GetCompetition( + ctx context.Context, + req *pb.GetCompetitionRequest, +) (*pb.Competition, error) { return h.service.GetCompetition(ctx, req) } @@ -37,14 +41,23 @@ func (h *CompetitionHandler) EditCompetition(ctx context.Context, req *pb.Compet return h.service.EditCompetition(ctx, req) } -func (h *CompetitionHandler) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) { +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) { +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) { +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/middleware/auth.go b/internal/competition/middleware/auth.go index 148f462..50856cc 100644 --- a/internal/competition/middleware/auth.go +++ b/internal/competition/middleware/auth.go @@ -64,4 +64,4 @@ func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor { return handler(newCtx, req) } -} \ No newline at end of file +} diff --git a/internal/competition/repository/competition.go b/internal/competition/repository/competition.go index e82bb23..c82717a 100644 --- a/internal/competition/repository/competition.go +++ b/internal/competition/repository/competition.go @@ -23,4 +23,4 @@ type CompetitionRepository interface { Delete(ctx context.Context, id uuid.UUID) 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 45dc692..476d929 100644 --- a/internal/competition/repository/postgres/competition.go +++ b/internal/competition/repository/postgres/competition.go @@ -25,7 +25,11 @@ type CompetitionRepository struct { cacheEnabled bool } -func NewCompetitionRepository(db *sqlx.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { +func NewCompetitionRepository( + db *sqlx.DB, + redisClient *redis.Client, + cacheEnabled bool, +) repository.CompetitionRepository { return &CompetitionRepository{ db: db, redisClient: redisClient, @@ -140,7 +144,10 @@ func (r *CompetitionRepository) Delete(ctx context.Context, id uuid.UUID) error return nil } -func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*pb.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 @@ -163,7 +170,10 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo } if *opts.IsParticipating { - whereClauses = append(whereClauses, fmt.Sprintf("id IN (SELECT competition_id FROM user_competitions 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 user_competitions WHERE user_id = $%d)", argId)) } @@ -182,7 +192,12 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo 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) + 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-1)*opts.PageSize) var competitions []*pb.Competition @@ -194,7 +209,11 @@ func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCo return competitions, total, nil } -func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, state pb.CompetitionState) (*pb.Competition, error) { +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() var competition pb.Competition @@ -208,4 +227,4 @@ func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, s } 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 4e0e82f..528ab3a 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -29,7 +29,7 @@ const ( ) type Server struct { - grpcServer *grpc.Server + grpcServer *grpc.Server config *config.Config db *sqlx.DB redisClient *redis.Client diff --git a/internal/competition/service/service.go b/internal/competition/service/service.go index d339888..7007b9a 100644 --- a/internal/competition/service/service.go +++ b/internal/competition/service/service.go @@ -22,7 +22,10 @@ func (s *CompetitionService) CreateCompetition(ctx context.Context, req *pb.Comp return s.repo.Create(ctx, req) } -func (s *CompetitionService) GetCompetition(ctx context.Context, req *pb.GetCompetitionRequest) (*pb.Competition, error) { +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 @@ -34,7 +37,10 @@ func (s *CompetitionService) EditCompetition(ctx context.Context, req *pb.Compet return s.repo.Update(ctx, req) } -func (s *CompetitionService) DeleteCompetition(ctx context.Context, req *pb.DeleteCompetitionRequest) (*emptypb.Empty, error) { +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 @@ -43,7 +49,10 @@ func (s *CompetitionService) DeleteCompetition(ctx context.Context, req *pb.Dele return &emptypb.Empty{}, err } -func (s *CompetitionService) ListCompetitions(ctx context.Context, req *pb.ListCompetitionsRequest) (*pb.ListCompetitionsResponse, error) { +func (s *CompetitionService) ListCompetitions( + ctx context.Context, + req *pb.ListCompetitionsRequest, +) (*pb.ListCompetitionsResponse, error) { opts := repository.ListCompetitionsOptions{ Page: int(req.PageToken), PageSize: int(req.PageSize), @@ -69,7 +78,10 @@ func (s *CompetitionService) ListCompetitions(ctx context.Context, req *pb.ListC }, nil } -func (s *CompetitionService) ChangeCompetitionState(ctx context.Context, req *pb.ChangeCompetitionStateRequest) (*pb.Competition, error) { +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 From e6bbaa769a36af69b4f560deca75d709ad045ca3 Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:42:11 +0300 Subject: [PATCH 10/11] remove generated shit from competitions --- pkg/api/competition/competition.pb.go | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/pkg/api/competition/competition.pb.go b/pkg/api/competition/competition.pb.go index 0c980b9..fe3873b 100644 --- a/pkg/api/competition/competition.pb.go +++ b/pkg/api/competition/competition.pb.go @@ -10,6 +10,10 @@ import ( "context" "io" "net/http" + "unsafe" + + "reflect" + "sync" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" @@ -21,8 +25,6 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" - "reflect" - "sync" ) const ( @@ -739,24 +741,3 @@ func file_api_proto_competition_proto_init() { file_api_proto_competition_proto_goTypes = nil file_api_proto_competition_proto_depIdxs = nil } - -func request_CompetitionService_CreateCompetition_0(ctx context.Context, marshaler runtime.Marshaler, client CompetitionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq Competition - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.CreateCompetition(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func forward_CompetitionService_CreateCompetition_0(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message)) { - runtime.ForwardResponseMessage(ctx, mux, marshaler, w, req, resp, opts...) -} From ec098b43b97e0f19700e81d3b96458ed0bc13a2f Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Wed, 17 Dec 2025 12:45:53 +0300 Subject: [PATCH 11/11] add competition service to compose --- compose.yaml | 44 ++++++++++++++++++++++++ infrastructure/competition/.env.template | 31 +++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 infrastructure/competition/.env.template diff --git a/compose.yaml b/compose.yaml index e40a5eb..2afbdcf 100644 --- a/compose.yaml +++ b/compose.yaml @@ -33,6 +33,46 @@ services: restart: unless-stopped shm_size: 4mb + competition: + build: + context: . + dockerfile: Containerfile + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + redis: + restart: false + condition: service_healthy + required: true + auth: + restart: false + condition: service_started + required: true + env_file: + - path: ./infrastructure/competition/.env.template + required: true + - path: ./infrastructure/competition/.env + required: false + ports: + - name: http + target: 8082 + published: 13445 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + - name: grpc + target: 50053 + published: 13446 + host_ip: 127.0.0.1 + protocol: tcp + app_protocol: http + networks: + - default + restart: unless-stopped + shm_size: 4mb + core: build: context: . @@ -54,6 +94,10 @@ services: restart: false condition: service_started required: true + competition: + restart: false + condition: service_started + required: true env_file: - path: ./infrastructure/core/.env.template required: true diff --git a/infrastructure/competition/.env.template b/infrastructure/competition/.env.template new file mode 100644 index 0000000..2526129 --- /dev/null +++ b/infrastructure/competition/.env.template @@ -0,0 +1,31 @@ +# Competition Service Configuration + +# gRPC server port +COMPETITION_GRPC_PORT=50053 + +# Enable/disable gRPC reflection +COMPETITION_GRPC_ENABLE_REFLECTION=true + +# HTTP server port (if applicable) +COMPETITION_HTTP_PORT=8082 + +# Log level (e.g., debug, info, warn, error) +LOG_LEVEL=info + +# PostgreSQL database connection +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DATABASE=postgres + +# Address of the authentication gRPC service +AUTH_SVC_ADDR=auth:50052 + +# Redis connection for caching +REDIS_ADDR=redis:6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# Enable/disable caching +CACHE_ENABLED=true \ No newline at end of file