From 81c2eecd7761bdf81dbde5ee99fe6b5336fb210a Mon Sep 17 00:00:00 2001 From: ae Date: Sun, 4 May 2025 11:11:38 +0300 Subject: [PATCH] feat!: db schema revisions to support note expiration --- server/internal/data/db.go | 2 +- server/internal/data/models.go | 3 +- server/internal/data/note_versions.sql.go | 2 +- server/internal/data/notes.sql.go | 83 +++++++++++++++++++++- server/internal/data/refresh_tokens.sql.go | 15 ++-- server/internal/data/users.sql.go | 2 +- server/sql/migrations/0001_initial.up.sql | 2 + server/sql/queries/notes.sql | 23 ++++++ server/sql/queries/refresh_tokens.sql | 4 +- 9 files changed, 122 insertions(+), 14 deletions(-) diff --git a/server/internal/data/db.go b/server/internal/data/db.go index 062888c..c0466e4 100644 --- a/server/internal/data/db.go +++ b/server/internal/data/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package data diff --git a/server/internal/data/models.go b/server/internal/data/models.go index 17f5b93..668ca86 100644 --- a/server/internal/data/models.go +++ b/server/internal/data/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package data @@ -15,6 +15,7 @@ type Note struct { UserID uuid.UUID `json:"user_id"` CurrentVersion int32 `json:"current_version"` LatestVersion int32 `json:"latest_version"` + ExpiresAt *time.Time `json:"expires_at"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` } diff --git a/server/internal/data/note_versions.sql.go b/server/internal/data/note_versions.sql.go index 7f36ea8..48f157a 100644 --- a/server/internal/data/note_versions.sql.go +++ b/server/internal/data/note_versions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: note_versions.sql package data diff --git a/server/internal/data/notes.sql.go b/server/internal/data/notes.sql.go index c5a444b..0cc5909 100644 --- a/server/internal/data/notes.sql.go +++ b/server/internal/data/notes.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: notes.sql package data @@ -15,7 +15,7 @@ import ( const createNote = `-- name: CreateNote :one INSERT INTO notes (user_id) VALUES ($1) -RETURNING id, user_id, current_version, latest_version, created_at, updated_at +RETURNING id, user_id, current_version, latest_version, expires_at, created_at, updated_at ` func (q *Queries) CreateNote(ctx context.Context, userID uuid.UUID) (Note, error) { @@ -26,12 +26,23 @@ func (q *Queries) CreateNote(ctx context.Context, userID uuid.UUID) (Note, error &i.UserID, &i.CurrentVersion, &i.LatestVersion, + &i.ExpiresAt, &i.CreatedAt, &i.UpdatedAt, ) return i, err } +const deleteExpiredNotes = `-- name: DeleteExpiredNotes :exec +DELETE FROM notes +WHERE expires_at < NOW() +` + +func (q *Queries) DeleteExpiredNotes(ctx context.Context) error { + _, err := q.db.Exec(ctx, deleteExpiredNotes) + return err +} + const deleteNote = `-- name: DeleteNote :exec DELETE FROM notes WHERE id = $1 AND user_id = $2 @@ -55,6 +66,7 @@ SELECT nv.content, nv.version_number, nv.created_at AS version_created_at, + n.expires_at AS note_expires_at, n.created_at AS note_created_at, n.updated_at AS note_updated_at FROM notes n @@ -70,6 +82,7 @@ type GetFullNoteRow struct { Content string `json:"content"` VersionNumber int32 `json:"version_number"` VersionCreatedAt *time.Time `json:"version_created_at"` + NoteExpiresAt *time.Time `json:"note_expires_at"` NoteCreatedAt *time.Time `json:"note_created_at"` NoteUpdatedAt *time.Time `json:"note_updated_at"` } @@ -84,17 +97,64 @@ func (q *Queries) GetFullNote(ctx context.Context, id uuid.UUID) (GetFullNoteRow &i.Content, &i.VersionNumber, &i.VersionCreatedAt, + &i.NoteExpiresAt, &i.NoteCreatedAt, &i.NoteUpdatedAt, ) return i, err } +const listExpiredNotes = `-- name: ListExpiredNotes :many +SELECT + n.id AS note_id, + n.user_id AS owner_id, + nv.title, + n.expires_at +FROM notes n +JOIN note_versions nv + ON n.id = nv.note_id AND n.current_version = nv.version_number +WHERE n.expires_at <= NOW() +ORDER BY n.expires_at +` + +type ListExpiredNotesRow struct { + NoteID uuid.UUID `json:"note_id"` + OwnerID uuid.UUID `json:"owner_id"` + Title string `json:"title"` + ExpiresAt *time.Time `json:"expires_at"` +} + +func (q *Queries) ListExpiredNotes(ctx context.Context) ([]ListExpiredNotesRow, error) { + rows, err := q.db.Query(ctx, listExpiredNotes) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListExpiredNotesRow + for rows.Next() { + var i ListExpiredNotesRow + if err := rows.Scan( + &i.NoteID, + &i.OwnerID, + &i.Title, + &i.ExpiresAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listNotes = `-- name: ListNotes :many SELECT n.id AS note_id, n.user_id AS owner_id, nv.title, + n.expires_at, n.updated_at FROM notes n JOIN note_versions nv @@ -114,6 +174,7 @@ type ListNotesRow struct { NoteID uuid.UUID `json:"note_id"` OwnerID uuid.UUID `json:"owner_id"` Title string `json:"title"` + ExpiresAt *time.Time `json:"expires_at"` UpdatedAt *time.Time `json:"updated_at"` } @@ -130,6 +191,7 @@ func (q *Queries) ListNotes(ctx context.Context, arg ListNotesParams) ([]ListNot &i.NoteID, &i.OwnerID, &i.Title, + &i.ExpiresAt, &i.UpdatedAt, ); err != nil { return nil, err @@ -141,3 +203,20 @@ func (q *Queries) ListNotes(ctx context.Context, arg ListNotesParams) ([]ListNot } return items, nil } + +const setNoteExpiration = `-- name: SetNoteExpiration :exec +UPDATE notes +SET expires_at = $1, updated_at = NOW() +WHERE id = $2 AND user_id = $3 +` + +type SetNoteExpirationParams struct { + ExpiresAt *time.Time `json:"expires_at"` + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` +} + +func (q *Queries) SetNoteExpiration(ctx context.Context, arg SetNoteExpirationParams) error { + _, err := q.db.Exec(ctx, setNoteExpiration, arg.ExpiresAt, arg.ID, arg.UserID) + return err +} diff --git a/server/internal/data/refresh_tokens.sql.go b/server/internal/data/refresh_tokens.sql.go index 2fb3453..5007d1f 100644 --- a/server/internal/data/refresh_tokens.sql.go +++ b/server/internal/data/refresh_tokens.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: refresh_tokens.sql package data @@ -41,14 +41,17 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken return i, err } -const deleteExpiredRefreshTokens = `-- name: DeleteExpiredRefreshTokens :exec +const deleteExpiredRefreshTokens = `-- name: DeleteExpiredRefreshTokens :execrows DELETE FROM refresh_tokens -WHERE expires_at < NOW() +WHERE expires_at < NOW() OR revoked = TRUE ` -func (q *Queries) DeleteExpiredRefreshTokens(ctx context.Context) error { - _, err := q.db.Exec(ctx, deleteExpiredRefreshTokens) - return err +func (q *Queries) DeleteExpiredRefreshTokens(ctx context.Context) (int64, error) { + result, err := q.db.Exec(ctx, deleteExpiredRefreshTokens) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil } const getRefreshTokenByHash = `-- name: GetRefreshTokenByHash :one diff --git a/server/internal/data/users.sql.go b/server/internal/data/users.sql.go index 9b5455e..75a2edc 100644 --- a/server/internal/data/users.sql.go +++ b/server/internal/data/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: users.sql package data diff --git a/server/sql/migrations/0001_initial.up.sql b/server/sql/migrations/0001_initial.up.sql index 8a7eca3..ada305c 100644 --- a/server/sql/migrations/0001_initial.up.sql +++ b/server/sql/migrations/0001_initial.up.sql @@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS notes ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, current_version INT NOT NULL DEFAULT 1, -- active version (can be historical) latest_version INT NOT NULL DEFAULT 1, -- highest version number + expires_at TIMESTAMPTZ DEFAULT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); @@ -45,5 +46,6 @@ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expir CREATE INDEX IF NOT EXISTS idx_notes_user_updated ON notes(user_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_notes_current_version ON notes(current_version); +CREATE INDEX IF NOT EXISTS idx_notes_expires_at ON notes(expires_at) WHERE expires_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_note_versions_content_hash ON note_versions(note_id, content_hash); diff --git a/server/sql/queries/notes.sql b/server/sql/queries/notes.sql index 15140a6..d30d1c2 100644 --- a/server/sql/queries/notes.sql +++ b/server/sql/queries/notes.sql @@ -8,6 +8,7 @@ SELECT n.id AS note_id, n.user_id AS owner_id, nv.title, + n.expires_at, n.updated_at FROM notes n JOIN note_versions nv @@ -24,6 +25,7 @@ SELECT nv.content, nv.version_number, nv.created_at AS version_created_at, + n.expires_at AS note_expires_at, n.created_at AS note_created_at, n.updated_at AS note_updated_at FROM notes n @@ -34,3 +36,24 @@ WHERE n.id = $1; -- name: DeleteNote :exec DELETE FROM notes WHERE id = $1 AND user_id = $2; + +-- name: SetNoteExpiration :exec +UPDATE notes +SET expires_at = $1, updated_at = NOW() +WHERE id = $2 AND user_id = $3; + +-- name: ListExpiredNotes :many +SELECT + n.id AS note_id, + n.user_id AS owner_id, + nv.title, + n.expires_at +FROM notes n +JOIN note_versions nv + ON n.id = nv.note_id AND n.current_version = nv.version_number +WHERE n.expires_at <= NOW() +ORDER BY n.expires_at; + +-- name: DeleteExpiredNotes :exec +DELETE FROM notes +WHERE expires_at < NOW(); diff --git a/server/sql/queries/refresh_tokens.sql b/server/sql/queries/refresh_tokens.sql index 81724f6..f6f2239 100644 --- a/server/sql/queries/refresh_tokens.sql +++ b/server/sql/queries/refresh_tokens.sql @@ -20,6 +20,6 @@ UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1; --- name: DeleteExpiredRefreshTokens :exec +-- name: DeleteExpiredRefreshTokens :execrows DELETE FROM refresh_tokens -WHERE expires_at < NOW(); \ No newline at end of file +WHERE expires_at < NOW() OR revoked = TRUE;