feat(db): queries for the deleted-annotation history
ListDeletedAnnotationsForBook unions tombstoned highlights, notes, and bookmarks for a user+book regardless of the sync TTL cutoff (the history must show everything still restorable, not just recent deletes), with display text, secondary text, color, and both timestamps. Restore queries clear deleted/deleted_at (lossless — the row was soft- deleted, never removed) and are scoped to the owning user and media item so a restore can never touch another user's annotation. Purge queries hard-delete an already-tombstoned row: the user-driven counterpart of the TTL maintenance sweep, for explicit 'delete permanently' actions from the history. All six write queries are :execrows so callers can distinguish 'restored' from 'nothing matched' without a follow-up read.
This commit is contained in:
@@ -8127,6 +8127,98 @@ func (q *Queries) ListConflictsByUser(ctx context.Context, userID pgtype.UUID) (
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListDeletedAnnotationsForBook = `-- name: ListDeletedAnnotationsForBook :many
|
||||
|
||||
SELECT
|
||||
mh.id,
|
||||
mh.dedup_key,
|
||||
'highlight' as annotation_type,
|
||||
mh.selection_text as display_text,
|
||||
mh.note_text as secondary_text,
|
||||
mh.color,
|
||||
mh.deleted_at,
|
||||
mh.created_at
|
||||
FROM media_highlights mh
|
||||
WHERE mh.media_item_id = $1 AND mh.user_id = $2 AND mh.deleted = TRUE
|
||||
UNION ALL
|
||||
SELECT
|
||||
mn.id,
|
||||
mn.dedup_key,
|
||||
'note' as annotation_type,
|
||||
mn.content as display_text,
|
||||
NULL::text as secondary_text,
|
||||
NULL::text as color,
|
||||
mn.deleted_at,
|
||||
mn.created_at
|
||||
FROM media_notes mn
|
||||
WHERE mn.media_item_id = $1 AND mn.user_id = $2 AND mn.deleted = TRUE
|
||||
UNION ALL
|
||||
SELECT
|
||||
mb.id,
|
||||
mb.dedup_key,
|
||||
'bookmark' as annotation_type,
|
||||
mb.title as display_text,
|
||||
mb.notes as secondary_text,
|
||||
NULL::text as color,
|
||||
mb.deleted_at,
|
||||
mb.created_at
|
||||
FROM media_bookmarks mb
|
||||
WHERE mb.media_item_id = $1 AND mb.user_id = $2 AND mb.deleted = TRUE
|
||||
ORDER BY deleted_at DESC
|
||||
`
|
||||
|
||||
type ListDeletedAnnotationsForBookParams struct {
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
}
|
||||
|
||||
type ListDeletedAnnotationsForBookRow struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
DedupKey pgtype.Text `db:"dedup_key" json:"dedup_key"`
|
||||
AnnotationType string `db:"annotation_type" json:"annotation_type"`
|
||||
DisplayText string `db:"display_text" json:"display_text"`
|
||||
SecondaryText pgtype.Text `db:"secondary_text" json:"secondary_text"`
|
||||
Color pgtype.Text `db:"color" json:"color"`
|
||||
DeletedAt pgtype.Timestamptz `db:"deleted_at" json:"deleted_at"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ANNOTATION HISTORY (deleted-annotation archive)
|
||||
// ============================================
|
||||
// Lists every currently-tombstoned annotation for a book regardless of the
|
||||
// tombstone TTL: this backs the book page's "recently deleted" history where
|
||||
// users can restore or permanently remove entries. Rows whose tombstones have
|
||||
// been purged by the daily maintenance sweep no longer exist at all.
|
||||
func (q *Queries) ListDeletedAnnotationsForBook(ctx context.Context, arg ListDeletedAnnotationsForBookParams) ([]ListDeletedAnnotationsForBookRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListDeletedAnnotationsForBook, arg.MediaItemID, arg.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListDeletedAnnotationsForBookRow{}
|
||||
for rows.Next() {
|
||||
var i ListDeletedAnnotationsForBookRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.DedupKey,
|
||||
&i.AnnotationType,
|
||||
&i.DisplayText,
|
||||
&i.SecondaryText,
|
||||
&i.Color,
|
||||
&i.DeletedAt,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListDevicesByType = `-- name: ListDevicesByType :many
|
||||
SELECT id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at FROM devices WHERE device_type = $1 ORDER BY created_at DESC
|
||||
`
|
||||
@@ -9419,6 +9511,65 @@ func (q *Queries) PurgeExpiredNoteTombstones(ctx context.Context, deletedAt pgty
|
||||
return err
|
||||
}
|
||||
|
||||
const PurgeMediaBookmarkByID = `-- name: PurgeMediaBookmarkByID :execrows
|
||||
DELETE FROM media_bookmarks
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type PurgeMediaBookmarkByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) PurgeMediaBookmarkByID(ctx context.Context, arg PurgeMediaBookmarkByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, PurgeMediaBookmarkByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const PurgeMediaHighlightByID = `-- name: PurgeMediaHighlightByID :execrows
|
||||
DELETE FROM media_highlights
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type PurgeMediaHighlightByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
// Permanent removal from the history (distinct from the TTL-driven purge,
|
||||
// which is maintenance). Scoped to the owning user and book.
|
||||
func (q *Queries) PurgeMediaHighlightByID(ctx context.Context, arg PurgeMediaHighlightByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, PurgeMediaHighlightByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const PurgeMediaNoteByID = `-- name: PurgeMediaNoteByID :execrows
|
||||
DELETE FROM media_notes
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type PurgeMediaNoteByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) PurgeMediaNoteByID(ctx context.Context, arg PurgeMediaNoteByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, PurgeMediaNoteByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const QueryMediaItemsByIdentifiers = `-- name: QueryMediaItemsByIdentifiers :many
|
||||
SELECT
|
||||
mi.id,
|
||||
@@ -9752,6 +9903,72 @@ func (q *Queries) ResolveUnlinkedBook(ctx context.Context, arg ResolveUnlinkedBo
|
||||
return i, err
|
||||
}
|
||||
|
||||
const RestoreMediaBookmarkByID = `-- name: RestoreMediaBookmarkByID :execrows
|
||||
UPDATE media_bookmarks SET
|
||||
deleted = FALSE,
|
||||
deleted_at = NULL,
|
||||
last_modified_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type RestoreMediaBookmarkByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RestoreMediaBookmarkByID(ctx context.Context, arg RestoreMediaBookmarkByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, RestoreMediaBookmarkByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const RestoreMediaHighlightByID = `-- name: RestoreMediaHighlightByID :execrows
|
||||
UPDATE media_highlights SET
|
||||
deleted = FALSE,
|
||||
deleted_at = NULL,
|
||||
last_modified_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type RestoreMediaHighlightByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RestoreMediaHighlightByID(ctx context.Context, arg RestoreMediaHighlightByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, RestoreMediaHighlightByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const RestoreMediaNoteByID = `-- name: RestoreMediaNoteByID :execrows
|
||||
UPDATE media_notes SET
|
||||
deleted = FALSE,
|
||||
deleted_at = NULL,
|
||||
last_modified_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2 AND media_item_id = $3 AND deleted = TRUE
|
||||
`
|
||||
|
||||
type RestoreMediaNoteByIDParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RestoreMediaNoteByID(ctx context.Context, arg RestoreMediaNoteByIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, RestoreMediaNoteByID, arg.ID, arg.UserID, arg.MediaItemID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const RevokeAllUserRefreshTokens = `-- name: RevokeAllUserRefreshTokens :exec
|
||||
UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user