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:
2026-03-21 00:16:05 -04:00
parent a63394f429
commit e17a96123f
5 changed files with 222 additions and 0 deletions
+148
View File
@@ -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