Files
kjol/dbutil/filters.go

307 lines
7.2 KiB
Go

package dbutil
import (
. "kjol/basic"
"net/http"
"strconv"
"strings"
)
const (
PAGE_NUM_KEY = "page_num"
ORDER_BY_KEY = "order_by"
ITEMS_PER_PAGE_KEY = "items_per_page"
SEARCH_KEY_PREFIX = "search_"
FILTER_DEFAULT_MAX_ITEMS = 25
)
type Search struct {
Values []string
Identifier string
CaseSensitive bool
IgnoreWhitespace bool
}
type OrderBy struct {
Identifier string
Descending bool
}
type Pagination struct {
Disabled bool
CurrentPage int
NextPage int
PreviousPage int
TotalPages int
TotalItems int
MaxItemsPerPage int
ItemsThisPage int
ViewRangeLower int
ViewRangeUpper int
}
type Filter struct {
Search []Search
Pagination Pagination
OrderBy OrderBy
}
// GetSearch returns the Search struct for the given identifier, or nil if not found.
func (f *Filter) GetSearch(identifier string) *Search {
for i := range f.Search {
if f.Search[i].Identifier == identifier {
return &f.Search[i]
}
}
return nil
}
// BindOrderBy applies an ORDER BY clause to the select builder if the filter's
// OrderBy identifier matches.
func BindOrderBy(identifier string, col Col, f Filter, sb *SelectBuilder) {
if sb == nil {
return
}
if identifier == f.OrderBy.Identifier {
if f.OrderBy.Descending {
sb.OrderBy(col.Desc())
} else {
sb.OrderBy(col.Asc())
}
}
}
// BindOrderByMultiCols applies ORDER BY with multiple columns if the filter's
// OrderBy identifier matches.
func BindOrderByMultiCols(identifier string, f Filter, sb *SelectBuilder, cols ...Col) {
if sb == nil {
return
}
if identifier == f.OrderBy.Identifier {
exprs := make([]OrderExpr, len(cols))
for i, c := range cols {
if f.OrderBy.Descending {
exprs[i] = c.Desc()
} else {
exprs[i] = c.Asc()
}
}
sb.OrderBy(exprs...)
}
}
// SetDefaultOrderBy applies the given order if no ORDER BY has been set yet.
func SetDefaultOrderBy(defaultExpr OrderExpr, sb *SelectBuilder) {
if sb == nil {
return
}
if !sb.HasOrderBy() {
sb.OrderBy(defaultExpr)
}
}
// ApplyPagination applies LIMIT and OFFSET to the select builder based on the filter.
func ApplyPagination(f Filter, sb *SelectBuilder) {
if !f.Pagination.Disabled {
if f.Pagination.MaxItemsPerPage > 0 {
sb.Limit(int64(f.Pagination.MaxItemsPerPage))
sb.Offset(int64((f.Pagination.CurrentPage - 1) * f.Pagination.MaxItemsPerPage))
}
}
}
func ParseFilterFromRequest(r *http.Request) Filter {
if r.Body != nil {
defer r.Body.Close()
}
r.ParseForm()
filter := Filter{}
filter.Pagination.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
filter.Pagination.CurrentPage = 1
// Parse pagination
if pageNum := r.FormValue(PAGE_NUM_KEY); pageNum != "" {
if n, err := strconv.Atoi(pageNum); err == nil && n > 0 {
filter.Pagination.CurrentPage = n
}
}
if itemsPerPage := r.FormValue(ITEMS_PER_PAGE_KEY); itemsPerPage != "" {
if n, err := strconv.Atoi(itemsPerPage); err == nil {
if n > 0 {
filter.Pagination.MaxItemsPerPage = n
} else if n == -1 {
filter.Pagination.MaxItemsPerPage = -1
}
}
}
// Parse order by
if orderByValue := r.FormValue(ORDER_BY_KEY); orderByValue != "" {
filter.OrderBy.Identifier = orderByValue
filter.OrderBy.Descending = r.FormValue("order_desc") == "true"
}
// Parse search parameters (keys prefixed with search_)
for key, values := range r.Form {
if strings.HasPrefix(key, SEARCH_KEY_PREFIX) && len(values) > 0 {
identifier := strings.TrimPrefix(key, SEARCH_KEY_PREFIX)
if len(values) == 1 && values[0] == "__EMPTY_ARRAY__" {
filter.Search = append(filter.Search, Search{
Identifier: identifier,
Values: []string{},
})
} else {
filter.Search = append(filter.Search, Search{
Identifier: identifier,
Values: values,
})
}
}
}
return filter
}
func (p *Pagination) GeneratePagination(totalItemsInSet int64, itemsDisplayedThisPage int) {
p.TotalItems = int(totalItemsInSet)
p.ItemsThisPage = itemsDisplayedThisPage
if p.MaxItemsPerPage == 0 {
p.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
}
if p.MaxItemsPerPage == -1 {
p.TotalPages = 1
p.CurrentPage = 1
p.PreviousPage = 1
p.NextPage = 1
if p.TotalItems != 0 {
p.ViewRangeLower = 1
} else {
p.ViewRangeLower = 0
}
p.ViewRangeUpper = p.TotalItems
return
}
if p.MaxItemsPerPage == 0 {
p.TotalPages = 1
} else {
p.TotalPages = p.TotalItems / p.MaxItemsPerPage
if p.TotalItems%p.MaxItemsPerPage != 0 {
p.TotalPages++
}
}
if p.TotalPages == 0 {
p.TotalPages = 1
}
if p.CurrentPage < 1 {
p.CurrentPage = 1
p.PreviousPage = 1
} else {
p.PreviousPage = p.CurrentPage - 1
}
if p.TotalItems != 0 {
p.ViewRangeLower = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + 1
} else {
p.ViewRangeLower = 0
}
p.ViewRangeUpper = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + p.ItemsThisPage
if p.CurrentPage >= p.TotalPages {
p.CurrentPage = p.TotalPages
p.NextPage = p.TotalPages
} else {
p.NextPage = p.CurrentPage + 1
}
}
// PaginateSlice performs in-memory pagination on a slice.
func PaginateSlice[T any](arr []T, f Filter) []T {
if !f.Pagination.Disabled {
if f.Pagination.CurrentPage <= 0 {
f.Pagination.CurrentPage = 1
}
if f.Pagination.MaxItemsPerPage > 0 {
offset := (f.Pagination.CurrentPage - 1) * f.Pagination.MaxItemsPerPage
limit := f.Pagination.MaxItemsPerPage
if offset > len(arr) {
arr = []T{}
} else if offset+limit > len(arr) {
arr = arr[offset:]
} else {
arr = arr[offset : offset+limit]
}
}
}
return arr
}
// LikeNonAlphaNumeric creates a condition that strips non-alphanumeric (except space)
// characters from the column and matches against the sanitized search value.
func LikeNonAlphaNumeric(columnName string, searchValue string, cond Cond) Cond {
searchSanitized := SanitizeAlphaNum(strings.ToLower(searchValue))
return cond.And(RawCond(
"regexp_replace(lower("+columnName+"), '[^a-zA-Z0-9 ]', '', 'g') LIKE ?",
"%"+searchSanitized+"%",
))
}
// LikeNonAlphaNumericStrict creates a condition that strips ALL non-alphanumeric
// characters (including spaces) from the column and matches against the sanitized search value.
func LikeNonAlphaNumericStrict(columnName string, searchValue string, cond Cond) Cond {
searchSanitized := SanitizeAlphaNumStrict(strings.ToLower(searchValue))
return cond.And(RawCond(
"regexp_replace(lower("+columnName+"), '[^a-zA-Z0-9]', '', 'g') LIKE ?",
"%"+searchSanitized+"%",
))
}
// IsEmpty returns true if the filter has no search values and no order by set.
func (f *Filter) IsEmpty() bool {
if f.OrderBy.Identifier != "" {
return false
}
for _, s := range f.Search {
if len(s.Values) > 0 && s.Values[0] != "" {
return false
}
}
return true
}
// ToQueryString converts the filter to a URL query string.
func (f *Filter) ToQueryString() string {
params := make([]string, 0)
for _, s := range f.Search {
for _, v := range s.Values {
if v != "" {
params = append(params, SEARCH_KEY_PREFIX+s.Identifier+"="+v)
}
}
}
if f.OrderBy.Identifier != "" {
params = append(params, ORDER_BY_KEY+"="+f.OrderBy.Identifier)
if f.OrderBy.Descending {
params = append(params, "order_desc=true")
}
}
return strings.Join(params, "&")
}