all repos

onasty @ 86dba8ea0dc14c9a08dbb0baa9b0441168638328

a one-time notes service
6 files changed, 119 insertions(+), 1 deletions(-)
feat(api): get note metadata (#148)

* feat(api): get note metadata

* fix(note): get the metadata

* test: get note metadata

* fixup! fix(note): get the metadata

* fixup! fix(note): get the metadata
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-25 16:06:29 +0300
Parent: e4abd99
M e2e/apiv1_notes_test.go

@@ -222,3 +222,62 @@ }),

) e.Equal(httpResp.Code, http.StatusNotFound) } + +type apiv1NoteMetadataResponse struct { + CreatedAt time.Time `json:"created_at"` + HasPassword bool `json:"has_password"` +} + +func (e *AppTestSuite) TestNoteV1_GetMetadata() { + // create note + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{Content: "content"}), //nolint:exhaustruct + ) + e.Equal(http.StatusCreated, httpResp.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated) + + // get metadata + metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug+"/meta", []byte{}) + e.Equal(metaResp.Code, http.StatusOK) + + var metadata apiv1NoteMetadataResponse + e.readBodyAndUnjsonify(metaResp.Body, &metadata) + + e.False(metadata.HasPassword) + e.NotEmpty(metadata.CreatedAt) +} + +func (e *AppTestSuite) TestNoteV1_GetMetadata_withPassword() { + // create note + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: "content", + Password: "pass", + }), + ) + e.Equal(http.StatusCreated, httpResp.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated) + + // get metadata + metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug+"/meta", []byte{}) + e.Equal(metaResp.Code, http.StatusOK) + + var metadata apiv1NoteMetadataResponse + e.readBodyAndUnjsonify(metaResp.Body, &metadata) + + e.True(metadata.HasPassword) + e.NotEmpty(metadata.CreatedAt) +} + +func (e *AppTestSuite) TestNoteV1_GetMetadata_notFound() { + metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+e.uuid()+"/meta", []byte{}) + e.Equal(http.StatusNotFound, metaResp.Code) +}
M internal/dtos/note.go

@@ -15,6 +15,11 @@ CreatedAt time.Time

ExpiresAt time.Time } +type NoteMetadata struct { + HasPassword bool + CreatedAt time.Time +} + type CreateNote struct { Content string UserID uuid.UUID
M internal/service/notesrv/notesrv.go

@@ -23,11 +23,15 @@ // 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. - // If notes is not found returns [models.ErrNoteNotFound]. + // If note is not found returns [models.ErrNoteNotFound]. GetBySlugAndRemoveIfNeeded( ctx context.Context, input GetNoteBySlugInput, ) (dtos.GetNote, error) + + // GetNoteMetadataBySlug returns note metadata by slug. + // 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(

@@ -140,6 +144,14 @@ return respNote, nil

} return respNote, n.noterepo.RemoveBySlug(ctx, inp.Slug, time.Now()) +} + +func (n *NoteSrv) GetNoteMetadataBySlug( + ctx context.Context, + slug dtos.NoteSlug, +) (dtos.NoteMetadata, error) { + note, err := n.noterepo.GetMetadataBySlug(ctx, slug) + return note, err } func (n *NoteSrv) GetAllByAuthorID(
M internal/store/psql/noterepo/noterepo.go

@@ -21,6 +21,10 @@ // GetBySlug gets a note by slug.

// Returns [models.ErrNoteNotFound] if note is not found. GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error) + // GetMetadataBySlug gets note's metadata by its slug. + // Returns [models.ErrNoteNotFound] if note is not found. + GetMetadataBySlug(ctx context.Context, slug dtos.NoteSlug) (dtos.NoteMetadata, error) + // GetAllByAuthorID returns all notes with specified author. GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error)

@@ -112,6 +116,25 @@ return models.Note{}, models.ErrNoteNotFound

} return note, err +} + +func (s *NoteRepo) GetMetadataBySlug( + 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 + from notes n + where slug = $1 + ` + + var metadata dtos.NoteMetadata + err := s.db.QueryRow(ctx, query, slug).Scan(&metadata.CreatedAt, &metadata.HasPassword) + if errors.Is(err, pgx.ErrNoRows) { + return dtos.NoteMetadata{}, models.ErrNoteNotFound + } + + return metadata, err } func (s *NoteRepo) GetAllByAuthorID(
M internal/transport/http/apiv1/apiv1.go

@@ -59,6 +59,7 @@

note := r.Group("/note") { note.GET("/:slug", a.getNoteBySlugHandler) + note.GET("/:slug/meta", a.getNoteMetadataByIDHandler) possiblyAuthorized := note.Group("", a.couldBeAuthorizedMiddleware) {
M internal/transport/http/apiv1/note.go

@@ -92,6 +92,24 @@ ExpiresAt: note.ExpiresAt,

}) } +type getNoteMetadataBySlugResponse struct { + CreatedAt time.Time `json:"created_at"` + HasPassword bool `json:"has_password"` +} + +func (a *APIV1) getNoteMetadataByIDHandler(c *gin.Context) { + meta, err := a.notesrv.GetNoteMetadataBySlug(c.Request.Context(), c.Param("slug")) + if err != nil { + errorResponse(c, err) + return + } + + c.JSON(http.StatusOK, getNoteMetadataBySlugResponse{ + CreatedAt: meta.CreatedAt, + HasPassword: meta.HasPassword, + }) +} + type getNotesResponse struct { Content string `json:"content"` Slug string `json:"slug"`