You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
- 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>
333 lines
9.2 KiB
Go
333 lines
9.2 KiB
Go
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
|
|
}
|