new endpoint to return product list

This commit is contained in:
Daniel Goc
2026-03-18 11:39:18 +01:00
parent a0dcb56fda
commit 6cebcacb5d
23 changed files with 1243 additions and 66 deletions

View File

@@ -0,0 +1,43 @@
package query_params
import (
"fmt"
"reflect"
"strings"
mreflect "git.ma-al.com/goc_daniel/b2b/app/utils/reflect"
)
// MapParamsKeyToDbColumn will attempt to map provided key into unique (prefixed
// with table) column name. It will do so using following priority of sources of
// mapping:
// 1. `formColumnMapping` argument. If the mapped values contain a dot, the part
// before the dot will be used for the table name. Otherwise the table name will
// be derived from the generic parameter `T`.
// 2. json tags of provided as generic `T` struct. The table name will be also
// derived from the generic if not provided as dot prefix.
func MapParamsKeyToDbColumn[DEFAULT_TABLE_MODEL any](key string, mapping ...map[string]string) (string, error) {
ERR := "Failed to find appropiate mapping from form field to database column for key: '%s', and default table name: '%s'"
if len(mapping) > 0 {
if field, ok := (mapping[0])[key]; ok {
return field, nil
}
} else {
var t DEFAULT_TABLE_MODEL
if table, field, ok := strings.Cut(key, "."); ok {
if column, err := mreflect.GetGormColumnFromJsonField(field, reflect.TypeOf(t)); err == nil {
return table + "." + column, nil
}
return "", fmt.Errorf(ERR, key, table)
} else {
table := mreflect.GetTableName[DEFAULT_TABLE_MODEL]()
if column, err := mreflect.GetGormColumnFromJsonField(key, reflect.TypeOf(t)); err == nil {
return table + "." + column, nil
} else {
return "", fmt.Errorf(ERR, key, table)
}
}
}
return "", fmt.Errorf(ERR, key, mreflect.GetTableName[DEFAULT_TABLE_MODEL]())
}

View File

@@ -0,0 +1,63 @@
package query_params
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
"github.com/gofiber/fiber/v3"
)
var FunctionalQueryParams = []string{
// Used to specidy order of results
"sort",
// Used to specify page of search resulst
"p",
// Used to specify number of elements on a page
"elems",
// Used to specify allowed values of features on products
"values",
}
func ParseFilters[T any](c fiber.Ctx, formColumnMappimg ...map[string]string) (find.Paging, *filters.FiltersList, error) {
// field/column based filters
filters, err := ParseFieldFilters[T](c, formColumnMappimg...)
if err != nil {
return find.Paging{}, filters, err
}
// pagination
pageNum, pageSize := ParsePagination(c)
// ret
return find.Paging{Page: pageNum, Elements: pageSize}, filters, nil
}
// Parse field related filters from params query. Produces where clauses and
// order rules.
func ParseFieldFilters[T any](c fiber.Ctx, formColumnMapping ...map[string]string) (*filters.FiltersList, error) {
// var model T
list := filters.NewFiltersList()
whereScopefilters := ParseWhereScopes[T](c, []string{}, formColumnMapping...)
list.Append(whereScopefilters...)
ord, err := ParseOrdering[T](c, formColumnMapping...)
if err != nil {
return &list, err
}
// addDefaultOrderingIfNeeded(&ord, model)
for i := range ord {
if err == nil {
list.Append(filters.Order(ord[i].Column, ord[i].IsDesc))
}
}
return &list, nil
}
// TODO: Add some source of defaults for pagination size here
func ParsePagination(c fiber.Ctx) (uint, uint) {
pageNum, _ := strconv.ParseInt(c.Query("p", "1"), 10, 64)
pageSize, _ := strconv.ParseInt(c.Query("elems", "30"), 10, 64)
return uint(pageNum), uint(pageSize)
}

View File

