new endpoint to return product list
This commit is contained in:
159
app/utils/query/find/find.go
Normal file
159
app/utils/query/find/find.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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++
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user