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/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/go.mod b/go.mod index 8b1782b..ca47a4b 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,6 @@ toolchain go1.24.9 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/aws/aws-sdk-go-v2 v1.41.0 - github.com/aws/aws-sdk-go-v2/config v1.32.6 - github.com/aws/aws-sdk-go-v2/credentials v1.19.6 - github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 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 @@ -19,7 +15,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 7bd671f..b38652d 100644 --- a/go.sum +++ b/go.sum @@ -152,6 +152,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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go new file mode 100644 index 0000000..20e1d75 --- /dev/null +++ b/internal/competition/config/config.go @@ -0,0 +1,84 @@ +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 + AuthSvcAddr string + RedisAddr string + RedisPassword string + RedisDB int + CacheEnabled bool +} + +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"), + AuthSvcAddr: getEnv("AUTH_SVC_ADDR", "localhost:50051"), + RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", ""), + RedisDB: mustGetInt("REDIS_DB", 0), + CacheEnabled: mustGetBool("CACHE_ENABLED", true), + }, 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) +} diff --git a/internal/competition/handler/grpc/handler.go b/internal/competition/handler/grpc/handler.go new file mode 100644 index 0000000..71a8ad9 --- /dev/null +++ b/internal/competition/handler/grpc/handler.go @@ -0,0 +1,63 @@ +package grpc + +import ( + "context" + + "google.golang.org/protobuf/types/known/emptypb" + + pb "datarush/pkg/api/competition" +) + +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) +} diff --git a/internal/competition/middleware/auth.go b/internal/competition/middleware/auth.go new file mode 100644 index 0000000..50856cc --- /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) + } +} diff --git a/internal/competition/repository/competition.go b/internal/competition/repository/competition.go new file mode 100644 index 0000000..c82717a --- /dev/null +++ b/internal/competition/repository/competition.go @@ -0,0 +1,26 @@ +package repository + +import ( + "context" + + "github.com/google/uuid" + + pb "datarush/pkg/api/competition" +) + +type ListCompetitionsOptions struct { + Page int + PageSize int + State *pb.CompetitionState + IsParticipating *bool + SearchQuery *string +} + +type CompetitionRepository interface { + 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) ([]*pb.Competition, int, error) + ChangeState(ctx context.Context, id uuid.UUID, state pb.CompetitionState) (*pb.Competition, error) +} diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go new file mode 100644 index 0000000..476d929 --- /dev/null +++ b/internal/competition/repository/postgres/competition.go @@ -0,0 +1,230 @@ +package postgres + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "datarush/internal/competition/repository" + pb "datarush/pkg/api/competition" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" +) + +const ( + competitionCachePrefix = "competition:" +) + +type CompetitionRepository struct { + db *sqlx.DB + redisClient *redis.Client + cacheEnabled bool +} + +func NewCompetitionRepository( + db *sqlx.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, 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` + + 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(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) + 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) { + 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 = 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 nil, err + } + 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) + 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) { + 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").(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 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)) + } + 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.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-1)*opts.PageSize) + + var competitions []*pb.Competition + err := r.db.SelectContext(ctx, &competitions, query, args...) + if err != nil { + return nil, 0, err + } + + return competitions, total, nil +} + +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 + 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) +} diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go new file mode 100644 index 0000000..528ab3a --- /dev/null +++ b/internal/competition/server/server.go @@ -0,0 +1,130 @@ +package server + +import ( + "fmt" + "log" + "net" + "time" + + "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" +) + +const ( + httpReadTimeout = 10 * time.Second + httpWriteTimeout = 10 * time.Second + httpIdleTimeout = 60 * time.Second +) + +type Server struct { + grpcServer *grpc.Server + config *config.Config + db *sqlx.DB + redisClient *redis.Client + authConn *grpc.ClientConn +} + +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 + + s.redisClient = redis.NewClient(&redis.Options{ + Addr: s.config.RedisAddr, + Password: s.config.RedisPassword, + 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) + } + + go func() { + 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) + } + }() + + return nil +} + +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) + + pb.RegisterCompetitionServiceServer(s.grpcServer, compHandler) + + if s.config.GRPCEnableReflection { + reflection.Register(s.grpcServer) + } + + return nil +} + +func (s *Server) Stop() { + log.Println("shutting down competition server...") + + if s.grpcServer != nil { + 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) + } + } + + 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") +} diff --git a/internal/competition/service/service.go b/internal/competition/service/service.go new file mode 100644 index 0000000..7007b9a --- /dev/null +++ b/internal/competition/service/service.go @@ -0,0 +1,90 @@ +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) +} diff --git a/internal/migrate/config/config.go b/internal/migrate/config/config.go index 8a27c43..47280f9 100644 --- a/internal/migrate/config/config.go +++ b/internal/migrate/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/pkg/api/competition/competition.pb.go b/pkg/api/competition/competition.pb.go index 767908e..fe3873b 100644 --- a/pkg/api/competition/competition.pb.go +++ b/pkg/api/competition/competition.pb.go @@ -7,13 +7,24 @@ package competition import ( + "context" + "io" + "net/http" + "unsafe" + + "reflect" + "sync" + + "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/status" + "google.golang.org/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" ) const (