mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-16 18:22:59 +03:00
feat: add various improvements to the table component (#961)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
205
backend/internal/utils/list_request_util.go
Normal file
205
backend/internal/utils/list_request_util.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PaginationResponse struct {
|
||||
TotalPages int64 `json:"totalPages"`
|
||||
TotalItems int64 `json:"totalItems"`
|
||||
CurrentPage int `json:"currentPage"`
|
||||
ItemsPerPage int `json:"itemsPerPage"`
|
||||
}
|
||||
|
||||
type ListRequestOptions struct {
|
||||
Pagination struct {
|
||||
Page int `form:"pagination[page]"`
|
||||
Limit int `form:"pagination[limit]"`
|
||||
} `form:"pagination"`
|
||||
Sort struct {
|
||||
Column string `form:"sort[column]"`
|
||||
Direction string `form:"sort[direction]"`
|
||||
} `form:"sort"`
|
||||
Filters map[string][]any
|
||||
}
|
||||
|
||||
type FieldMeta struct {
|
||||
ColumnName string
|
||||
IsSortable bool
|
||||
IsFilterable bool
|
||||
}
|
||||
|
||||
func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOptions) {
|
||||
if err := ctx.ShouldBindQuery(&listRequestOptions); err != nil {
|
||||
return listRequestOptions
|
||||
}
|
||||
|
||||
listRequestOptions.Filters = parseNestedFilters(ctx)
|
||||
return listRequestOptions
|
||||
}
|
||||
|
||||
func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
meta := extractModelMetadata(result)
|
||||
|
||||
query = applyFilters(params.Filters, query, meta)
|
||||
query = applySorting(params.Sort.Column, params.Sort.Direction, query, meta)
|
||||
|
||||
return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result)
|
||||
}
|
||||
|
||||
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
} else if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
var totalItems int64
|
||||
if err := query.Count(&totalItems).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize)
|
||||
if totalItems == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
if int64(page) > totalPages {
|
||||
page = int(totalPages)
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(result).Error; err != nil {
|
||||
return PaginationResponse{}, err
|
||||
}
|
||||
|
||||
return PaginationResponse{
|
||||
TotalPages: totalPages,
|
||||
TotalItems: totalItems,
|
||||
CurrentPage: page,
|
||||
ItemsPerPage: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NormalizeSortDirection(direction string) string {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
if d != "asc" && d != "desc" {
|
||||
return "asc"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func IsValidSortDirection(direction string) bool {
|
||||
d := strings.ToLower(strings.TrimSpace(direction))
|
||||
return d == "asc" || d == "desc"
|
||||
}
|
||||
|
||||
// parseNestedFilters handles ?filters[field][0]=val1&filters[field][1]=val2
|
||||
func parseNestedFilters(ctx *gin.Context) map[string][]any {
|
||||
result := make(map[string][]any)
|
||||
query := ctx.Request.URL.Query()
|
||||
|
||||
for key, values := range query {
|
||||
if !strings.HasPrefix(key, "filters[") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Keys can be "filters[field]" or "filters[field][0]"
|
||||
raw := strings.TrimPrefix(key, "filters[")
|
||||
// Take everything up to the first closing bracket
|
||||
if idx := strings.IndexByte(raw, ']'); idx != -1 {
|
||||
field := raw[:idx]
|
||||
for _, v := range values {
|
||||
result[field] = append(result[field], ConvertStringToType(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// applyFilters applies filtering to the GORM query based on the provided filters
|
||||
func applyFilters(filters map[string][]any, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||
for key, values := range filters {
|
||||
if key == "" || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := CapitalizeFirstLetter(key)
|
||||
fieldMeta, ok := meta[fieldName]
|
||||
if !ok || !fieldMeta.IsFilterable {
|
||||
continue
|
||||
}
|
||||
|
||||
query = query.Where(fieldMeta.ColumnName+" IN ?", values)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// applySorting applies sorting to the GORM query based on the provided column and direction
|
||||
func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta map[string]FieldMeta) *gorm.DB {
|
||||
fieldName := CapitalizeFirstLetter(sortColumn)
|
||||
fieldMeta, ok := meta[fieldName]
|
||||
if !ok || !fieldMeta.IsSortable {
|
||||
return query
|
||||
}
|
||||
|
||||
sortDirection = NormalizeSortDirection(sortDirection)
|
||||
|
||||
query = query.Clauses(clause.OrderBy{
|
||||
Columns: []clause.OrderByColumn{
|
||||
{Column: clause.Column{Name: fieldMeta.ColumnName}, Desc: sortDirection == "desc"},
|
||||
},
|
||||
})
|
||||
return query
|
||||
}
|
||||
|
||||
// extractModelMetadata extracts FieldMeta from the model struct using reflection
|
||||
func extractModelMetadata(model interface{}) map[string]FieldMeta {
|
||||
meta := make(map[string]FieldMeta)
|
||||
|
||||
// Unwrap pointers and slices to get the element struct type
|
||||
t := reflect.TypeOf(model)
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
if t == nil {
|
||||
return meta
|
||||
}
|
||||
}
|
||||
|
||||
// recursive parser that merges fields from embedded structs
|
||||
var parseStruct func(reflect.Type)
|
||||
parseStruct = func(st reflect.Type) {
|
||||
for i := 0; i < st.NumField(); i++ {
|
||||
field := st.Field(i)
|
||||
ft := field.Type
|
||||
|
||||
// If the field is an embedded/anonymous struct, recurse into it
|
||||
if field.Anonymous && ft.Kind() == reflect.Struct {
|
||||
parseStruct(ft)
|
||||
continue
|
||||
}
|
||||
|
||||
// Normal field: record metadata
|
||||
name := field.Name
|
||||
meta[name] = FieldMeta{
|
||||
ColumnName: CamelCaseToSnakeCase(name),
|
||||
IsSortable: field.Tag.Get("sortable") == "true",
|
||||
IsFilterable: field.Tag.Get("filterable") == "true",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseStruct(t)
|
||||
return meta
|
||||
}
|
||||
Reference in New Issue
Block a user