all repos

onasty @ eb4c605

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,7 +13,7 @@ var notesData = []struct {

id string content string slug string - burnBeforeExpiration bool + keepBeforeExpiration bool password string expiresAt time.Time hasAuthor bool

@@ -22,12 +22,12 @@ }{

{ //nolint:exhaustruct content: "that test note one", slug: "one", - burnBeforeExpiration: false, + keepBeforeExpiration: false, }, { //nolint:exhaustruct content: "that test note two", slug: "two", - burnBeforeExpiration: true, + keepBeforeExpiration: true, password: "", expiresAt: time.Now().Add(24 * time.Hour), },

@@ -74,17 +74,17 @@ }

err := db.QueryRow( ctx, ` - insert into notes (content, slug, burn_before_expiration, password, expires_at) + insert into notes (content, slug, keep_before_expiration, password, expires_at) values ($1, $2, $3, $4, $5) on conflict (slug) do update set content = excluded.content, - burn_before_expiration = excluded.burn_before_expiration, + keep_before_expiration = excluded.keep_before_expiration, password = excluded.password, expires_at = excluded.expires_at returning id`, note.content, note.slug, - note.burnBeforeExpiration, + note.keepBeforeExpiration, passwd, note.expiresAt, ).Scan(&notesData[i].id)
M e2e/apiv1_notes_authorized_test.go

@@ -60,7 +60,7 @@ }

type apiV1NotePatchRequest struct { ExpiresAt time.Time `json:"expires_at"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` } func (e *AppTestSuite) TestNoteV1_updateExpirationTime() {

@@ -71,7 +71,7 @@ "/api/v1/note",

e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct Content: "sample content for the test", ExpiresAt: time.Now().Add(time.Minute), - BurnBeforeExpiration: false, + KeepBeforeExpiration: false, }), toks.AccessToken, )

@@ -87,7 +87,7 @@ http.MethodPatch,

"/api/v1/note/"+body.Slug+"/expires", e.jsonify(apiV1NotePatchRequest{ ExpiresAt: patchTime, - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, }), toks.AccessToken, )

@@ -95,7 +95,7 @@

e.Equal(httpResp.Code, http.StatusOK) dbNote := e.getNoteBySlug(body.Slug) - e.Equal(true, dbNote.BurnBeforeExpiration) + e.Equal(true, dbNote.KeepBeforeExpiration) e.Equal(patchTime.Unix(), dbNote.ExpiresAt.Unix()) }

@@ -106,7 +106,7 @@ http.MethodPatch,

"/api/v1/note/"+e.uuid(), e.jsonify(apiV1NotePatchRequest{ ExpiresAt: time.Now().Add(time.Hour), - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, }), toks.AccessToken, )

@@ -204,7 +204,7 @@

type apiv1NoteGetAllResponse struct { Content string `json:"content"` Slug string `json:"slug"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` HasPassword bool `json:"has_password"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"`
M e2e/apiv1_notes_test.go

@@ -16,7 +16,7 @@ apiv1NoteCreateRequest struct {

Content string `json:"content"` Slug string `json:"slug"` Password string `json:"password"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` ExpiresAt time.Time `json:"expires_at"` } apiv1NoteCreateResponse struct {

@@ -137,16 +137,16 @@ e.Equal(inp.Content, dbNote.Content)

}, }, { - name: "burn before expiration, but without expiration time", + name: "keep before expiration, but without expiration time", inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Content: e.uuid(), - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, }, assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { var body errorResponse e.readBodyAndUnjsonify(r.Body, &body) - e.Equal(models.ErrNoteCannotBeBurnt.Error(), body.Message) + e.Equal(models.ErrNoteCannotBeKept.Error(), body.Message) }, }, {

@@ -163,7 +163,7 @@ {

name: "all possible fields", inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Content: e.uuid(), - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, ExpiresAt: time.Now().Add(time.Hour), }, assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {

@@ -176,7 +176,7 @@ dbNote := e.getNoteBySlug(body.Slug)

e.NotEmpty(dbNote) e.Equal(dbNote.Content, inp.Content) - e.Equal(dbNote.BurnBeforeExpiration, inp.BurnBeforeExpiration) + e.Equal(dbNote.KeepBeforeExpiration, inp.KeepBeforeExpiration) e.Equal(dbNote.ExpiresAt.Unix(), inp.ExpiresAt.Unix()) }, },

