restructure project, add claudemd
This commit is contained in:
306
go/dbutil/filters.go
Normal file
306
go/dbutil/filters.go
Normal file
@@ -0,0 +1,306 @@
|
||||
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, "&")
|
||||
}
|
||||
Reference in New Issue
Block a user