feat(dashboard): global (All Libraries) preferences scope

The dashboard's All Libraries view could render sections across every
library but could not persist its own preferences: the PUT required a
UUID library_id, and the prefs GET-without-library_id matched an
arbitrary library's row through a loose NULL OR-clause.

Storage: a dedicated user_dashboard_preferences_global table (one row
per user, user_id PK → users CASCADE) instead of a NULL/sentinel
library_id — chosen over both because (a) the column's FK to libraries
rules out a sentinel UUID without dropping the constraint or minting a
fake library row, and (b) intent becomes structural: only the explicit
global path writes that table, so a buggy or outside-the-API write can
never masquerade as global. System and user collections are
user-scoped, so the order/hidden arrays are library-independent and
valid in this scope.

API: PUT /api/dashboard/preferences binds library_id as *string —
JSON null/absent = global, empty string = 400 (a lost value must stay
loud, not silently clobber the global scope), valid UUID = per-library
(unchanged). GET without library_id now returns the global row instead
of an arbitrary one; the per-library query's dead NULL OR-arm is
removed. GET /sections keeps requiring library_id for per-library
views and aggregates across libraries when absent (unchanged).

Tests: global save/update-dedupe/read round-trip, empty-string 400,
global-vs-per-library independence. Two suite failures are the
handoff's documented pre-existing rot (duplicate system collections in
the dev DB; a sections test expecting a 400 the handler never
implemented) — reproduced and confirmed unrelated to this change.
Live-verified on the dev stack: null save 200 x2 with a single row
updated, empty string 400, global read carries the second save.
This commit is contained in:
John O'Keefe committed 2026-10-07 19:46:17 -04:00
1 parent b8db1d109c
commit 25d44452c0
8 files changed
+304 -7

No files matched your search

+14
View File
@@ -1160,6 +1160,20 @@ CREATE TABLE IF NOT EXISTS user_dashboard_preferences (
-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_dashboard_prefs_user_library ON user_dashboard_preferences(user_id, library_id);
-- Global (all-libraries) dashboard preferences: exactly one row per user.
-- Backs the dashboard's "All Libraries" view (library_id NULL convention
-- rejected: the upsert's ON CONFLICT cannot dedupe NULLs and intent would
-- be invisible — a dedicated table makes the scope structural). System
-- and user collections are user-scoped, so collection_order/
-- hidden_collections IDs are library-independent and valid in this scope.
CREATE TABLE IF NOT EXISTS user_dashboard_preferences_global (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
hidden_collections TEXT[] DEFAULT '{}',
collection_order TEXT[] DEFAULT '{}',
items_per_section INT DEFAULT 20,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create collection_items table (which books belong to each collection)
CREATE TABLE IF NOT EXISTS collection_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),