all repos

onasty @ 423cbf0

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
refactor: fix annoyances (#97)..., 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.CreateNote, userID uuid.UUID) (dtos.NoteSlug, error)
21
22
	// GetBySlugAndRemoveIfNeeded returns note by slug, and removes if if needed
23
	GetBySlugAndRemoveIfNeeded(
24
		ctx context.Context,
25
		input GetNoteBySlugInput,
26
	) (dtos.GetNote, error)
27
}
28
29
var _ NoteServicer = (*NoteSrv)(nil)
30
31
type NoteSrv struct {
32
	noterepo noterepo.NoteStorer
33
	hasher   hasher.Hasher
34
	cache    notecache.NoteCacher
35
}
36
37
func New(noterepo noterepo.NoteStorer, hasher hasher.Hasher, cache notecache.NoteCacher) *NoteSrv {
38
	return &NoteSrv{
39
		noterepo: noterepo,
40
		hasher:   hasher,
41
		cache:    cache,
42
	}
43
}
44
45
func (n *NoteSrv) Create(
46
	ctx context.Context,
47
	inp dtos.CreateNote,
48
	userID uuid.UUID,
49
) (dtos.NoteSlug, error) {
50
	slog.DebugContext(ctx, "creating", "inp", inp)
51
52
	if inp.Slug == "" {
53
		inp.Slug = uuid.Must(uuid.NewV4()).String()
54
	}
55
56
	if inp.Password != "" {
57
		hashedPassword, err := n.hasher.Hash(inp.Password)
58
		if err != nil {
59
			return "", err
60
		}
61
		inp.Password = hashedPassword
62
	}
63
64
	//nolint:exhaustruct // ID - cannot be predicted, and ReadAt will be set on read
65
	note := models.Note{
66
		Content:              inp.Content,
67
		Slug:                 inp.Slug,
68
		Password:             inp.Password,
69
		BurnBeforeExpiration: inp.BurnBeforeExpiration,
70
		CreatedAt:            inp.CreatedAt,
71
		ExpiresAt:            inp.ExpiresAt,
72
	}
73
	if err := note.Validate(); err != nil {
74
		return "", err
75
	}
76
77
	if err := n.noterepo.Create(ctx, note); err != nil {
78
		return "", err
79
	}
80
81
	if !userID.IsNil() {
82
		if err := n.noterepo.SetAuthorIDBySlug(ctx, inp.Slug, userID); err != nil {
83
			return "", err
84
		}
85
	}
86
87
	return inp.Slug, nil
88
}
89
90
func (n *NoteSrv) GetBySlugAndRemoveIfNeeded(
91
	ctx context.Context,
92
	inp GetNoteBySlugInput,
93
) (dtos.GetNote, error) {
94
	note, err := n.getNote(ctx, inp)
95
	if err != nil {
96
		return dtos.GetNote{}, err
97
	}
98
99
	if note.IsExpired() {
100
		return dtos.GetNote{}, models.ErrNoteExpired
101
	}
102
103
	respNote := dtos.GetNote{
104
		Content:   note.Content,
105
		ReadAt:    note.ReadAt,
106
		CreatedAt: note.CreatedAt,
107
		ExpiresAt: note.ExpiresAt,
108
	}
109
110
	// since not every note should be burn before expiration
111
	// we return early if it's not
112
	if note.ShouldBeBurnt() {
113
		return respNote, nil
114
	}
115
116
	return respNote, n.noterepo.RemoveBySlug(ctx, inp.Slug, time.Now())
117
}
118
119
func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (models.Note, error) {
120
	if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil {
121
		return r, nil
122
	}
123
124
	note, err := n.getNoteFromDBasedOnInput(ctx, inp)
125
	if err != nil {
126
		return models.Note{}, err
127
	}
128
129
	if !note.IsRead() {
130
		if err = n.cache.SetNote(ctx, inp.Slug, note); err != nil {
131
			slog.ErrorContext(ctx, "notecache", "err", err)
132
		}
133
	}
134
135
	return note, err
136
}
137
138
func (n *NoteSrv) getNoteFromDBasedOnInput(
139
	ctx context.Context,
140
	inp GetNoteBySlugInput,
141
) (models.Note, error) {
142
	if inp.HasPassword() {
143
		hashedPassword, err := n.hasher.Hash(inp.Password)
144
		if err != nil {
145
			return models.Note{}, err
146
		}
147
148
		return n.noterepo.GetBySlugAndPassword(ctx, inp.Slug, hashedPassword)
149
	}
150
	return n.noterepo.GetBySlug(ctx, inp.Slug)
151
}