Files
oslstats/internal/db/paginate.go
2026-02-14 19:48:59 +11:00

201 lines
4.1 KiB
Go

package db
import (
"net/http"
"strings"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/oslstats/internal/validation"
"github.com/uptrace/bun"
)
type PageOpts struct {
Page int
PerPage int
Order bun.Order
OrderBy string
}
type OrderOpts struct {
Order bun.Order
OrderBy string
Label string
}
func GetPageOpts(s *hws.Server, w http.ResponseWriter, r *http.Request) (*PageOpts, bool) {
var getter validation.Getter
switch r.Method {
case "GET":
getter = validation.NewQueryGetter(r)
case "POST":
var ok bool
getter, ok = validation.ParseFormOrError(s, w, r)
if !ok {
return nil, false
}
default:
return nil, false
}
return getPageOpts(s, w, r, getter), true
}
func getPageOpts(s *hws.Server, w http.ResponseWriter, r *http.Request, g validation.Getter) *PageOpts {
page := g.Int("page").Optional().Min(1).Value
perPage := g.Int("per_page").Optional().Min(1).Max(100).Value
order := g.String("order").TrimSpace().ToUpper().Optional().AllowedValues([]string{"ASC", "DESC"}).Value
orderBy := g.String("order_by").TrimSpace().Optional().ToLower().Value
valid := g.ValidateAndError(s, w, r)
if !valid {
return nil
}
pageOpts := &PageOpts{
Page: page,
PerPage: perPage,
Order: bun.Order(order),
OrderBy: orderBy,
}
return pageOpts
}
func setPageOpts(q *bun.SelectQuery, p, d *PageOpts, totalitems int) (*bun.SelectQuery, *PageOpts) {
if p == nil {
p = new(PageOpts)
}
if p.Page <= 0 {
p.Page = d.Page
}
if p.PerPage == 0 {
p.PerPage = d.PerPage
}
maxpage := p.TotalPages(totalitems)
if p.Page > maxpage && maxpage > 0 {
p.Page = maxpage
}
if p.Order == "" {
p.Order = d.Order
}
if p.OrderBy == "" {
p.OrderBy = d.OrderBy
}
p.OrderBy = sanitiseOrderBy(p.OrderBy)
q = q.OrderBy(p.OrderBy, p.Order).
Limit(p.PerPage).
Offset(p.PerPage * (p.Page - 1))
return q, p
}
func sanitiseOrderBy(orderby string) string {
result := strings.ToLower(orderby)
var builder strings.Builder
for _, r := range result {
if isValidChar(r) {
builder.WriteRune(r)
}
}
sanitized := builder.String()
if sanitized == "" {
return "_"
}
if !isValidFirstChar(rune(sanitized[0])) {
sanitized = "_" + sanitized
}
if len(sanitized) > 63 {
sanitized = sanitized[:63]
}
return sanitized
}
func isValidChar(r rune) bool {
return (r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_'
}
func isValidFirstChar(r rune) bool {
return (r >= 'a' && r <= 'z') || r == '_'
}
// TotalPages calculates the total number of pages
func (p *PageOpts) TotalPages(total int) int {
if p.PerPage == 0 {
return 0
}
pages := total / p.PerPage
if total%p.PerPage > 0 {
pages++
}
return pages
}
// HasPrevPage checks if there is a previous page
func (p *PageOpts) HasPrevPage() bool {
return p.Page > 1
}
// HasNextPage checks if there is a next page
func (p *PageOpts) HasNextPage(total int) bool {
return p.Page < p.TotalPages(total)
}
// GetPageRange returns an array of page numbers to display
// maxButtons controls how many page buttons to show
func (p *PageOpts) GetPageRange(total int, maxButtons int) []int {
totalPages := p.TotalPages(total)
if totalPages == 0 {
return []int{}
}
// If total pages is less than max buttons, show all pages
if totalPages <= maxButtons {
pages := make([]int, totalPages)
for i := range totalPages {
pages[i] = i + 1
}
return pages
}
// Calculate range around current page
halfButtons := maxButtons / 2
start := p.Page - halfButtons
end := p.Page + halfButtons
// Adjust if at beginning
if start < 1 {
start = 1
end = maxButtons
}
// Adjust if at end
if end > totalPages {
end = totalPages
start = totalPages - maxButtons + 1
}
pages := make([]int, 0, maxButtons)
for i := start; i <= end; i++ {
pages = append(pages, i)
}
return pages
}
// StartItem returns the number of the first item on the current page
func (p *PageOpts) StartItem() int {
if p.Page < 1 {
return 0
}
return (p.Page-1)*p.PerPage + 1
}
// EndItem returns the number of the last item on the current page
func (p *PageOpts) EndItem(total int) int {
end := p.Page * p.PerPage
if end > total {
return total
}
return end
}