feat(treasurechest): #64 implement hirarchical treasure chests
This commit was merged in pull request #65.
This commit is contained in:
+28
-23
@@ -27,10 +27,10 @@ var (
|
||||
|
||||
type Account interface {
|
||||
Add(user *types.User, name string) (*types.Account, error)
|
||||
Update(user *types.User, id uuid.UUID, name string) (*types.Account, error)
|
||||
Get(user *types.User, id uuid.UUID) (*types.Account, error)
|
||||
Update(user *types.User, id string, name string) (*types.Account, error)
|
||||
Get(user *types.User, id string) (*types.Account, error)
|
||||
GetAll(user *types.User) ([]*types.Account, error)
|
||||
Delete(user *types.User, id uuid.UUID) error
|
||||
Delete(user *types.User, id string) error
|
||||
}
|
||||
|
||||
type AccountImpl struct {
|
||||
@@ -40,11 +40,11 @@ type AccountImpl struct {
|
||||
settings *types.Settings
|
||||
}
|
||||
|
||||
func NewAccountImpl(db db.Account, random Random, clock Clock, settings *types.Settings) Account {
|
||||
func NewAccount(db db.Account, random Random, clock Clock, settings *types.Settings) Account {
|
||||
return AccountImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: NewRandomImpl(),
|
||||
random: random,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
err = s.validateAccount(name)
|
||||
err = validateString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,17 +95,22 @@ func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error)
|
||||
return savedAccount, nil
|
||||
}
|
||||
|
||||
func (s AccountImpl) Update(user *types.User, id uuid.UUID, name string) (*types.Account, error) {
|
||||
func (s AccountImpl) Update(user *types.User, id string, name string) (*types.Account, error) {
|
||||
accountMetric.WithLabelValues("update").Inc()
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
err := s.validateAccount(name)
|
||||
err := validateString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("account update: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
account, err := s.db.Get(user.Id, id)
|
||||
account, err := s.db.Get(user.Id, uuid)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
|
||||
@@ -126,14 +131,19 @@ func (s AccountImpl) Update(user *types.User, id uuid.UUID, name string) (*types
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s AccountImpl) Get(user *types.User, id uuid.UUID) (*types.Account, error) {
|
||||
func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
|
||||
accountMetric.WithLabelValues("get").Inc()
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("account get: %v", err)
|
||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
account, err := s.db.Get(user.Id, id)
|
||||
account, err := s.db.Get(user.Id, uuid)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
|
||||
@@ -158,13 +168,18 @@ func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (s AccountImpl) Delete(user *types.User, id uuid.UUID) error {
|
||||
func (s AccountImpl) Delete(user *types.User, id string) error {
|
||||
accountMetric.WithLabelValues("delete").Inc()
|
||||
if user == nil {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
log.Error("account delete: %v", err)
|
||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
account, err := s.db.Get(user.Id, id)
|
||||
account, err := s.db.Get(user.Id, uuid)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
|
||||
@@ -183,13 +198,3 @@ func (s AccountImpl) Delete(user *types.User, id uuid.UUID) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s AccountImpl) validateAccount(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("field \"name\" needs to be set: %w", ErrBadRequest)
|
||||
} else if !safeInputRegex.MatchString(name) {
|
||||
return fmt.Errorf("use only letters, dashes and spaces for \"name\": %w", ErrBadRequest)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ type AuthImpl struct {
|
||||
serverSettings *types.Settings
|
||||
}
|
||||
|
||||
func NewAuthImpl(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *types.Settings) *AuthImpl {
|
||||
func NewAuth(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *types.Settings) *AuthImpl {
|
||||
return &AuthImpl{
|
||||
db: db,
|
||||
random: random,
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestSignUp(t *testing.T) {
|
||||
mockClock := mocks.NewMockClock(t)
|
||||
mockMail := mocks.NewMockMail(t)
|
||||
|
||||
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
underTest := NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
|
||||
_, err := underTest.SignUp("invalid email address", "SomeStrongPassword123!")
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestSignUp(t *testing.T) {
|
||||
mockClock := mocks.NewMockClock(t)
|
||||
mockMail := mocks.NewMockMail(t)
|
||||
|
||||
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
underTest := NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
|
||||
weakPasswords := []string{
|
||||
"123!ab", // too short
|
||||
@@ -73,7 +73,7 @@ func TestSignUp(t *testing.T) {
|
||||
mockClock.EXPECT().Now().Return(createTime)
|
||||
mockAuthDb.EXPECT().InsertUser(expected).Return(nil)
|
||||
|
||||
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
underTest := NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
actual, err := underTest.SignUp(email, password)
|
||||
|
||||
assert.Nil(t, err)
|
||||
@@ -101,7 +101,7 @@ func TestSignUp(t *testing.T) {
|
||||
|
||||
mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists)
|
||||
|
||||
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
underTest := NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
|
||||
_, err := underTest.SignUp(user.Email, password)
|
||||
assert.Equal(t, ErrAccountExists, err)
|
||||
@@ -131,7 +131,7 @@ func TestSendVerificationMail(t *testing.T) {
|
||||
return strings.Contains(message, token.Token)
|
||||
})).Return()
|
||||
|
||||
underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
underTest := NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{})
|
||||
|
||||
underTest.SendVerificationMail(userId, email)
|
||||
})
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ type Clock interface {
|
||||
|
||||
type ClockImpl struct{}
|
||||
|
||||
func NewClockImpl() Clock {
|
||||
func NewClock() Clock {
|
||||
return &ClockImpl{}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package service
|
||||
|
||||
import "fmt"
|
||||
|
||||
func validateString(value string) error {
|
||||
if value == "" {
|
||||
return fmt.Errorf("field \"name\" needs to be set: %w", ErrBadRequest)
|
||||
} else if !safeInputRegex.MatchString(value) {
|
||||
return fmt.Errorf("use only letters, dashes and spaces for \"name\": %w", ErrBadRequest)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ type MailImpl struct {
|
||||
server *types.Settings
|
||||
}
|
||||
|
||||
func NewMailImpl(server *types.Settings) MailImpl {
|
||||
func NewMail(server *types.Settings) MailImpl {
|
||||
return MailImpl{server: server}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type Random interface {
|
||||
type RandomImpl struct {
|
||||
}
|
||||
|
||||
func NewRandomImpl() *RandomImpl {
|
||||
func NewRandom() *RandomImpl {
|
||||
return &RandomImpl{}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"spend-sparrow/db"
|
||||
"spend-sparrow/log"
|
||||
"spend-sparrow/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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 db.TreasureChest
|
||||
clock Clock
|
||||
random Random
|
||||
settings *types.Settings
|
||||
}
|
||||
|
||||
func NewTreasureChest(db db.TreasureChest, random Random, clock Clock, settings *types.Settings) TreasureChest {
|
||||
return TreasureChestImpl{
|
||||
db: db,
|
||||
clock: clock,
|
||||
random: random,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentUuid := uuid.Nil
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != uuid.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,
|
||||
}
|
||||
|
||||
err = s.db.Insert(user.Id, treasureChest)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
savedtreasureChest, err := s.db.Get(user.Id, newId)
|
||||
if err != nil {
|
||||
log.Error("treasureChest %v not found after insert: %v", newId, err)
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
return savedtreasureChest, 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)
|
||||
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)
|
||||
}
|
||||
|
||||
treasureChest, err := s.db.Get(user.Id, id)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, ErrBadRequest)
|
||||
}
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
parentUuid := uuid.Nil
|
||||
if parentId != "" {
|
||||
parent, err := s.Get(user, parentId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ParentId != uuid.Nil {
|
||||
return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
children, err := s.db.GetAllByParentId(user.Id, treasureChest.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest)
|
||||
}
|
||||
|
||||
parentUuid = parent.Id
|
||||
}
|
||||
|
||||
timestamp := s.clock.Now()
|
||||
treasureChest.Name = name
|
||||
treasureChest.ParentId = parentUuid
|
||||
treasureChest.UpdatedAt = ×tamp
|
||||
treasureChest.UpdatedBy = &user.Id
|
||||
|
||||
err = s.db.Update(user.Id, treasureChest)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
treasureChest, err := s.db.Get(user.Id, uuid)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return nil, fmt.Errorf("treasureChest %v not found: %w", id, ErrBadRequest)
|
||||
}
|
||||
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, err := s.db.GetAll(user.Id)
|
||||
if err != nil {
|
||||
return nil, types.ErrInternal
|
||||
}
|
||||
|
||||
return 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)
|
||||
}
|
||||
|
||||
treasureChest, err := s.db.Get(user.Id, id)
|
||||
if err != nil {
|
||||
if err == db.ErrNotFound {
|
||||
return fmt.Errorf("treasureChest %v not found: %w", idStr, ErrBadRequest)
|
||||
}
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
if treasureChest.UserId != user.Id {
|
||||
return types.ErrUnauthorized
|
||||
}
|
||||
|
||||
children, err := s.db.GetAllByParentId(user.Id, treasureChest.Id)
|
||||
if err != nil {
|
||||
log.Error("treasureChest delete: %v", err)
|
||||
return types.ErrInternal
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return fmt.Errorf("TreasureChest %v has children: %w", idStr, ErrBadRequest)
|
||||
}
|
||||
|
||||
err = s.db.Delete(user.Id, treasureChest.Id)
|
||||
if err != nil {
|
||||
return types.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user