jobs-monorepo/shared/infrastructure/db.go
Elshimy Ziad Magdy Taha 0ab6954231 feat: add job analysis infrastructure and Glassdoor support
- Added new job analysis result repository and database migration system
- Expanded job creation to support Glassdoor as a new provider alongside LinkedIn
- Added CV analysis service launch configuration in VS Code
- Updated Makefile with new commands for scraper service and migrations
- Improved environment loading in cron analyzer to support local development
- Added error handling for unsupported job providers
- Integrated job analysis results
2025-10-24 15:10:20 +05:00

171 lines
4.4 KiB
Go

package infrastructure
import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
)
// Config holds database configuration
type Config struct {
Host string
Port string
User string
Password string
DBName string
SSLMode string
}
// NewConnection creates a new database connection
func NewConnection(config Config) (*sql.DB, error) {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
// LoadConfigFromEnv loads database configuration from environment variables
func LoadConfigFromEnv() Config {
return Config{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "5432"),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "password"),
DBName: getEnv("DB_NAME", "linkedin_jobs"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// RunMigrations runs all pending migrations
func RunMigrations(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
// Get the migrations directory path
migrationsPath, err := getMigrationsPath()
if err != nil {
return fmt.Errorf("failed to get migrations path: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s", migrationsPath),
"postgres",
driver,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed to run migrations: %w", err)
}
return nil
}
// RollbackMigrations rolls back migrations by the specified number of steps
func RollbackMigrations(db *sql.DB, steps int) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
migrationsPath, err := getMigrationsPath()
if err != nil {
return fmt.Errorf("failed to get migrations path: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s", migrationsPath),
"postgres",
driver,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
if err := m.Steps(-steps); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("failed to rollback migrations: %w", err)
}
return nil
}
// GetMigrationVersion returns the current migration version
func GetMigrationVersion(db *sql.DB) (uint, bool, error) {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return 0, false, fmt.Errorf("failed to create postgres driver: %w", err)
}
migrationsPath, err := getMigrationsPath()
if err != nil {
return 0, false, fmt.Errorf("failed to get migrations path: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s", migrationsPath),
"postgres",
driver,
)
if err != nil {
return 0, false, fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
version, dirty, err := m.Version()
if err != nil {
if errors.Is(err, migrate.ErrNilVersion) {
return 0, false, nil
}
return 0, false, fmt.Errorf("failed to get migration version: %w", err)
}
return version, dirty, nil
}
func getMigrationsPath() (string, error) {
// Get the directory of the current source file
_, filename, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("failed to get current file path")
}
// Get the project root (go up from infrastructure/ to project root)
projectRoot := filepath.Dir(filepath.Dir(filename))
migrationsPath := filepath.Join(projectRoot, "migrations")
// Verify the migrations directory exists
if _, err := os.Stat(migrationsPath); err != nil {
return "", fmt.Errorf("migrations directory not found at %s: %w", migrationsPath, err)
}
return migrationsPath, nil
}