all repos

onasty @ eb4c60509f134e49485bff1ee0c6422ced9d5e03

a one-time notes service
17 files changed, 142 insertions(+), 97 deletions(-)
refactor!: rename "burn before expiration" to "keep before expiration" (#199)

* refactor: rename `.ShouldBeBurnt()` to `.ShouldPreserveOnRead()` which is more logical in context of the app

* refactor: rename "burnBeforeExpiration" to "keepBeforeExpiration" throughout the application

* test(e2e): test case of expired note

* fix(web): update logic to be "logical" with current naming
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-08-28 16:57:36 +0300
Parent: d2c87a8
M cmd/seed/notes.go
···
        13
        13
         	id                   string

      
        14
        14
         	content              string

      
        15
        15
         	slug                 string

      
        16
        
        -	burnBeforeExpiration bool

      
        
        16
        +	keepBeforeExpiration bool

      
        17
        17
         	password             string

      
        18
        18
         	expiresAt            time.Time

      
        19
        19
         	hasAuthor            bool

      ···
        22
        22
         	{ //nolint:exhaustruct

      
        23
        23
         		content:              "that test note one",

      
        24
        24
         		slug:                 "one",

      
        25
        
        -		burnBeforeExpiration: false,

      
        
        25
        +		keepBeforeExpiration: false,

      
        26
        26
         	},

      
        27
        27
         	{ //nolint:exhaustruct

      
        28
        28
         		content:              "that test note two",

      
        29
        29
         		slug:                 "two",

      
        30
        
        -		burnBeforeExpiration: true,

      
        
        30
        +		keepBeforeExpiration: true,

      
        31
        31
         		password:             "",

      
        32
        32
         		expiresAt:            time.Now().Add(24 * time.Hour),

      
        33
        33
         	},

      ···
        74
        74
         

      
        75
        75
         		err := db.QueryRow(

      
        76
        76
         			ctx, `

      
        77
        
        -		insert into notes (content, slug, burn_before_expiration, password, expires_at)

      
        
        77
        +		insert into notes (content, slug, keep_before_expiration, password, expires_at)

      
        78
        78
         		values ($1, $2, $3, $4, $5)

      
        79
        79
         		on conflict (slug) do update set

      
        80
        80
         			content = excluded.content,

      
        81
        
        -			burn_before_expiration = excluded.burn_before_expiration,

      
        
        81
        +			keep_before_expiration = excluded.keep_before_expiration,

      
        82
        82
         			password = excluded.password,

      
        83
        83
         			expires_at = excluded.expires_at

      
        84
        84
         		returning id`,

      
        85
        85
         			note.content,

      
        86
        86
         			note.slug,

      
        87
        
        -			note.burnBeforeExpiration,

      
        
        87
        +			note.keepBeforeExpiration,

      
        88
        88
         			passwd,

      
        89
        89
         			note.expiresAt,

      
        90
        90
         		).Scan(&notesData[i].id)

      
M e2e/apiv1_notes_authorized_test.go
···
        60
        60
         

      
        61
        61
         type apiV1NotePatchRequest struct {

      
        62
        62
         	ExpiresAt            time.Time `json:"expires_at"`

      
        63
        
        -	BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        63
        +	KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        64
        64
         }

      
        65
        65
         

      
        66
        66
         func (e *AppTestSuite) TestNoteV1_updateExpirationTime() {

      ···
        71
        71
         		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        72
        72
         			Content:              "sample content for the test",

      
        73
        73
         			ExpiresAt:            time.Now().Add(time.Minute),

      
        74
        
        -			BurnBeforeExpiration: false,

      
        
        74
        +			KeepBeforeExpiration: false,

      
        75
        75
         		}),

      
        76
        76
         		toks.AccessToken,

      
        77
        77
         	)

      ···
        87
        87
         		"/api/v1/note/"+body.Slug+"/expires",

      
        88
        88
         		e.jsonify(apiV1NotePatchRequest{

      
        89
        89
         			ExpiresAt:            patchTime,

      
        90
        
        -			BurnBeforeExpiration: true,

      
        
        90
        +			KeepBeforeExpiration: true,

      
        91
        91
         		}),

      
        92
        92
         		toks.AccessToken,

      
        93
        93
         	)

      ···
        95
        95
         	e.Equal(httpResp.Code, http.StatusOK)

      
        96
        96
         

      
        97
        97
         	dbNote := e.getNoteBySlug(body.Slug)

      
        98
        
        -	e.Equal(true, dbNote.BurnBeforeExpiration)

      
        
        98
        +	e.Equal(true, dbNote.KeepBeforeExpiration)

      
        99
        99
         	e.Equal(patchTime.Unix(), dbNote.ExpiresAt.Unix())

      
        100
        100
         }

      
        101
        101
         

      ···
        106
        106
         		"/api/v1/note/"+e.uuid(),

      
        107
        107
         		e.jsonify(apiV1NotePatchRequest{

      
        108
        108
         			ExpiresAt:            time.Now().Add(time.Hour),

      
        109
        
        -			BurnBeforeExpiration: true,

      
        
        109
        +			KeepBeforeExpiration: true,

      
        110
        110
         		}),

      
        111
        111
         		toks.AccessToken,

      
        112
        112
         	)

      ···
        204
        204
         type apiv1NoteGetAllResponse struct {

      
        205
        205
         	Content              string    `json:"content"`

      
        206
        206
         	Slug                 string    `json:"slug"`

      
        207
        
        -	BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        207
        +	KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        208
        208
         	HasPassword          bool      `json:"has_password"`

      
        209
        209
         	CreatedAt            time.Time `json:"created_at"`

      
        210
        210
         	ExpiresAt            time.Time `json:"expires_at"`

      
M e2e/apiv1_notes_test.go
···
        16
        16
         		Content              string    `json:"content"`

      
        17
        17
         		Slug                 string    `json:"slug"`

      
        18
        18
         		Password             string    `json:"password"`

      
        19
        
        -		BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        19
        +		KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        20
        20
         		ExpiresAt            time.Time `json:"expires_at"`

      
        21
        21
         	}

      
        22
        22
         	apiv1NoteCreateResponse struct {

      ···
        137
        137
         			},

      
        138
        138
         		},

      
        139
        139
         		{

      
        140
        
        -			name: "burn before expiration, but without expiration time",

      
        
        140
        +			name: "keep before expiration, but without expiration time",

      
        141
        141
         			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        142
        142
         				Content:              e.uuid(),

      
        143
        
        -				BurnBeforeExpiration: true,

      
        
        143
        +				KeepBeforeExpiration: true,

      
        144
        144
         			},

      
        145
        145
         			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {

      
        146
        146
         				var body errorResponse

      
        147
        147
         				e.readBodyAndUnjsonify(r.Body, &body)

      
        148
        148
         

      
        149
        
        -				e.Equal(models.ErrNoteCannotBeBurnt.Error(), body.Message)

      
        
        149
        +				e.Equal(models.ErrNoteCannotBeKept.Error(), body.Message)

      
        150
        150
         			},

      
        151
        151
         		},

      
        152
        152
         		{

      ···
        163
        163
         			name: "all possible fields",

      
        164
        164
         			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        165
        165
         				Content:              e.uuid(),

      
        166
        
        -				BurnBeforeExpiration: true,

      
        
        166
        +				KeepBeforeExpiration: true,

      
        167
        167
         				ExpiresAt:            time.Now().Add(time.Hour),

      
        168
        168
         			},

      
        169
        169
         			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {

      ···
        176
        176
         				e.NotEmpty(dbNote)

      
        177
        177
         

      
        178
        178
         				e.Equal(dbNote.Content, inp.Content)

      
        179
        
        -				e.Equal(dbNote.BurnBeforeExpiration, inp.BurnBeforeExpiration)

      
        
        179
        +				e.Equal(dbNote.KeepBeforeExpiration, inp.KeepBeforeExpiration)

      
        180
        180
         				e.Equal(dbNote.ExpiresAt.Unix(), inp.ExpiresAt.Unix())

      
        181
        181
         			},

      
        182
        182
         		},

      ···
        266
        266
         	e.Equal(dbNote2.ExpiresAt.Unix(), bodyRead2.ExpiresAt.Unix())

      
        267
        267
         }

      
        268
        268
         

      
        269
        
        -func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBurnBeforeExpiration() {

      
        
        269
        +func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBeKeptBeforeExpiration() {

      
        270
        270
         	// create note

      
        271
        271
         	content := e.uuid()

      
        272
        272
         	httpRespCreated := e.httpRequest(

      ···
        275
        275
         		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        276
        276
         			Content:              content,

      
        277
        277
         			ExpiresAt:            time.Now().Add(time.Hour),

      
        278
        
        -			BurnBeforeExpiration: true,

      
        
        278
        +			KeepBeforeExpiration: true,

      
        279
        279
         		}),

      
        280
        280
         	)

      
        281
        281
         	e.Equal(http.StatusCreated, httpRespCreated.Code)

      ···
        295
        295
         	dbNote := e.getNoteBySlug(bodyCreated.Slug)

      
        296
        296
         	e.Equal(content, dbNote.Content)

      
        297
        297
         	e.True(dbNote.ReadAt.IsZero())

      
        
        298
        +

      
        
        299
        +	// read note again

      
        
        300
        +	httpRespRead2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)

      
        
        301
        +	e.Equal(http.StatusOK, httpRespRead2.Code)

      
        
        302
        +

      
        
        303
        +	var body2 apiv1NoteGetResponse

      
        
        304
        +	e.readBodyAndUnjsonify(httpRespRead2.Body, &body2)

      
        
        305
        +

      
        
        306
        +	dbNote2 := e.getNoteBySlug(bodyCreated.Slug)

      
        
        307
        +	e.Equal(content, dbNote2.Content)

      
        
        308
        +	e.Equal(body2.Content, dbNote.Content)

      
        
        309
        +	e.True(dbNote2.ReadAt.IsZero())

      
        
        310
        +

      
        
        311
        +	e.Equal(bodyRead, body2)

      
        
        312
        +	e.Equal(dbNote, dbNote2)

      
        298
        313
         }

      
        299
        314
         

      
        300
        
        -func (e *AppTestSuite) TestNoteV1_Get_ShouldBurnBeforeExpiration() {

      
        
        315
        +func (e *AppTestSuite) TestNoteV1_Get_ShouldKeepBeforeExpiration_expired() {

      
        301
        316
         	// synctest is used here to ensure proper synchronization and isolation of test execution

      
        302
        317
         	// it still feels wrong to use synctest in e2e test, but it works nonetheless

      
        303
        318
         	synctest.Test(e.T(), func(_ *testing.T) {

      ···
        309
        324
         			e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        310
        325
         				Content:              content,

      
        311
        326
         				ExpiresAt:            time.Now().Add(time.Hour),

      
        312
        
        -				BurnBeforeExpiration: true,

      
        
        327
        +				KeepBeforeExpiration: true,

      
        
        328
        +			}),

      
        
        329
        +		)

      
        
        330
        +		e.Equal(http.StatusCreated, httpRespCreated.Code)

      
        
        331
        +

      
        
        332
        +		var bodyCreated apiv1NoteCreateResponse

      
        
        333
        +		e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)

      
        
        334
        +

      
        
        335
        +		time.Sleep(2 * time.Hour)

      
        
        336
        +

      
        
        337
        +		// read note

      
        
        338
        +		httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)

      
        
        339
        +		e.Equal(http.StatusGone, httpRespRead.Code)

      
        
        340
        +

      
        
        341
        +		dbNote := e.getNoteBySlug(bodyCreated.Slug)

      
        
        342
        +		e.Equal(content, dbNote.Content)

      
        
        343
        +		e.True(dbNote.ReadAt.IsZero())

      
        
        344
        +	})

      
        
        345
        +}

      
        
        346
        +

      
        
        347
        +func (e *AppTestSuite) TestNoteV1_Get_expired() {

      
        
        348
        +	// synctest is used here to ensure proper synchronization and isolation of test execution

      
        
        349
        +	// it still feels wrong to use synctest in e2e test, but it works nonetheless

      
        
        350
        +	synctest.Test(e.T(), func(_ *testing.T) {

      
        
        351
        +		// create note

      
        
        352
        +		content := e.uuid()

      
        
        353
        +		httpRespCreated := e.httpRequest(

      
        
        354
        +			http.MethodPost,

      
        
        355
        +			"/api/v1/note",

      
        
        356
        +			e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        
        357
        +				Content:   content,

      
        
        358
        +				ExpiresAt: time.Now().Add(time.Hour),

      
        313
        359
         			}),

      
        314
        360
         		)

      
        315
        361
         		e.Equal(http.StatusCreated, httpRespCreated.Code)

      
M e2e/e2e_utils_db_test.go
···
        105
        105
         			"id",

      
        106
        106
         			"content",

      
        107
        107
         			"slug",

      
        108
        
        -			"burn_before_expiration",

      
        
        108
        +			"keep_before_expiration",

      
        109
        109
         			"password",

      
        110
        110
         			"read_at",

      
        111
        111
         			"created_at",

      ···
        119
        119
         	var readAt sql.NullTime

      
        120
        120
         	var note models.Note

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

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

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

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

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

      
        125
        125
         	}

      
