Merge branch 'feature/competition'
* feature/competition: add competition service to compose remove generated shit from competitions lint competition service use auth service for validating user in competitions service use auth service for validating user in competitions service add redis cache to competitions rewrite competition service: remove http related things rewrite competition service: remove http related things fix proto files fix issues add competition service (it`s broken, I will fix it later)
This commit is contained in:
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user