Passed
Push — main ( c818dd...e58f6e )
by Acho
02:01
created

repositories.NewGormPhoneRepository   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nop 3
dl 0
loc 9
rs 9.95
c 0
b 0
f 0
1
package repositories
2
3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"time"
8
9
	"github.com/NdoleStudio/httpsms/pkg/entities"
10
	"github.com/NdoleStudio/httpsms/pkg/telemetry"
11
	"github.com/dgraph-io/ristretto/v2"
12
	"github.com/google/uuid"
13
	"github.com/palantir/stacktrace"
14
	"gorm.io/gorm"
15
)
16
17
// gormPhoneRepository is responsible for persisting entities.Phone
18
type gormPhoneRepository struct {
19
	logger telemetry.Logger
20
	tracer telemetry.Tracer
21
	cache  *ristretto.Cache[string, *entities.Phone]
22
	db     *gorm.DB
23
}
24
25
// NewGormPhoneRepository creates the GORM version of the PhoneRepository
26
func NewGormPhoneRepository(
27
	logger telemetry.Logger,
28
	tracer telemetry.Tracer,
29
	db *gorm.DB,
30
	cache *ristretto.Cache[string, *entities.Phone],
31
) PhoneRepository {
32
	return &gormPhoneRepository{
33
		logger: logger.WithService(fmt.Sprintf("%T", &gormPhoneRepository{})),
34
		tracer: tracer,
35
		db:     db,
36
		cache:  cache,
37
	}
38
}
39
40
func (repository *gormPhoneRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error {
41
	ctx, span := repository.tracer.Start(ctx)
42
	defer span.End()
43
44
	if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Phone{}).Error; err != nil {
45
		msg := fmt.Sprintf("cannot delete all [%T] for user with ID [%s]", &entities.Phone{}, userID)
46
		return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
47
	}
48
49
	repository.cache.Clear()
50
	return nil
51
}
52
53
// LoadByID loads a phone by ID
54
func (repository *gormPhoneRepository) LoadByID(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) (*entities.Phone, error) {
55
	ctx, span := repository.tracer.Start(ctx)
56
	defer span.End()
57
58
	phone := new(entities.Phone)
59
	err := repository.db.WithContext(ctx).
60
		Where("user_id = ?", userID).
61
		Where("id = ?", phoneID).
62
		First(&phone).Error
63
	if errors.Is(err, gorm.ErrRecordNotFound) {
64
		msg := fmt.Sprintf("phone with ID [%s] does not exist", phoneID)
65
		return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))
66
	}
67
68
	if err != nil {
69
		msg := fmt.Sprintf("cannot load phone with ID [%s]", phoneID)
70
		return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
71
	}
72
73
	return phone, nil
74
}
75
76
// Delete an entities.Phone
77
func (repository *gormPhoneRepository) Delete(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) error {
78
	ctx, span := repository.tracer.Start(ctx)
79
	defer span.End()
80
81
	err := repository.db.WithContext(ctx).
82
		Where("user_id = ?", userID).
83
		Where("id = ?", phoneID).
84
		Delete(&entities.Phone{}).Error
85
	if err != nil {
86
		msg := fmt.Sprintf("cannot delete phone with ID [%s] and userID [%s]", phoneID, userID)
87
		return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
88
	}
89
90
	repository.cache.Clear()
91
	return nil
92
}
93
94
// Save a new entities.Phone
95
func (repository *gormPhoneRepository) Save(ctx context.Context, phone *entities.Phone) error {
96
	ctx, span, ctxLogger := repository.tracer.StartWithLogger(ctx, repository.logger)
97
	defer span.End()
98
99
	err := repository.db.WithContext(ctx).Save(phone).Error
100
	if errors.Is(err, gorm.ErrDuplicatedKey) {
101
		ctxLogger.Info(fmt.Sprintf("phone with user [%s] and number[%s] already exists", phone.UserID, phone.PhoneNumber))
102
		loadedPhone, err := repository.Load(ctx, phone.UserID, phone.PhoneNumber)
103
		if err != nil {
104
			msg := fmt.Sprintf("cannot load phone for user [%s] and number [%s]", phone.UserID, phone.PhoneNumber)
105
			return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
106
		}
107
		*phone = *loadedPhone
108
		return nil
109
	}
110
111
	if err != nil {
112
		msg := fmt.Sprintf("cannot save phone with ID [%s]", phone.ID)
113
		return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
114
	}
115
116
	repository.cache.Del(repository.getCacheKey(phone.UserID, phone.PhoneNumber))
117
	return nil
118
}
119
120
// Load a phone based on entities.UserID and phoneNumber
121
func (repository *gormPhoneRepository) Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.Phone, error) {
122
	ctx, span, ctxLogger := repository.tracer.StartWithLogger(ctx, repository.logger)
123
	defer span.End()
124
125
	if phone, found := repository.cache.Get(repository.getCacheKey(userID, phoneNumber)); found {
126
		ctxLogger.Info(fmt.Sprintf("cache hit for [%T] with ID [%s]", phone, userID))
127
		return phone, nil
128
	}
129
130
	phone := new(entities.Phone)
131
	err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("phone_number = ?", phoneNumber).First(phone).Error
132
	if errors.Is(err, gorm.ErrRecordNotFound) {
133
		msg := fmt.Sprintf("phone with userID [%s] and phoneNumber [%s] does not exist", userID, phoneNumber)
134
		return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))
135
	}
136
137
	if err != nil {
138
		msg := fmt.Sprintf("cannot load phone phone with userID [%s] and phoneNumber [%s]", userID, phoneNumber)
139
		return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
140
	}
141
142
	if result := repository.cache.SetWithTTL(repository.getCacheKey(userID, phoneNumber), phone, 1, 30*time.Minute); !result {
143
		msg := fmt.Sprintf("cannot cache [%T] with ID [%s] and result [%t]", phone, phone.ID, result)
144
		ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError(msg)))
145
	}
146
147
	return phone, nil
148
}
149
150
func (repository *gormPhoneRepository) Index(ctx context.Context, userID entities.UserID, params IndexParams) (*[]entities.Phone, error) {
151
	ctx, span := repository.tracer.Start(ctx)
152
	defer span.End()
153
154
	query := repository.db.WithContext(ctx).Where("user_id = ?", userID)
155
	if len(params.Query) > 0 {
156
		queryPattern := "%" + params.Query + "%"
157
		query.Where("phone_number ILIKE ?", queryPattern)
158
	}
159
160
	phones := new([]entities.Phone)
161
	if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&phones).Error; err != nil {
162
		msg := fmt.Sprintf("cannot fetch phones with userID [%s] and params [%+#v]", userID, params)
163
		return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
164
	}
165
166
	return phones, nil
167
}
168
169
func (repository *gormPhoneRepository) getCacheKey(userID entities.UserID, phoneNumber string) string {
170
	return fmt.Sprintf("user:%s:phone:%s", userID, phoneNumber)
171
}
172