feat(db): add saved_filters table and CRUD operations
Add database schema and SQL queries for generic saved filters system that allows users to save custom filter presets for any resource type. Database Schema: - Add saved_filters table with user_id, name, resource_type, filters (JSONB) - Create composite index on (user_id, resource_type) for efficient lookups - Create index on (user_id, name) for future name search feature - Add update_updated_at_column() trigger to auto-update timestamps - Make trigger creation idempotent with DROP TRIGGER IF EXISTS SQL Queries (5 new queries): - GetSavedFilters: List all filters for user + resource type - GetSavedFilterByID: Retrieve single filter by ID - CreateSavedFilter: Create new saved filter - UpdateSavedFilter: Update filter name/criteria - DeleteSavedFilter: Remove saved filter Design Decisions: - Generic resource_type field supports any resource (media-items, collections, devices) - JSONB filters field allows flexible schema without migrations - User-scoped via JWT (user_id foreign key with CASCADE delete) - Automatic updated_at timestamp via database trigger Generated Code: - database.SavedFilters model (10 fields including JSONB filters) - All 5 CRUD query functions with proper parameter types - pgtype.UUID wrappers for UUID parameters Part of: Saved Filters Implementation (Phase 1: Database) Related: #saved-filters-feature
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user