all repos

onasty @ 86dba8ea0dc14c9a08dbb0baa9b0441168638328

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat(api): get note metadata (#148)..., 11 months ago
1
package notesrv
2
3
import (
4
	"context"
5
	"errors"
6
	"log/slog"
7
	"time"
8
9
	"github.com/gofrs/uuid/v5"
10
	"github.com/olexsmir/onasty/internal/dtos"
11
	"github.com/olexsmir/onasty/internal/hasher"
12
	"github.com/olexsmir/onasty/internal/models"
13
	"github.com/olexsmir/onasty/internal/store/psql/noterepo"
14
	"github.com/olexsmir/onasty/internal/store/rdb/notecache"
15
)
16
17
var ErrNotePasswordNotProvided = errors.New("note: password was not provided")
18
19
type NoteServicer interface {
20
	// Create creates note
21
	// if slug is empty it will be generated, otherwise used as is
22
	// if userID is empty it means user isn't authorized so it will be used
23
	Create(ctx context.Context, note dtos.CreateNote, userID uuid.UUID) (dtos.NoteSlug, error)
24
25
	// GetBySlugAndRemoveIfNeeded returns note by slug, and removes if if needed.
26
	// If note is not found returns [models.ErrNoteNotFound].
27
	GetBySlugAndRemoveIfNeeded(
28
		ctx context.Context,
29
		input GetNoteBySlugInput,
30
	) (dtos.GetNote, error)
31
32
	// GetNoteMetadataBySlug returns note metadata by slug.
33
	// If note is not found returns [models.ErrNoteNotFound].
34
	GetNoteMetadataBySlug(ctx context.Context, slug dtos.NoteSlug) (dtos.NoteMetadata, error)
35
36
	// GetAllByAuthorID returns all notes by author id.
37
	GetAllByAuthorID(
38
		ctx context.Context,
39
		authorID uuid.UUID,
40
	) ([]dtos.NoteDetailed, error)
41
42
	// UpdateExpirationTimeSettings updates expiresAt and burnBeforeExpiration.
43
	// If notes is not found returns [models.ErrNoteNotFound].
44
	UpdateExpirationTimeSettings(
45
		ctx context.Context,
46
		patchData dtos.PatchNote,
47
		slug dtos.NoteSlug,
48
		userID uuid.UUID,
49
	) error
50
51
	// UpdatePassword sets or updates notes password.
52
	// If notes is not found returns [models.ErrNoteNotFound].
53
	UpdatePassword(ctx context.Context, slug dtos.NoteSlug, passwd string, userID uuid.UUID) error
54
55
	// DeleteBySlug deletes note by slug
56
	DeleteBySlug(ctx context.Context, slug dtos.NoteSlug, userID uuid.UUID) error
57
}
58
59
var _ NoteServicer = (*NoteSrv)(nil)
60
61
type NoteSrv struct {
62
	noterepo noterepo.NoteStorer
63
	hasher   hasher.Hasher
64
	cache    notecache.NoteCacher
65
}
66
67
func New(noterepo noterepo.NoteStorer, hasher hasher.Hasher, cache notecache.NoteCacher) *NoteSrv {
68
	return &NoteSrv{
69
		noterepo: noterepo,
70
		hasher:   hasher,
71
		cache:    cache,
72
	}
73
}
74
75
func (n *NoteSrv) Create(
76
	ctx context.Context,
77
	inp dtos.CreateNote,
78
	userID uuid.UUID,
79
) (dtos.NoteSlug, error) {
80
	slog.DebugContext(ctx, "creating", "inp", inp)
81
82
	if inp.Slug == "" {
83
		inp.Slug = uuid.Must(uuid.NewV4()).String()
84
	}
85
86
	if inp.Password != "" {
87
		hashedPassword, err := n.hasher.Hash(inp.Password)
88
		if err != nil {
89
			return "", err
90
		}
91
		inp.Password = hashedPassword
92
	}
93
94
	//nolint:exhaustruct // ID - cannot be predicted, and ReadAt will be set on read
95
	note := models.Note{
96
		Content:              inp.Content,
97
		Slug:                 inp.Slug,
98
		Password:             inp.Password,
99
		BurnBeforeExpiration: inp.BurnBeforeExpiration,
100
		CreatedAt:            inp.CreatedAt,
101
		ExpiresAt:            inp.ExpiresAt,
102
	}
103
	if err := note.Validate(); err != nil {
104
		return "", err
105
	}
106
107
	if err := n.noterepo.Create(ctx, note); err != nil {
108
		return "", err
109
	}
110
111
	if !userID.IsNil() {
112
		if err := n.noterepo.SetAuthorIDBySlug(ctx, inp.Slug, userID); err != nil {
113
			return "", err
114
		}
115
	}
116
117
	return inp.Slug, nil
118
}
119
120
func (n *NoteSrv) GetBySlugAndRemoveIfNeeded(
121
	ctx context.Context,
122
	inp GetNoteBySlugInput,
123
) (dtos.GetNote, error) {
124
	note, err := n.getNote(ctx, inp)
125
	if err != nil {
126
		return dtos.GetNote{}, err
127
	}
128
129
	if note.IsExpired() {
130
		return dtos.GetNote{}, models.ErrNoteExpired
131
	}
132
133
	respNote := dtos.GetNote{
134
		Content:   note.Content,
135
		ReadAt:    note.ReadAt,
136
		CreatedAt: note.CreatedAt,
137
		ExpiresAt: note.ExpiresAt,
138
	}
139
140
	// since not every note should be burn before expiration
141
	// we return early if it's not
142
	if note.ShouldBeBurnt() {
143
		return respNote, nil
144
	}
145
146
	return respNote, n.noterepo.RemoveBySlug(ctx, inp.Slug, time.Now())
147
}
148
149
func (n *NoteSrv) GetNoteMetadataBySlug(
150
	ctx context.Context,
151
	slug dtos.NoteSlug,
152
) (dtos.NoteMetadata, error) {
153
	note, err := n.noterepo.GetMetadataBySlug(ctx, slug)
154
	return note, err
155
}
156
157
func (n *NoteSrv) GetAllByAuthorID(
158
	ctx context.Context,
159
	authorID uuid.UUID,
160
) ([]dtos.NoteDetailed, error) {
161
	notes, err := n.noterepo.GetAllByAuthorID(ctx, authorID)
162
	if err != nil {
163
		return nil, err
164
	}
165
166
	var resNotes []dtos.NoteDetailed
167
	for _, note := range notes {
168
		resNotes = append(resNotes, dtos.NoteDetailed{
169
			Content:              note.Content,
170
			Slug:                 note.Slug,
171
			BurnBeforeExpiration: note.BurnBeforeExpiration,
172
			HasPassword:          note.Password != "",
173
			CreatedAt:            note.CreatedAt,
174
			ExpiresAt:            note.ExpiresAt,
175
			ReadAt:               note.ReadAt,
176
		})
177
	}
178
179
	return resNotes, nil
180
}
181
182
func (n *NoteSrv) UpdateExpirationTimeSettings(
183
	ctx context.Context,
184
	patchData dtos.PatchNote,
185
	slug dtos.NoteSlug,
186
	userID uuid.UUID,
187
) error {
188
	return n.noterepo.UpdateExpirationTimeSettingsBySlug(ctx, slug, patchData, userID)
189
}
190
191
func (n *NoteSrv) UpdatePassword(
192
	ctx context.Context,
193
	slug dtos.NoteSlug,
194
	passwd string,
195
	userID uuid.UUID,
196
) error {
197
	if len(passwd) == 0 {
198
		return ErrNotePasswordNotProvided
199
	}
200
201
	hashedPassword, err := n.hasher.Hash(passwd)
202
	if err != nil {
203
		return err
204
	}
205
206
	return n.noterepo.UpdatePasswordBySlug(ctx, slug, userID, hashedPassword)
207
}
208
209
func (n *NoteSrv) DeleteBySlug(
210
	ctx context.Context,
211
	slug dtos.NoteSlug,
212
	authorID uuid.UUID,
213
) error {
214
	return n.noterepo.DeleteNoteBySlug(ctx, slug, authorID)
215
}
216
217
func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (models.Note, error) {
218
	if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil {
219
		return r, nil
220
	}
221
222
	note, err := n.getNoteFromDBasedOnInput(ctx, inp)
223
	if err != nil {
224
		return models.Note{}, err
225
	}
226
227
	if !note.IsRead() {
228
		if err = n.cache.SetNote(ctx, inp.Slug, note); err != nil {
229
			slog.ErrorContext(ctx, "notecache", "err", err)
230
		}
231
	}
232
233
	return note, err
234
}
235
236
func (n *NoteSrv) getNoteFromDBasedOnInput(
237
	ctx context.Context,
238
	inp GetNoteBySlugInput,
239
) (models.Note, error) {
240
	if inp.HasPassword() {
241
		hashedPassword, err := n.hasher.Hash(inp.Password)
242
		if err != nil {
243
			return models.Note{}, err
244
		}
245
246
		return n.noterepo.GetBySlugAndPassword(ctx, inp.Slug, hashedPassword)
247
	}
248
	return n.noterepo.GetBySlug(ctx, inp.Slug)
249
}