fix: move implementation to "internal" package
This commit was merged in pull request #138.
This commit is contained in:
327
internal/service/treasure_chest.go
Normal file
327
internal/service/treasure_chest.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"spend-sparrow/internal/db"
|
||||
"spend-sparrow/internal/log"
|
||||
"spend-sparrow/internal/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
treasureChestMetric = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "spendsparrow_treasurechest_total",
|
||||
Help: "The total of treasurechest operations",
|
||||
},
|
||||
[]string{"operation"},
|
||||
)
|
||||
)
|
||||
|
||||
type TreasureChest interface {
|
||||
Add(user *types.User, parentId, name string) (*types.TreasureChest, error)
|
||||
Update(user *types.User, id, parentId, name string) (*types.TreasureChest, error)
|
||||
Get(user *types.User, id string) (*types.TreasureChest, error)
|
||||
GetAll(user *types.User) ([]*types.TreasureChest, error)
|
||||
Delete(user *types.User, id string) error
|
||||
}
|
||||
|
||||
type TreasureChestImpl struct {
|
||||
db *sqlx.DB
|
||||
clock Clock
|
||||
random Random
|
||||
}
|
||||
|
||||
func NewTreasureChest(db *sqlx.DB, random Random, clock Clock) TreasureChest {
|
||||
return TreasureChestImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
}
|
||||
}
|
||||
|
||||
func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) {
|
||||
treasureChestMetric.WithLabelValues("add").Inc()
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
newId, err := s.random.UUID()
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
err = validateString(name, "name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parentUuid *uuid.UUID
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != nil {
|
||||
return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest)
|
||||
}
|
||||
parentUuid = &parent.Id
|
||||
}
|
||||
|
||||
treasureChest := &types.TreasureChest{
|
||||
Id: newId,
|
||||
ParentId: parentUuid,
|
||||
UserId: user.Id,
|
||||
|
||||
Name: name,
|
||||
|
||||
CurrentBalance: 0,
|
||||
|
||||
CreatedAt: s.clock.Now(),
|
||||
CreatedBy: user.Id,
|
||||
UpdatedAt: nil,
|
||||
UpdatedBy: nil,
|
||||
}
|
||||
|
||||
r, err := s.db.NamedExec(`
|
||||
INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by)
|
||||
VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest)
|
||||
err = db.TransformAndLogDbError("treasureChest Insert", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return treasureChest, nil
|
||||
}
|
||||
|
||||
func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) {
|
||||
treasureChestMetric.WithLabelValues("update").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
err := validateString(name, "name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
log.Error("treasureChest update: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.Beginx()
|
||||
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
treasureChest := &types.TreasureChest{}
|
||||
err = tx.Get(treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id)
|
||||
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
var parentUuid *uuid.UUID
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var childCount int
|
||||
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != nil || childCount > 0 {
|
||||
return nil, fmt.Errorf("only one level allowed: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
parentUuid = &parent.Id
|
||||
}
|
||||
|
||||
timestamp := s.clock.Now()
|
||||
treasureChest.Name = name
|
||||
treasureChest.ParentId = parentUuid
|
||||
treasureChest.UpdatedAt = ×tamp
|
||||
treasureChest.UpdatedBy = &user.Id
|
||||
|
||||
r, err := tx.NamedExec(`
|
||||
UPDATE treasure_chest
|
||||
SET
|
||||
parent_id = :parent_id,
|
||||
name = :name,
|
||||
current_balance = :current_balance,
|
||||
updated_at = :updated_at,
|
||||
updated_by = :updated_by
|
||||
WHERE id = :id
|
||||
AND user_id = :user_id`, treasureChest)
|
||||
err = db.TransformAndLogDbError("treasureChest Update", r, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return treasureChest, nil
|
||||
}
|
||||
|
||||
func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) {
|
||||
treasureChestMetric.WithLabelValues("get").Inc()
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("treasureChest get: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
var treasureChest types.TreasureChest
|
||||
err = s.db.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||
err = db.TransformAndLogDbError("treasureChest Get", nil, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNotFound) {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", id, err)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return &treasureChest, nil
|
||||
}
|
||||
|
||||
func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) {
|
||||
treasureChestMetric.WithLabelValues("get_all").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
treasureChests := make([]*types.TreasureChest, 0)
|
||||
err := s.db.Select(&treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
|
||||
err = db.TransformAndLogDbError("treasureChest GetAll", nil, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sortTree(treasureChests), nil
|
||||
}
|
||||
|
||||
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
|
||||
treasureChestMetric.WithLabelValues("delete").Inc()
|
||||
if user == nil {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
log.Error("treasureChest delete: %v", err)
|
||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := s.db.Beginx()
|
||||
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
childCount := 0
|
||||
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if childCount > 0 {
|
||||
return fmt.Errorf("treasure chest has children: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
transactionsCount := 0
|
||||
err = tx.Get(&transactionsCount,
|
||||
`SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`,
|
||||
user.Id, id)
|
||||
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if transactionsCount > 0 {
|
||||
return fmt.Errorf("treasure chest has transactions: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
r, err := tx.Exec(`DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id)
|
||||
err = db.TransformAndLogDbError("treasureChest Delete", r, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest {
|
||||
var (
|
||||
roots []*types.TreasureChest
|
||||
)
|
||||
children := make(map[uuid.UUID][]*types.TreasureChest)
|
||||
result := make([]*types.TreasureChest, 0)
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.ParentId == nil {
|
||||
roots = append(roots, node)
|
||||
} else {
|
||||
children[*node.ParentId] = append(children[*node.ParentId], node)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(roots, func(a, b *types.TreasureChest) int {
|
||||
return compareStrings(a.Name, b.Name)
|
||||
})
|
||||
|
||||
for _, root := range roots {
|
||||
result = append(result, root)
|
||||
|
||||
childList := children[root.Id]
|
||||
|
||||
slices.SortFunc(childList, func(a, b *types.TreasureChest) int {
|
||||
return compareStrings(a.Name, b.Name)
|
||||
})
|
||||
result = append(result, childList...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func compareStrings(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a < b {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user