@@ -266,7 +266,7 @@ e.Equal(dbNote2.CreatedAt.Unix(), bodyRead2.CreatedAt.Unix())

e.Equal(dbNote2.ExpiresAt.Unix(), bodyRead2.ExpiresAt.Unix()) } -func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBurnBeforeExpiration() { +func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBeKeptBeforeExpiration() { // create note content := e.uuid() httpRespCreated := e.httpRequest(

@@ -275,7 +275,7 @@ "/api/v1/note",

e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct Content: content, ExpiresAt: time.Now().Add(time.Hour), - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, }), ) e.Equal(http.StatusCreated, httpRespCreated.Code)

@@ -295,9 +295,24 @@

dbNote := e.getNoteBySlug(bodyCreated.Slug) e.Equal(content, dbNote.Content) e.True(dbNote.ReadAt.IsZero()) + + // read note again + httpRespRead2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusOK, httpRespRead2.Code) + + var body2 apiv1NoteGetResponse + e.readBodyAndUnjsonify(httpRespRead2.Body, &body2) + + dbNote2 := e.getNoteBySlug(bodyCreated.Slug) + e.Equal(content, dbNote2.Content) + e.Equal(body2.Content, dbNote.Content) + e.True(dbNote2.ReadAt.IsZero()) + + e.Equal(bodyRead, body2) + e.Equal(dbNote, dbNote2) } -func (e *AppTestSuite) TestNoteV1_Get_ShouldBurnBeforeExpiration() { +func (e *AppTestSuite) TestNoteV1_Get_ShouldKeepBeforeExpiration_expired() { // synctest is used here to ensure proper synchronization and isolation of test execution // it still feels wrong to use synctest in e2e test, but it works nonetheless synctest.Test(e.T(), func(_ *testing.T) {

@@ -309,7 +324,38 @@ "/api/v1/note",

e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct Content: content, ExpiresAt: time.Now().Add(time.Hour), - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, + }), + ) + e.Equal(http.StatusCreated, httpRespCreated.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated) + + time.Sleep(2 * time.Hour) + + // read note + httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusGone, httpRespRead.Code) + + dbNote := e.getNoteBySlug(bodyCreated.Slug) + e.Equal(content, dbNote.Content) + e.True(dbNote.ReadAt.IsZero()) + }) +} + +func (e *AppTestSuite) TestNoteV1_Get_expired() { + // synctest is used here to ensure proper synchronization and isolation of test execution + // it still feels wrong to use synctest in e2e test, but it works nonetheless + synctest.Test(e.T(), func(_ *testing.T) { + // create note + content := e.uuid() + httpRespCreated := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: content, + ExpiresAt: time.Now().Add(time.Hour), }), ) e.Equal(http.StatusCreated, httpRespCreated.Code)
M e2e/e2e_utils_db_test.go

