Files
Snip/backend/internal/adapter/memory/link_repo.go
T
2026-06-15 21:25:57 +07:00

172 lines
3.6 KiB
Go

// Package memory provides in-memory adapters used by tests and for running the
// stack without Postgres/Redis (STORE=memory).
package memory
import (
"context"
"sort"
"strings"
"sync"
"time"
"github.com/snip/backend/internal/domain"
)
type LinkRepo struct {
mu sync.RWMutex
byID map[string]*domain.Link
seq int64
clock func() time.Time
}
func NewLinkRepo() *LinkRepo {
return &LinkRepo{byID: make(map[string]*domain.Link), clock: time.Now}
}
func clone(l *domain.Link) *domain.Link {
cp := *l
cp.Last7Days = append([]domain.DayCount(nil), l.Last7Days...)
return &cp
}
func (r *LinkRepo) Create(_ context.Context, l *domain.Link) error {
r.mu.Lock()
defer r.mu.Unlock()
for _, e := range r.byID {
if e.Code == l.Code {
return domain.ErrCodeTaken
}
}
r.byID[l.ID] = clone(l)
return nil
}
func (r *LinkRepo) GetByID(_ context.Context, id string) (*domain.Link, error) {
r.mu.RLock()
defer r.mu.RUnlock()
l, ok := r.byID[id]
if !ok {
return nil, domain.ErrNotFound
}
return clone(l), nil
}
func (r *LinkRepo) GetByCode(_ context.Context, code string) (*domain.Link, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, l := range r.byID {
if l.Code == code {
return clone(l), nil
}
}
return nil, domain.ErrNotFound
}
func (r *LinkRepo) ListByOwner(_ context.Context, ownerID, query string, limit, offset int) ([]domain.Link, int, error) {
r.mu.RLock()
defer r.mu.RUnlock()
q := strings.ToLower(query)
var matched []domain.Link
for _, l := range r.byID {
if l.OwnerID != ownerID {
continue
}
if q != "" && !strings.Contains(strings.ToLower(l.Code), q) && !strings.Contains(strings.ToLower(l.LongURL), q) {
continue
}
matched = append(matched, *clone(l))
}
sort.Slice(matched, func(i, j int) bool { return matched[i].CreatedAt.After(matched[j].CreatedAt) })
total := len(matched)
if offset > total {
offset = total
}
end := offset + limit
if limit <= 0 || end > total {
end = total
}
return matched[offset:end], total, nil
}
func (r *LinkRepo) StatsByOwner(_ context.Context, ownerID string) (domain.OwnerStats, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var s domain.OwnerStats
for _, l := range r.byID {
if l.OwnerID != ownerID {
continue
}
s.TotalLinks++
s.TotalClicks += l.TotalClicks
for _, d := range l.Last7Days {
s.WeekClicks += d.Count
}
}
return s, nil
}
func (r *LinkRepo) Update(_ context.Context, l *domain.Link) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.byID[l.ID]; !ok {
return domain.ErrNotFound
}
r.byID[l.ID] = clone(l)
return nil
}
func (r *LinkRepo) Delete(_ context.Context, id, ownerID string) error {
r.mu.Lock()
defer r.mu.Unlock()
l, ok := r.byID[id]
if !ok || l.OwnerID != ownerID {
return domain.ErrNotFound
}
delete(r.byID, id)
return nil
}
func (r *LinkRepo) ExistsCode(_ context.Context, code string) (bool, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, l := range r.byID {
if l.Code == code {
return true, nil
}
}
return false, nil
}
func (r *LinkRepo) NextSequence(_ context.Context) (int64, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.seq++
return r.seq, nil
}
func (r *LinkRepo) RecordClicks(_ context.Context, code string, day time.Time, n int64) error {
r.mu.Lock()
defer r.mu.Unlock()
key := day.Format("2006-01-02")
for _, l := range r.byID {
if l.Code != code {
continue
}
l.TotalClicks += n
found := false
for i := range l.Last7Days {
if l.Last7Days[i].Date == key {
l.Last7Days[i].Count += n
found = true
break
}
}
if !found {
l.Last7Days = append(l.Last7Days, domain.DayCount{Date: key, Count: n})
}
return nil
}
return domain.ErrNotFound
}