package find import ( "errors" "reflect" "strings" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "gorm.io/gorm" ) type Paging struct { Page uint `json:"page_number" example:"5"` Elements uint `json:"elements_per_page" example:"30"` } func (p Paging) Offset() int { return int(p.Elements) * int(p.Page-1) } func (p Paging) Limit() int { return int(p.Elements) } type Found[T any] struct { Items []T `json:"items,omitempty"` Count uint `json:"items_count" example:"56"` Spec map[string]interface{} `json:"spec,omitempty"` } // Wraps given query adding limit, offset clauses and SQL_CALC_FOUND_ROWS to it // and running SELECT FOUND_ROWS() afterwards to fetch the total number // (ignoring LIMIT) of results. The final results are wrapped into the // [find.Found] type. func Paginate[T any](langID uint, paging Paging, stmt *gorm.DB) (Found[T], error) { var items []T var count uint64 // stmt.Debug() err := stmt. Clauses(SqlCalcFound()). Offset(paging.Offset()). Limit(paging.Limit()). Find(&items). Error if err != nil { return Found[T]{}, err } countInterface, ok := stmt.Get(FOUND_ROWS_CTX_KEY) if !ok { return Found[T]{}, errors.New(FOUND_ROWS_CTX_KEY + " value was not found in the gorm db context") } if count, ok = countInterface.(uint64); !ok { return Found[T]{}, errors.New("failed to cast value under " + FOUND_ROWS_CTX_KEY + " to uint64") } columnsSpec := GetColumnsSpec[T](langID) return Found[T]{ Items: items, Count: uint(count), Spec: map[string]interface{}{ "columns": columnsSpec, }, }, err } // GetColumnsSpec[T any] generates a column specification map for a given struct type T. // Each key is the JSON property name, and the value is a map containing: // - "filter_type": suggested filter type based on field type or `filt` tag // - To disable filtering for a field, set `filt:"none"` in the struct tag // - "sortable": currently hardcoded to true // - "order": order of fields as they appear // // Returns nil if T is not a struct. func GetColumnsSpec[T any](langID uint) map[string]map[string]interface{} { result := make(map[string]map[string]interface{}) typ := reflect.TypeOf((*T)(nil)).Elem() if typ.Kind() != reflect.Struct { return nil } order := 1 processStructFields(langID, typ, result, &order) return result } type FilterType string const ( FilterTypeRange FilterType = "range" FilterTypeTimerange FilterType = "timerange" FilterTypeLike FilterType = "like" FilterTypeSwitch FilterType = "switch" FilterTypeNone FilterType = "none" ) func isValidFilterType(ft string) bool { switch FilterType(ft) { case FilterTypeRange, FilterTypeTimerange, FilterTypeLike, FilterTypeSwitch: return true default: return false } } // processStructFields recursively processes struct fields to populate the result map. // It handles inline structs, reads `json` and `filt` tags, and determines filter types // based on the field type when `filt` tag is absent. // `order` is incremented for each field to track field ordering. func processStructFields(langID uint, typ reflect.Type, result map[string]map[string]interface{}, order *int) { for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) jsonTag := field.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { continue } propName := strings.Split(jsonTag, ",")[0] if propName == "" { propName = field.Name } if strings.Contains(jsonTag, ",inline") && field.Type.Kind() == reflect.Struct { processStructFields(langID, field.Type, result, order) continue } filterType := field.Tag.Get("filt") if filterType != "" { if !isValidFilterType(filterType) { filterType = string(FilterTypeNone) } } else { fieldType := field.Type.String() switch { case strings.HasPrefix(fieldType, "int"), strings.HasPrefix(fieldType, "uint"), strings.HasPrefix(fieldType, "float"), strings.HasPrefix(fieldType, "decimal.Decimal"): filterType = string(FilterTypeRange) case strings.Contains(fieldType, "Time"): filterType = string(FilterTypeTimerange) case fieldType == "string": filterType = string(FilterTypeLike) case fieldType == "bool": filterType = string(FilterTypeSwitch) default: filterType = string(FilterTypeNone) } } result[propName] = map[string]interface{}{ "filter_type": filterType, "sortable": func() bool { val, ok := field.Tag.Lookup("sortable"); return !ok || val == "true" }(), "order": *order, "title": i18n.T___(langID, field.Tag.Get("title")), "display": func() bool { val, ok := field.Tag.Lookup("display"); return !ok || val == "true" }(), "hidden": field.Tag.Get("hidden") == "true", } *order++ } }