You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

126 lines
3.5 KiB
Go

// Package postgres provides PostgreSQL database client for the KnowFoolery application.
package postgres
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"knowfoolery/backend/shared/infra/utils/envutil"
)
// Config holds the configuration for the PostgreSQL client.
type Config struct {
Host string
Port int
User string
Password string
Database string
SSLMode string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
}
// DefaultConfig returns a default configuration for development.
func DefaultConfig() Config {
return Config{
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "postgres",
Database: "knowfoolery",
SSLMode: "disable",
MaxOpenConns: 25,
MaxIdleConns: 10,
ConnMaxLifetime: 5 * time.Minute,
ConnMaxIdleTime: 1 * time.Minute,
}
}
// DSN returns the PostgreSQL connection string.
func (c Config) DSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode,
)
}
// URL returns the PostgreSQL connection URL.
func (c Config) URL() string {
return fmt.Sprintf(
"postgresql://%s:%s@%s:%d/%s?sslmode=%s",
c.User, c.Password, c.Host, c.Port, c.Database, c.SSLMode,
)
}
// HealthCheck performs a stateless health check on PostgreSQL.
// Services should initialize and own Ent clients directly. This helper is
// intentionally lightweight and does not expose a long-lived shared DB client.
func HealthCheck(ctx context.Context, config Config) error {
if err := validateConfig(config); err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
poolConfig, err := pgxpool.ParseConfig(config.URL())
if err != nil {
return fmt.Errorf("parse postgres config: %w", err)
}
poolConfig.MaxConns = 1
poolConfig.MinConns = 0
poolConfig.MaxConnLifetime = config.ConnMaxLifetime
poolConfig.MaxConnIdleTime = config.ConnMaxIdleTime
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return fmt.Errorf("create postgres pool: %w", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
return fmt.Errorf("ping postgres: %w", err)
}
return nil
}
// ConfigFromEnv creates a Config from environment variables with safe fallbacks.
func ConfigFromEnv() Config {
cfg := DefaultConfig()
cfg.Host = envutil.String("POSTGRES_HOST", cfg.Host)
cfg.Port = envutil.Int("POSTGRES_PORT", cfg.Port)
cfg.User = envutil.String("POSTGRES_USER", cfg.User)
cfg.Password = envutil.String("POSTGRES_PASSWORD", cfg.Password)
cfg.Database = envutil.String("POSTGRES_DB", cfg.Database)
cfg.SSLMode = envutil.String("POSTGRES_SSLMODE", cfg.SSLMode)
cfg.MaxOpenConns = envutil.Int("POSTGRES_MAX_OPEN_CONNS", cfg.MaxOpenConns)
cfg.MaxIdleConns = envutil.Int("POSTGRES_MAX_IDLE_CONNS", cfg.MaxIdleConns)
cfg.ConnMaxLifetime = envutil.Duration("POSTGRES_CONN_MAX_LIFETIME", cfg.ConnMaxLifetime)
cfg.ConnMaxIdleTime = envutil.Duration("POSTGRES_CONN_MAX_IDLE_TIME", cfg.ConnMaxIdleTime)
return cfg
}
func validateConfig(c Config) error {
switch {
case c.Host == "":
return errors.New("postgres host is required")
case c.Port <= 0:
return errors.New("postgres port must be greater than 0")
case c.User == "":
return errors.New("postgres user is required")
case c.Database == "":
return errors.New("postgres database is required")
}
return nil
}