You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
feat: add GORM integration with repository pattern
- Add GORM models in models/ directory with proper column tags - Create repository interfaces and implementations in core/repository/ - Add database package with MySQL and SQLite support - Add UUID ID utility for GORM models - Implement complete repository layer replacing SQL-based data access - Add database migrations and index creation - Support both MySQL and SQLite drivers with auto-migration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
332
database/database.go
Normal file
332
database/database.go
Normal file
@@ -0,0 +1,332 @@
|
||||
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{},
|
||||
)
|
||||
}
|
||||
|
||||
// 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)",
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user