all repos

onasty @ 09a22a8

a one-time notes service

onasty/internal/service/notesrv/notesrv.go (view raw)

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat: keep metadate on note removal (#96)..., 1 year ago
1
package notesrv
2
3
import (
4
	"context"
5
	"log/slog"
6
	"time"
7
8
	"github.com/gofrs/uuid/v5"
9
	"github.com/olexsmir/onasty/internal/dtos"
10
	"github.com/olexsmir/onasty/internal/hasher"
11
	"github.com/olexsmir/onasty/internal/models"
12
	"github.com/olexsmir/onasty/internal/store/psql/noterepo"
13
	"github.com/olexsmir/onasty/internal/store/rdb/notecache"
14
)
15
16
type NoteServicer interface {
17
	// Create creates note
18
	// if slug is empty it will be generated, otherwise used as is
19
	// if userID is empty it means user isn't authorized so it will be used
20
	Create(ctx context.Context, note dtos.CreateNoteDTO, userID uuid.UUID) (dtos.NoteSlugDTO, error)
21
22
	// GetBySlugAndRemoveIfNeeded returns note by slug, and removes if if needed
23
	GetBySlugAndRemoveIfNeeded(ctx context.Context, input GetNoteBySlugInput) (dtos.NoteDTO, error)
24
}
25
26
var _ NoteServicer = (*NoteSrv)(nil)
27
28
type NoteSrv struct {
29
	noterepo noterepo.NoteStorer
30
	hasher   hasher.Hasher
31
	cache    notecache.NoteCacher
32
}
33
34
func New(noterepo noterepo.NoteStorer, hasher hasher.Hasher, cache notecache.NoteCacher) *NoteSrv {
35
	return &NoteSrv{
36
		noterepo: noterepo,
37
		hasher:   hasher,
38
		cache:    cache,
39
	}
40
}
41
42
func (n *NoteSrv) Create(
43
	ctx context.Context,
44
	inp dtos.CreateNoteDTO,
45
	userID uuid.UUID,
46
) (dtos.NoteSlugDTO, error) {
47
	slog.DebugContext(ctx, "creating", "inp", inp)
48
49
	if inp.Slug == "" {
50
		inp.Slug = uuid.Must(uuid.NewV4()).String()
51
	}
52
53
	if inp.Password != "" {
54
		hashedPassword, err := n.hasher.Hash(inp.Password)
55
		if err != nil {
56
			return "", err
57
		}
58
		inp.Password = hashedPassword
59
	}
60
61
	if err := n.noterepo.Create(ctx, inp); err != nil {
62
		return "", err
63
	}
64
65
	if !userID.IsNil() {
66
		if err := n.noterepo.SetAuthorIDBySlug(ctx, inp.Slug, userID); err != nil {
67
			return "", err
68
		}
69
	}
70
71
	return inp.Slug, nil
72
}
73
74
func (n *NoteSrv) GetBySlugAndRemoveIfNeeded(
75
	ctx context.Context,
76
	inp GetNoteBySlugInput,
77
) (dtos.NoteDTO, error) {
78
	note, err := n.getNote(ctx, inp)
79
	if err != nil {
80
		return dtos.NoteDTO{}, err
81
	}
82
83
	m := models.Note{ //nolint:exhaustruct
84
		ExpiresAt:            note.ExpiresAt,
85
		BurnBeforeExpiration: note.BurnBeforeExpiration,
86
	}
87
88
	if m.IsExpired() {
89
		return dtos.NoteDTO{}, models.ErrNoteExpired
90
	}
91
92
	// since not every note should be burn before expiration
93
	// we return early if it's not
94
	if m.ShouldBeBurnt() {
95
		return note, nil
96
	}
97
98
	return note, n.noterepo.RemoveBySlug(ctx, inp.Slug, time.Now())
99
}
100
101
func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (dtos.NoteDTO, error) {
102
	if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil {
103
		return r, nil
104
	}
105
106
	note, err := n.getNoteFromDBasedOnInput(ctx, inp)
107
	if err != nil {
108
		return dtos.NoteDTO{}, err
109
	}
110
111
	if note.ReadAt != nil && !note.ReadAt.IsZero() {
112
		if err = n.cache.SetNote(ctx, inp.Slug, note); err != nil {
113
			slog.ErrorContext(ctx, "notecache", "err", err)
114
		}
115
	}
116
117
	return note, err
118
}
119
120
func (n *NoteSrv) getNoteFromDBasedOnInput(
121
	ctx context.Context,
122
	inp GetNoteBySlugInput,
123
) (dtos.NoteDTO, error) {
124
	if inp.HasPassword() {
125
		hashedPassword, err := n.hasher.Hash(inp.Password)
126
		if err != nil {
127
			return dtos.NoteDTO{}, err
128
		}
129
130
		return n.noterepo.GetBySlugAndPassword(ctx, inp.Slug, hashedPassword)
131
	}
132
	return n.noterepo.GetBySlug(ctx, inp.Slug)
133
}