feat!: db schema revisions to support note expiration

This commit is contained in:
ae 2025-05-04 11:11:38 +03:00
parent 7026ee4c76
commit 81c2eecd77
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
9 changed files with 122 additions and 14 deletions

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
package data package data

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
package data package data
@ -15,6 +15,7 @@ type Note struct {
UserID uuid.UUID `json:"user_id"` UserID uuid.UUID `json:"user_id"`
CurrentVersion int32 `json:"current_version"` CurrentVersion int32 `json:"current_version"`
LatestVersion int32 `json:"latest_version"` LatestVersion int32 `json:"latest_version"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt *time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: note_versions.sql // source: note_versions.sql
package data package data

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: notes.sql // source: notes.sql
package data package data
@ -15,7 +15,7 @@ import (
const createNote = `-- name: CreateNote :one const createNote = `-- name: CreateNote :one
INSERT INTO notes (user_id) INSERT INTO notes (user_id)
VALUES ($1) 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) { 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.UserID,
&i.CurrentVersion, &i.CurrentVersion,
&i.LatestVersion, &i.LatestVersion,
&i.ExpiresAt,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
return i, err 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 const deleteNote = `-- name: DeleteNote :exec
DELETE FROM notes DELETE FROM notes
WHERE id = $1 AND user_id = $2 WHERE id = $1 AND user_id = $2
@ -55,6 +66,7 @@ SELECT
nv.content, nv.content,
nv.version_number, nv.version_number,
nv.created_at AS version_created_at, nv.created_at AS version_created_at,
n.expires_at AS note_expires_at,
n.created_at AS note_created_at, n.created_at AS note_created_at,
n.updated_at AS note_updated_at n.updated_at AS note_updated_at
FROM notes n FROM notes n
@ -70,6 +82,7 @@ type GetFullNoteRow struct {
Content string `json:"content"` Content string `json:"content"`
VersionNumber int32 `json:"version_number"` VersionNumber int32 `json:"version_number"`
VersionCreatedAt *time.Time `json:"version_created_at"` VersionCreatedAt *time.Time `json:"version_created_at"`
NoteExpiresAt *time.Time `json:"note_expires_at"`
NoteCreatedAt *time.Time `json:"note_created_at"` NoteCreatedAt *time.Time `json:"note_created_at"`
NoteUpdatedAt *time.Time `json:"note_updated_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.Content,
&i.VersionNumber, &i.VersionNumber,
&i.VersionCreatedAt, &i.VersionCreatedAt,
&i.NoteExpiresAt,
&i.NoteCreatedAt, &i.NoteCreatedAt,
&i.NoteUpdatedAt, &i.NoteUpdatedAt,
) )
return i, err 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 const listNotes = `-- name: ListNotes :many
SELECT SELECT
n.id AS note_id, n.id AS note_id,
n.user_id AS owner_id, n.user_id AS owner_id,
nv.title, nv.title,
n.expires_at,
n.updated_at n.updated_at
FROM notes n FROM notes n
JOIN note_versions nv JOIN note_versions nv
@ -114,6 +174,7 @@ type ListNotesRow struct {
NoteID uuid.UUID `json:"note_id"` NoteID uuid.UUID `json:"note_id"`
OwnerID uuid.UUID `json:"owner_id"` OwnerID uuid.UUID `json:"owner_id"`
Title string `json:"title"` Title string `json:"title"`
ExpiresAt *time.Time `json:"expires_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }
@ -130,6 +191,7 @@ func (q *Queries) ListNotes(ctx context.Context, arg ListNotesParams) ([]ListNot
&i.NoteID, &i.NoteID,
&i.OwnerID, &i.OwnerID,
&i.Title, &i.Title,
&i.ExpiresAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -141,3 +203,20 @@ func (q *Queries) ListNotes(ctx context.Context, arg ListNotesParams) ([]ListNot
} }
return items, nil 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
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: refresh_tokens.sql // source: refresh_tokens.sql
package data package data
@ -41,14 +41,17 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken
return i, err return i, err
} }
const deleteExpiredRefreshTokens = `-- name: DeleteExpiredRefreshTokens :exec const deleteExpiredRefreshTokens = `-- name: DeleteExpiredRefreshTokens :execrows
DELETE FROM refresh_tokens DELETE FROM refresh_tokens
WHERE expires_at < NOW() WHERE expires_at < NOW() OR revoked = TRUE
` `
func (q *Queries) DeleteExpiredRefreshTokens(ctx context.Context) error { func (q *Queries) DeleteExpiredRefreshTokens(ctx context.Context) (int64, error) {
_, err := q.db.Exec(ctx, deleteExpiredRefreshTokens) result, err := q.db.Exec(ctx, deleteExpiredRefreshTokens)
return err if err != nil {
return 0, err
}
return result.RowsAffected(), nil
} }
const getRefreshTokenByHash = `-- name: GetRefreshTokenByHash :one const getRefreshTokenByHash = `-- name: GetRefreshTokenByHash :one

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: users.sql // source: users.sql
package data package data

View File

@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS notes (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
current_version INT NOT NULL DEFAULT 1, -- active version (can be historical) current_version INT NOT NULL DEFAULT 1, -- active version (can be historical)
latest_version INT NOT NULL DEFAULT 1, -- highest version number latest_version INT NOT NULL DEFAULT 1, -- highest version number
expires_at TIMESTAMPTZ DEFAULT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_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_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_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); CREATE INDEX IF NOT EXISTS idx_note_versions_content_hash ON note_versions(note_id, content_hash);

View File

@ -8,6 +8,7 @@ SELECT
n.id AS note_id, n.id AS note_id,
n.user_id AS owner_id, n.user_id AS owner_id,
nv.title, nv.title,
n.expires_at,
n.updated_at n.updated_at
FROM notes n FROM notes n
JOIN note_versions nv JOIN note_versions nv
@ -24,6 +25,7 @@ SELECT
nv.content, nv.content,
nv.version_number, nv.version_number,
nv.created_at AS version_created_at, nv.created_at AS version_created_at,
n.expires_at AS note_expires_at,
n.created_at AS note_created_at, n.created_at AS note_created_at,
n.updated_at AS note_updated_at n.updated_at AS note_updated_at
FROM notes n FROM notes n
@ -34,3 +36,24 @@ WHERE n.id = $1;
-- name: DeleteNote :exec -- name: DeleteNote :exec
DELETE FROM notes DELETE FROM notes
WHERE id = $1 AND user_id = $2; 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();

View File

@ -20,6 +20,6 @@ UPDATE refresh_tokens
SET revoked = TRUE SET revoked = TRUE
WHERE user_id = $1; WHERE user_id = $1;
-- name: DeleteExpiredRefreshTokens :exec -- name: DeleteExpiredRefreshTokens :execrows
DELETE FROM refresh_tokens DELETE FROM refresh_tokens
WHERE expires_at < NOW(); WHERE expires_at < NOW() OR revoked = TRUE;