@@ -0,0 +1,82 @@
package query_params
import (
"strings"
"github.com/gofiber/fiber/v3"
)
type Ordering struct {
Column string
IsDesc bool
}
func ParseOrdering[T any](c fiber.Ctx, columnMapping ...map[string]string) ([]Ordering, error) {
param := c.Query("sort")
if len(param) < 1 {
return []Ordering{}, nil
}
rules := strings.Split(param, ";")
var orderings []Ordering
for _, r := range rules {
ord, err := parseOrderingRule[T](r, columnMapping...)
if err != nil {
return orderings, err
}
orderings = append(orderings, ord)
}
return orderings, nil
}
func parseOrderingRule[T any](rule string, columnMapping ...map[string]string) (Ordering, error) {
var desc bool
if key, descStr, ok := strings.Cut(rule, ","); ok {
switch {
case strings.Compare(descStr, "desc") == 0:
desc = true
case strings.Compare(descStr, "asc") == 0:
desc = false
default:
desc = true
}
if col, err := MapParamsKeyToDbColumn[T](key, columnMapping...); err == nil {
return Ordering{
Column: col,
IsDesc: desc,
}, nil
} else {
return Ordering{}, err
}
} else {
if col, err := MapParamsKeyToDbColumn[T](key, columnMapping...); err == nil {
return Ordering{
Column: col,
IsDesc: true,
}, nil
} else {
return Ordering{}, err
}
}
}
// func addDefaultOrderingIfNeeded[T any](previousOrderings *[]Ordering, model T) {
// newOrderings := new([]Ordering)
// var t T
// if len(*previousOrderings) < 1 {
// if col, err := mreflect.GetGormColumnFromJsonField("id", reflect.TypeOf(t)); err == nil {
// *newOrderings = append(*newOrderings, Ordering{
// Column: mreflect.GetTableName[T]() + "." + col,
// IsDesc: true,
// })
// }
// if col, err := mreflect.GetGormColumnFromJsonField("iso_code", reflect.TypeOf(t)); err == nil {
// *newOrderings = append(*newOrderings, Ordering{
// Column: mreflect.GetTableName[T]() + "." + col,
// IsDesc: false,
// })
// }
// *newOrderings = append(*newOrderings, *previousOrderings...)
// *previousOrderings = *newOrderings
// }
// }

View File

@@ -0,0 +1,75 @@
package query_params
import (
"strings"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"github.com/gofiber/fiber/v3"
)
// ParseWhereScopes will attempt to create where scope query filters from url
// query params. It will map form fields to a database column name using
// `MapParamsKeyToDbColumn` function.
func ParseWhereScopes[T any](c fiber.Ctx, ignoredKeys []string, formColumnMapping ...map[string]string) []filters.Filter {
var parsedFilters []filters.Filter
//nolint
for key, value := range c.Request().URI().QueryArgs().All() {
keyStr := string(key)
valStr := string(value)
isIgnored := false
for _, ignoredKey := range ignoredKeys {
if keyStr == ignoredKey {
isIgnored = true
break
}
}
if isIgnored {
continue
}
baseKey, operator := extractOperator(keyStr)
if col, err := MapParamsKeyToDbColumn[T](baseKey, formColumnMapping...); err == nil {
if strings.HasPrefix(valStr, "~") {
parsedFilters = append(parsedFilters, filters.WhereFromStrings(col, "LIKE", valStr))
continue
}
op := resolveOperator(operator)
parsedFilters = append(parsedFilters, filters.WhereFromStrings(col, op, valStr))
}
}
return parsedFilters
}
func extractOperator(key string) (base string, operatorSuffix string) {
suffixes := []string{"_gt", "_gte", "_lt", "_lte", "_eq", "_neq"}
for _, suf := range suffixes {
if strings.HasSuffix(key, suf) {
return strings.TrimSuffix(key, suf), suf[1:]
}
}
return key, ""
}
func resolveOperator(suffix string) string {
switch suffix {
case "gt":
return ">"
case "gte":
return ">="
case "lt":
return "<"
case "lte":
return "<="
case "neq":
return "!="
case "eq":
return "="
default:
return "LIKE"
}
}