547 lines
14 KiB
Go
547 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/freeflowuniverse/heroagent/pkg/data/ourdb"
|
|
"github.com/freeflowuniverse/heroagent/pkg/data/radixtree"
|
|
"github.com/freeflowuniverse/heroagent/pkg/tools"
|
|
)
|
|
|
|
// DBStore represents the central database store for all models
|
|
type DBStore struct {
|
|
DB *ourdb.OurDB
|
|
Meta *radixtree.RadixTree
|
|
}
|
|
|
|
// NewDBStore creates a new database store
|
|
func NewDBStore() (*DBStore, error) {
|
|
// Get home directory
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
|
}
|
|
|
|
// Create paths
|
|
dbPath := filepath.Join(homeDir, "hero", "var", "server", "users", "db")
|
|
metaPath := filepath.Join(homeDir, "hero", "var", "server", "users", "meta")
|
|
|
|
// Create directories if they don't exist
|
|
if err := os.MkdirAll(dbPath, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create db directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(metaPath, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create meta directory: %w", err)
|
|
}
|
|
|
|
// Create ourdb
|
|
dbConfig := ourdb.DefaultConfig()
|
|
dbConfig.Path = dbPath
|
|
dbConfig.IncrementalMode = true
|
|
db, err := ourdb.New(dbConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create ourdb: %w", err)
|
|
}
|
|
|
|
// Create radixtree
|
|
meta, err := radixtree.New(radixtree.NewArgs{
|
|
Path: metaPath,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create radixtree: %w", err)
|
|
}
|
|
|
|
return &DBStore{
|
|
DB: db,
|
|
Meta: meta,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the database connections
|
|
func (s *DBStore) Close() error {
|
|
err1 := s.DB.Close()
|
|
err2 := s.Meta.Close()
|
|
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
return err2
|
|
}
|
|
|
|
// UserStore handles the storage and retrieval of User objects
|
|
type UserStore struct {
|
|
Store *DBStore
|
|
}
|
|
|
|
// NewUserStore creates a new UserStore
|
|
func NewUserStore() (*UserStore, *DBStore, error) {
|
|
store, err := NewDBStore()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &UserStore{
|
|
Store: store,
|
|
}, store, nil
|
|
}
|
|
|
|
// Save saves a User to the database
|
|
func (s *UserStore) Save(user *User) error {
|
|
// If ID is 0, this is a new user
|
|
if user.ID == 0 {
|
|
// Fix the key
|
|
fixedKey := tools.NameFix(user.Key)
|
|
if fixedKey == "" {
|
|
return errors.New("key cannot be empty")
|
|
}
|
|
user.Key = fixedKey
|
|
|
|
// Check if key already exists
|
|
existingID, err := s.GetIDByKey(user.Key)
|
|
if err == nil && existingID != 0 {
|
|
return fmt.Errorf("user with key %s already exists", user.Key)
|
|
}
|
|
|
|
// Get next ID
|
|
nextID, err := s.Store.DB.GetNextID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get next ID: %w", err)
|
|
}
|
|
user.ID = nextID
|
|
|
|
// Save to ourdb
|
|
data := user.Serialize()
|
|
_, err = s.Store.DB.Set(ourdb.OurDBSetArgs{
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save user to ourdb: %w", err)
|
|
}
|
|
|
|
// Save key to radixtree
|
|
idBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(idBytes, user.ID)
|
|
err = s.Store.Meta.Set(user.Key, idBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save user key to radixtree: %w", err)
|
|
}
|
|
} else {
|
|
// Update existing user
|
|
// Get existing user to verify key
|
|
existingUser, err := s.GetByID(user.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get existing user: %w", err)
|
|
}
|
|
|
|
// If key changed, update radixtree
|
|
if existingUser.Key != user.Key {
|
|
// Fix the new key
|
|
fixedKey := tools.NameFix(user.Key)
|
|
if fixedKey == "" {
|
|
return errors.New("key cannot be empty")
|
|
}
|
|
user.Key = fixedKey
|
|
|
|
// Check if new key already exists
|
|
existingID, err := s.GetIDByKey(user.Key)
|
|
if err == nil && existingID != 0 && existingID != user.ID {
|
|
return fmt.Errorf("user with key %s already exists", user.Key)
|
|
}
|
|
|
|
// Delete old key from radixtree
|
|
err = s.Store.Meta.Delete(existingUser.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete old user key from radixtree: %w", err)
|
|
}
|
|
|
|
// Save new key to radixtree
|
|
idBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(idBytes, user.ID)
|
|
err = s.Store.Meta.Set(user.Key, idBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save new user key to radixtree: %w", err)
|
|
}
|
|
}
|
|
|
|
// Save to ourdb
|
|
data := user.Serialize()
|
|
id := user.ID
|
|
_, err = s.Store.DB.Set(ourdb.OurDBSetArgs{
|
|
ID: &id,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update user in ourdb: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves a User by ID
|
|
func (s *UserStore) GetByID(id uint32) (*User, error) {
|
|
data, err := s.Store.DB.Get(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user from ourdb: %w", err)
|
|
}
|
|
|
|
user, err := DeserializeUser(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize user: %w", err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetByKey retrieves a User by key
|
|
func (s *UserStore) GetByKey(key string) (*User, error) {
|
|
// Fix the key
|
|
fixedKey := tools.NameFix(key)
|
|
if fixedKey == "" {
|
|
return nil, errors.New("key cannot be empty")
|
|
}
|
|
|
|
// Get ID from radixtree
|
|
id, err := s.GetIDByKey(fixedKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user ID from radixtree: %w", err)
|
|
}
|
|
|
|
// Get user from ourdb
|
|
return s.GetByID(id)
|
|
}
|
|
|
|
// GetIDByKey retrieves a user ID by key
|
|
func (s *UserStore) GetIDByKey(key string) (uint32, error) {
|
|
// Get ID from radixtree
|
|
idBytes, err := s.Store.Meta.Get(key)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get user ID from radixtree: %w", err)
|
|
}
|
|
|
|
if len(idBytes) != 4 {
|
|
return 0, fmt.Errorf("invalid ID bytes length: %d", len(idBytes))
|
|
}
|
|
|
|
return binary.LittleEndian.Uint32(idBytes), nil
|
|
}
|
|
|
|
// Delete deletes a User by ID
|
|
func (s *UserStore) Delete(id uint32) error {
|
|
// Get user to get key
|
|
user, err := s.GetByID(id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// Delete from radixtree
|
|
err = s.Store.Meta.Delete(user.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user key from radixtree: %w", err)
|
|
}
|
|
|
|
// Delete from ourdb
|
|
err = s.Store.DB.Delete(id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user from ourdb: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AccountStore handles the storage and retrieval of Account objects
|
|
type AccountStore struct {
|
|
Store *DBStore
|
|
}
|
|
|
|
// NewAccountStore creates a new AccountStore using the provided DBStore
|
|
func NewAccountStore(store *DBStore) *AccountStore {
|
|
return &AccountStore{
|
|
Store: store,
|
|
}
|
|
}
|
|
|
|
// Save saves an Account to the database
|
|
func (s *AccountStore) Save(account *Account) error {
|
|
// Validate account data
|
|
if err := account.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If ID is 0, this is a new account
|
|
if account.ID == 0 {
|
|
// Get next ID
|
|
nextID, err := s.Store.DB.GetNextID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get next ID: %w", err)
|
|
}
|
|
account.ID = nextID
|
|
|
|
// Save to ourdb
|
|
data := account.Serialize()
|
|
_, err = s.Store.DB.Set(ourdb.OurDBSetArgs{
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save account to ourdb: %w", err)
|
|
}
|
|
|
|
// Update user's accounts list
|
|
userStore := &UserStore{Store: s.Store}
|
|
user, err := userStore.GetByID(account.UserID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// Add account ID to user's accounts
|
|
user.Accounts = append(user.Accounts, account.ID)
|
|
|
|
// Save updated user
|
|
err = userStore.Save(user)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update user's accounts: %w", err)
|
|
}
|
|
} else {
|
|
// Update existing account
|
|
// Get existing account to verify it exists
|
|
_, err := s.GetByID(account.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get existing account: %w", err)
|
|
}
|
|
|
|
// Save to ourdb
|
|
data := account.Serialize()
|
|
id := account.ID
|
|
_, err = s.Store.DB.Set(ourdb.OurDBSetArgs{
|
|
ID: &id,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update account in ourdb: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves an Account by ID
|
|
func (s *AccountStore) GetByID(id uint32) (*Account, error) {
|
|
data, err := s.Store.DB.Get(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get account from ourdb: %w", err)
|
|
}
|
|
|
|
account, err := DeserializeAccount(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize account: %w", err)
|
|
}
|
|
|
|
return account, nil
|
|
}
|
|
|
|
// GetByUserID retrieves all Accounts for a user
|
|
func (s *AccountStore) GetByUserID(userID uint32) ([]*Account, error) {
|
|
// Get user to get account IDs
|
|
userStore := &UserStore{Store: s.Store}
|
|
user, err := userStore.GetByID(userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// Get accounts
|
|
accounts := make([]*Account, 0, len(user.Accounts))
|
|
for _, accountID := range user.Accounts {
|
|
account, err := s.GetByID(accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get account %d: %w", accountID, err)
|
|
}
|
|
accounts = append(accounts, account)
|
|
}
|
|
|
|
return accounts, nil
|
|
}
|
|
|
|
// Delete deletes an Account by ID
|
|
func (s *AccountStore) Delete(id uint32) error {
|
|
// Get account to get user ID
|
|
account, err := s.GetByID(id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get account: %w", err)
|
|
}
|
|
|
|
// Get user to remove account from accounts list
|
|
userStore := &UserStore{Store: s.Store}
|
|
user, err := userStore.GetByID(account.UserID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user: %w", err)
|
|
}
|
|
|
|
// Remove account ID from user's accounts
|
|
for i, accountID := range user.Accounts {
|
|
if accountID == id {
|
|
user.Accounts = append(user.Accounts[:i], user.Accounts[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Save updated user
|
|
err = userStore.Save(user)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update user's accounts: %w", err)
|
|
}
|
|
|
|
// Delete from ourdb
|
|
err = s.Store.DB.Delete(id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete account from ourdb: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TransactionStore handles the storage and retrieval of Transaction objects
|
|
type TransactionStore struct {
|
|
Store *DBStore
|
|
}
|
|
|
|
// NewTransactionStore creates a new TransactionStore using the provided DBStore
|
|
func NewTransactionStore(store *DBStore) *TransactionStore {
|
|
return &TransactionStore{
|
|
Store: store,
|
|
}
|
|
}
|
|
|
|
// Save saves a Transaction to the database and updates account balances
|
|
func (s *TransactionStore) Save(transaction *Transaction) error {
|
|
// Validate transaction data
|
|
if err := transaction.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get account store
|
|
accountStore := &AccountStore{Store: s.Store}
|
|
|
|
// Get source account
|
|
fromAccount, err := accountStore.GetByID(transaction.From)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get source account: %w", err)
|
|
}
|
|
|
|
// Get destination account
|
|
toAccount, err := accountStore.GetByID(transaction.To)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get destination account: %w", err)
|
|
}
|
|
|
|
// Check if source account has enough balance
|
|
if fromAccount.Amount < transaction.Amount {
|
|
return errors.New("insufficient balance in source account")
|
|
}
|
|
|
|
// If TxID is 0, this is a new transaction
|
|
if transaction.TxID == 0 {
|
|
// Get next ID
|
|
nextID, err := s.Store.DB.GetNextID()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get next ID: %w", err)
|
|
}
|
|
transaction.TxID = nextID
|
|
|
|
// Update account balances
|
|
fromAccount.Amount -= transaction.Amount
|
|
toAccount.Amount += transaction.Amount
|
|
|
|
// Save updated accounts
|
|
if err := accountStore.Save(fromAccount); err != nil {
|
|
return fmt.Errorf("failed to update source account: %w", err)
|
|
}
|
|
|
|
if err := accountStore.Save(toAccount); err != nil {
|
|
// Rollback source account change if destination update fails
|
|
fromAccount.Amount += transaction.Amount
|
|
if rollbackErr := accountStore.Save(fromAccount); rollbackErr != nil {
|
|
return fmt.Errorf("failed to update destination account and rollback failed: %w, rollback error: %v", err, rollbackErr)
|
|
}
|
|
return fmt.Errorf("failed to update destination account: %w", err)
|
|
}
|
|
|
|
// Save transaction to ourdb
|
|
data := transaction.Serialize()
|
|
_, err = s.Store.DB.Set(ourdb.OurDBSetArgs{
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
// Rollback account changes if transaction save fails
|
|
fromAccount.Amount += transaction.Amount
|
|
toAccount.Amount -= transaction.Amount
|
|
if rollbackErr1 := accountStore.Save(fromAccount); rollbackErr1 != nil {
|
|
return fmt.Errorf("failed to save transaction and rollback failed: %w, rollback error 1: %v", err, rollbackErr1)
|
|
}
|
|
if rollbackErr2 := accountStore.Save(toAccount); rollbackErr2 != nil {
|
|
return fmt.Errorf("failed to save transaction and rollback failed: %w, rollback error 2: %v", err, rollbackErr2)
|
|
}
|
|
return fmt.Errorf("failed to save transaction to ourdb: %w", err)
|
|
}
|
|
} else {
|
|
// Updating existing transactions is not allowed as it would affect account balances
|
|
return errors.New("updating existing transactions is not allowed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves a Transaction by ID
|
|
func (s *TransactionStore) GetByID(id uint32) (*Transaction, error) {
|
|
data, err := s.Store.DB.Get(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get transaction from ourdb: %w", err)
|
|
}
|
|
|
|
transaction, err := DeserializeTransaction(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize transaction: %w", err)
|
|
}
|
|
|
|
return transaction, nil
|
|
}
|
|
|
|
// GetByAccount retrieves all Transactions for an account (either as source or destination)
|
|
func (s *TransactionStore) GetByAccount(accountID uint32) ([]*Transaction, error) {
|
|
// This is a simplified implementation that would be inefficient in a real system
|
|
// In a real system, we would maintain indexes for account transactions
|
|
|
|
// Get the next ID to determine the range of transaction IDs
|
|
nextID, err := s.Store.DB.GetNextID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get next ID: %w", err)
|
|
}
|
|
|
|
transactions := make([]*Transaction, 0)
|
|
|
|
// Iterate through all transactions (this is inefficient but works for a simple implementation)
|
|
for i := uint32(1); i < nextID; i++ {
|
|
transaction, err := s.GetByID(i)
|
|
if err != nil {
|
|
// Skip if transaction doesn't exist or can't be deserialized
|
|
continue
|
|
}
|
|
|
|
// Include transaction if it involves the specified account
|
|
if transaction.From == accountID || transaction.To == accountID {
|
|
transactions = append(transactions, transaction)
|
|
}
|
|
}
|
|
|
|
return transactions, nil
|
|
}
|
|
|
|
// Delete is not implemented for transactions as it would affect account balances
|
|
// In a real system, you might want to implement a "void" or "reverse" transaction instead
|
|
func (s *TransactionStore) Delete(id uint32) error {
|
|
return errors.New("deleting transactions is not allowed")
|
|
}
|