Documentation
¶
Overview ¶
Package sortspec parses, validates, builds SQL for, and resolves multi-level book sort specifications (e.g. "author:asc,series:asc").
Index ¶
Constants ¶
const ( FieldTitle = "title" FieldAuthor = "author" FieldSeries = "series" FieldDateAdded = "date_added" FieldDateReleased = "date_released" FieldPageCount = "page_count" FieldDuration = "duration" )
Canonical sort field tokens accepted by Parse.
When adding a new field here, you MUST also:
- Update AllFields() below (order matters — it's pinned in tests).
- Add an SQL case to OrderClauses in sql.go.
- Update the TS whitelist in app/libraries/sortSpec.ts (it mirrors this file).
- Document semantics in website/docs/gallery-sort.md (the user-facing reference).
const MaxLevels = 10
MaxLevels is the hard cap on how many levels a spec may contain.
Variables ¶
This section is empty.
Functions ¶
func AllFields ¶
func AllFields() []string
AllFields returns the canonical field list in UI display order.
func IsValidField ¶
IsValidField returns true if s is a whitelisted sort field token.
Types ¶
type LibrarySettingsReader ¶
type LibrarySettingsReader interface {
GetLibrarySettings(ctx context.Context, userID, libraryID int) (*models.UserLibrarySettings, error)
}
LibrarySettingsReader is the dependency-injection seam ResolveForLibrary uses to read a user's stored sort preference for a library. In production it is satisfied by *pkg/settings.Service; tests pass a fake.
It exists here (not in pkg/settings) to break what would otherwise be a circular import: pkg/settings already imports pkg/sortspec to validate sort_spec strings at write time, so pkg/sortspec cannot turn around and import pkg/settings to read them back. Defining the narrow read contract on this side of the boundary lets the resolver stay in pkg/sortspec without dragging in the rest of the settings service surface.
Implementations MUST return (nil, nil) — not an error — when no row exists for (userID, libraryID); ResolveForLibrary treats that as "no stored preference" and falls back to the builtin default.
type OrderClause ¶
type OrderClause struct {
Expression string
}
OrderClause is one unit of ordering for a Bun query, ready to pass to q.OrderExpr(clause.Expression).
Each user-visible sort level produces ONE OrderClause, except the series level which expands to two (series sort name, then series number ASC).
Expressions are built from the whitelisted field branches in OrderClauses and never embed user input, so no parameter args are needed — callers pass Expression directly to OrderExpr.
func OrderClauses ¶
func OrderClauses(levels []SortLevel) []OrderClause
OrderClauses maps a parsed sort spec to the SQL ORDER BY clauses that implement it on the `books` table (aliased `b` per pkg/CLAUDE.md).
Every clause includes a NULLS-LAST indicator (`<expr> IS NULL`) so books missing the sort key always sit at the end regardless of direction. SQLite has no native NULLS LAST.
type SortLevel ¶
SortLevel is one field+direction pair in a spec.
func BuiltinDefault ¶
func BuiltinDefault() []SortLevel
BuiltinDefault is the fallback sort applied when neither an explicit URL sort nor a stored per-(user, library) preference exists. It is the same default the React frontend's app/libraries/sortSpec.ts exposes as `BUILTIN_DEFAULT_SORT` — keep both in sync.
Returns a fresh slice each call so callers can mutate the result without surprising other consumers.
The books service (pkg/books/service.go ListBooksWithTotal) applies this default when Sort is nil and no series filter is active, so all surfaces that hit the books service — the /books REST endpoint, OPDS feeds, the eReader browser, and the React gallery — share the same "newest first" fallback. OPDS and the eReader browser still resolve to this default explicitly in their handlers so a stored preference takes priority over the builtin; the books service default is the safety net for callers that pass Sort=nil with no stored preference (e.g. third-party API consumers).
func Parse ¶
Parse reads a serialized spec string (e.g. "author:asc,series:desc") into a slice of SortLevel. It rejects unknown fields, bad directions, duplicates, empty pairs, stray whitespace, and specs longer than MaxLevels.
func ResolveForLibrary ¶
func ResolveForLibrary( ctx context.Context, reader LibrarySettingsReader, userID, libraryID int, explicit []SortLevel, ) []SortLevel
ResolveForLibrary picks the sort levels to apply for a given caller.
Priority:
- explicit — if non-empty (caller passed an explicit URL param), it wins.
- stored — look up user_library_settings for (userID, libraryID); if a row exists with a parseable sort_spec, use it.
- nil — caller should fall back to whatever hard-coded default it was using before this feature shipped.
Errors from the reader are swallowed: sort is a non-critical UX affordance and should never fail a request. An invalid stored spec is treated the same as no spec (returns nil).