sorting

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: MIT Imports: 3 Imported by: 0

README

sorting

import "github.com/brpaz/lib-go/sorting"

Package sorting provides types for validated ORDER BY clauses in repository queries.

NewSort validates the sort field against a caller-supplied allow-list, preventing SQL injection when field names originate from user input.

Usage

Define the allowed fields once per repository, then parse user input:

var allowedSortFields = []string{"created_at", "name", "email"}

func (r *UserRepo) List(ctx context.Context, pager pagination.OffsetPager, s sorting.Sort) ([]*User, error) {
    s = sorting.Default(s, "created_at", sorting.DESC)
    var users []User
    err := r.db.WithContext(ctx).
        Order(s.SQL()).
        Offset(pager.Offset()).
        Limit(pager.Limit()).
        Find(&users).Error
    return users, err
}

Parse sort parameters from an HTTP request and pass them down:

func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
    field := r.URL.Query().Get("sort_by")   // e.g. "name"
    dir   := r.URL.Query().Get("sort_dir")  // e.g. "asc"

    s, err := sorting.NewSort(field, dir, allowedSortFields)
    if err != nil {
        // field not in allow-list or direction invalid — return 400
    }

    users, err := h.repo.List(ctx, pager, s)
    // ...
}

When no sort parameters are provided, pass a zero Sort and rely on Default:

s, _ := sorting.NewSort("", "ASC", allowedSortFields) // error — use zero value instead
s    := sorting.Sort{}                                 // zero value signals "no preference"
s     = sorting.Default(s, "created_at", sorting.DESC) // repo applies fallback
Multi\-column sorting

Build a Sorts from one NewSort call per ORDER BY column — each is validated against the same allow-list — then join them with Sorts.SQL:

var sorts sorting.Sorts
for _, p := range parsedSortParams { // e.g. [{"status","asc"}, {"created_at","desc"}]
    s, err := sorting.NewSort(p.Field, p.Dir, allowedSortFields)
    if err != nil {
        // invalid field or direction — return 400
    }
    sorts = append(sorts, s)
}

r.db.WithContext(ctx).Order(sorts.SQL()) // "status ASC, created_at DESC"

How sort parameters are encoded on the wire (repeated query params, a comma-separated list, JSON:API-style "-field" prefixes, ...) is an API contract decision left to the caller — this package only validates and renders already-parsed (field, direction) pairs.

Index

type Direction

Direction is a sort direction: ASC or DESC.

type Direction string

const (
    ASC  Direction = "ASC"
    DESC Direction = "DESC"
)

func ParseDirection
func ParseDirection(s string) (Direction, error)

ParseDirection parses a case-insensitive string into a Direction.

type Sort

Sort holds a validated field and direction for ORDER BY clauses.

type Sort struct {
    Field string
    Dir   Direction
}

func Default
func Default(s Sort, defaultField string, defaultDir Direction) Sort

Default returns s if its Field is non-empty, otherwise a Sort with defaultField and defaultDir.

func NewSort
func NewSort(field, dir string, allowed []string) (Sort, error)

NewSort creates a Sort after validating field against allowed and parsing dir. Returns an error if field is not in the allow-list or dir is not ASC/DESC. The allow-list prevents SQL injection when field names come from user input.

func (Sort) SQL
func (s Sort) SQL() string

SQL returns the ORDER BY fragment (e.g. "created_at DESC") safe for direct use in queries. Both Field and Dir are validated at construction, so this output is injection-safe.

type Sorts

Sorts is an ordered list of validated Sort clauses for multi-column ORDER BY.

type Sorts []Sort

func (Sorts) SQL
func (s Sorts) SQL() string

SQL returns the joined ORDER BY fragment (e.g. "status ASC, created_at DESC") safe for direct use in queries. Each element is validated at construction via NewSort, so this output is injection-safe.

Generated by gomarkdoc

Documentation

Overview

Package sorting provides types for validated ORDER BY clauses in repository queries.

NewSort validates the sort field against a caller-supplied allow-list, preventing SQL injection when field names originate from user input.

Usage

Define the allowed fields once per repository, then parse user input:

var allowedSortFields = []string{"created_at", "name", "email"}

func (r *UserRepo) List(ctx context.Context, pager pagination.OffsetPager, s sorting.Sort) ([]*User, error) {
    s = sorting.Default(s, "created_at", sorting.DESC)
    var users []User
    err := r.db.WithContext(ctx).
        Order(s.SQL()).
        Offset(pager.Offset()).
        Limit(pager.Limit()).
        Find(&users).Error
    return users, err
}

Parse sort parameters from an HTTP request and pass them down:

func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
    field := r.URL.Query().Get("sort_by")   // e.g. "name"
    dir   := r.URL.Query().Get("sort_dir")  // e.g. "asc"

    s, err := sorting.NewSort(field, dir, allowedSortFields)
    if err != nil {
        // field not in allow-list or direction invalid — return 400
    }

    users, err := h.repo.List(ctx, pager, s)
    // ...
}

When no sort parameters are provided, pass a zero Sort and rely on Default:

s, _ := sorting.NewSort("", "ASC", allowedSortFields) // error — use zero value instead
s    := sorting.Sort{}                                 // zero value signals "no preference"
s     = sorting.Default(s, "created_at", sorting.DESC) // repo applies fallback

Multi-column sorting

Build a Sorts from one NewSort call per ORDER BY column — each is validated against the same allow-list — then join them with Sorts.SQL:

var sorts sorting.Sorts
for _, p := range parsedSortParams { // e.g. [{"status","asc"}, {"created_at","desc"}]
    s, err := sorting.NewSort(p.Field, p.Dir, allowedSortFields)
    if err != nil {
        // invalid field or direction — return 400
    }
    sorts = append(sorts, s)
}

r.db.WithContext(ctx).Order(sorts.SQL()) // "status ASC, created_at DESC"

How sort parameters are encoded on the wire (repeated query params, a comma-separated list, JSON:API-style "-field" prefixes, ...) is an API contract decision left to the caller — this package only validates and renders already-parsed (field, direction) pairs.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Direction

type Direction string

Direction is a sort direction: ASC or DESC.

const (
	ASC  Direction = "ASC"
	DESC Direction = "DESC"
)

func ParseDirection

func ParseDirection(s string) (Direction, error)

ParseDirection parses a case-insensitive string into a Direction.

type Sort

type Sort struct {
	Field string
	Dir   Direction
}

Sort holds a validated field and direction for ORDER BY clauses.

func Default

func Default(s Sort, defaultField string, defaultDir Direction) Sort

Default returns s if its Field is non-empty, otherwise a Sort with defaultField and defaultDir.

func NewSort

func NewSort(field, dir string, allowed []string) (Sort, error)

NewSort creates a Sort after validating field against allowed and parsing dir. Returns an error if field is not in the allow-list or dir is not ASC/DESC. The allow-list prevents SQL injection when field names come from user input.

func (Sort) SQL

func (s Sort) SQL() string

SQL returns the ORDER BY fragment (e.g. "created_at DESC") safe for direct use in queries. Both Field and Dir are validated at construction, so this output is injection-safe.

type Sorts

type Sorts []Sort

Sorts is an ordered list of validated Sort clauses for multi-column ORDER BY.

func (Sorts) SQL

func (s Sorts) SQL() string

SQL returns the joined ORDER BY fragment (e.g. "status ASC, created_at DESC") safe for direct use in queries. Each element is validated at construction via NewSort, so this output is injection-safe.

Jump to

Keyboard shortcuts

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