package database import ( "fmt" "log" "os" "time" "github.com/openaccounting/oa-server/core/util/id" "github.com/openaccounting/oa-server/models" "gorm.io/driver/mysql" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) var DB *gorm.DB type Config struct { Driver string // "mysql" or "sqlite" Host string Port string User string Password string DBName string SSLMode string // SQLite specific File string // SQLite database file path } func Connect(config *Config) error { // Configure GORM logger newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Second, LogLevel: logger.Info, IgnoreRecordNotFoundError: true, Colorful: true, }, ) var db *gorm.DB var err error // Choose driver based on config switch config.Driver { case "sqlite": // Use SQLite dbFile := config.File if dbFile == "" { dbFile = "./openaccounting.db" // Default SQLite file } db, err = gorm.Open(sqlite.Open(dbFile), &gorm.Config{ Logger: newLogger, }) case "mysql": // Use MySQL (existing logic) dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", config.User, config.Password, config.Host, config.Port, config.DBName) db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: newLogger, }) default: return fmt.Errorf("unsupported database driver: %s (supported: mysql, sqlite)", config.Driver) } if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } // Configure connection pool (only for MySQL, SQLite handles this internally) if config.Driver == "mysql" { sqlDB, err := db.DB() if err != nil { return fmt.Errorf("failed to get database instance: %w", err) } sqlDB.SetMaxOpenConns(25) sqlDB.SetMaxIdleConns(25) sqlDB.SetConnMaxLifetime(5 * time.Minute) } DB = db return nil } // AutoMigrate runs automatic migrations for all models func AutoMigrate() error { return DB.AutoMigrate( &models.Org{}, &models.User{}, &models.UserOrg{}, &models.Token{}, &models.Account{}, &models.Transaction{}, &models.Split{}, &models.Balance{}, &models.Permission{}, &models.Price{}, &models.Session{}, &models.APIKey{}, &models.Invite{}, &models.BudgetItem{}, &models.Attachment{}, ) } // Migrate runs custom migrations func Migrate() error { // Create indexes if err := createIndexes(); err != nil { return err } // Insert default data - temporarily disabled for testing // if err := seedDefaultData(); err != nil { // return err // } return nil } func createIndexes() error { // Create custom indexes that GORM doesn't handle automatically // Based on original indexes.sql file indexes := []string{ // Original indexes from indexes.sql "CREATE INDEX IF NOT EXISTS account_orgId_index ON accounts(orgId)", "CREATE INDEX IF NOT EXISTS split_accountId_index ON splits(accountId)", "CREATE INDEX IF NOT EXISTS split_transactionId_index ON splits(transactionId)", "CREATE INDEX IF NOT EXISTS split_date_index ON splits(date)", "CREATE INDEX IF NOT EXISTS split_updated_index ON splits(updated)", "CREATE INDEX IF NOT EXISTS budgetitem_orgId_index ON budget_items(orgId)", "CREATE INDEX IF NOT EXISTS attachment_transactionId_index ON attachment(transactionId)", "CREATE INDEX IF NOT EXISTS attachment_orgId_index ON attachment(orgId)", "CREATE INDEX IF NOT EXISTS attachment_userId_index ON attachment(userId)", "CREATE INDEX IF NOT EXISTS attachment_uploaded_index ON attachment(uploaded)", // Additional useful indexes for performance "CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(date)", "CREATE INDEX IF NOT EXISTS idx_transaction_org ON transactions(orgId)", "CREATE INDEX IF NOT EXISTS idx_account_parent ON accounts(parent)", "CREATE INDEX IF NOT EXISTS idx_userorg_user ON user_orgs(userId)", "CREATE INDEX IF NOT EXISTS idx_userorg_org ON user_orgs(orgId)", "CREATE INDEX IF NOT EXISTS idx_balance_account_date ON balances(accountId, date)", "CREATE INDEX IF NOT EXISTS idx_permission_org_account ON permissions(orgId, accountId)", } for _, idx := range indexes { if err := DB.Exec(idx).Error; err != nil { return fmt.Errorf("failed to create index: %w", err) } } return nil } func seedDefaultData() error { // Check if we already have data var count int64 if err := DB.Model(&models.Org{}).Count(&count).Error; err != nil { return err } if count > 0 { return nil // Data already exists } // Create a default organization defaultOrg := models.Org{ ID: id.New(), // You'll need to implement this Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Default Organization", Currency: "USD", Precision: 2, Timezone: "UTC", } if err := DB.Create(&defaultOrg).Error; err != nil { return fmt.Errorf("failed to create default organization: %w", err) } // Create default accounts for the organization defaultAccounts := []models.Account{ { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Assets", Parent: []byte{0}, // Root account has zero parent Currency: "USD", Precision: 2, DebitBalance: true, }, { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Liabilities", Parent: []byte{0}, Currency: "USD", Precision: 2, DebitBalance: false, }, { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Equity", Parent: []byte{0}, Currency: "USD", Precision: 2, DebitBalance: false, }, { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Revenue", Parent: []byte{0}, Currency: "USD", Precision: 2, DebitBalance: false, }, { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Expenses", Parent: []byte{0}, Currency: "USD", Precision: 2, DebitBalance: true, }, } // Create accounts and store their IDs for parent-child relationships accountMap := make(map[string]*models.Account) for _, acc := range defaultAccounts { account := acc if err := DB.Create(&account).Error; err != nil { return fmt.Errorf("failed to create account %s: %w", acc.Name, err) } accountMap[acc.Name] = &account } // Create Current Assets first var assetsParent []byte if assetsAccount, exists := accountMap["Assets"]; exists { assetsParent = assetsAccount.ID } else { return fmt.Errorf("Assets account not found in accountMap") } currentAssets := models.Account{ ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Current Assets", Parent: assetsParent, Currency: "USD", Precision: 2, DebitBalance: true, } if err := DB.Create(¤tAssets).Error; err != nil { return fmt.Errorf("failed to create Current Assets: %w", err) } accountMap["Current Assets"] = ¤tAssets // Create Accounts Payable var liabilitiesParent []byte if liabilitiesAccount, exists := accountMap["Liabilities"]; exists { liabilitiesParent = liabilitiesAccount.ID } else { return fmt.Errorf("Liabilities account not found in accountMap") } accountsPayable := models.Account{ ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Accounts Payable", Parent: liabilitiesParent, Currency: "USD", Precision: 2, DebitBalance: false, } if err := DB.Create(&accountsPayable).Error; err != nil { return fmt.Errorf("failed to create Accounts Payable: %w", err) } // Now create sub-accounts under Current Assets subAccounts := []models.Account{ { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Cash", Parent: currentAssets.ID, Currency: "USD", Precision: 2, DebitBalance: true, }, { ID: id.New(), OrgID: defaultOrg.ID, Inserted: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Updated: uint64(time.Now().UnixNano() / int64(time.Millisecond)), Name: "Accounts Receivable", Parent: currentAssets.ID, Currency: "USD", Precision: 2, DebitBalance: true, }, } for _, acc := range subAccounts { if err := DB.Create(&acc).Error; err != nil { return fmt.Errorf("failed to create sub-account %s: %w", acc.Name, err) } } return nil }