all repos

onasty @ efd970430597e242aa15c24f868fc0e7103b45ac

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
        1
         package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"database/sql"

      
        4
        5
         	"errors"

      
        5
        6
         	"time"

      
        6
        7
         

      ···
        8
        9
         	"github.com/henvic/pgq"

      
        9
        10
         	"github.com/jackc/pgx/v5"

      
        10
        11
         	"github.com/olexsmir/onasty/internal/models"

      
        
        12
        +	"github.com/olexsmir/onasty/internal/store/psqlutil"

      
        11
        13
         )

      
        12
        14
         

      
        13
        15
         // getUserByEmail queries user from db by it's email

      ···
        114
        116
         		SQL()

      
        115
        117
         	e.require.NoError(err)

      
        116
        118
         

      
        
        119
        +	var readAt sql.NullTime

      
        117
        120
         	var note models.Note

      
        118
        121
         	err = e.postgresDB.QueryRow(e.ctx, query, args...).

      
        119
        
        -		Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)

      
        
        122
        +		Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        120
        123
         	if errors.Is(err, pgx.ErrNoRows) {

      
        121
        124
         		return models.Note{} //nolint:exhaustruct

      
        122
        125
         	}

      
        
        126
        +

      
        
        127
        +	note.ReadAt = psqlutil.NullTimeToTime(readAt)

      
        123
        128
         

      
        124
        129
         	e.require.NoError(err)

      
        125
        130
         	return note

      
M internal/store/psql/noterepo/noterepo.go
···
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"context"

      
        
        5
        +	"database/sql"

      
        5
        6
         	"errors"

      
        6
        7
         	"time"

      
        7
        8
         

      ···
        111
        112
         	}

      
        112
        113
         

      
        113
        114
         	var note models.Note

      
        
        115
        +	var readAt sql.NullTime

      
        114
        116
         	err = s.db.QueryRow(ctx, query, args...).

      
        115
        
        -		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)

      
        116
        
        -

      
        
        117
        +		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        117
        118
         	if errors.Is(err, pgx.ErrNoRows) {

      
        118
        119
         		return models.Note{}, models.ErrNoteNotFound

      
        119
        120
         	}

      
        
        121
        +

      
        
        122
        +	note.ReadAt = psqlutil.NullTimeToTime(readAt)

      
        120
        123
         

      
        121
        124
         	return note, err

      
        122
        125
         }

      ···
        131
        134
         	where slug = $1

      
        132
        135
         	`

      
        133
        136
         

      
        134
        
        -	var readAt time.Time

      
        
        137
        +	var readAt sql.NullTime

      
        135
        138
         	var metadata dtos.NoteMetadata

      
        136
        139
         	err := s.db.QueryRow(ctx, query, slug).Scan(&metadata.CreatedAt, &metadata.HasPassword, &readAt)

      
        137
        140
         	if errors.Is(err, pgx.ErrNoRows) {

      
        138
        141
         		return dtos.NoteMetadata{}, models.ErrNoteNotFound

      
        139
        142
         	}

      
        140
        143
         

      
        141
        
        -	if !readAt.IsZero() {

      
        
        144
        +	if !psqlutil.NullTimeToTime(readAt).IsZero() {

      
        142
        145
         		return dtos.NoteMetadata{}, models.ErrNoteNotFound

      
        143
        146
         	}

      
        144
        147
         

      ···
        165
        168
         	var notes []models.Note

      
        166
        169
         	for rows.Next() {

      
        167
        170
         		var note models.Note

      
        
        171
        +		var readAt sql.NullTime

      
        168
        172
         		if err := rows.Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password,

      
        169
        
        -			&note.ReadAt, &note.CreatedAt, &note.ExpiresAt); err != nil {

      
        
        173
        +			&readAt, &note.CreatedAt, &note.ExpiresAt); err != nil {

      
        170
        174
         			return nil, err

      
        171
        175
         		}

      
        
        176
        +

      
        
        177
        +		note.ReadAt = psqlutil.NullTimeToTime(readAt)

      
        172
        178
         		notes = append(notes, note)

      
        173
        179
         	}

      
        174
        180
         

      ···
        207
        213
         	}

      
        208
        214
         

      
        209
        215
         	var note models.Note

      
        
        216
        +	var readAt sql.NullTime

      
        210
        217
         	err = s.db.QueryRow(ctx, query, args...).

      
        211
        
        -		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.ReadAt, &note.CreatedAt, &note.ExpiresAt)

      
        
        218
        +		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        212
        219
         

      
        213
        220
         	if errors.Is(err, pgx.ErrNoRows) {

      
        214
        221
         		return models.Note{}, models.ErrNoteNotFound

      
        215
        222
         	}

      
        
        223
        +

      
        
        224
        +	note.ReadAt = psqlutil.NullTimeToTime(readAt)

      
        216
        225
         

      
        217
        226
         	return note, err

      
        218
        227
         }

      ···
        255
        264
         		Update("notes").

      
        256
        265
         		Set("content", "").

      
        257
        266
         		Set("read_at", readAt).

      
        258
        
        -		Where(pgq.Eq{

      
        259
        
        -			"slug":    slug,

      
        260
        
        -			"read_at": time.Time{}, // check if time is null

      
        261
        
        -		}).

      
        
        267
        +		Where(pgq.Eq{"slug": slug}).

      
        
        268
        +		Where("read_at is null").

      
        262
        269
         		SQL()

      
        263
        270
         	if err != nil {

      
        264
        271
         		return err

      
M internal/store/psqlutil/psqlutil.go
···
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"context"

      
        
        5
        +	"database/sql"

      
        5
        6
         	"errors"

      
        
        7
        +	"time"

      
        6
        8
         

      
        7
        9
         	"github.com/jackc/pgx/v5/pgconn"

      
        8
        10
         	"github.com/jackc/pgx/v5/pgxpool"

      ···
        39
        41
         	}

      
        40
        42
         	return false

      
        41
        43
         }

      
        
        44
        +

      
        
        45
        +// NullTimeToTime converts sql.NullTime to time.Time.

      
        
        46
        +// Returns zero [time.Time] if NullTime is not valid.

      
        
        47
        +func NullTimeToTime(t sql.NullTime) time.Time {

      
        
        48
        +	if t.Valid {

      
        
        49
        +		return t.Time

      
        
        50
        +	}

      
        
        51
        +	return time.Time{}

      
        
        52
        +}

      
A migrations/20250811132505_note_nullable_read_at.down.sql
···
        
        1
        +ALTER TABLE notes

      
        
        2
        +  ALTER COLUMN "read_at" SET NOT NULL,

      
        
        3
        +  ALTER COLUMN "read_at" SET DEFAULT '0001-01-01 00:00:00'::timestamptz;

      
A migrations/20250811132505_note_nullable_read_at.up.sql
···
        
        1
        +ALTER TABLE notes

      
        
        2
        +  ALTER COLUMN "read_at" DROP NOT NULL,

      
        
        3
        +  ALTER COLUMN "read_at" SET DEFAULT NULL;