@@ -105,7 +105,7 @@ Select(

"id", "content", "slug", - "burn_before_expiration", + "keep_before_expiration", "password", "read_at", "created_at",

@@ -119,7 +119,7 @@

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, &readAt, &note.CreatedAt, &note.ExpiresAt) + Scan(&note.ID, &note.Content, &note.Slug, &note.KeepBeforeExpiration, &note.Password, &readAt, &note.CreatedAt, &note.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { return models.Note{} //nolint:exhaustruct }
M internal/dtos/note.go

@@ -10,7 +10,7 @@ type NoteSlug = string

type GetNote struct { Content string - BurnBeforeExpiration bool + KeepBeforeExpiration bool ReadAt time.Time CreatedAt time.Time ExpiresAt time.Time

@@ -25,7 +25,7 @@ type CreateNote struct {

Content string UserID uuid.UUID Slug NoteSlug - BurnBeforeExpiration bool + KeepBeforeExpiration bool Password string CreatedAt time.Time ExpiresAt time.Time

@@ -34,7 +34,7 @@

type NoteDetailed struct { Content string Slug NoteSlug - BurnBeforeExpiration bool + KeepBeforeExpiration bool HasPassword bool CreatedAt time.Time ExpiresAt time.Time

@@ -43,5 +43,5 @@ }

type PatchNote struct { ExpiresAt *time.Time - BurnBeforeExpiration *bool + KeepBeforeExpiration *bool }
M internal/models/note.go

@@ -18,8 +18,8 @@ var (

ErrNoteContentIsEmpty = errors.New("note: content is empty") ErrNoteSlugIsAlreadyInUse = errors.New("note: slug is already in use") ErrNoteSlugIsInvalid = errors.New("note: slug is invalid") - ErrNoteCannotBeBurnt = errors.New( - "note: cannot be burnt before expiration if expiration time is not provided", + ErrNoteCannotBeKept = errors.New( + "note: cannot be kept before expiration if expiration time is not provided", ) ErrNoteExpired = errors.New("note: expired") ErrNoteNotFound = errors.New("note: not found")

@@ -30,7 +30,7 @@ ID uuid.UUID

Content string Slug string Password string - BurnBeforeExpiration bool + KeepBeforeExpiration bool ReadAt time.Time CreatedAt time.Time ExpiresAt time.Time

@@ -51,8 +51,8 @@ if n.IsExpired() {

return ErrNoteExpired } - if n.BurnBeforeExpiration && n.ExpiresAt.IsZero() { - return ErrNoteCannotBeBurnt + if n.KeepBeforeExpiration && n.ExpiresAt.IsZero() { + return ErrNoteCannotBeKept } if _, exists := notAllowedSlugs[n.Slug]; exists {

@@ -67,9 +67,9 @@ return !n.ExpiresAt.IsZero() &&

n.ExpiresAt.Before(time.Now()) } -func (n Note) ShouldBeBurnt() bool { +func (n Note) ShouldPreserveOnRead() bool { return !n.ExpiresAt.IsZero() && - n.BurnBeforeExpiration + n.KeepBeforeExpiration } func (n Note) IsRead() bool {
M internal/models/note_test.go

@@ -31,7 +31,7 @@ t.Run("should fail if content is missing and other fields are set", func(t *testing.T) {

n := Note{ Slug: "some-slug", Password: "some-password", - BurnBeforeExpiration: false, + KeepBeforeExpiration: false, } assert.EqualError(t, n.Validate(), ErrNoteContentIsEmpty.Error()) })

@@ -43,14 +43,14 @@ ExpiresAt: time.Now().Add(-time.Hour),

} assert.EqualError(t, n.Validate(), ErrNoteExpired.Error()) }) - t.Run("should fail if burn before expiration is set, and expiration time is not", + t.Run("should fail if keep before expiration is set, and expiration time is not", func(t *testing.T) { n := Note{ Content: "content", - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, } - assert.EqualError(t, n.Validate(), ErrNoteCannotBeBurnt.Error()) + assert.EqualError(t, n.Validate(), ErrNoteCannotBeKept.Error()) }, ) t.Run("should not fail if slug is not provided", func(t *testing.T) {

@@ -90,27 +90,27 @@ })

} //nolint:exhaustruct -func TestNote_ShouldBeBurnt(t *testing.T) { - t.Run("should be burnt", func(t *testing.T) { +func TestNote_ShouldPreserveOnRead(t *testing.T) { + t.Run("should keep", func(t *testing.T) { note := Note{ - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, ExpiresAt: time.Now().Add(time.Hour), } - assert.True(t, note.ShouldBeBurnt()) + assert.True(t, note.ShouldPreserveOnRead()) }) - t.Run("should not be burnt", func(t *testing.T) { + t.Run("should not be kept", func(t *testing.T) { note := Note{ - BurnBeforeExpiration: true, + KeepBeforeExpiration: true, ExpiresAt: time.Time{}, } - assert.False(t, note.ShouldBeBurnt()) + assert.False(t, note.ShouldPreserveOnRead()) }) - t.Run("could not be burnt when expiration and shouldBurn set to false", func(t *testing.T) { + t.Run("cannot be kept when expiration and shouldKeep set to false", func(t *testing.T) { note := Note{ - BurnBeforeExpiration: false, + KeepBeforeExpiration: false, ExpiresAt: time.Time{}, } - assert.False(t, note.ShouldBeBurnt()) + assert.False(t, note.ShouldPreserveOnRead()) }) }
M internal/service/notesrv/notesrv.go

@@ -42,7 +42,7 @@

// 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. + // UpdateExpirationTimeSettings updates expiresAt and keepBeforeExpiration. // If notes is not found returns [models.ErrNoteNotFound]. UpdateExpirationTimeSettings( ctx context.Context,

@@ -99,7 +99,7 @@ note := models.Note{

Content: inp.Content, Slug: inp.Slug, Password: inp.Password, - BurnBeforeExpiration: inp.BurnBeforeExpiration, + KeepBeforeExpiration: inp.KeepBeforeExpiration, CreatedAt: inp.CreatedAt, ExpiresAt: inp.ExpiresAt, }

@@ -135,7 +135,7 @@ }

respNote := dtos.GetNote{ Content: note.Content, - BurnBeforeExpiration: note.BurnBeforeExpiration, + KeepBeforeExpiration: note.KeepBeforeExpiration, ReadAt: note.ReadAt, CreatedAt: note.CreatedAt, ExpiresAt: note.ExpiresAt,

@@ -143,8 +143,7 @@ }

// since not every note should be burn before expiration // we return early if it's not - // TODO: fix naming - if note.ShouldBeBurnt() { + if note.ShouldPreserveOnRead() { return respNote, nil }

@@ -270,7 +269,7 @@ for _, note := range notes {

resNotes = append(resNotes, dtos.NoteDetailed{ Content: note.Content, Slug: note.Slug, - BurnBeforeExpiration: note.BurnBeforeExpiration, + KeepBeforeExpiration: note.KeepBeforeExpiration, HasPassword: note.Password != "", CreatedAt: note.CreatedAt, ExpiresAt: note.ExpiresAt,
M internal/store/psql/noterepo/noterepo.go

@@ -48,7 +48,7 @@ slug dtos.NoteSlug,

password string, ) (models.Note, error) - // UpdateExpirationTimeSettingsBySlug patches note by updating expiresAt and burnBeforeExpiration if one is passwd + // UpdateExpirationTimeSettingsBySlug patches note by updating expiresAt and keepBeforeExpiration if one is passwd // Returns [models.ErrNoteNotFound] if note is not found. UpdateExpirationTimeSettingsBySlug( ctx context.Context,

@@ -91,8 +91,8 @@

func (s *NoteRepo) Create(ctx context.Context, inp models.Note) error { query, args, err := pgq. Insert("notes"). - Columns("content", "slug", "password", "burn_before_expiration", "created_at", "expires_at"). - Values(inp.Content, inp.Slug, inp.Password, inp.BurnBeforeExpiration, inp.CreatedAt, inp.ExpiresAt). + Columns("content", "slug", "password", "keep_before_expiration", "created_at", "expires_at"). + Values(inp.Content, inp.Slug, inp.Password, inp.KeepBeforeExpiration, inp.CreatedAt, inp.ExpiresAt). SQL() if err != nil { return err

@@ -108,7 +108,7 @@ }

func (s *NoteRepo) GetBySlug(ctx context.Context, slug dtos.NoteSlug) (models.Note, error) { query, args, err := pgq. - Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at"). + Select("content", "slug", "keep_before_expiration", "read_at", "created_at", "expires_at"). From("notes"). Where("(password is null or password = '')"). Where(pgq.Eq{"slug": slug}).

@@ -120,7 +120,7 @@

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

@@ -158,7 +158,7 @@ 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 +select n.content, n.slug, n.keep_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`

@@ -171,7 +171,7 @@ 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 +select n.content, n.slug, n.keep_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

@@ -185,7 +185,7 @@ 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 +select n.content, n.slug, n.keep_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

@@ -214,7 +214,7 @@ slug dtos.NoteSlug,

passwd string, ) (models.Note, error) { query, args, err := pgq. - Select("content", "slug", "burn_before_expiration", "read_at", "created_at", "expires_at"). + Select("content", "slug", "keep_before_expiration", "read_at", "created_at", "expires_at"). From("notes"). Where(pgq.Eq{ "slug": slug,

@@ -228,7 +228,7 @@

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

@@ -247,7 +247,7 @@ authorID uuid.UUID,

) error { query := `--sql update notes n -set burn_before_expiration = COALESCE($1, n.burn_before_expiration), +set keep_before_expiration = COALESCE($1, n.keep_before_expiration), expires_at = COALESCE($2, n.expires_at) from notes_authors na where n.slug = $3

@@ -255,7 +255,7 @@ and na.user_id = $4

and na.note_id = n.id` ct, err := s.db.Exec(ctx, query, - patch.BurnBeforeExpiration, patch.ExpiresAt, + patch.KeepBeforeExpiration, patch.ExpiresAt, slug, authorID.String()) if err != nil { return err

@@ -392,7 +392,7 @@ 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, + if err := rows.Scan(&note.Content, &note.Slug, &note.KeepBeforeExpiration, &note.Password, &readAt, &note.CreatedAt, &note.ExpiresAt); err != nil { return nil, err }
M internal/transport/http/apiv1/note.go

@@ -13,7 +13,7 @@ type createNoteRequest struct {

Content string `json:"content"` Slug string `json:"slug"` Password string `json:"password"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` ExpiresAt time.Time `json:"expires_at"` }

@@ -28,14 +28,12 @@ newError(c, http.StatusBadRequest, "invalid request")

return } - // TODO: burn_before_expiration shouldn't be set if user has not set or specified expires_at - slug, err := a.notesrv.Create(c.Request.Context(), dtos.CreateNote{ Content: req.Content, UserID: a.getUserID(c), Slug: req.Slug, Password: req.Password, - BurnBeforeExpiration: req.BurnBeforeExpiration, + KeepBeforeExpiration: req.KeepBeforeExpiration, CreatedAt: time.Now(), ExpiresAt: req.ExpiresAt, }, a.getUserID(c))

@@ -50,7 +48,7 @@

type getNoteBySlugResponse struct { Content string `json:"content"` ReadAt time.Time `json:"read_at,omitzero"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at,omitzero"` }

@@ -78,7 +76,7 @@ Content: note.Content,

ReadAt: note.ReadAt, CreatedAt: note.CreatedAt, ExpiresAt: note.ExpiresAt, - BurnBeforeExpiration: note.BurnBeforeExpiration, + KeepBeforeExpiration: note.KeepBeforeExpiration, }) }

@@ -115,7 +113,7 @@ Content: note.Content,

ReadAt: note.ReadAt, CreatedAt: note.CreatedAt, ExpiresAt: note.ExpiresAt, - BurnBeforeExpiration: note.BurnBeforeExpiration, + KeepBeforeExpiration: note.KeepBeforeExpiration, }) }

@@ -140,7 +138,7 @@

type getNotesResponse struct { Content string `json:"content"` Slug string `json:"slug"` - BurnBeforeExpiration bool `json:"burn_before_expiration"` + KeepBeforeExpiration bool `json:"keep_before_expiration"` HasPassword bool `json:"has_password"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at,omitzero"`

@@ -179,7 +177,7 @@ }

type updateNoteRequest struct { ExpiresAt *time.Time `json:"expires_at,omitempty"` - BurnBeforeExpiration *bool `json:"burn_before_expiration,omitempty"` + KeepBeforeExpiration *bool `json:"keep_before_expiration,omitempty"` } func (a *APIV1) updateNoteHandler(c *gin.Context) {

@@ -189,12 +187,10 @@ newError(c, http.StatusBadRequest, "invalid request")

return } - // TODO: burn_before_expiration shouldn't be set if user has not set or specified expires_at - if err := a.notesrv.UpdateExpirationTimeSettings( c.Request.Context(), dtos.PatchNote{ - BurnBeforeExpiration: req.BurnBeforeExpiration, + KeepBeforeExpiration: req.KeepBeforeExpiration, ExpiresAt: req.ExpiresAt, }, c.Param("slug"),

@@ -250,7 +246,7 @@ for _, note := range notes {

response = append(response, getNotesResponse{ Content: note.Content, Slug: note.Slug, - BurnBeforeExpiration: note.BurnBeforeExpiration, + KeepBeforeExpiration: note.KeepBeforeExpiration, HasPassword: note.HasPassword, CreatedAt: note.CreatedAt, ExpiresAt: note.ExpiresAt,
M internal/transport/http/apiv1/response.go

@@ -32,7 +32,7 @@ errors.Is(err, models.ErrUserWrongCredentials) ||

// notes errors.Is(err, notesrv.ErrNotePasswordNotProvided) || errors.Is(err, models.ErrNoteContentIsEmpty) || - errors.Is(err, models.ErrNoteCannotBeBurnt) || + errors.Is(err, models.ErrNoteCannotBeKept) || errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) || errors.Is(err, models.ErrNoteSlugIsInvalid) { newError(c, http.StatusBadRequest, err.Error())
A migrations/20250828132538_notes_rename_burnBeforeExpiration_to_KeepBeforeExpiration.down.sql

@@ -0,0 +1,2 @@

+ALTER TABLE notes + RENAME COLUMN keep_before_expiration TO burn_before_expiration ;
A migrations/20250828132538_notes_rename_burnBeforeExpiration_to_KeepBeforeExpiration.up.sql

@@ -0,0 +1,2 @@

+ALTER TABLE notes + RENAME COLUMN burn_before_expiration TO keep_before_expiration;
M web/src/Api/Note.elm

@@ -15,7 +15,7 @@ , content : String

, slug : Maybe String , password : Maybe String , expiresAt : Posix - , burnBeforeExpiration : Bool + , keepBeforeExpiration : Bool } -> Effect msg create options =

@@ -34,7 +34,7 @@ E.object

[ ( "content", E.string options.content ) , encodeMaybe "slug" E.string options.slug , encodeMaybe "password" E.string options.password - , ( "burn_before_expiration", E.bool options.burnBeforeExpiration ) + , ( "keep_before_expiration", E.bool options.keepBeforeExpiration ) , if options.expiresAt == Time.millisToPosix 0 then ( "expires_at", E.null )
M web/src/Data/Note.elm

@@ -17,7 +17,7 @@

type alias Note = { content : String , readAt : Maybe Posix - , burnBeforeExpiration : Bool + , keepBeforeExpiration : Bool , createdAt : Posix , expiresAt : Maybe Posix }

@@ -28,7 +28,7 @@ decode =

D.map5 Note (D.field "content" D.string) (D.maybe (D.field "read_at" Iso8601.decoder)) - (D.field "burn_before_expiration" D.bool) + (D.field "keep_before_expiration" D.bool) (D.field "created_at" Iso8601.decoder) (D.maybe (D.field "expires_at" Iso8601.decoder))
M web/src/Pages/Home_.elm

@@ -42,7 +42,7 @@ , content : String

, slug : Maybe String , password : Maybe String , expirationTime : Maybe Int - , dontBurnBeforeExpiration : Bool + , keepBeforeExpiration : Bool , apiError : Maybe Api.Error , userClickedCopyLink : Bool , now : Maybe Posix

@@ -61,7 +61,7 @@ , content = ""

, slug = Nothing , password = Nothing , expirationTime = Nothing - , dontBurnBeforeExpiration = True + , keepBeforeExpiration = True , userClickedCopyLink = False , apiError = Nothing , now = Nothing

@@ -117,7 +117,7 @@ { onResponse = ApiCreateNoteResponded

, content = model.content , slug = model.slug , password = model.password - , burnBeforeExpiration = not model.dontBurnBeforeExpiration + , keepBeforeExpiration = model.keepBeforeExpiration , expiresAt = expiresAt } )

@@ -165,8 +165,8 @@

else ( { model | expirationTime = String.toInt expirationTime }, Effect.none ) - UserClickedCheckbox burnBeforeExpiration -> - ( { model | dontBurnBeforeExpiration = burnBeforeExpiration }, Effect.none ) + UserClickedCheckbox keepBeforeExpiration -> + ( { model | keepBeforeExpiration = keepBeforeExpiration }, Effect.none ) ApiCreateNoteResponded (Ok response) -> ( { model | pageVariant = NoteCreated response.slug, slug = Just response.slug, apiError = Nothing }, Effect.none )

@@ -287,7 +287,7 @@ }

] , H.div [ A.class "space-y-6" ] [ viewExpirationTimeSelector - , viewBurnBeforeExpirationCheckbox (isCheckBoxDisabled model.expirationTime) + , viewKeepBeforeExpirationCheckbox (isCheckBoxDisabled model.expirationTime) ] ] , H.div [ A.class "flex justify-end" ]

@@ -341,20 +341,20 @@ )

] -viewBurnBeforeExpirationCheckbox : Bool -> Html Msg -viewBurnBeforeExpirationCheckbox isDisabled = +viewKeepBeforeExpirationCheckbox : Bool -> Html Msg +viewKeepBeforeExpirationCheckbox isDisabled = H.div [ A.class "space-y-2" ] [ H.div [ A.class "flex items-start space-x-3" ] [ H.input [ E.onCheck UserClickedCheckbox - , A.id "burn" + , A.id "kept" , A.type_ "checkbox" , A.class "mt-1 h-4 w-4 text-black border-gray-300 rounded focus:ring-black focus:ring-2" , A.disabled isDisabled ] [] , H.div [ A.class "flex-1" ] - [ H.label [ A.for "burn", A.class "block text-sm font-medium text-gray-700 cursor-pointer" ] + [ H.label [ A.for "kept", A.class "block text-sm font-medium text-gray-700 cursor-pointer" ] [ H.text "Keep the note until its expiration time, even if it has already been read." ] , H.span [ A.class "block text-sm font-medium text-gray-500 cursor-pointer" ] [ H.text "Can only be used if expiration time is set" ]
M web/src/Pages/Secret/Slug_.elm

@@ -193,7 +193,7 @@

viewShowNoteHeader : Zone -> String -> Note -> Html Msg viewShowNoteHeader zone slug note = H.div [] - [ Components.Utils.viewIf note.burnBeforeExpiration + [ Components.Utils.viewIf note.keepBeforeExpiration (H.div [ A.class "bg-orange-50 border-b border-orange-200 p-4" ] [ H.div [ A.class "flex items-center gap-3" ] [ H.div [ A.class "w-6 h-6 bg-orange-100 rounded-full flex items-center justify-center flex-shrink-0" ]