// 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 }