307 lines
7.2 KiB
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, "&")
|
|
}
|