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