all repos

onasty @ c5b3657

a one-time notes service

onasty/internal/store/psql/noterepo/noterepo.go (view raw)

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
refactor: fix annoyances (#97)..., 1 year ago
1
package noterepo
2
3
import (
4
	"context"
5
	"errors"
6
	"time"
7
8
	"github.com/gofrs/uuid/v5"
9
	"github.com/henvic/pgq"
10
	"github.com/jackc/pgx/v5"
11
	"github.com/olexsmir/onasty/internal/dtos"
12
	"github.com/olexsmir/onasty/internal/models"
13
	"github.com/olexsmir/onasty/internal/store/psqlutil"
14
)
15
16
type NoteStorer interface {
17
	// Create creates a note.
18
	Create(ctx context.Context, note models.Note) error
19
20
	// GetBySlug gets a note by slug.
21
	// Returns [models.ErrNoteNotFound] if note is not found.
22
	GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error)
23
24
	// GetBySlugAndPassword gets a note by slug and password.
25
	// the "password" should be hashed.
26
	//
27
	// Returns [models.ErrNoteNotFound] if note is not found.
28
	GetBySlugAndPassword(
29
		ctx context.Context,
30
		slug dtos.NoteSlug,
31
		password string,
32
	) (models.Note, error)
33
34
	// RemoveBySlug marks note as read, deletes it's content, and keeps meta data
35
	// Returns [models.ErrNoteNotFound] if note is not found.
36
	RemoveBySlug(ctx context.Context, slug dtos.NoteSlug, readAt time.Time) error
37
38
	// SetAuthorIDBySlug assigns author to note by slug.
39
	// Returns [models.ErrNoteNotFound] if note is not found.
40
	SetAuthorIDBySlug(ctx context.Context, slug dtos.NoteSlug, authorID uuid.UUID) error
41
}
42
43
var _ NoteStorer = (*NoteRepo)(nil)
44
45
type NoteRepo struct {
46
	db *psqlutil.DB
47
}
48
49
func New(db *psqlutil.DB) *NoteRepo {
50
	return &NoteRepo{db}
51
}
52
53
func (s *NoteRepo) Create(ctx context.Context, inp models.Note) error {
54
	query, args, err := pgq.
55
		Insert("notes").
56
		Columns("content", "slug", "password", "burn_before_expiration", "created_at", "expires_at").
57
		Values(inp.Content, inp.Slug, inp.Password, inp.BurnBeforeExpiration, inp.CreatedAt, inp.ExpiresAt).
58
		SQL()
59
	if err != nil {
60
		return err
61
	}
62
63
	_, err = s.db.Exec(ctx, query, args...)
64
	if psqlutil.IsDuplicateErr(err) {
65
		return models.ErrNoteSlugIsAlreadyInUse
66
	}
67
68
	return err
69
}
70
71
func (s *NoteRepo) GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error) {
72
	query, args, err := pgq.
73
		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").
74
		From("notes").
75
		Where("(password is null or password = '')").
76
		Where(pgq.Eq{"slug": slug}).
77
		SQL()
78
	if err != nil {
79
		return models.Note{}, err
80
	}
81
82
	var note models.Note
83
	err = s.db.QueryRow(ctx, query, args...).
84
		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)
85
86
	if errors.Is(err, pgx.ErrNoRows) {
87
		return models.Note{}, models.ErrNoteNotFound
88
	}
89
90
	return note, err
91
}
92
93
func (s *NoteRepo) GetBySlugAndPassword(
94
	ctx context.Context,
95
	slug dtos.NoteSlug,
96
	passwd string,
97
) (models.Note, error) {
98
	query, args, err := pgq.
99
		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").
100
		From("notes").
101
		Where(pgq.Eq{
102
			"slug":     slug,
103
			"password": passwd,
104
		}).
105
		SQL()
106
	if err != nil {
107
		return models.Note{}, err
108
	}
109
110
	var note models.Note
111
	err = s.db.QueryRow(ctx, query, args...).
112
		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)
113
114
	if errors.Is(err, pgx.ErrNoRows) {
115
		return models.Note{}, models.ErrNoteNotFound
116
	}
117
118
	return note, err
119
}
120
121
func (s *NoteRepo) RemoveBySlug(
122
	ctx context.Context,
123
	slug dtos.NoteSlug,
124
	readAt time.Time,
125
) error {
126
	query, args, err := pgq.
127
		Update("notes").
128
		Set("content", "").
129
		Set("read_at", readAt).
130
		Where(pgq.Eq{
131
			"slug":    slug,
132
			"read_at": time.Time{}, // check if time is null
133
		}).
134
		SQL()
135
	if err != nil {
136
		return err
137
	}
138
139
	_, err = s.db.Exec(ctx, query, args...)
140
	if errors.Is(err, pgx.ErrNoRows) {
141
		return models.ErrNoteNotFound
142
	}
143
144
	return err
145
}
146
147
func (s *NoteRepo) SetAuthorIDBySlug(
148
	ctx context.Context,
149
	slug dtos.NoteSlug,
150
	authorID uuid.UUID,
151
) error {
152
	tx, err := s.db.Begin(ctx)
153
	if err != nil {
154
		return err
155
	}
156
	defer tx.Rollback(ctx) //nolint:errcheck
157
158
	var noteID uuid.UUID
159
	err = tx.QueryRow(ctx, "select id from notes where slug = $1", slug).Scan(&noteID)
160
	if err != nil {
161
		if errors.Is(err, pgx.ErrNoRows) {
162
			return models.ErrNoteNotFound
163
		}
164
		return err
165
	}
166
167
	_, err = tx.Exec(
168
		ctx,
169
		"insert into notes_authors (note_id, user_id) values ($1, $2)",
170
		noteID, authorID,
171
	)
172
	if err != nil {
173
		return err
174
	}
175
176
	return tx.Commit(ctx)
177
}