package storage import ( "context" "database/sql" "fmt" "time" "git.warky.dev/wdevs/whatshooked/pkg/config" "git.warky.dev/wdevs/whatshooked/pkg/models" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/driver/pgdriver" "github.com/uptrace/bun/driver/sqliteshim" "github.com/uptrace/bun/extra/bundebug" ) // DB is the global database instance var DB *bun.DB var dbType string // Store the database type for later use // Initialize sets up the database connection based on configuration func Initialize(cfg *config.DatabaseConfig) error { var sqldb *sql.DB var err error dbType = cfg.Type switch cfg.Type { case "postgres", "postgresql": dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database) sqldb = sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn))) DB = bun.NewDB(sqldb, pgdialect.New()) case "sqlite": sqldb, err = sql.Open(sqliteshim.ShimName, cfg.SQLitePath) if err != nil { return fmt.Errorf("failed to open sqlite database: %w", err) } DB = bun.NewDB(sqldb, sqlitedialect.New()) default: return fmt.Errorf("unsupported database type: %s", cfg.Type) } // Add query hook for debugging (optional, can be removed in production) DB.AddQueryHook(bundebug.NewQueryHook( bundebug.WithVerbose(true), bundebug.FromEnv("BUNDEBUG"), )) // Set connection pool settings sqldb.SetMaxIdleConns(10) sqldb.SetMaxOpenConns(100) sqldb.SetConnMaxLifetime(time.Hour) // Test the connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := sqldb.PingContext(ctx); err != nil { return fmt.Errorf("failed to ping database: %w", err) } return nil } // CreateTables creates database tables based on BUN models func CreateTables(ctx context.Context) error { if DB == nil { return fmt.Errorf("database not initialized") } // For SQLite, use raw SQL with compatible syntax if dbType == "sqlite" { return createTablesSQLite(ctx) } // For PostgreSQL, use BUN's auto-generation models := []interface{}{ (*models.ModelPublicUser)(nil), (*models.ModelPublicAPIKey)(nil), (*models.ModelPublicHook)(nil), (*models.ModelPublicWhatsappAccount)(nil), (*models.ModelPublicEventLog)(nil), (*models.ModelPublicSession)(nil), (*models.ModelPublicMessageCache)(nil), } for _, model := range models { _, err := DB.NewCreateTable().Model(model).IfNotExists().Exec(ctx) if err != nil { return fmt.Errorf("failed to create table: %w", err) } } return nil } // createTablesSQLite creates tables using SQLite-compatible SQL func createTablesSQLite(ctx context.Context) error { tables := []string{ // Users table `CREATE TABLE IF NOT EXISTS users ( id VARCHAR(36) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, full_name VARCHAR(255), role VARCHAR(50) NOT NULL DEFAULT 'user', active BOOLEAN NOT NULL DEFAULT 1, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP )`, // API Keys table `CREATE TABLE IF NOT EXISTS api_keys ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, key VARCHAR(255) NOT NULL UNIQUE, key_prefix VARCHAR(20), permissions TEXT, active BOOLEAN NOT NULL DEFAULT 1, last_used_at TIMESTAMP, expires_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // Hooks table `CREATE TABLE IF NOT EXISTS hooks ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, url TEXT NOT NULL, method VARCHAR(10) NOT NULL DEFAULT 'POST', headers TEXT, events TEXT, active BOOLEAN NOT NULL DEFAULT 1, retry_count INTEGER NOT NULL DEFAULT 3, timeout_seconds INTEGER NOT NULL DEFAULT 30, description TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // WhatsApp Accounts table `CREATE TABLE IF NOT EXISTS whatsapp_accounts ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, phone_number VARCHAR(20) NOT NULL UNIQUE, account_type VARCHAR(50) NOT NULL DEFAULT 'whatsmeow', business_api_config TEXT, active BOOLEAN NOT NULL DEFAULT 1, connected BOOLEAN NOT NULL DEFAULT 0, last_connected_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // Event Logs table `CREATE TABLE IF NOT EXISTS event_logs ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36), account_id VARCHAR(36), event_type VARCHAR(100) NOT NULL, event_data TEXT, from_number VARCHAR(20), to_number VARCHAR(20), message_id VARCHAR(255), status VARCHAR(50), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL )`, // Sessions table `CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, token VARCHAR(500) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // Message Cache table `CREATE TABLE IF NOT EXISTS message_cache ( id VARCHAR(36) PRIMARY KEY, account_id VARCHAR(36), event_type VARCHAR(100) NOT NULL, event_data TEXT NOT NULL, message_id VARCHAR(255), from_number VARCHAR(20), to_number VARCHAR(20), processed BOOLEAN NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, processed_at TIMESTAMP, FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL )`, } for _, sql := range tables { if _, err := DB.ExecContext(ctx, sql); err != nil { return fmt.Errorf("failed to create table: %w", err) } } return nil } // Close closes the database connection func Close() error { if DB == nil { return nil } return DB.Close() } // GetDB returns the database instance func GetDB() *bun.DB { return DB } // HealthCheck checks if the database connection is healthy func HealthCheck() error { if DB == nil { return fmt.Errorf("database not initialized") } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return DB.PingContext(ctx) }