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,10 +34,13 @@ // If note is not found returns [models.ErrNoteNotFound].
GetNoteMetadataBySlug(ctx context.Context, slug dtos.NoteSlug) (dtos.NoteMetadata, error) // GetAllByAuthorID returns all notes by author id. - GetAllByAuthorID( - ctx context.Context, - authorID uuid.UUID, - ) ([]dtos.NoteDetailed, error) + GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) + + // GetAllReadByAuthorID returns all notes that ARE READ and authored by author id. + GetAllReadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) + + // GetAllUnreadByAuthorID returns all notes that ARE UNREAD and authored by author id. + GetAllUnreadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error) // UpdateExpirationTimeSettings updates expiresAt and burnBeforeExpiration. // If notes is not found returns [models.ErrNoteNotFound].@@ -164,20 +167,31 @@ if err != nil {
return nil, err } - var resNotes []dtos.NoteDetailed - for _, note := range notes { - resNotes = append(resNotes, dtos.NoteDetailed{ - Content: note.Content, - Slug: note.Slug, - BurnBeforeExpiration: note.BurnBeforeExpiration, - HasPassword: note.Password != "", - CreatedAt: note.CreatedAt, - ExpiresAt: note.ExpiresAt, - ReadAt: note.ReadAt, - }) + return n.mapNoteModelToDto(notes), nil +} + +func (n *NoteSrv) GetAllReadByAuthorID( + ctx context.Context, + authorID uuid.UUID, +) ([]dtos.NoteDetailed, error) { + notes, err := n.noterepo.GetAllReadByAuthorID(ctx, authorID) + if err != nil { + return nil, err + } + + return n.mapNoteModelToDto(notes), nil +} + +func (n *NoteSrv) GetAllUnreadByAuthorID( + ctx context.Context, + authorID uuid.UUID, +) ([]dtos.NoteDetailed, error) { + notes, err := n.noterepo.GetAllUnreadByAuthorID(ctx, authorID) + if err != nil { + return nil, err } - return resNotes, nil + return n.mapNoteModelToDto(notes), nil } func (n *NoteSrv) UpdateExpirationTimeSettings(@@ -248,3 +262,20 @@ return n.noterepo.GetBySlugAndPassword(ctx, inp.Slug, hashedPassword)
} return n.noterepo.GetBySlug(ctx, inp.Slug) } + +func (n *NoteSrv) mapNoteModelToDto(notes []models.Note) []dtos.NoteDetailed { + var resNotes []dtos.NoteDetailed + for _, note := range notes { + resNotes = append(resNotes, dtos.NoteDetailed{ + Content: note.Content, + Slug: note.Slug, + BurnBeforeExpiration: note.BurnBeforeExpiration, + HasPassword: note.Password != "", + CreatedAt: note.CreatedAt, + ExpiresAt: note.ExpiresAt, + ReadAt: note.ReadAt, + }) + } + + return resNotes +}
M
internal/store/psql/noterepo/noterepo.go
@@ -29,6 +29,12 @@
// GetAllByAuthorID returns all notes with specified author. GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) + // GetAllReadByAuthorID returns all notes that are read and authored by specified author. + GetAllReadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) + + // GetAllUnreadByAuthorID returns all notes that are unread and authored by specified author. + GetAllUnreadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) + // GetCountOfNotesByAuthorID returns count of notes created by specified author. GetCountOfNotesByAuthorID(ctx context.Context, authorID uuid.UUID) (int64, error)@@ -129,10 +135,9 @@ ctx context.Context,
slug dtos.NoteSlug, ) (dtos.NoteMetadata, error) { query := `--sql - select n.created_at, (n.password is not null and n.password <> '') has_password, n.read_at - from notes n - where slug = $1 - ` +select n.created_at, (n.password is not null and n.password <> '') has_password, n.read_at +from notes n +where slug = $1` var readAt sql.NullTime var metadata dtos.NoteMetadata@@ -153,32 +158,40 @@ ctx context.Context,
authorID uuid.UUID, ) ([]models.Note, error) { query := `--sql - select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at - from notes n - right join notes_authors na on n.id = na.note_id - where na.user_id = $1` +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at +from notes n +inner join notes_authors na on n.id = na.note_id +where na.user_id = $1` - rows, err := s.db.Query(ctx, query, authorID.String()) - if err != nil { - return nil, err - } + return s.getAllNotes(ctx, query, authorID) +} - defer rows.Close() +func (s *NoteRepo) GetAllReadByAuthorID( + ctx context.Context, + authorID uuid.UUID, +) ([]models.Note, error) { + query := `--sql +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at +from notes n +inner join notes_authors na on n.id = na.note_id +where na.user_id = $1 + and n.read_at is not null` - var notes []models.Note - for rows.Next() { - var note models.Note - var readAt sql.NullTime - if err := rows.Scan(¬e.Content, ¬e.Slug, ¬e.BurnBeforeExpiration, ¬e.Password, - &readAt, ¬e.CreatedAt, ¬e.ExpiresAt); err != nil { - return nil, err - } + return s.getAllNotes(ctx, query, authorID) +} - note.ReadAt = psqlutil.NullTimeToTime(readAt) - notes = append(notes, note) - } +func (s *NoteRepo) GetAllUnreadByAuthorID( + ctx context.Context, + authorID uuid.UUID, +) ([]models.Note, error) { + query := `--sql +select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at +from notes n +inner join notes_authors na on n.id = na.note_id +where na.user_id = $1 + and n.read_at is null` - return notes, rows.Err() + return s.getAllNotes(ctx, query, authorID) } func (s *NoteRepo) GetCountOfNotesByAuthorID(@@ -359,3 +372,34 @@ }
return nil } + +// getAllNotes is a helper function for [NoteRepo.GetAllByAuthorID], [NoteRepo.GetAllReadByAuthorID], +// and [NoteRepo.GetAllUnreadByAuthorID]. +// The query's SELECT elements order should be consistent across all function calls. +func (s *NoteRepo) getAllNotes( + ctx context.Context, + query string, + authorID uuid.UUID, +) ([]models.Note, error) { + rows, err := s.db.Query(ctx, query, authorID.String()) + if err != nil { + return nil, err + } + + defer rows.Close() + + var notes []models.Note + for rows.Next() { + var note models.Note + var readAt sql.NullTime + if err := rows.Scan(¬e.Content, ¬e.Slug, ¬e.BurnBeforeExpiration, ¬e.Password, + &readAt, ¬e.CreatedAt, ¬e.ExpiresAt); err != nil { + return nil, err + } + + note.ReadAt = psqlutil.NullTimeToTime(readAt) + notes = append(notes, note) + } + + return notes, rows.Err() +}
M
internal/transport/http/apiv1/apiv1.go
@@ -34,6 +34,9 @@ }
func (a *APIV1) Routes(r *gin.RouterGroup) { r.Use(a.metricsMiddleware) + + r.GET("/me", a.authorizedMiddleware, a.getMeHandler) + auth := r.Group("/auth") { auth.POST("/signup", a.signUpHandler)@@ -58,8 +61,6 @@ authorized.POST("/change-password", a.changePasswordHandler)
} } - r.GET("/me", a.authorizedMiddleware, a.getMeHandler) - note := r.Group("/note") { note.GET("/:slug", a.getNoteBySlugHandler)@@ -74,6 +75,11 @@
authorized := note.Group("", a.authorizedMiddleware) { authorized.GET("", a.getNotesHandler) + + // FIXME: those links make slugs `read` and `unread` unavailable + authorized.GET("/read", a.getReadNotesHandler) + authorized.GET("/unread", a.getUnReadNotesHandler) + authorized.PATCH(":slug/expires", a.updateNoteHandler) authorized.PATCH(":slug/password", a.setNotePasswordHandler) authorized.DELETE(":slug", a.deleteNoteHandler)
M
internal/transport/http/apiv1/note.go
@@ -154,20 +154,27 @@ errorResponse(c, err)
return } - var response []getNotesResponse - for _, note := range notes { - response = append(response, getNotesResponse{ - Content: note.Content, - Slug: note.Slug, - BurnBeforeExpiration: note.BurnBeforeExpiration, - HasPassword: note.HasPassword, - CreatedAt: note.CreatedAt, - ExpiresAt: note.ExpiresAt, - ReadAt: note.ReadAt, - }) + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) +} + +func (a *APIV1) getReadNotesHandler(c *gin.Context) { + notes, err := a.notesrv.GetAllReadByAuthorID(c.Request.Context(), a.getUserID(c)) + if err != nil { + errorResponse(c, err) + return + } + + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) +} + +func (a *APIV1) getUnReadNotesHandler(c *gin.Context) { + notes, err := a.notesrv.GetAllUnreadByAuthorID(c.Request.Context(), a.getUserID(c)) + if err != nil { + errorResponse(c, err) + return } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, mapNotesDTOToResponse(notes)) } type updateNoteRequest struct {@@ -236,3 +243,20 @@ }
c.Status(http.StatusOK) } + +func mapNotesDTOToResponse(notes []dtos.NoteDetailed) []getNotesResponse { + var response []getNotesResponse + for _, note := range notes { + response = append(response, getNotesResponse{ + Content: note.Content, + Slug: note.Slug, + BurnBeforeExpiration: note.BurnBeforeExpiration, + HasPassword: note.HasPassword, + CreatedAt: note.CreatedAt, + ExpiresAt: note.ExpiresAt, + ReadAt: note.ReadAt, + }) + } + + return response +}