desktop/backend/services/db_service.go
package services
import (
"context"
"database/sql"
"errors"
"fmt"
"pennyapp/backend/config"
"pennyapp/backend/internal/dberror"
"pennyapp/backend/model"
"pennyapp/db/migrations"
"sync"
_ "github.com/mattn/go-sqlite3"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
)
const DefaultAccountTypeName string = "AccountType"
type dbService struct {
ctx context.Context
DB *sql.DB
conf config.Config
// string filepath of the sqlite3 dbfile which is set during Start().
dbFile string
defaultEntity *model.Entity
grandParentAccountType *model.AccountType
}
var db *dbService
var onceDB sync.Once
func DB() *dbService {
if db == nil {
onceDB.Do(func() {
db = &dbService{}
})
}
return db
}
func (d *dbService) Start(ctx context.Context, conf config.Config) {
var err error
d.ctx = ctx
d.conf = conf
dbFile := conf.DBFile
err = migrations.DoMigrateDb("sqlite3://" + dbFile)
if err != nil {
panic(err)
}
db, err := sql.Open("sqlite3", dbFile)
if err != nil {
panic(err)
}
boil.SetDB(db)
boil.DebugMode = d.conf.Debug
d.DB = db
err = d.initAllData(ctx)
if err != nil {
panic(err)
}
d.dbFile = dbFile
}
func (d *dbService) initAllData(ctx context.Context) error {
if err := d.initAccountTypes(ctx); err != nil {
return err
}
if err := d.initEntityTypes(ctx); err != nil {
return err
}
if err := d.initEntity(ctx); err != nil {
return err
}
if err := d.initAccount(ctx); err != nil {
return err
}
if err := d.initCurrency(ctx); err != nil {
return err
}
if err := d.initTransactionsHashType(ctx); err != nil {
return err
}
if err := d.initTransactionImporterStatus(); err != nil {
return err
}
return nil
}
func (d *dbService) initTransactionImporterStatus() error {
transactionImporterStatuses := []model.TransactionImporterStatus{
{Name: "pending"},
{Name: "completed"},
{Name: "failed"},
}
for _, transactionImporterStatus := range transactionImporterStatuses {
if err := transactionImporterStatus.Insert(d.ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert transaction importer status: %w", err)
}
}
}
return nil
}
func (d *dbService) initTransactionsHashType(ctx context.Context) error {
transactionHashType := &model.TransactionsHashType{Name: "v1"}
if err := transactionHashType.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
default:
return fmt.Errorf("failed to insert transaction hash type: %w", err)
}
}
return nil
}
func (d *dbService) initCurrency(ctx context.Context) error {
currency := &model.Currency{Name: "Australian Dollar"}
if err := currency.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
currency, err = model.Currencies([]qm.QueryMod{
qm.Where("name=?", currency.Name),
}...).One(ctx, d.DB)
if err != nil {
return fmt.Errorf("failed to insert currency: %w", err)
}
default:
return fmt.Errorf("failed to insert currency: %w", err)
}
}
currencyAttributes := []model.CurrencyAttribute{
{
CurrencyID: currency.ID,
Name: "code",
Value: "AUD",
},
}
for _, i := range currencyAttributes {
if err := i.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert currency attributes: %w", err)
}
}
}
return nil
}
func (d *dbService) initEntity(ctx context.Context) error {
var q []qm.QueryMod
entityType, err := model.EntityTypes(qm.Where("name=?", "Person")).One(ctx, d.DB)
if err != nil {
return fmt.Errorf("failed to find person entity: %w", err)
}
entity := &model.Entity{Name: "Default", EntityTypeID: entityType.ID}
if err := entity.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
q = []qm.QueryMod{
qm.Where("name=?", "Default"),
qm.Where("entity_type_id=?", entityType.ID),
}
entity, err = model.Entities(q...).One(ctx, d.DB)
if err != nil {
return fmt.Errorf("failed to insert default entity: %w", err)
}
default:
return fmt.Errorf("failed to insert entity: %w", err)
}
}
d.defaultEntity = entity
return nil
}
func (d *dbService) getDefaultEntity() (*model.Entity, error) {
if d.defaultEntity == nil {
return nil, fmt.Errorf("no default entity")
}
return d.defaultEntity, nil
}
func (d *dbService) getGrantParentAccountType() (*model.AccountType, error) {
if d.grandParentAccountType == nil {
return nil, fmt.Errorf("no grand parent account type")
}
return d.grandParentAccountType, nil
}
func (d *dbService) initAccount(ctx context.Context) error {
defaultEntity, err := d.getDefaultEntity()
if err != nil {
return err
}
grandParentAccountType, err := d.getGrantParentAccountType()
if err != nil {
return err
}
grandParentAccount, err := model.Accounts(qm.Where("name=? AND parent_id=0 AND entity_id=? AND account_type_id=?", "Account", defaultEntity.ID, grandParentAccountType.ID)).One(ctx, d.DB)
switch {
case dberror.IsNoRowsFound(err):
grandParentAccount = &model.Account{
Name: "Account",
EntityID: defaultEntity.ID,
AccountTypeID: grandParentAccountType.ID,
ParentID: int64(0),
}
if err := grandParentAccount.Insert(ctx, d.DB, boil.Infer()); err != nil {
return err
}
case err != nil:
return fmt.Errorf("failed to insert grand parent account: %w", err)
}
assetAccountType, err := model.AccountTypes(qm.Where("name=?", "Asset")).One(ctx, d.DB)
if err != nil {
return err
}
accounts := []model.Account{
{Name: "Imbalanced", ParentID: grandParentAccount.ID, EntityID: defaultEntity.ID, AccountTypeID: assetAccountType.ID},
}
for _, account := range accounts {
if err := account.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert account: %w", err)
}
}
}
return nil
}
func (d *dbService) initEntityTypes(ctx context.Context) error {
entityTypes := []model.EntityType{
{Name: "Person"},
}
for _, entityType := range entityTypes {
if err := entityType.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert entity type: %w", err)
}
}
}
return nil
}
func (d *dbService) initAccountTypes(ctx context.Context) error {
grandParentAccountType, err := model.AccountTypes(qm.Where("name=?", DefaultAccountTypeName)).One(ctx, d.DB)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
grandParentAccountType = &model.AccountType{Name: DefaultAccountTypeName}
if err := grandParentAccountType.Insert(ctx, d.DB, boil.Infer()); err != nil {
return err
}
}
}
d.grandParentAccountType = grandParentAccountType
parentAccountTypes := []model.AccountType{
{Name: "Liabilities", ParentID: grandParentAccountType.ID},
{Name: "Assets", ParentID: grandParentAccountType.ID},
{Name: "Revenue", ParentID: grandParentAccountType.ID},
{Name: "Expenses", ParentID: grandParentAccountType.ID},
{Name: "Equity", ParentID: grandParentAccountType.ID},
}
for _, parentAccountType := range parentAccountTypes {
if err := parentAccountType.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert parent account type: %w", err)
}
}
}
type parentAccountTypeName string
childAccountTypes := map[parentAccountTypeName][]model.AccountType{
parentAccountTypeName("Liabilities"): {
{Name: "Liability"},
},
parentAccountTypeName("Expenses"): {
{Name: "Expense"},
},
parentAccountTypeName("Revenue"): {
{Name: "Income"},
},
parentAccountTypeName("Assets"): {
{Name: "Asset"},
},
}
for k, v := range childAccountTypes {
parentAccountType, err := model.AccountTypes(qm.Where("name=? AND parent_id=?", string(k), grandParentAccountType.ID)).One(ctx, d.DB)
if err != nil {
return fmt.Errorf("failed to find parent account type: %w", err)
}
for _, accountType := range v {
accountType.ParentID = parentAccountType.ID
if err := accountType.Insert(ctx, d.DB, boil.Infer()); err != nil {
switch {
case dberror.IsUniqueConstraint(err):
continue
default:
return fmt.Errorf("failed to insert child account type: %w", err)
}
}
}
}
return nil
}