package database import ( "fmt" "log" "github.com/ydn/yourdreamnamehere/internal/models" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) type Database struct { DB *gorm.DB } func NewDatabase(dsn string, logLevel logger.LogLevel) (*Database, error) { db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logLevel), }) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } return &Database{DB: db}, nil } func (d *Database) Migrate() error { log.Println("Running database migrations...") err := d.DB.AutoMigrate( &models.User{}, &models.Customer{}, &models.Subscription{}, &models.Domain{}, &models.VPS{}, &models.DeploymentLog{}, &models.Invitation{}, ) if err != nil { return fmt.Errorf("failed to run migrations: %w", err) } // Add additional constraints that GORM doesn't handle if err := d.addConstraints(); err != nil { return fmt.Errorf("failed to add constraints: %w", err) } // Create indexes for performance if err := d.createIndexes(); err != nil { return fmt.Errorf("failed to create indexes: %w", err) } log.Println("Database migrations completed successfully") return nil } func (d *Database) addConstraints() error { // Add check constraints constraints := []string{ "ALTER TABLE users ADD CONSTRAINT check_users_role CHECK (role IN ('user', 'admin'))", "ALTER TABLE customers ADD CONSTRAINT check_customers_status CHECK (status IN ('pending', 'active', 'canceled', 'past_due'))", "ALTER TABLE subscriptions ADD CONSTRAINT check_subscriptions_status CHECK (status IN ('active', 'trialing', 'past_due', 'canceled', 'unpaid'))", "ALTER TABLE subscriptions ADD CONSTRAINT check_subscriptions_interval CHECK (interval IN ('month', 'year'))", "ALTER TABLE domains ADD CONSTRAINT check_domains_status CHECK (status IN ('pending', 'registered', 'active', 'error', 'expired'))", "ALTER TABLE vps ADD CONSTRAINT check_vps_status CHECK (status IN ('pending', 'provisioning', 'active', 'error', 'terminated'))", "ALTER TABLE deployment_logs ADD CONSTRAINT check_deployment_logs_status CHECK (status IN ('started', 'completed', 'failed', 'in_progress'))", "ALTER TABLE invitations ADD CONSTRAINT check_invitations_status CHECK (status IN ('pending', 'accepted', 'expired'))", } for _, constraint := range constraints { if err := d.DB.Exec(constraint).Error; err != nil { // Log but don't fail if constraint already exists log.Printf("Warning: Failed to add constraint (may already exist): %v", err) } } return nil } func (d *Database) createIndexes() error { indexes := []string{ "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)", "CREATE INDEX IF NOT EXISTS idx_customers_user_id ON customers(user_id)", "CREATE INDEX IF NOT EXISTS idx_customers_stripe_id ON customers(stripe_id)", "CREATE INDEX IF NOT EXISTS idx_customers_status ON customers(status)", "CREATE INDEX IF NOT EXISTS idx_subscriptions_customer_id ON subscriptions(customer_id)", "CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_id ON subscriptions(stripe_id)", "CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status)", "CREATE INDEX IF NOT EXISTS idx_domains_customer_id ON domains(customer_id)", "CREATE INDEX IF NOT EXISTS idx_domains_name ON domains(name)", "CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status)", "CREATE INDEX IF NOT EXISTS idx_vps_domain_id ON vps(domain_id)", "CREATE INDEX IF NOT EXISTS idx_vps_ovh_instance_id ON vps(ovh_instance_id)", "CREATE INDEX IF NOT EXISTS idx_vps_status ON vps(status)", "CREATE INDEX IF NOT EXISTS idx_deployment_logs_vps_id ON deployment_logs(vps_id)", "CREATE INDEX IF NOT EXISTS idx_deployment_logs_created_at ON deployment_logs(created_at)", "CREATE INDEX IF NOT EXISTS idx_invitations_token ON invitations(token)", "CREATE INDEX IF NOT EXISTS idx_invitations_vps_id ON invitations(vps_id)", "CREATE INDEX IF NOT EXISTS idx_invitations_expires_at ON invitations(expires_at)", } for _, index := range indexes { if err := d.DB.Exec(index).Error; err != nil { log.Printf("Warning: Failed to create index (may already exist): %v", err) } } return nil } func (d *Database) Close() error { sqlDB, err := d.DB.DB() if err != nil { return fmt.Errorf("failed to get underlying sql.DB: %w", err) } return sqlDB.Close() } func (d *Database) Health() error { sqlDB, err := d.DB.DB() if err != nil { return fmt.Errorf("failed to get underlying sql.DB: %w", err) } if err := sqlDB.Ping(); err != nil { return fmt.Errorf("failed to ping database: %w", err) } return nil }