package app import ( "context" "database/sql" "errors" "net/http" "os" "path/filepath" "strings" "time" _ "github.com/go-sql-driver/mysql" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/redis/go-redis/v9" "secunda-test/internal/cache" "secunda-test/internal/config" "secunda-test/internal/email" "secunda-test/internal/handler" "secunda-test/internal/httpx" mysqlrepo "secunda-test/internal/repository/mysql" "secunda-test/internal/service" ) type Container struct { Config config.Config DB *sql.DB Redis *redis.Client Server *http.Server } func New(ctx context.Context) (*Container, error) { cfg, err := config.Load() if err != nil { return nil, err } db, err := sql.Open("mysql", cfg.Database.DSN) if err != nil { return nil, err } db.SetMaxOpenConns(cfg.Database.MaxOpenConns) db.SetMaxIdleConns(cfg.Database.MaxIdleConns) db.SetConnMaxLifetime(time.Hour) if err := db.PingContext(ctx); err != nil { return nil, err } redisClient := redis.NewClient(&redis.Options{Addr: cfg.Redis.Addr}) requests := prometheus.NewCounterVec(prometheus.CounterOpts{Name: "http_requests_total", Help: "Total HTTP requests."}, []string{"method", "path", "status"}) duration := prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "http_request_duration_seconds", Help: "HTTP request duration."}, []string{"method", "path"}) prometheus.MustRegister(requests, duration) users := mysqlrepo.NewUserRepository(db) teams := mysqlrepo.NewTeamRepository(db) tasks := mysqlrepo.NewTaskRepository(db) taskCache := cache.NewTaskCache(redisClient, time.Duration(cfg.Redis.TTLSeconds)*time.Second) emailSender := email.NewSender(cfg.Email.Endpoint) authService := service.NewAuthService(users, cfg.JWT.Secret, time.Duration(cfg.JWT.TTLMinutes)*time.Minute) teamService := service.NewTeamService(teams, users, emailSender) taskService := service.NewTaskService(tasks, teams, taskCache) middleware := httpx.NewMiddleware(cfg.JWT.Secret, cfg.RateLimit.RequestsPerMinute, requests, duration) h := handler.New(authService, teamService, taskService, middleware) root := http.NewServeMux() root.Handle("/", h.Routes()) root.Handle("/metrics", promhttp.Handler()) return &Container{ Config: cfg, DB: db, Redis: redisClient, Server: &http.Server{Addr: cfg.HTTP.Addr, Handler: root, ReadHeaderTimeout: 5 * time.Second}, }, nil } func (c *Container) Migrate(ctx context.Context) error { files, err := filepath.Glob("migrations/*.sql") if err != nil { return err } if len(files) == 0 { files, err = filepath.Glob("/app/migrations/*.sql") if err != nil { return err } } for _, file := range files { data, err := os.ReadFile(file) if err != nil { return err } for _, stmt := range strings.Split(string(data), ";") { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := c.DB.ExecContext(ctx, stmt); err != nil { return err } } } return nil } func (c *Container) Close() error { var errs []error if c.Redis != nil { errs = append(errs, c.Redis.Close()) } if c.DB != nil { errs = append(errs, c.DB.Close()) } return errors.Join(errs...) }