diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 2dcb0e3..3f1abc0 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -992,3 +992,36 @@ CREATE TABLE IF NOT EXISTS unlinked_books ( CREATE INDEX IF NOT EXISTS idx_unlinked_books_device ON unlinked_books(device_id); CREATE INDEX IF NOT EXISTS idx_unlinked_books_content_id ON unlinked_books(content_id); CREATE INDEX IF NOT EXISTS idx_unlinked_books_resolved ON unlinked_books(resolved); + +-- Create saved_filters for users to be able to filter different parts of the application +CREATE TABLE IF NOT EXISTS saved_filters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + resource_type TEXT NOT NULL, -- 'media-items', 'collections', 'devices', etc. + filters JSONB NOT NULL, -- {search: "", author_filter: "", genre: "", ...} + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Index for efficient user+resource lookups +CREATE INDEX IF NOT EXISTS idx_saved_filters_user_resource ON saved_filters(user_id, resource_type); + +-- Index for name searches (future feature) +CREATE INDEX IF NOT EXISTS idx_saved_filters_name ON saved_filters(user_id, name); + +-- Trigger to auto-update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +DROP TRIGGER IF EXISTS update_saved_filters_updated_at ON saved_filters; + +CREATE TRIGGER update_saved_filters_updated_at + BEFORE UPDATE ON saved_filters + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/internal/database/models.go b/internal/database/models.go index 516ff6c..6473f3e 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -319,6 +319,16 @@ type RefreshTokens struct { RevokedAt pgtype.Timestamptz `db:"revoked_at" json:"revoked_at"` } +type SavedFilters struct { + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Name string `db:"name" json:"name"` + ResourceType string `db:"resource_type" json:"resource_type"` + Filters []byte `db:"filters" json:"filters"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + type SyncConflicts struct { ID pgtype.UUID `db:"id" json:"id"` MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` diff --git a/internal/database/querier.go b/internal/database/querier.go index 74f65e6..22e2df6 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -72,6 +72,7 @@ type Querier interface { CreateReadingHistory(ctx context.Context, arg CreateReadingHistoryParams) (ReadingHistory, error) // Refresh Tokens queries CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshTokens, error) + CreateSavedFilter(ctx context.Context, arg CreateSavedFilterParams) (SavedFilters, error) // Conflict Resolution CreateSyncConflict(ctx context.Context, arg CreateSyncConflictParams) (SyncConflicts, error) CreateSyncHistoryEntry(ctx context.Context, arg CreateSyncHistoryEntryParams) (SyncQueue, error) @@ -104,6 +105,7 @@ type Querier interface { DeleteMediaNote(ctx context.Context, id pgtype.UUID) error DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error + DeleteSavedFilter(ctx context.Context, arg DeleteSavedFilterParams) error DeleteSyncConflict(ctx context.Context, id pgtype.UUID) error DeleteSyncQueueItem(ctx context.Context, id pgtype.UUID) error // Delete system config @@ -212,6 +214,8 @@ type Querier interface { GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAddedItemsParams) ([]MediaItems, error) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadItemsParams) ([]MediaItems, error) GetRefreshToken(ctx context.Context, token pgtype.UUID) (GetRefreshTokenRow, error) + GetSavedFilterByID(ctx context.Context, arg GetSavedFilterByIDParams) (SavedFilters, error) + GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams) ([]SavedFilters, error) GetStuckSyncQueueItems(ctx context.Context) ([]SyncQueue, error) GetSyncConflict(ctx context.Context, id pgtype.UUID) (SyncConflicts, error) GetSyncQueueItem(ctx context.Context, id pgtype.UUID) (SyncQueue, error) @@ -328,6 +332,7 @@ type Querier interface { UpdateMediaRating(ctx context.Context, arg UpdateMediaRatingParams) (MediaRatings, error) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) + UpdateSavedFilter(ctx context.Context, arg UpdateSavedFilterParams) (SavedFilters, error) UpdateSyncQueueItemStatus(ctx context.Context, arg UpdateSyncQueueItemStatusParams) (SyncQueue, error) UpdateSystemSetting(ctx context.Context, arg UpdateSystemSettingParams) error // Update universal progress diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index f0c5414..db92391 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -960,6 +960,39 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken return i, err } +const CreateSavedFilter = `-- name: CreateSavedFilter :one +INSERT INTO saved_filters (user_id, name, resource_type, filters) +VALUES ($1, $2, $3, $4) +RETURNING id, user_id, name, resource_type, filters, created_at, updated_at +` + +type CreateSavedFilterParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Name string `db:"name" json:"name"` + ResourceType string `db:"resource_type" json:"resource_type"` + Filters []byte `db:"filters" json:"filters"` +} + +func (q *Queries) CreateSavedFilter(ctx context.Context, arg CreateSavedFilterParams) (SavedFilters, error) { + row := q.db.QueryRow(ctx, CreateSavedFilter, + arg.UserID, + arg.Name, + arg.ResourceType, + arg.Filters, + ) + var i SavedFilters + err := row.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.ResourceType, + &i.Filters, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const CreateSyncConflict = `-- name: CreateSyncConflict :one INSERT INTO sync_conflicts (media_item_id, user_id, conflict_type, conflict_data) VALUES ($1, $2, $3, $4) @@ -1389,6 +1422,21 @@ func (q *Queries) DeleteReadingProgress(ctx context.Context, arg DeleteReadingPr return err } +const DeleteSavedFilter = `-- name: DeleteSavedFilter :exec +DELETE FROM saved_filters +WHERE id = $1 AND user_id = $2 +` + +type DeleteSavedFilterParams struct { + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) DeleteSavedFilter(ctx context.Context, arg DeleteSavedFilterParams) error { + _, err := q.db.Exec(ctx, DeleteSavedFilter, arg.ID, arg.UserID) + return err +} + const DeleteSyncConflict = `-- name: DeleteSyncConflict :exec DELETE FROM sync_conflicts WHERE id = $1 ` @@ -4393,6 +4441,70 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token pgtype.UUID) (GetRe return i, err } +const GetSavedFilterByID = `-- name: GetSavedFilterByID :one +SELECT id, user_id, name, resource_type, filters, created_at, updated_at FROM saved_filters +WHERE id = $1 AND user_id = $2 +` + +type GetSavedFilterByIDParams struct { + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) GetSavedFilterByID(ctx context.Context, arg GetSavedFilterByIDParams) (SavedFilters, error) { + row := q.db.QueryRow(ctx, GetSavedFilterByID, arg.ID, arg.UserID) + var i SavedFilters + err := row.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.ResourceType, + &i.Filters, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetSavedFilters = `-- name: GetSavedFilters :many +SELECT id, user_id, name, resource_type, filters, created_at, updated_at FROM saved_filters +WHERE user_id = $1 AND resource_type = $2 +ORDER BY created_at DESC +` + +type GetSavedFiltersParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ResourceType string `db:"resource_type" json:"resource_type"` +} + +func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams) ([]SavedFilters, error) { + rows, err := q.db.Query(ctx, GetSavedFilters, arg.UserID, arg.ResourceType) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SavedFilters{} + for rows.Next() { + var i SavedFilters + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.ResourceType, + &i.Filters, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetStuckSyncQueueItems = `-- name: GetStuckSyncQueueItems :many SELECT id, device_id, media_item_id, sync_type, sync_data, priority, attempts, max_attempts, status, error_message, created_at, processed_at FROM sync_queue WHERE status = 'processing' AND created_at < NOW() - INTERVAL '1 hour' @@ -8373,6 +8485,42 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr return i, err } +const UpdateSavedFilter = `-- name: UpdateSavedFilter :one +UPDATE saved_filters +SET name = $1, + filters = $2, + updated_at = NOW() +WHERE id = $3 AND user_id = $4 +RETURNING id, user_id, name, resource_type, filters, created_at, updated_at +` + +type UpdateSavedFilterParams struct { + Name string `db:"name" json:"name"` + Filters []byte `db:"filters" json:"filters"` + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) UpdateSavedFilter(ctx context.Context, arg UpdateSavedFilterParams) (SavedFilters, error) { + row := q.db.QueryRow(ctx, UpdateSavedFilter, + arg.Name, + arg.Filters, + arg.ID, + arg.UserID, + ) + var i SavedFilters + err := row.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.ResourceType, + &i.Filters, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const UpdateSyncQueueItemStatus = `-- name: UpdateSyncQueueItemStatus :one UPDATE sync_queue SET diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index b56b5a9..baa54cf 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -1721,3 +1721,29 @@ LIMIT $3; SELECT mi.* FROM media_items mi WHERE mi.library_id = $1 ORDER BY mi.created_at DESC; + +-- name: GetSavedFilters :many +SELECT * FROM saved_filters +WHERE user_id = @user_id AND resource_type = @resource_type +ORDER BY created_at DESC; + +-- name: GetSavedFilterByID :one +SELECT * FROM saved_filters +WHERE id = @id AND user_id = @user_id; + +-- name: CreateSavedFilter :one +INSERT INTO saved_filters (user_id, name, resource_type, filters) +VALUES (@user_id, @name, @resource_type, @filters) +RETURNING *; + +-- name: UpdateSavedFilter :one +UPDATE saved_filters +SET name = @name, + filters = @filters, + updated_at = NOW() +WHERE id = @id AND user_id = @user_id +RETURNING *; + +-- name: DeleteSavedFilter :exec +DELETE FROM saved_filters +WHERE id = @id AND user_id = @user_id;