all repos

onasty @ efd9704

a one-time notes service
5 files changed, 40 insertions(+), 11 deletions(-)
refactor: make the `read_at` field nullable (#187)

* refactor: make `read_at` nullable

* feat(psqlutil): add util func to work with sql.NullTime

* fix: follow new db schema
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-08-11 19:39:03 +0300
Parent: e771742
M e2e/e2e_utils_db_test.go

@@ -1,6 +1,7 @@

package e2e_test import ( + "database/sql" "errors" "time"

@@ -8,6 +9,7 @@ "github.com/gofrs/uuid/v5"

"github.com/henvic/pgq" "github.com/jackc/pgx/v5" "github.com/olexsmir/onasty/internal/models" + "github.com/olexsmir/onasty/internal/store/psqlutil" ) // getUserByEmail queries user from db by it's email

@@ -114,12 +116,15 @@ Where(pgq.Eq{"slug": slug}).

SQL() e.require.NoError(err) + var readAt sql.NullTime var note models.Note err = e.postgresDB.QueryRow(e.ctx, query, args...). - Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt) + Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password, &readAt, &note.CreatedAt, &note.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { return models.Note{} //nolint:exhaustruct } + + note.ReadAt = psqlutil.NullTimeToTime(readAt) e.require.NoError(err) return note
M internal/store/psql/noterepo/noterepo.go

@@ -2,6 +2,7 @@ package noterepo

import ( "context" + "database/sql" "errors" "time"

@@ -111,12 +112,14 @@ return models.Note{}, err

} var note models.Note + var readAt sql.NullTime err = s.db.QueryRow(ctx, query, args...). - Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt) - + Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { return models.Note{}, models.ErrNoteNotFound } + + note.ReadAt = psqlutil.NullTimeToTime(readAt) return note, err }

@@ -131,14 +134,14 @@ from notes n

where slug = $1 ` - var readAt time.Time + var readAt sql.NullTime var metadata dtos.NoteMetadata err := s.db.QueryRow(ctx, query, slug).Scan(&metadata.CreatedAt, &metadata.HasPassword, &readAt) if errors.Is(err, pgx.ErrNoRows) { return dtos.NoteMetadata{}, models.ErrNoteNotFound } - if !readAt.IsZero() { + if !psqlutil.NullTimeToTime(readAt).IsZero() { return dtos.NoteMetadata{}, models.ErrNoteNotFound }

@@ -165,10 +168,13 @@

var notes []models.Note for rows.Next() { var note models.Note + var readAt sql.NullTime if err := rows.Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password, - &note.ReadAt, &note.CreatedAt, &note.ExpiresAt); err != nil { + &readAt, &note.CreatedAt, &note.ExpiresAt); err != nil { return nil, err } + + note.ReadAt = psqlutil.NullTimeToTime(readAt) notes = append(notes, note) }

@@ -207,12 +213,15 @@ return models.Note{}, err

} var note models.Note + var readAt sql.NullTime err = s.db.QueryRow(ctx, query, args...). - Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt) + Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { return models.Note{}, models.ErrNoteNotFound } + + note.ReadAt = psqlutil.NullTimeToTime(readAt) return note, err }

@@ -255,10 +264,8 @@ query, args, err := pgq.

Update("notes"). Set("content", ""). Set("read_at", readAt). - Where(pgq.Eq{ - "slug": slug, - "read_at": time.Time{}, // check if time is null - }). + Where(pgq.Eq{"slug": slug}). + Where("read_at is null"). SQL() if err != nil { return err
M internal/store/psqlutil/psqlutil.go

@@ -2,7 +2,9 @@ package psqlutil

import ( "context" + "database/sql" "errors" + "time" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool"

@@ -39,3 +41,12 @@ pgErr.ConstraintName == constraintName

} return false } + +// NullTimeToTime converts sql.NullTime to time.Time. +// Returns zero [time.Time] if NullTime is not valid. +func NullTimeToTime(t sql.NullTime) time.Time { + if t.Valid { + return t.Time + } + return time.Time{} +}
A migrations/20250811132505_note_nullable_read_at.down.sql

@@ -0,0 +1,3 @@

+ALTER TABLE notes + ALTER COLUMN "read_at" SET NOT NULL, + ALTER COLUMN "read_at" SET DEFAULT '0001-01-01 00:00:00'::timestamptz;
A migrations/20250811132505_note_nullable_read_at.up.sql

@@ -0,0 +1,3 @@

+ALTER TABLE notes + ALTER COLUMN "read_at" DROP NOT NULL, + ALTER COLUMN "read_at" SET DEFAULT NULL;