5 files changed,
326 insertions(+),
55 deletions(-)
Author:
Olexandr Smirnov
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-08-13 15:12:37 +0300
Parent:
efd9704
M
internal/service/notesrv/notesrv.go
··· 34 34 GetNoteMetadataBySlug(ctx context.Context, slug dtos.NoteSlug) (dtos.NoteMetadata, error) 35 35 36 36 // GetAllByAuthorID returns all notes by author id. 37 - GetAllByAuthorID( 38 - ctx context.Context, 39 - authorID uuid.UUID, 40 - ) ([]dtos.NoteDetailed, error) 37 + GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) 38 + 39 + // GetAllReadByAuthorID returns all notes that ARE READ and authored by author id. 40 + GetAllReadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) 41 + 42 + // GetAllUnreadByAuthorID returns all notes that ARE UNREAD and authored by author id. 43 + GetAllUnreadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) 41 44 42 45 // UpdateExpirationTimeSettings updates expiresAt and burnBeforeExpiration. 43 46 // If notes is not found returns [models.ErrNoteNotFound]. ··· 164 167 return nil, err 165 168 } 166 169 167 - var resNotes []dtos.NoteDetailed 168 - for _, note := range notes { 169 - resNotes = append(resNotes, dtos.NoteDetailed{ 170 - Content: note.Content, 171 - Slug: note.Slug, 172 - BurnBeforeExpiration: note.BurnBeforeExpiration, 173 - HasPassword: note.Password != "", 174 - CreatedAt: note.CreatedAt, 175 - ExpiresAt: note.ExpiresAt, 176 - ReadAt: note.ReadAt, 177 - }) 170 + return n.mapNoteModelToDto(notes), nil 171 +} 172 + 173 +func (n *NoteSrv) GetAllReadByAuthorID( 174 + ctx context.Context, 175 + authorID uuid.UUID, 176 +) ([]dtos.NoteDetailed, error) { 177 + notes, err := n.noterepo.GetAllReadByAuthorID(ctx, authorID) 178 + if err != nil { 179 + return nil, err 180 + } 181 + 182 + return n.mapNoteModelToDto(notes), nil 183 +} 184 + 185 +func (n *NoteSrv) GetAllUnreadByAuthorID( 186 + ctx context.Context, 187 + authorID uuid.UUID, 188 +) ([]dtos.NoteDetailed, error) { 189 + notes, err := n.noterepo.GetAllUnreadByAuthorID(ctx, authorID) 190 + if err != nil { 191 + return nil, err 178 192 } 179 193 180 - return resNotes, nil 194 + return n.mapNoteModelToDto(notes), nil 181 195 } 182 196 183 197 func (n *NoteSrv) UpdateExpirationTimeSettings( ··· 248 262 } 249 263 return n.noterepo.GetBySlug(ctx, inp.Slug) 250 264 } 265 + 266 +func (n *NoteSrv) mapNoteModelToDto(notes []models.Note) []dtos.NoteDetailed { 267 + var resNotes []dtos.NoteDetailed 268 + for _, note := range notes { 269 + resNotes = append(resNotes, dtos.NoteDetailed{ 270 + Content: note.Content, 271 + Slug: note.Slug, 272 + BurnBeforeExpiration: note.BurnBeforeExpiration, 273 + HasPassword: note.Password != "", 274 + CreatedAt: note.CreatedAt, 275 + ExpiresAt: note.ExpiresAt, 276 + ReadAt: note.ReadAt, 277 + }) 278 + } 279 + 280 + return resNotes 281 +}
M
internal/store/psql/noterepo/noterepo.go
··· 29 29 // GetAllByAuthorID returns all notes with specified author. 30 30 GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) 31 31 32 + // GetAllReadByAuthorID returns all notes that are read and authored by specified author. 33 + GetAllReadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) 34 + 35 + // GetAllUnreadByAuthorID returns all notes that are unread and authored by specified author. 36 + GetAllUnreadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) 37 + 32 38 // GetCountOfNotesByAuthorID returns count of notes created by specified author. 33 39 GetCountOfNotesByAuthorID(ctx context.Context, authorID uuid.UUID) (int64, error) 34 40 ··· 129 135 slug dtos.NoteSlug, 130 136 ) (dtos.NoteMetadata, error) { 131 137 query := `--sql 132 - select n.created_at, (n.password is not null and n.password <> '') has_password, n.read_at 133 - from notes n 134 - where slug = $1 135 - ` 138 +select n.created_at, (n.password is not null and n.password <> '') has_password, n.read_at 139 +from notes n 140 +where slug = $1` 136 141 137 142 var readAt sql.NullTime 138 143 var metadata dtos.NoteMetadata ··· 153 158 authorID uuid.UUID, 154 159 ) ([]models.Note, error) { 155 160 query := `--sql 156 - select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at 157 - from notes n 158 - right join notes_authors na on n.id = na.note_id 159 - where na.user_id = $1` 161 +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at 162 +from notes n 163 +inner join notes_authors na on n.id = na.note_id 164 +where na.user_id = $1` 160 165 161 - rows, err := s.db.Query(ctx, query, authorID.String()) 162 - if err != nil { 163 - return nil, err 164 - } 166 + return s.getAllNotes(ctx, query, authorID) 167 +} 165 168 166 - defer rows.Close() 169 +func (s *NoteRepo) GetAllReadByAuthorID( 170 + ctx context.Context, 171 + authorID uuid.UUID, 172 +) ([]models.Note, error) { 173 + query := `--sql 174 +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at 175 +from notes n 176 +inner join notes_authors na on n.id = na.note_id 177 +where na.user_id = $1 178 + and n.read_at is not null` 167 179 168 - var notes []models.Note 169 - for rows.Next() { 170 - var note models.Note 171 - var readAt sql.NullTime 172 - if err := rows.Scan(¬e.Content, ¬e.Slug, ¬e.BurnBeforeExpiration, ¬e.Password, 173 - &readAt, ¬e.CreatedAt, ¬e.ExpiresAt); err != nil { 174 - return nil, err 175 - } 180 + return s.getAllNotes(ctx, query, authorID) 181 +} 176 182 177 - note.ReadAt = psqlutil.NullTimeToTime(readAt) 178 - notes = append(notes, note) 179 - } 183 +func (s *NoteRepo) GetAllUnreadByAuthorID( 184 + ctx context.Context, 185 + authorID uuid.UUID, 186 +) ([]models.Note, error) { 187 + query := `--sql 188 +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at 189 +from notes n 190 +inner join notes_authors na on n.id = na.note_id 191 +where na.user_id = $1 192 + and n.read_at is null` 180 193 181 - return notes, rows.Err() 194 + return s.getAllNotes(ctx, query, authorID) 182 195 } 183 196 184 197 func (s *NoteRepo) GetCountOfNotesByAuthorID( ··· 359 372 360 373 return nil 361 374 } 375 + 376 +// getAllNotes is a helper function for [NoteRepo.GetAllByAuthorID], [NoteRepo.GetAllReadByAuthorID], 377 +// and [NoteRepo.GetAllUnreadByAuthorID]. 378 +// The query's SELECT elements order should be consistent across all function calls. 379 +func (s *NoteRepo) getAllNotes( 380 + ctx context.Context, 381 + query string, 382 + authorID uuid.UUID, 383 +) ([]models.Note, error) { 384 + rows, err := s.db.Query(ctx, query, authorID.String()) 385 + if err != nil { 386 + return nil, err 387 + } 388 + 389 + defer rows.Close() 390 + 391 + var notes []models.Note 392 + for rows.Next() { 393 + var note models.Note 394 + var readAt sql.NullTime 395 + if err := rows.Scan(¬e.Content, ¬e.Slug, ¬e.BurnBeforeExpiration, ¬e.Password, 396 + &readAt, ¬e.CreatedAt, ¬e.ExpiresAt); err != nil { 397 + return nil, err 398 + } 399 + 400 + note.ReadAt = psqlutil.NullTimeToTime(readAt) 401 + notes = append(notes, note) 402 + } 403 + 404 + return notes, rows.Err() 405 +}
M
internal/transport/http/apiv1/apiv1.go
··· 34 34 35 35 func (a *APIV1) Routes(r *gin.RouterGroup) { 36 36 r.Use(a.metricsMiddleware) 37 + 38 + r.GET("/me", a.authorizedMiddleware, a.getMeHandler) 39 + 37 40 auth := r.Group("/auth") 38 41 { 39 42 auth.POST("/signup", a.signUpHandler) ··· 58 61 } 59 62 } 60 63 61 - r.GET("/me", a.authorizedMiddleware, a.getMeHandler) 62 - 63 64 note := r.Group("/note") 64 65 { 65 66 note.GET("/:slug", a.getNoteBySlugHandler) ··· 74 75 authorized := note.Group("", a.authorizedMiddleware) 75 76 { 76 77 authorized.GET("", a.getNotesHandler) 78 + 79 + // FIXME: those links make slugs `read` and `unread` unavailable 80 + authorized.GET("/read", a.getReadNotesHandler) 81 + authorized.GET("/unread", a.getUnReadNotesHandler) 82 + 77 83 authorized.PATCH(":slug/expires", a.updateNoteHandler) 78 84 authorized.PATCH(":slug/password", a.setNotePasswordHandler) 79 85 authorized.DELETE(":slug", a.deleteNoteHandler)
M
internal/transport/http/apiv1/note.go
··· 154 154 return 155 155 } 156 156 157 - var response []getNotesResponse 158 - for _, note := range notes { 159 - response = append(response, getNotesResponse{ 160 - Content: note.Content, 161 - Slug: note.Slug, 162 - BurnBeforeExpiration: note.BurnBeforeExpiration, 163 - HasPassword: note.HasPassword, 164 - CreatedAt: note.CreatedAt, 165 - ExpiresAt: note.ExpiresAt, 166 - ReadAt: note.ReadAt, 167 - }) 157 + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) 158 +} 159 + 160 +func (a *APIV1) getReadNotesHandler(c *gin.Context) { 161 + notes, err := a.notesrv.GetAllReadByAuthorID(c.Request.Context(), a.getUserID(c)) 162 + if err != nil { 163 + errorResponse(c, err) 164 + return 165 + } 166 + 167 + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) 168 +} 169 + 170 +func (a *APIV1) getUnReadNotesHandler(c *gin.Context) { 171 + notes, err := a.notesrv.GetAllUnreadByAuthorID(c.Request.Context(), a.getUserID(c)) 172 + if err != nil { 173 + errorResponse(c, err) 174 + return 168 175 } 169 176 170 - c.JSON(http.StatusOK, response) 177 + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) 171 178 } 172 179 173 180 type updateNoteRequest struct { ··· 236 243 237 244 c.Status(http.StatusOK) 238 245 } 246 + 247 +func mapNotesDTOToResponse(notes []dtos.NoteDetailed) []getNotesResponse { 248 + var response []getNotesResponse 249 + for _, note := range notes { 250 + response = append(response, getNotesResponse{ 251 + Content: note.Content, 252 + Slug: note.Slug, 253 + BurnBeforeExpiration: note.BurnBeforeExpiration, 254 + HasPassword: note.HasPassword, 255 + CreatedAt: note.CreatedAt, 256 + ExpiresAt: note.ExpiresAt, 257 + ReadAt: note.ReadAt, 258 + }) 259 + } 260 + 261 + return response 262 +}