all repos

onasty @ 757b945

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat: notes manipulations for the note authors (#117)..., 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
	// GetAllByAuthorID returns all notes with specified author.
25
	GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error)
26
27
	// GetBySlugAndPassword gets a note by slug and password.
28
	// the "password" should be hashed.
29
	//
30
	// Returns [models.ErrNoteNotFound] if note is not found.
31
	GetBySlugAndPassword(
32
		ctx context.Context,
33
		slug dtos.NoteSlug,
34
		password string,
35
	) (models.Note, error)
36
37
	// UpdateExpirationTimeSettingsBySlug patches note by updating expiresAt and burnBeforeExpiration if one is passwd
38
	// Returns [models.ErrNoteNotFound] if note is not found.
39
	UpdateExpirationTimeSettingsBySlug(
40
		ctx context.Context,
41
		slug dtos.NoteSlug,
42
		patch dtos.PatchNote,
43
		authorID uuid.UUID,
44
	) error
45
46
	// RemoveBySlug marks note as read, deletes it's content, and keeps meta data
47
	// Returns [models.ErrNoteNotFound] if note is not found.
48
	RemoveBySlug(ctx context.Context, slug dtos.NoteSlug, readAt time.Time) error
49
50
	// DeleteNoteBySlug deletes(unlike [RemoveBySlug]) note by slug.
51
	// Returns [models.ErrNoteNotFound] if note is not found.
52
	DeleteNoteBySlug(ctx context.Context, slug dtos.NoteSlug, authorID uuid.UUID) error
53
54
	// SetAuthorIDBySlug assigns author to note by slug.
55
	// Returns [models.ErrNoteNotFound] if note is not found.
56
	SetAuthorIDBySlug(ctx context.Context, slug dtos.NoteSlug, authorID uuid.UUID) error
57
58
	// UpdatePasswordBySlug updates or sets password on a note.
59
	UpdatePasswordBySlug(
60
		ctx context.Context,
61
		slug dtos.NoteSlug,
62
		authorID uuid.UUID,
63
		passwd string,
64
	) error
65
}
66
67
var _ NoteStorer = (*NoteRepo)(nil)
68
69
type NoteRepo struct {
70
	db *psqlutil.DB
71
}
72
73
func New(db *psqlutil.DB) *NoteRepo {
74
	return &NoteRepo{db}
75
}
76
77
func (s *NoteRepo) Create(ctx context.Context, inp models.Note) error {
78
	query, args, err := pgq.
79
		Insert("notes").
80
		Columns("content", "slug", "password", "burn_before_expiration", "created_at", "expires_at").
81
		Values(inp.Content, inp.Slug, inp.Password, inp.BurnBeforeExpiration, inp.CreatedAt, inp.ExpiresAt).
82
		SQL()
83
	if err != nil {
84
		return err
85
	}
86
87
	_, err = s.db.Exec(ctx, query, args...)
88
	if psqlutil.IsDuplicateErr(err) {
89
		return models.ErrNoteSlugIsAlreadyInUse
90
	}
91
92
	return err
93
}
94
95
func (s *NoteRepo) GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error) {
96
	query, args, err := pgq.
97
		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").
98
		From("notes").
99
		Where("(password is null or password = '')").
100
		Where(pgq.Eq{"slug": slug}).
101
		SQL()
102
	if err != nil {
103
		return models.Note{}, err
104
	}
105
106
	var note models.Note
107
	err = s.db.QueryRow(ctx, query, args...).
108
		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)
109
110
	if errors.Is(err, pgx.ErrNoRows) {
111
		return models.Note{}, models.ErrNoteNotFound
112
	}
113
114
	return note, err
115
}
116
117
func (s *NoteRepo) GetAllByAuthorID(
118
	ctx context.Context,
119
	authorID uuid.UUID,
120
) ([]models.Note, error) {
121
	query := `--sql
122
	select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at
123
	from notes n
124
	right join notes_authors na on n.id = na.note_id
125
	where na.user_id = $1`
126
127
	rows, err := s.db.Query(ctx, query, authorID.String())
128
	if err != nil {
129
		return nil, err
130
	}
131
132
	defer rows.Close()
133
134
	var notes []models.Note
135
	for rows.Next() {
136
		var note models.Note
137
		if err := rows.Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password,
138
			&note.ReadAt, &note.CreatedAt, &note.ExpiresAt); err != nil {
139
			return nil, err
140
		}
141
		notes = append(notes, note)
142
	}
143
144
	return notes, rows.Err()
145
}
146
147
func (s *NoteRepo) GetBySlugAndPassword(
148
	ctx context.Context,
149
	slug dtos.NoteSlug,
150
	passwd string,
151
) (models.Note, error) {
152
	query, args, err := pgq.
153
		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").
154
		From("notes").
155
		Where(pgq.Eq{
156
			"slug":     slug,
157
			"password": passwd,
158
		}).
159
		SQL()
160
	if err != nil {
161
		return models.Note{}, err
162
	}
163
164
	var note models.Note
165
	err = s.db.QueryRow(ctx, query, args...).
166
		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)
167
168
	if errors.Is(err, pgx.ErrNoRows) {
169
		return models.Note{}, models.ErrNoteNotFound
170
	}
171
172
	return note, err
173
}
174
175
func (s *NoteRepo) UpdateExpirationTimeSettingsBySlug(
176
	ctx context.Context,
177
	slug dtos.NoteSlug,
178
	patch dtos.PatchNote,
179
	authorID uuid.UUID,
180
) error {
181
	query := `--sql
182
update notes n
183
set burn_before_expiration = COALESCE($1, n.burn_before_expiration),
184
    expires_at = COALESCE($2, n.expires_at)
185
from notes_authors na
186
where n.slug = $3
187
  and na.user_id = $4
188
  and na.note_id = n.id`
189
190
	ct, err := s.db.Exec(ctx, query,
191
		patch.BurnBeforeExpiration, patch.ExpiresAt,
192
		slug, authorID.String())
193
	if err != nil {
194
		return err
195
	}
196
197
	if ct.RowsAffected() == 0 {
198
		return models.ErrNoteNotFound
199
	}
200
201
	return nil
202
}
203
204
func (s *NoteRepo) RemoveBySlug(
205
	ctx context.Context,
206
	slug dtos.NoteSlug,
207
	readAt time.Time,
208
) error {
209
	query, args, err := pgq.
210
		Update("notes").
211
		Set("content", "").
212
		Set("read_at", readAt).
213
		Where(pgq.Eq{
214
			"slug":    slug,
215
			"read_at": time.Time{}, // check if time is null
216
		}).
217
		SQL()
218
	if err != nil {
219
		return err
220
	}
221
222
	_, err = s.db.Exec(ctx, query, args...)
223
	if errors.Is(err, pgx.ErrNoRows) {
224
		return models.ErrNoteNotFound
225
	}
226
227
	return err
228
}
229
230
func (s *NoteRepo) DeleteNoteBySlug(
231
	ctx context.Context,
232
	slug dtos.NoteSlug,
233
	authorID uuid.UUID,
234
) error {
235
	query := `--sql
236
delete from notes n
237
using notes_authors na
238
where n.slug = $1
239
  and na.user_id = $2`
240
241
	ct, err := s.db.Exec(ctx, query, slug, authorID.String())
242
	if err != nil {
243
		return err
244
	}
245
246
	if ct.RowsAffected() == 0 {
247
		return models.ErrNoteNotFound
248
	}
249
250
	return nil
251
}
252
253
func (s *NoteRepo) SetAuthorIDBySlug(
254
	ctx context.Context,
255
	slug dtos.NoteSlug,
256
	authorID uuid.UUID,
257
) error {
258
	tx, err := s.db.Begin(ctx)
259
	if err != nil {
260
		return err
261
	}
262
	defer tx.Rollback(ctx) //nolint:errcheck
263
264
	var noteID uuid.UUID
265
	err = tx.QueryRow(ctx, "select id from notes where slug = $1", slug).Scan(&noteID)
266
	if err != nil {
267
		if errors.Is(err, pgx.ErrNoRows) {
268
			return models.ErrNoteNotFound
269
		}
270
		return err
271
	}
272
273
	_, err = tx.Exec(
274
		ctx,
275
		"insert into notes_authors (note_id, user_id) values ($1, $2)",
276
		noteID, authorID,
277
	)
278
	if err != nil {
279
		return err
280
	}
281
282
	return tx.Commit(ctx)
283
}
284
285
func (s *NoteRepo) UpdatePasswordBySlug(
286
	ctx context.Context,
287
	slug dtos.NoteSlug,
288
	authorID uuid.UUID,
289
	passwd string,
290
) error {
291
	query := `--sql
292
update notes n
293
set password = $1
294
from notes_authors na
295
where n.slug = $2
296
  and na.user_id = $3
297
  and na.note_id = n.id`
298
299
	ct, err := s.db.Exec(ctx, query, passwd, slug, authorID.String())
300
	if err != nil {
301
		return err
302
	}
303
304
	if ct.RowsAffected() == 0 {
305
		return models.ErrNoteNotFound
306
	}
307
308
	return nil
309
}