M internal/dtos/note.go
···
        10
        10
         

      
        11
        11
         type GetNote struct {

      
        12
        12
         	Content              string

      
        13
        
        -	BurnBeforeExpiration bool

      
        
        13
        +	KeepBeforeExpiration bool

      
        14
        14
         	ReadAt               time.Time

      
        15
        15
         	CreatedAt            time.Time

      
        16
        16
         	ExpiresAt            time.Time

      ···
        25
        25
         	Content              string

      
        26
        26
         	UserID               uuid.UUID

      
        27
        27
         	Slug                 NoteSlug

      
        28
        
        -	BurnBeforeExpiration bool

      
        
        28
        +	KeepBeforeExpiration bool

      
        29
        29
         	Password             string

      
        30
        30
         	CreatedAt            time.Time

      
        31
        31
         	ExpiresAt            time.Time

      ···
        34
        34
         type NoteDetailed struct {

      
        35
        35
         	Content              string

      
        36
        36
         	Slug                 NoteSlug

      
        37
        
        -	BurnBeforeExpiration bool

      
        
        37
        +	KeepBeforeExpiration bool

      
        38
        38
         	HasPassword          bool

      
        39
        39
         	CreatedAt            time.Time

      
        40
        40
         	ExpiresAt            time.Time

      ···
        43
        43
         

      
        44
        44
         type PatchNote struct {

      
        45
        45
         	ExpiresAt            *time.Time

      
        46
        
        -	BurnBeforeExpiration *bool

      
        
        46
        +	KeepBeforeExpiration *bool

      
        47
        47
         }

      
M internal/models/note.go
···
        18
        18
         	ErrNoteContentIsEmpty     = errors.New("note: content is empty")

      
        19
        19
         	ErrNoteSlugIsAlreadyInUse = errors.New("note: slug is already in use")

      
        20
        20
         	ErrNoteSlugIsInvalid      = errors.New("note: slug is invalid")

      
        21
        
        -	ErrNoteCannotBeBurnt      = errors.New(

      
        22
        
        -		"note: cannot be burnt before expiration if expiration time is not provided",

      
        
        21
        +	ErrNoteCannotBeKept       = errors.New(

      
        
        22
        +		"note: cannot be kept before expiration if expiration time is not provided",

      
        23
        23
         	)

      
        24
        24
         	ErrNoteExpired  = errors.New("note: expired")

      
        25
        25
         	ErrNoteNotFound = errors.New("note: not found")

      ···
        30
        30
         	Content              string

      
        31
        31
         	Slug                 string

      
        32
        32
         	Password             string

      
        33
        
        -	BurnBeforeExpiration bool

      
        
        33
        +	KeepBeforeExpiration bool

      
        34
        34
         	ReadAt               time.Time

      
        35
        35
         	CreatedAt            time.Time

      
        36
        36
         	ExpiresAt            time.Time

      ···
        51
        51
         		return ErrNoteExpired

      
        52
        52
         	}

      
        53
        53
         

      
        54
        
        -	if n.BurnBeforeExpiration && n.ExpiresAt.IsZero() {

      
        55
        
        -		return ErrNoteCannotBeBurnt

      
        
        54
        +	if n.KeepBeforeExpiration && n.ExpiresAt.IsZero() {

      
        
        55
        +		return ErrNoteCannotBeKept

      
        56
        56
         	}

      
        57
        57
         

      
        58
        58
         	if _, exists := notAllowedSlugs[n.Slug]; exists {

      ···
        67
        67
         		n.ExpiresAt.Before(time.Now())

      
        68
        68
         }

      
        69
        69
         

      
        70
        
        -func (n Note) ShouldBeBurnt() bool {

      
        
        70
        +func (n Note) ShouldPreserveOnRead() bool {

      
        71
        71
         	return !n.ExpiresAt.IsZero() &&

      
        72
        
        -		n.BurnBeforeExpiration

      
        
        72
        +		n.KeepBeforeExpiration

      
        73
        73
         }

      
        74
        74
         

      
        75
        75
         func (n Note) IsRead() bool {

      
M internal/models/note_test.go
···
        31
        31
         		n := Note{

      
        32
        32
         			Slug:                 "some-slug",

      
        33
        33
         			Password:             "some-password",

      
        34
        
        -			BurnBeforeExpiration: false,

      
        
        34
        +			KeepBeforeExpiration: false,

      
        35
        35
         		}

      
        36
        36
         		assert.EqualError(t, n.Validate(), ErrNoteContentIsEmpty.Error())

      
        37
        37
         	})

      ···
        43
        43
         		}

      
        44
        44
         		assert.EqualError(t, n.Validate(), ErrNoteExpired.Error())

      
        45
        45
         	})

      
        46
        
        -	t.Run("should fail if burn before expiration is set, and expiration time is not",

      
        
        46
        +	t.Run("should fail if keep before expiration is set, and expiration time is not",

      
        47
        47
         		func(t *testing.T) {

      
        48
        48
         			n := Note{

      
        49
        49
         				Content:              "content",

      
        50
        
        -				BurnBeforeExpiration: true,

      
        
        50
        +				KeepBeforeExpiration: true,

      
        51
        51
         			}

      
        52
        52
         

      
        53
        
        -			assert.EqualError(t, n.Validate(), ErrNoteCannotBeBurnt.Error())

      
        
        53
        +			assert.EqualError(t, n.Validate(), ErrNoteCannotBeKept.Error())

      
        54
        54
         		},

      
        55
        55
         	)

      
        56
        56
         	t.Run("should not fail if slug is not provided", func(t *testing.T) {

      ···
        90
        90
         }

      
        91
        91
         

      
        92
        92
         //nolint:exhaustruct

      
        93
        
        -func TestNote_ShouldBeBurnt(t *testing.T) {

      
        94
        
        -	t.Run("should be burnt", func(t *testing.T) {

      
        
        93
        +func TestNote_ShouldPreserveOnRead(t *testing.T) {

      
        
        94
        +	t.Run("should keep", func(t *testing.T) {

      
        95
        95
         		note := Note{

      
        96
        
        -			BurnBeforeExpiration: true,

      
        
        96
        +			KeepBeforeExpiration: true,

      
        97
        97
         			ExpiresAt:            time.Now().Add(time.Hour),

      
        98
        98
         		}

      
        99
        
        -		assert.True(t, note.ShouldBeBurnt())

      
        
        99
        +		assert.True(t, note.ShouldPreserveOnRead())

      
        100
        100
         	})

      
        101
        
        -	t.Run("should not be burnt", func(t *testing.T) {

      
        
        101
        +	t.Run("should not be kept", func(t *testing.T) {

      
        102
        102
         		note := Note{

      
        103
        
        -			BurnBeforeExpiration: true,

      
        
        103
        +			KeepBeforeExpiration: true,

      
        104
        104
         			ExpiresAt:            time.Time{},

      
        105
        105
         		}

      
        106
        
        -		assert.False(t, note.ShouldBeBurnt())

      
        
        106
        +		assert.False(t, note.ShouldPreserveOnRead())

      
        107
        107
         	})

      
        108
        
        -	t.Run("could not be burnt when expiration and shouldBurn set to false", func(t *testing.T) {

      
        
        108
        +	t.Run("cannot be kept when expiration and shouldKeep set to false", func(t *testing.T) {

      
        109
        109
         		note := Note{

      
        110
        
        -			BurnBeforeExpiration: false,

      
        
        110
        +			KeepBeforeExpiration: false,

      
        111
        111
         			ExpiresAt:            time.Time{},

      
        112
        112
         		}

      
        113
        
        -		assert.False(t, note.ShouldBeBurnt())

      
        
        113
        +		assert.False(t, note.ShouldPreserveOnRead())

      
        114
        114
         	})

      
        115
        115
         }

      
        116
        116
         

      
M internal/service/notesrv/notesrv.go
···
        42
        42
         	// GetAllUnreadByAuthorID returns all notes that ARE UNREAD and authored by author id.

      
        43
        43
         	GetAllUnreadByAuthorID(ctx context.Context, authorID uuid.UUID) ([]dtos.NoteDetailed, error)

      
        44
        44
         

      
        45
        
        -	// UpdateExpirationTimeSettings updates expiresAt and burnBeforeExpiration.

      
        
        45
        +	// UpdateExpirationTimeSettings updates expiresAt and keepBeforeExpiration.

      
        46
        46
         	// If notes is not found returns [models.ErrNoteNotFound].

      
        47
        47
         	UpdateExpirationTimeSettings(

      
        48
        48
         		ctx context.Context,

      ···
        99
        99
         		Content:              inp.Content,

      
        100
        100
         		Slug:                 inp.Slug,

      
        101
        101
         		Password:             inp.Password,

      
        102
        
        -		BurnBeforeExpiration: inp.BurnBeforeExpiration,

      
        
        102
        +		KeepBeforeExpiration: inp.KeepBeforeExpiration,

      
        103
        103
         		CreatedAt:            inp.CreatedAt,

      
        104
        104
         		ExpiresAt:            inp.ExpiresAt,

      
        105
        105
         	}

      ···
        135
        135
         

      
        136
        136
         	respNote := dtos.GetNote{

      
        137
        137
         		Content:              note.Content,

      
        138
        
        -		BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        138
        +		KeepBeforeExpiration: note.KeepBeforeExpiration,

      
        139
        139
         		ReadAt:               note.ReadAt,

      
        140
        140
         		CreatedAt:            note.CreatedAt,

      
        141
        141
         		ExpiresAt:            note.ExpiresAt,

      ···
        143
        143
         

      
        144
        144
         	// since not every note should be burn before expiration

      
        145
        145
         	// we return early if it's not

      
        146
        
        -	// TODO: fix naming

      
        147
        
        -	if note.ShouldBeBurnt() {

      
        
        146
        +	if note.ShouldPreserveOnRead() {

      
        148
        147
         		return respNote, nil

      
        149
        148
         	}

      
        150
        149
         

      ···
        270
        269
         		resNotes = append(resNotes, dtos.NoteDetailed{

      
        271
        270
         			Content:              note.Content,

      
        272
        271
         			Slug:                 note.Slug,

      
        273
        
        -			BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        272
        +			KeepBeforeExpiration: note.KeepBeforeExpiration,

      
        274
        273
         			HasPassword:          note.Password != "",

      
        275
        274
         			CreatedAt:            note.CreatedAt,

      
        276
        275
         			ExpiresAt:            note.ExpiresAt,

      
M internal/store/psql/noterepo/noterepo.go
···
        48
        48
         		password string,

      
        49
        49
         	) (models.Note, error)

      
        50
        50
         

      
        51
        
        -	// UpdateExpirationTimeSettingsBySlug patches note by updating expiresAt and burnBeforeExpiration if one is passwd

      
        
        51
        +	// UpdateExpirationTimeSettingsBySlug patches note by updating expiresAt and keepBeforeExpiration if one is passwd

      
        52
        52
         	// Returns [models.ErrNoteNotFound] if note is not found.

      
        53
        53
         	UpdateExpirationTimeSettingsBySlug(

      
        54
        54
         		ctx context.Context,

      ···
        91
        91
         func (s *NoteRepo) Create(ctx context.Context, inp models.Note) error {

      
        92
        92
         	query, args, err := pgq.

      
        93
        93
         		Insert("notes").

      
        94
        
        -		Columns("content", "slug", "password", "burn_before_expiration", "created_at", "expires_at").

      
        95
        
        -		Values(inp.Content, inp.Slug, inp.Password, inp.BurnBeforeExpiration, inp.CreatedAt, inp.ExpiresAt).

      
        
        94
        +		Columns("content", "slug", "password", "keep_before_expiration", "created_at", "expires_at").

      
        
        95
        +		Values(inp.Content, inp.Slug, inp.Password, inp.KeepBeforeExpiration, inp.CreatedAt, inp.ExpiresAt).

      
        96
        96
         		SQL()

      
        97
        97
         	if err != nil {

      
        98
        98
         		return err

      ···
        108
        108
         

      
        109
        109
         func (s *NoteRepo) GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error) {

      
        110
        110
         	query, args, err := pgq.

      
        111
        
        -		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").

      
        
        111
        +		Select("content", "slug", "keep_before_expiration", "read_at", "created_at", "expires_at").

      
        112
        112
         		From("notes").

      
        113
        113
         		Where("(password is null or password = '')").

      
        114
        114
         		Where(pgq.Eq{"slug": slug}).

      ···
        120
        120
         	var note models.Note

      
        121
        121
         	var readAt sql.NullTime

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

      
        123
        
        -		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        
        123
        +		Scan(&note.Content, &note.Slug, &note.KeepBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        124
        124
         	if errors.Is(err, pgx.ErrNoRows) {

      
        125
        125
         		return models.Note{}, models.ErrNoteNotFound

      
        126
        126
         	}

      ···
        158
        158
         	authorID uuid.UUID,

      
        159
        159
         ) ([]models.Note, error) {

      
        160
        160
         	query := `--sql

      
        161
        
        -select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        
        161
        +select n.content, n.slug, n.keep_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        162
        162
         from notes n

      
        163
        163
         inner join notes_authors na on n.id = na.note_id

      
        164
        164
         where na.user_id = $1`

      ···
        171
        171
         	authorID uuid.UUID,

      
        172
        172
         ) ([]models.Note, error) {

      
        173
        173
         	query := `--sql

      
        174
        
        -select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        
        174
        +select n.content, n.slug, n.keep_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        175
        175
         from notes n

      
        176
        176
         inner join notes_authors na on n.id = na.note_id

      
        177
        177
         where na.user_id = $1

      ···
        185
        185
         	authorID uuid.UUID,

      
        186
        186
         ) ([]models.Note, error) {

      
        187
        187
         	query := `--sql

      
        188
        
        -select n.content, n.slug, n.burn_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        
        188
        +select n.content, n.slug, n.keep_before_expiration, n.password, n.read_at, n.created_at, n.expires_at

      
        189
        189
         from notes n

      
        190
        190
         inner join notes_authors na on n.id = na.note_id

      
        191
        191
         where na.user_id = $1

      ···
        214
        214
         	passwd string,

      
        215
        215
         ) (models.Note, error) {

      
        216
        216
         	query, args, err := pgq.

      
        217
        
        -		Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at").

      
        
        217
        +		Select("content", "slug", "keep_before_expiration", "read_at", "created_at", "expires_at").

      
        218
        218
         		From("notes").

      
        219
        219
         		Where(pgq.Eq{

      
        220
        220
         			"slug":     slug,

      ···
        228
        228
         	var note models.Note

      
        229
        229
         	var readAt sql.NullTime

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

      
        231
        
        -		Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        
        231
        +		Scan(&note.Content, &note.Slug, &note.KeepBeforeExpiration, &readAt, &note.CreatedAt, &note.ExpiresAt)

      
        232
        232
         

      
        233
        233
         	if errors.Is(err, pgx.ErrNoRows) {

      
        234
        234
         		return models.Note{}, models.ErrNoteNotFound

      ···
        247
        247
         ) error {

      
        248
        248
         	query := `--sql

      
        249
        249
         update notes n

      
        250
        
        -set burn_before_expiration = COALESCE($1, n.burn_before_expiration),

      
        
        250
        +set keep_before_expiration = COALESCE($1, n.keep_before_expiration),

      
        251
        251
             expires_at = COALESCE($2, n.expires_at)

      
        252
        252
         from notes_authors na

      
        253
        253
         where n.slug = $3

      ···
        255
        255
           and na.note_id = n.id`

      
        256
        256
         

      
        257
        257
         	ct, err := s.db.Exec(ctx, query,

      
        258
        
        -		patch.BurnBeforeExpiration, patch.ExpiresAt,

      
        
        258
        +		patch.KeepBeforeExpiration, patch.ExpiresAt,

      
        259
        259
         		slug, authorID.String())

      
        260
        260
         	if err != nil {

      
        261
        261
         		return err

      ···
        392
        392
         	for rows.Next() {

      
        393
        393
         		var note models.Note

      
        394
        394
         		var readAt sql.NullTime

      
        395
        
        -		if err := rows.Scan(&note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.Password,

      
        
        395
        +		if err := rows.Scan(&note.Content, &note.Slug, &note.KeepBeforeExpiration, &note.Password,

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

      
        397
        397
         			return nil, err

      
        398
        398
         		}

      
M internal/transport/http/apiv1/note.go
···
        13
        13
         	Content              string    `json:"content"`

      
        14
        14
         	Slug                 string    `json:"slug"`

      
        15
        15
         	Password             string    `json:"password"`

      
        16
        
        -	BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        16
        +	KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        17
        17
         	ExpiresAt            time.Time `json:"expires_at"`

      
        18
        18
         }

      
        19
        19
         

      ···
        28
        28
         		return

      
        29
        29
         	}

      
        30
        30
         

      
        31
        
        -	// TODO: burn_before_expiration shouldn't be set if user has not set or specified expires_at

      
        32
        
        -

      
        33
        31
         	slug, err := a.notesrv.Create(c.Request.Context(), dtos.CreateNote{

      
        34
        32
         		Content:              req.Content,

      
        35
        33
         		UserID:               a.getUserID(c),

      
        36
        34
         		Slug:                 req.Slug,

      
        37
        35
         		Password:             req.Password,

      
        38
        
        -		BurnBeforeExpiration: req.BurnBeforeExpiration,

      
        
        36
        +		KeepBeforeExpiration: req.KeepBeforeExpiration,

      
        39
        37
         		CreatedAt:            time.Now(),

      
        40
        38
         		ExpiresAt:            req.ExpiresAt,

      
        41
        39
         	}, a.getUserID(c))

      ···
        50
        48
         type getNoteBySlugResponse struct {

      
        51
        49
         	Content              string    `json:"content"`

      
        52
        50
         	ReadAt               time.Time `json:"read_at,omitzero"`

      
        53
        
        -	BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        51
        +	KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        54
        52
         	CreatedAt            time.Time `json:"created_at"`

      
        55
        53
         	ExpiresAt            time.Time `json:"expires_at,omitzero"`

      
        56
        54
         }

      ···
        78
        76
         		ReadAt:               note.ReadAt,

      
        79
        77
         		CreatedAt:            note.CreatedAt,

      
        80
        78
         		ExpiresAt:            note.ExpiresAt,

      
        81
        
        -		BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        79
        +		KeepBeforeExpiration: note.KeepBeforeExpiration,

      
        82
        80
         	})

      
        83
        81
         }

      
        84
        82
         

      ···
        115
        113
         		ReadAt:               note.ReadAt,

      
        116
        114
         		CreatedAt:            note.CreatedAt,

      
        117
        115
         		ExpiresAt:            note.ExpiresAt,

      
        118
        
        -		BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        116
        +		KeepBeforeExpiration: note.KeepBeforeExpiration,

      
        119
        117
         	})

      
        120
        118
         }

      
        121
        119
         

      ···
        140
        138
         type getNotesResponse struct {

      
        141
        139
         	Content              string    `json:"content"`

      
        142
        140
         	Slug                 string    `json:"slug"`

      
        143
        
        -	BurnBeforeExpiration bool      `json:"burn_before_expiration"`

      
        
        141
        +	KeepBeforeExpiration bool      `json:"keep_before_expiration"`

      
        144
        142
         	HasPassword          bool      `json:"has_password"`

      
        145
        143
         	CreatedAt            time.Time `json:"created_at"`

      
        146
        144
         	ExpiresAt            time.Time `json:"expires_at,omitzero"`

      ···
        179
        177
         

      
        180
        178
         type updateNoteRequest struct {

      
        181
        179
         	ExpiresAt            *time.Time `json:"expires_at,omitempty"`

      
        182
        
        -	BurnBeforeExpiration *bool      `json:"burn_before_expiration,omitempty"`

      
        
        180
        +	KeepBeforeExpiration *bool      `json:"keep_before_expiration,omitempty"`

      
        183
        181
         }

      
        184
        182
         

      
        185
        183
         func (a *APIV1) updateNoteHandler(c *gin.Context) {

      ···
        189
        187
         		return

      
        190
        188
         	}

      
        191
        189
         

      
        192
        
        -	// TODO: burn_before_expiration shouldn't be set if user has not set or specified expires_at

      
        193
        
        -

      
        194
        190
         	if err := a.notesrv.UpdateExpirationTimeSettings(

      
        195
        191
         		c.Request.Context(),

      
        196
        192
         		dtos.PatchNote{

      
        197
        
        -			BurnBeforeExpiration: req.BurnBeforeExpiration,

      
        
        193
        +			KeepBeforeExpiration: req.KeepBeforeExpiration,

      
        198
        194
         			ExpiresAt:            req.ExpiresAt,

      
        199
        195
         		},

      
        200
        196
         		c.Param("slug"),

      ···
        250
        246
         		response = append(response, getNotesResponse{

      
        251
        247
         			Content:              note.Content,

      
        252
        248
         			Slug:                 note.Slug,

      
        253
        
        -			BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        249
        +			KeepBeforeExpiration: note.KeepBeforeExpiration,

      
        254
        250
         			HasPassword:          note.HasPassword,

      
        255
        251
         			CreatedAt:            note.CreatedAt,

      
        256
        252
         			ExpiresAt:            note.ExpiresAt,

      
M internal/transport/http/apiv1/response.go
···
        32
        32
         		// notes

      
        33
        33
         		errors.Is(err, notesrv.ErrNotePasswordNotProvided) ||

      
        34
        34
         		errors.Is(err, models.ErrNoteContentIsEmpty) ||

      
        35
        
        -		errors.Is(err, models.ErrNoteCannotBeBurnt) ||

      
        
        35
        +		errors.Is(err, models.ErrNoteCannotBeKept) ||

      
        36
        36
         		errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) ||

      
        37
        37
         		errors.Is(err, models.ErrNoteSlugIsInvalid) {

      
        38
        38
         		newError(c, http.StatusBadRequest, err.Error())

      
A migrations/20250828132538_notes_rename_burnBeforeExpiration_to_KeepBeforeExpiration.down.sql
···
        
        1
        +ALTER TABLE notes

      
        
        2
        +    RENAME COLUMN keep_before_expiration TO burn_before_expiration ;

      
A migrations/20250828132538_notes_rename_burnBeforeExpiration_to_KeepBeforeExpiration.up.sql
···
        
        1
        +ALTER TABLE notes

      
        
        2
        +    RENAME COLUMN burn_before_expiration TO keep_before_expiration;

      
M web/src/Api/Note.elm
···
        15
        15
             , slug : Maybe String

      
        16
        16
             , password : Maybe String

      
        17
        17
             , expiresAt : Posix

      
        18
        
        -    , burnBeforeExpiration : Bool

      
        
        18
        +    , keepBeforeExpiration : Bool

      
        19
        19
             }

      
        20
        20
             -> Effect msg

      
        21
        21
         create options =

      ···
        34
        34
                         [ ( "content", E.string options.content )

      
        35
        35
                         , encodeMaybe "slug" E.string options.slug

      
        36
        36
                         , encodeMaybe "password" E.string options.password

      
        37
        
        -                , ( "burn_before_expiration", E.bool options.burnBeforeExpiration )

      
        
        37
        +                , ( "keep_before_expiration", E.bool options.keepBeforeExpiration )

      
        38
        38
                         , if options.expiresAt == Time.millisToPosix 0 then

      
        39
        39
                             ( "expires_at", E.null )

      
        40
        40
         

      
M web/src/Data/Note.elm
···
        17
        17
         type alias Note =

      
        18
        18
             { content : String

      
        19
        19
             , readAt : Maybe Posix

      
        20
        
        -    , burnBeforeExpiration : Bool

      
        
        20
        +    , keepBeforeExpiration : Bool

      
        21
        21
             , createdAt : Posix

      
        22
        22
             , expiresAt : Maybe Posix

      
        23
        23
             }

      ···
        28
        28
             D.map5 Note

      
        29
        29
                 (D.field "content" D.string)

      
        30
        30
                 (D.maybe (D.field "read_at" Iso8601.decoder))

      
        31
        
        -        (D.field "burn_before_expiration" D.bool)

      
        
        31
        +        (D.field "keep_before_expiration" D.bool)

      
        32
        32
                 (D.field "created_at" Iso8601.decoder)

      
        33
        33
                 (D.maybe (D.field "expires_at" Iso8601.decoder))

      
        34
        34
         

      
M web/src/Pages/Home_.elm
···
        42
        42
             , slug : Maybe String

      
        43
        43
             , password : Maybe String

      
        44
        44
             , expirationTime : Maybe Int

      
        45
        
        -    , dontBurnBeforeExpiration : Bool

      
        
        45
        +    , keepBeforeExpiration : Bool

      
        46
        46
             , apiError : Maybe Api.Error

      
        47
        47
             , userClickedCopyLink : Bool

      
        48
        48
             , now : Maybe Posix

      ···
        61
        61
               , slug = Nothing

      
        62
        62
               , password = Nothing

      
        63
        63
               , expirationTime = Nothing

      
        64
        
        -      , dontBurnBeforeExpiration = True

      
        
        64
        +      , keepBeforeExpiration = True

      
        65
        65
               , userClickedCopyLink = False

      
        66
        66
               , apiError = Nothing

      
        67
        67
               , now = Nothing

      ···
        117
        117
                         , content = model.content

      
        118
        118
                         , slug = model.slug

      
        119
        119
                         , password = model.password

      
        120
        
        -                , burnBeforeExpiration = not model.dontBurnBeforeExpiration

      
        
        120
        +                , keepBeforeExpiration = model.keepBeforeExpiration

      
        121
        121
                         , expiresAt = expiresAt

      
        122
        122
                         }

      
        123
        123
                     )

      ···
        165
        165
                     else

      
        166
        166
                         ( { model | expirationTime = String.toInt expirationTime }, Effect.none )

      
        167
        167
         

      
        168
        
        -        UserClickedCheckbox burnBeforeExpiration ->

      
        169
        
        -            ( { model | dontBurnBeforeExpiration = burnBeforeExpiration }, Effect.none )

      
        
        168
        +        UserClickedCheckbox keepBeforeExpiration ->

      
        
        169
        +            ( { model | keepBeforeExpiration = keepBeforeExpiration }, Effect.none )

      
        170
        170
         

      
        171
        171
                 ApiCreateNoteResponded (Ok response) ->

      
        172
        172
                     ( { model | pageVariant = NoteCreated response.slug, slug = Just response.slug, apiError = Nothing }, Effect.none )

      ···
        287
        287
                         ]

      
        288
        288
                     , H.div [ A.class "space-y-6" ]

      
        289
        289
                         [ viewExpirationTimeSelector

      
        290
        
        -                , viewBurnBeforeExpirationCheckbox (isCheckBoxDisabled model.expirationTime)

      
        
        290
        +                , viewKeepBeforeExpirationCheckbox (isCheckBoxDisabled model.expirationTime)

      
        291
        291
                         ]

      
        292
        292
                     ]

      
        293
        293
                 , H.div [ A.class "flex justify-end" ]

      ···
        341
        341
                 ]

      
        342
        342
         

      
        343
        343
         

      
        344
        
        -viewBurnBeforeExpirationCheckbox : Bool -> Html Msg

      
        345
        
        -viewBurnBeforeExpirationCheckbox isDisabled =

      
        
        344
        +viewKeepBeforeExpirationCheckbox : Bool -> Html Msg

      
        
        345
        +viewKeepBeforeExpirationCheckbox isDisabled =

      
        346
        346
             H.div [ A.class "space-y-2" ]

      
        347
        347
                 [ H.div [ A.class "flex items-start space-x-3" ]

      
        348
        348
                     [ H.input

      
        349
        349
                         [ E.onCheck UserClickedCheckbox

      
        350
        
        -                , A.id "burn"

      
        
        350
        +                , A.id "kept"

      
        351
        351
                         , A.type_ "checkbox"

      
        352
        352
                         , A.class "mt-1 h-4 w-4 text-black border-gray-300 rounded focus:ring-black focus:ring-2"

      
        353
        353
                         , A.disabled isDisabled

      
        354
        354
                         ]

      
        355
        355
                         []

      
        356
        356
                     , H.div [ A.class "flex-1" ]

      
        357
        
        -                [ H.label [ A.for "burn", A.class "block text-sm font-medium text-gray-700 cursor-pointer" ]

      
        
        357
        +                [ H.label [ A.for "kept", A.class "block text-sm font-medium text-gray-700 cursor-pointer" ]

      
        358
        358
                             [ H.text "Keep the note until its expiration time, even if it has already been read." ]

      
        359
        359
                         , H.span [ A.class "block text-sm font-medium text-gray-500 cursor-pointer" ]

      
        360
        360
                             [ H.text "Can only be used if expiration time is set" ]

      
M web/src/Pages/Secret/Slug_.elm
···
        193
        193
         viewShowNoteHeader : Zone -> String -> Note -> Html Msg

      
        194
        194
         viewShowNoteHeader zone slug note =

      
        195
        195
             H.div []

      
        196
        
        -        [ Components.Utils.viewIf note.burnBeforeExpiration

      
        
        196
        +        [ Components.Utils.viewIf note.keepBeforeExpiration

      
        197
        197
                     (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ]

      
        198
        198
                         [ H.div [ A.class "flex items-center gap-3" ]

      
        199
        199
                             [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ]