diff --git a/apps/api-server/internal/database/database.go b/apps/api-server/internal/database/database.go index da434ab..098ee50 100644 --- a/apps/api-server/internal/database/database.go +++ b/apps/api-server/internal/database/database.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os/exec" "time" "gorm.io/driver/postgres" @@ -31,36 +32,28 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s", cfg.Host, cfg.Username, cfg.Password, cfg.Database, cfg.Port, cfg.SSLMode) - // Configure GORM logger gormLogger := logger.Default.LogMode(logger.Info) - - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: gormLogger, - }) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger}) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } - // Get underlying SQL DB to configure connection pool sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) } - // Configure connection pool for optimal performance - sqlDB.SetMaxIdleConns(25) // Minimum idle connections - sqlDB.SetMaxOpenConns(100) // Maximum open connections - sqlDB.SetConnMaxLifetime(time.Hour) // Max connection lifetime - sqlDB.SetConnMaxIdleTime(30 * time.Minute) // Max idle time before closing + sqlDB.SetMaxIdleConns(25) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + sqlDB.SetConnMaxIdleTime(30 * time.Minute) database := &Database{DB: db} - // Auto-migrate schemas - if err := database.AutoMigrate(); err != nil { - return nil, fmt.Errorf("failed to auto-migrate: %w", err) + if err := runMigrations(dsn); err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) } - // Initialize IMSI allocation if err := database.InitializeIMSIAllocation(); err != nil { return nil, fmt.Errorf("failed to initialize IMSI allocation: %w", err) } @@ -68,20 +61,14 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { return database, nil } -func (d *Database) AutoMigrate() error { - return d.DB.AutoMigrate( - &models.Subscriber{}, - &models.ServicePlan{}, - &models.Session{}, - &models.UsageRecord{}, - &IMSIAllocation{}, - &models.Plugin{}, - &models.Automation{}, - &models.AutomationRun{}, - &models.ConfigEntry{}, - &models.DeploymentRecord{}, - &models.ChaosExperimentRecord{}, - ) +func runMigrations(dsn string) error { + cmd := exec.Command("goose", "postgres", dsn, "up", "migrations") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("goose migration failed: %w, output: %s", err, string(output)) + } + log.Printf("Database migrations completed successfully") + return nil } func (d *Database) InitializeIMSIAllocation() error { @@ -89,17 +76,10 @@ func (d *Database) InitializeIMSIAllocation() error { result := d.DB.First(&allocation) if result.Error == gorm.ErrRecordNotFound { - // Create initial allocation record - allocation = IMSIAllocation{ - LastIMSI: 0, - MinIMSI: 1, - MaxIMSI: 999999999, // 9-digit subscriber numbers - } - + allocation = IMSIAllocation{LastIMSI: 0, MinIMSI: 1, MaxIMSI: 999999999} if err := d.DB.Create(&allocation).Error; err != nil { return fmt.Errorf("failed to create IMSI allocation: %w", err) } - log.Printf("Created IMSI allocation record") } else if result.Error != nil { return fmt.Errorf("failed to query IMSI allocation: %w", result.Error) @@ -116,27 +96,6 @@ func (d *Database) Close() error { return sqlDB.Close() } -// PoolStats returns current connection pool statistics -func (d *Database) PoolStats() (map[string]any, error) { - sqlDB, err := d.DB.DB() - if err != nil { - return nil, err - } - stats := sqlDB.Stats() - return map[string]any{ - "max_open_connections": stats.MaxOpenConnections, - "open_connections": stats.OpenConnections, - "in_use": stats.InUse, - "idle": stats.Idle, - "wait_count": stats.WaitCount, - "wait_duration": stats.WaitDuration.String(), - "max_idle_closed": stats.MaxIdleClosed, - "max_idle_time_closed": stats.MaxIdleTimeClosed, - "max_lifetime_closed": stats.MaxLifetimeClosed, - }, nil -} - -// Ping verifies database connectivity func (d *Database) Ping(ctx context.Context) error { sqlDB, err := d.DB.DB() if err != nil { @@ -145,6 +104,7 @@ func (d *Database) Ping(ctx context.Context) error { return sqlDB.PingContext(ctx) } +// Subscriber CRUD func (d *Database) CreateSubscriber(ctx context.Context, subscriber *models.Subscriber) error { return d.DB.WithContext(ctx).Create(subscriber).Error } @@ -176,8 +136,6 @@ func (d *Database) ListSubscribers(ctx context.Context, req *ListSubscribersRequ var total int64 query := d.DB.WithContext(ctx).Model(&models.Subscriber{}) - - // Apply filters if req.Status != "" { query = query.Where("status = ?", req.Status) } @@ -189,31 +147,15 @@ func (d *Database) ListSubscribers(ctx context.Context, req *ListSubscribersRequ "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%") } - // Count total records if err := query.Count(&total).Error; err != nil { return nil, 0, err } - // Apply pagination and ordering offset := (req.Page - 1) * req.PageSize err := query.Preload("Plan").Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&subscribers).Error - return subscribers, total, err } -func (d *Database) GetIMSIAllocation(ctx context.Context) (*IMSIAllocation, error) { - var allocation IMSIAllocation - err := d.DB.WithContext(ctx).First(&allocation).Error - if err != nil { - return nil, err - } - return &allocation, nil -} - -func (d *Database) UpdateIMSIAllocation(ctx context.Context, allocation *IMSIAllocation) error { - return d.DB.WithContext(ctx).Save(allocation).Error -} - func (d *Database) GetActiveSessionsByIMSI(ctx context.Context, imsi models.IMSI) ([]models.Session, error) { var sessions []models.Session err := d.DB.WithContext(ctx).Where("subscriber_id = ? AND status = ?", imsi, "active").Find(&sessions).Error @@ -224,15 +166,7 @@ func (d *Database) UpdateSession(ctx context.Context, session *models.Session) e return d.DB.WithContext(ctx).Save(session).Error } -func (d *Database) CreateTransaction(ctx context.Context, transaction *models.Transaction) error { - return d.DB.WithContext(ctx).Create(transaction).Error -} - -func (d *Database) UpdateSubscriberBalance(ctx context.Context, subscriberID uint, amount float64) error { - return d.DB.WithContext(ctx).Model(&models.Subscriber{}).Where("id = ?", subscriberID). - UpdateColumn("balance", gorm.Expr("balance + ?", amount)).Error -} - +// Payment methods func (d *Database) CreatePaymentMethod(ctx context.Context, pm *models.PaymentMethod) error { return d.DB.WithContext(ctx).Create(pm).Error } @@ -246,16 +180,17 @@ func (d *Database) GetPaymentMethod(ctx context.Context, id string) (*models.Pay return &pm, nil } -func (d *Database) DeletePaymentMethod(ctx context.Context, id string) error { - return d.DB.WithContext(ctx).Where("id = ?", id).Delete(&models.PaymentMethod{}).Error -} - func (d *Database) ListPaymentMethods(ctx context.Context, subscriberID uint) ([]models.PaymentMethod, error) { var methods []models.PaymentMethod err := d.DB.WithContext(ctx).Where("subscriber_id = ?", subscriberID).Find(&methods).Error return methods, err } +// Transactions +func (d *Database) CreateTransaction(ctx context.Context, transaction *models.Transaction) error { + return d.DB.WithContext(ctx).Create(transaction).Error +} + func (d *Database) GetTransaction(ctx context.Context, transactionID string) (*models.Transaction, error) { var tx models.Transaction err := d.DB.WithContext(ctx).Where("transaction_id = ?", transactionID).First(&tx).Error @@ -293,6 +228,16 @@ func (d *Database) ListTransactions(ctx context.Context, subscriberID uint) ([]m return transactions, err } +// Additional CRUD +func (d *Database) UpdateSubscriberBalance(ctx context.Context, subscriberID uint, amount float64) error { + return d.DB.WithContext(ctx).Model(&models.Subscriber{}).Where("id = ?", subscriberID). + UpdateColumn("balance", gorm.Expr("balance + ?", amount)).Error +} + +func (d *Database) DeletePaymentMethod(ctx context.Context, id string) error { + return d.DB.WithContext(ctx).Where("id = ?", id).Delete(&models.PaymentMethod{}).Error +} + func (d *Database) CreateAlert(ctx context.Context, alert *models.Alert) error { return d.DB.WithContext(ctx).Create(alert).Error } @@ -301,11 +246,24 @@ func (d *Database) CreateNotification(ctx context.Context, notification *models. return d.DB.WithContext(ctx).Create(notification).Error } -// Request types for database operations +// IMSI allocation +func (d *Database) GetIMSIAllocation(ctx context.Context) (*IMSIAllocation, error) { + var allocation IMSIAllocation + err := d.DB.WithContext(ctx).First(&allocation).Error + if err != nil { + return nil, err + } + return &allocation, nil +} + +func (d *Database) UpdateIMSIAllocation(ctx context.Context, allocation *IMSIAllocation) error { + return d.DB.WithContext(ctx).Save(allocation).Error +} + type ListSubscribersRequest struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - Status models.SubscriberStatus `json:"status"` - OrganizationID string `json:"organization_id"` - Search string `json:"search"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Status string `json:"status"` + OrganizationID string `json:"organization_id"` + Search string `json:"search"` }