sortspec

package
v0.0.39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package sortspec parses, validates, builds SQL for, and resolves multi-level book sort specifications (e.g. "author:asc,series:asc").

Index

Constants

View Source
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).
View Source
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

func IsValidField(s string) bool

IsValidField returns true if s is a whitelisted sort field token.

func Serialize

func Serialize(levels []SortLevel) string

Serialize renders a level slice back into the URL-param form. The zero/nil slice serializes to the empty string.

Types

type Direction

type Direction string

Direction is "asc" or "desc".

const (
	DirAsc  Direction = "asc"
	DirDesc Direction = "desc"
)

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

type SortLevel struct {
	Field     string
	Direction Direction
}

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

func Parse(s string) ([]SortLevel, error)

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:

  1. explicit — if non-empty (caller passed an explicit URL param), it wins.
  2. stored — look up user_library_settings for (userID, libraryID); if a row exists with a parseable sort_spec, use it.
  3. 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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL