Files
whatshooked/pkg/storage/db.go
Hein cecbd2cef5
Some checks failed
CI / Test (1.22) (push) Failing after -22m18s
CI / Test (1.23) (push) Failing after -22m7s
CI / Lint (push) Failing after -22m32s
CI / Build (push) Failing after -22m38s
feat(models): rename ModelPublicUser to ModelPublicUsers and update references
* Update user-related models to use plural naming for consistency
* Add relationships to ModelPublicUsers in related models
* Adjust database migration and schema to reflect changes
* Remove deprecated ModelPublicUser
2026-02-20 17:56:02 +02:00

249 lines
6.9 KiB
Go

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.ModelPublicUsers)(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,
allow_insecure BOOLEAN NOT NULL DEFAULT 0,
retry_count INTEGER NOT NULL DEFAULT 3,
timeout INTEGER NOT NULL DEFAULT 30,
secret VARCHAR(255),
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)
}