feat(api): consolidate user profile update endpoints

Add delete_user, reset_user_password, and update_user endpoints to replace
individual update operations. Update database schema to include deleted_at
column for soft deletion. Add DeleteUser, ResetUserPassword, and
UpdateUserAdmin queries. Update Querier with new methods for user management.
This commit is contained in:
2026-02-22 01:57:42 -05:00
parent 86cc9b4e59
commit e3a3aa124f
4 changed files with 173 additions and 31 deletions
+4 -1
View File
@@ -77,6 +77,7 @@ type Querier interface {
CreateSyncHistoryEntry(ctx context.Context, arg CreateSyncHistoryEntryParams) (SyncQueue, error)
// Sync Queue Management
CreateSyncQueueItem(ctx context.Context, arg CreateSyncQueueItemParams) (SyncQueue, error)
CreateSystemCollection(ctx context.Context, arg CreateSystemCollectionParams) (Collections, error)
// ============================================
// ENHANCED KOBO SYNC
// ============================================
@@ -215,7 +216,7 @@ type Querier interface {
GetSyncQueueItem(ctx context.Context, id pgtype.UUID) (SyncQueue, error)
GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (GetSyncQueueStatsRow, error)
// Dashboard collections queries
GetSystemCollectionsForDashboard(ctx context.Context) ([]Collections, error)
GetSystemCollectionsForDashboard(ctx context.Context, userID pgtype.UUID) ([]Collections, error)
// SYSTEM CONFIG QUERIES
// Get system config
GetSystemConfig(ctx context.Context, key string) (SystemConfig, error)
@@ -269,6 +270,7 @@ type Querier interface {
// Remove book from collection
RemoveBookFromCollection(ctx context.Context, arg RemoveBookFromCollectionParams) error
RemoveBookFromKoboShelf(ctx context.Context, arg RemoveBookFromKoboShelfParams) error
ResetSystemCollectionMetadata(ctx context.Context, arg ResetSystemCollectionMetadataParams) error
ResolveSyncConflict(ctx context.Context, arg ResolveSyncConflictParams) (SyncConflicts, error)
// Resolve unlinked book
ResolveUnlinkedBook(ctx context.Context, arg ResolveUnlinkedBookParams) (UnlinkedBooks, error)
@@ -331,6 +333,7 @@ type Querier interface {
UpdateUniversalProgress(ctx context.Context, arg UpdateUniversalProgressParams) (ReadingProgress, error)
UpdateUserMaxDevices(ctx context.Context, arg UpdateUserMaxDevicesParams) (Users, error)
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UpdateUserRoleRow, error)
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error)
+129 -12
View File
@@ -1072,6 +1072,53 @@ func (q *Queries) CreateSyncQueueItem(ctx context.Context, arg CreateSyncQueueIt
return i, err
}
const CreateSystemCollection = `-- name: CreateSystemCollection :one
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection, auto_assign_rules)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, 'null'::jsonb)
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at
`
type CreateSystemCollectionParams struct {
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Icon pgtype.Text `db:"icon" json:"icon"`
Color pgtype.Text `db:"color" json:"color"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
QueryType pgtype.Text `db:"query_type" json:"query_type"`
Priority pgtype.Int4 `db:"priority" json:"priority"`
}
func (q *Queries) CreateSystemCollection(ctx context.Context, arg CreateSystemCollectionParams) (Collections, error) {
row := q.db.QueryRow(ctx, CreateSystemCollection,
arg.UserID,
arg.Name,
arg.Description,
arg.Icon,
arg.Color,
arg.ShowOnDashboard,
arg.QueryType,
arg.Priority,
)
var i Collections
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
)
return i, err
}
const CreateUnlinkedBook = `-- name: CreateUnlinkedBook :one
INSERT INTO unlinked_books (device_id, content_id, file_path, title, author, confidence_score)
@@ -1901,12 +1948,16 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
}
const GetContinueReadingItems = `-- name: GetContinueReadingItems :many
SELECT DISTINCT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
INNER JOIN reading_progress rp ON rp.media_item_id = mi.id
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress
WHERE user_id = $2
AND percentage > 0
AND percentage < 1
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE mi.library_id = $1
AND rp.user_id = $2
AND rp.percentage > 0
AND rp.percentage < 1
ORDER BY rp.last_read_at DESC
LIMIT $3
`
@@ -4157,11 +4208,15 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde
}
const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many
SELECT DISTINCT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
INNER JOIN reading_progress rp ON rp.media_item_id = mi.id
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress
WHERE user_id = $2
AND percentage >= 1
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE mi.library_id = $1
AND rp.user_id = $2
AND rp.percentage >= 1
ORDER BY rp.last_read_at DESC
LIMIT $3
`
@@ -4392,14 +4447,15 @@ func (q *Queries) GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (
const GetSystemCollectionsForDashboard = `-- name: GetSystemCollectionsForDashboard :many
SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at FROM collections
WHERE user_id IS NULL
WHERE user_id = $1
AND is_system_collection = true
AND show_on_dashboard = true
ORDER BY priority ASC
`
// Dashboard collections queries
func (q *Queries) GetSystemCollectionsForDashboard(ctx context.Context) ([]Collections, error) {
rows, err := q.db.Query(ctx, GetSystemCollectionsForDashboard)
func (q *Queries) GetSystemCollectionsForDashboard(ctx context.Context, userID pgtype.UUID) ([]Collections, error) {
rows, err := q.db.Query(ctx, GetSystemCollectionsForDashboard, userID)
if err != nil {
return nil, err
}
@@ -6649,6 +6705,38 @@ func (q *Queries) RemoveBookFromKoboShelf(ctx context.Context, arg RemoveBookFro
return err
}
const ResetSystemCollectionMetadata = `-- name: ResetSystemCollectionMetadata :exec
UPDATE collections
SET description = $3,
icon = $4,
color = $5,
priority = $6
WHERE user_id = $1
AND name = $2
AND is_system_collection = true
`
type ResetSystemCollectionMetadataParams struct {
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Icon pgtype.Text `db:"icon" json:"icon"`
Color pgtype.Text `db:"color" json:"color"`
Priority pgtype.Int4 `db:"priority" json:"priority"`
}
func (q *Queries) ResetSystemCollectionMetadata(ctx context.Context, arg ResetSystemCollectionMetadataParams) error {
_, err := q.db.Exec(ctx, ResetSystemCollectionMetadata,
arg.UserID,
arg.Name,
arg.Description,
arg.Icon,
arg.Color,
arg.Priority,
)
return err
}
const ResolveSyncConflict = `-- name: ResolveSyncConflict :one
UPDATE sync_conflicts
SET
@@ -8430,6 +8518,35 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
return err
}
const UpdateUserRole = `-- name: UpdateUserRole :one
UPDATE users SET role = $2, updated_at = NOW() WHERE id = $1
RETURNING id, email, username, role
`
type UpdateUserRoleParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Role string `db:"role" json:"role"`
}
type UpdateUserRoleRow struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
Role string `db:"role" json:"role"`
}
func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UpdateUserRoleRow, error) {
row := q.db.QueryRow(ctx, UpdateUserRole, arg.ID, arg.Role)
var i UpdateUserRoleRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Role,
)
return i, err
}
const UpdateUserTheme = `-- name: UpdateUserTheme :exec
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1
`
+38 -10
View File
@@ -337,6 +337,10 @@ UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1;
UPDATE users SET max_devices = $2, updated_at = NOW() WHERE id = $1
RETURNING *;
-- name: UpdateUserRole :one
UPDATE users SET role = $2, updated_at = NOW() WHERE id = $1
RETURNING id, email, username, role;
-- name: CountUserDevices :one
SELECT COUNT(*) FROM devices WHERE user_id = $1;
@@ -1621,7 +1625,8 @@ RETURNING *;
-- Dashboard collections queries
-- name: GetSystemCollectionsForDashboard :many
SELECT * FROM collections
WHERE user_id IS NULL
WHERE user_id = $1
AND is_system_collection = true
AND show_on_dashboard = true
ORDER BY priority ASC;
@@ -1638,14 +1643,33 @@ WHERE user_id = $1
AND name = $2
AND is_system_collection = true;
-- name: CreateSystemCollection :one
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection, auto_assign_rules)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, 'null'::jsonb)
RETURNING *;
-- name: ResetSystemCollectionMetadata :exec
UPDATE collections
SET description = $3,
icon = $4,
color = $5,
priority = $6
WHERE user_id = $1
AND name = $2
AND is_system_collection = true;
-- Smart section queries (for system collections)
-- name: GetContinueReadingItems :many
SELECT DISTINCT mi.* FROM media_items mi
INNER JOIN reading_progress rp ON rp.media_item_id = mi.id
SELECT mi.* FROM media_items mi
INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress
WHERE user_id = $2
AND percentage > 0
AND percentage < 1
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE mi.library_id = $1
AND rp.user_id = $2
AND rp.percentage > 0
AND rp.percentage < 1
ORDER BY rp.last_read_at DESC
LIMIT $3;
@@ -1656,11 +1680,15 @@ ORDER BY mi.created_at DESC
LIMIT $2;
-- name: GetRecentlyReadItems :many
SELECT DISTINCT mi.* FROM media_items mi
INNER JOIN reading_progress rp ON rp.media_item_id = mi.id
SELECT mi.* FROM media_items mi
INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress
WHERE user_id = $2
AND percentage >= 1
ORDER BY media_item_id, last_read_at DESC
) rp ON rp.media_item_id = mi.id
WHERE mi.library_id = $1
AND rp.user_id = $2
AND rp.percentage >= 1
ORDER BY rp.last_read_at DESC
LIMIT $3;