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, "&") }