new endpoint to return product list
This commit is contained in:
120
app/delivery/web/api/restricted/listProducts.go
Normal file
120
app/delivery/web/api/restricted/listProducts.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/listProductsService"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/query_params"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListProductsHandler handles endpoints that receive, save and translate product descriptions.
|
||||||
|
type ListProductsHandler struct {
|
||||||
|
listProductsService *listProductsService.ListProductsService
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListProductsHandler creates a new ListProductsHandler instance
|
||||||
|
func NewListProductsHandler() *ListProductsHandler {
|
||||||
|
listProductsService := listProductsService.New()
|
||||||
|
return &ListProductsHandler{
|
||||||
|
listProductsService: listProductsService,
|
||||||
|
config: config.Get(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewListProductsHandler()
|
||||||
|
|
||||||
|
r.Get("/get-listing", handler.GetListing)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ListProductsHandler) GetListing(c fiber.Ctx) error {
|
||||||
|
paging, filters, err := ParseProductFilters(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrides := map[string]string{
|
||||||
|
// "override_country": c.Query("override_country", ""),
|
||||||
|
// "override_currency": c.Query("override_currency", ""),
|
||||||
|
// }
|
||||||
|
|
||||||
|
listing, err := h.listProductsService.GetListing(paging, filters)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&listing.Items, int(listing.Count), i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnMapping map[string]string = map[string]string{}
|
||||||
|
|
||||||
|
// var columnMapping map[string]string = map[string]string{
|
||||||
|
// "product_id": "id",
|
||||||
|
// "price": "price_taxed",
|
||||||
|
// "name": "name",
|
||||||
|
// "category_id": "category_id",
|
||||||
|
// "feature_id": "feature_id",
|
||||||
|
// "feature": "feature_name",
|
||||||
|
// "value_id": "value_id",
|
||||||
|
// "value": "value_name",
|
||||||
|
// "status": "active_sale",
|
||||||
|
// "stock": "in_stock",
|
||||||
|
// }
|
||||||
|
|
||||||
|
func ParseProductFilters(c fiber.Ctx) (find.Paging, *filters.FiltersList, error) {
|
||||||
|
var p find.Paging
|
||||||
|
fl := filters.NewFiltersList()
|
||||||
|
// productFilters := new(model.ProductFilters)
|
||||||
|
|
||||||
|
// err := c.Bind().Query(productFilters)
|
||||||
|
// if err != nil {
|
||||||
|
// return p, &fl, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if productFilters.Name != "" {
|
||||||
|
// fl.Append(filters.Where("name LIKE ?", fmt.Sprintf("%%%s%%", productFilters.Name)))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if productFilters.Sort != "" {
|
||||||
|
// ord, err := query_params.ParseOrdering[model.Product](c, columnMapping)
|
||||||
|
// if err != nil {
|
||||||
|
// return p, &fl, err
|
||||||
|
// }
|
||||||
|
// for _, o := range ord {
|
||||||
|
// fl.Append(filters.Order(o.Column, o.IsDesc))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if len(productFilters.Features) > 0 {
|
||||||
|
// fl.Append(featureValueFilters(productFilters.Features))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fl.Append(query_params.ParseWhereScopes[model.Product](c, []string{"name"}, columnMapping)...)
|
||||||
|
|
||||||
|
pageNum, pageElems := query_params.ParsePagination(c)
|
||||||
|
p = find.Paging{Page: pageNum, Elements: pageElems}
|
||||||
|
|
||||||
|
return p, &fl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatVal = map[uint][]uint
|
||||||
|
|
||||||
|
func featureValueFilters(feats FeatVal) filters.Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("value_id IN ?", lo.Flatten(lo.Values(feats))).Group("id").Having("COUNT(id) = ?", len(lo.Keys(feats)))
|
||||||
|
}
|
||||||
|
return filters.NewFilter(filters.FEAT_VAL_PRODUCT_FILTER, filt)
|
||||||
|
}
|
||||||
@@ -70,9 +70,8 @@ func (h *ProductDescriptionHandler) GetProductDescription(c fiber.Ctx) error {
|
|||||||
|
|
||||||
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID))
|
description, err := h.productDescriptionService.GetProductDescription(userID, uint(productID), uint(productShopID), uint(productLangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
"error": responseErrors.GetErrorCode(c, err),
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(description, 1, i18n.T_(c, response.Message_OK)))
|
||||||
|
|||||||
@@ -89,10 +89,14 @@ func (s *Server) Setup() error {
|
|||||||
auth := s.public.Group("/auth")
|
auth := s.public.Group("/auth")
|
||||||
public.AuthHandlerRoutes(auth)
|
public.AuthHandlerRoutes(auth)
|
||||||
|
|
||||||
// Repo routes (restricted)
|
// product description routes (restricted)
|
||||||
productDescription := s.restricted.Group("/product-description")
|
productDescription := s.restricted.Group("/product-description")
|
||||||
restricted.ProductDescriptionHandlerRoutes(productDescription)
|
restricted.ProductDescriptionHandlerRoutes(productDescription)
|
||||||
|
|
||||||
|
// listing products routes (restricted)
|
||||||
|
listProducts := s.restricted.Group("/list-products")
|
||||||
|
restricted.ListProductsHandlerRoutes(listProducts)
|
||||||
|
|
||||||
// // Restricted routes example
|
// // Restricted routes example
|
||||||
// restricted := s.api.Group("/restricted")
|
// restricted := s.api.Group("/restricted")
|
||||||
// restricted.Use(middleware.AuthMiddleware())
|
// restricted.Use(middleware.AuthMiddleware())
|
||||||
|
|||||||
77
app/model/product.go
Normal file
77
app/model/product.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Product contains each and every column from the table ps_product.
|
||||||
|
type Product struct {
|
||||||
|
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
||||||
|
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
|
||||||
|
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
|
||||||
|
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
|
||||||
|
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
|
||||||
|
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
|
||||||
|
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
|
||||||
|
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
|
||||||
|
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
|
||||||
|
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
|
||||||
|
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
|
||||||
|
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
|
||||||
|
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
|
||||||
|
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
|
||||||
|
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
|
||||||
|
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
|
||||||
|
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
|
||||||
|
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
|
||||||
|
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
|
||||||
|
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
|
||||||
|
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
|
||||||
|
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
|
||||||
|
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
|
||||||
|
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
|
||||||
|
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
|
||||||
|
|
||||||
|
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
|
||||||
|
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
|
||||||
|
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
|
||||||
|
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
|
||||||
|
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
|
||||||
|
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
|
||||||
|
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
|
||||||
|
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
|
||||||
|
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
|
||||||
|
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
|
||||||
|
|
||||||
|
Active uint `gorm:"column:active" json:"active" form:"active"`
|
||||||
|
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
|
||||||
|
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
|
||||||
|
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
|
||||||
|
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
|
||||||
|
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
|
||||||
|
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
|
||||||
|
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
|
||||||
|
|
||||||
|
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
|
||||||
|
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
|
||||||
|
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
|
||||||
|
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
|
||||||
|
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
|
||||||
|
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
|
||||||
|
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
|
||||||
|
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
|
||||||
|
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
|
||||||
|
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
|
||||||
|
State uint `gorm:"column:state" json:"state" form:"state"`
|
||||||
|
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductFilters struct {
|
||||||
|
Sort string `json:"sort,omitempty" query:"sort,omitempty" example:"price,asc;name,desc"` // sort rule
|
||||||
|
ProductID uint `json:"product_id,omitempty" query:"product_id,omitempty" example:"1"`
|
||||||
|
Price float64 `json:"price,omitempty" query:"price,omitempty" example:"123.45"`
|
||||||
|
Name string `json:"name,omitempty" query:"name,omitempty" example:"Sztabka Złota Britannia"`
|
||||||
|
CategoryID uint `json:"category_id,omitempty" query:"category_id,omitempty" example:"2"`
|
||||||
|
CategoryName string `json:"category_name,omitempty" query:"category_name,omitempty" example:"Złote Monety"`
|
||||||
|
Features FeatVal `query:"features,omitempty"`
|
||||||
|
ActiveSale bool `query:"sale_active,omitempty"`
|
||||||
|
InStock uint `query:"stock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatVal = map[uint][]uint
|
||||||
59
app/service/listProductsService/listProductsService.go
Normal file
59
app/service/listProductsService/listProductsService.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package listProductsService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/repository/listProductsRepo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListProductsService struct {
|
||||||
|
listProductsRepo listProductsRepo.UIListProductsRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *ListProductsService {
|
||||||
|
return &ListProductsService{
|
||||||
|
listProductsRepo: listProductsRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ListProductsService) GetListing(p find.Paging, filters *filters.FiltersList) (find.Found[model.Product], error) {
|
||||||
|
var products find.Found[model.Product]
|
||||||
|
|
||||||
|
// currencyIso := c.Cookies("currency_iso", "")
|
||||||
|
// countryIso := c.Cookies("country_iso", "")
|
||||||
|
|
||||||
|
// if overrides["override_currency"] != "" {
|
||||||
|
// currencyIso = overrides["override_currency"]
|
||||||
|
// }
|
||||||
|
// if overrides["override_country"] != "" {
|
||||||
|
// countryIso = overrides["override_country"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
products, err := s.listProductsRepo.GetListing(p, filters)
|
||||||
|
if err != nil {
|
||||||
|
return products, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// var loopErr error
|
||||||
|
// parallel.ForEach(products.Items, func(t model.Product, i int) {
|
||||||
|
// // products.Items[i].PriceTaxed *= currRate.Rate.InexactFloat64()
|
||||||
|
// // products.Items[i].PriceTaxed = tiny_util.RoundUpMonetary(products.Items[i].PriceTaxed)
|
||||||
|
|
||||||
|
// if products.Items[i].Name.IsNull() {
|
||||||
|
// translation, err := s.listProductsRepo.GetTranslation(ctx, products.Items[i].ID, defaults.DefaultLanguageID)
|
||||||
|
// if err != nil {
|
||||||
|
// loopErr = err
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// products.Items[i].Name = nullable.FromPrimitiveString(translation.Name)
|
||||||
|
// products.Items[i].DescriptionShort = nullable.FromPrimitiveString(translation.DescriptionShort)
|
||||||
|
// products.Items[i].LinkRewrite = nullable.FromPrimitiveString(translation.LinkRewrite)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// if loopErr != nil {
|
||||||
|
// return products, errs.Handled(span, loopErr, errs.InternalError, errs.ERR_TODO)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
ProductDescriptionRepo "git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
|
"git.ma-al.com/goc_daniel/b2b/repository/productDescriptionRepo"
|
||||||
"github.com/openai/openai-go/v3"
|
"github.com/openai/openai-go/v3"
|
||||||
"github.com/openai/openai-go/v3/option"
|
"github.com/openai/openai-go/v3/option"
|
||||||
"github.com/openai/openai-go/v3/responses"
|
"github.com/openai/openai-go/v3/responses"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductDescriptionService struct {
|
type ProductDescriptionService struct {
|
||||||
productDescriptionRepo ProductDescriptionRepo.ProductDescriptionRepo
|
productDescriptionRepo productDescriptionRepo.UIProductDescriptionRepo
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
googleCli translate.TranslationClient
|
googleCli translate.TranslationClient
|
||||||
projectID string
|
projectID string
|
||||||
@@ -74,10 +74,11 @@ func New() *ProductDescriptionService {
|
|||||||
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
option.WithHTTPClient(&http.Client{Timeout: 300 * time.Second})) // five minutes timeout
|
||||||
|
|
||||||
return &ProductDescriptionService{
|
return &ProductDescriptionService{
|
||||||
ctx: ctx,
|
productDescriptionRepo: productDescriptionRepo.New(),
|
||||||
openAIClient: openAIClient,
|
ctx: ctx,
|
||||||
googleCli: *googleCli,
|
openAIClient: openAIClient,
|
||||||
projectID: cfg.GoogleTranslate.ProjectID,
|
googleCli: *googleCli,
|
||||||
|
projectID: cfg.GoogleTranslate.ProjectID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package pagination
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paginate[T any](paging Paging, stmt *gorm.DB) (Found[T], error) {
|
|
||||||
var items []T
|
|
||||||
var count int64
|
|
||||||
|
|
||||||
base := stmt.Session(&gorm.Session{})
|
|
||||||
|
|
||||||
countDB := stmt.Session(&gorm.Session{
|
|
||||||
NewDB: true, // critical: do NOT reuse statement
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := countDB.
|
|
||||||
Table("(?) as sub", base).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return Found[T]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := base.
|
|
||||||
Offset(paging.Offset()).
|
|
||||||
Limit(paging.Limit()).
|
|
||||||
Find(&items).
|
|
||||||
Error
|
|
||||||
if err != nil {
|
|
||||||
return Found[T]{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return Found[T]{
|
|
||||||
Items: items,
|
|
||||||
Count: uint(count),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
150
app/utils/query/filters/filters.go
Normal file
150
app/utils/query/filters/filters.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilterFunction = func(*gorm.DB) *gorm.DB
|
||||||
|
|
||||||
|
func Where(statement string, args ...interface{}) Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where(statement, args...)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StructToWhereScope[T any](model T) Filter {
|
||||||
|
filt := func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where(model)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Order(field string, desc bool) Filter {
|
||||||
|
var filt FilterFunction
|
||||||
|
if desc {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Order(field + " DESC")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Order(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: ORDER_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WhereFromStrings(column, conditionOperator, value string) Filter {
|
||||||
|
var filt func(*gorm.DB) *gorm.DB
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "~") {
|
||||||
|
value = strings.ReplaceAll(value, "~", "")
|
||||||
|
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where("lower("+column+`) LIKE lower(?)`, "%"+value+"%")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: LIKE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(value, "]") && strings.Contains(value, "[") {
|
||||||
|
period := strings.ReplaceAll(value, "[", "")
|
||||||
|
period = strings.ReplaceAll(period, "]", "")
|
||||||
|
vals := strings.Split(period, ",")
|
||||||
|
if len(vals) == 2 {
|
||||||
|
from, errA := time.Parse("2006-01-02", vals[0])
|
||||||
|
to, errB := time.Parse("2006-01-02", vals[1])
|
||||||
|
if errA == nil && errB == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` BETWEEN ? AND ?`, from.Format("2006-01-02"), to.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` BETWEEN ? AND ?`, vals[0], vals[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conditionOperator == "LIKE" {
|
||||||
|
value = fmt.Sprintf("%%%s%%", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// in future add more grouping functions
|
||||||
|
if strings.Contains(strings.ToLower(column), "count(") {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Having(column+` `+conditionOperator+` ?`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, i)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, f)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b, err := strconv.ParseBool(value); err == nil {
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, b)
|
||||||
|
}
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filt = func(d *gorm.DB) *gorm.DB {
|
||||||
|
return d.Where(column+` `+conditionOperator+` ?`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Filter{
|
||||||
|
category: WHERE_FILTER,
|
||||||
|
filter: filt,
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/utils/query/filters/filters_list.go
Normal file
107
app/utils/query/filters/filters_list.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use one of declared in the package constants to instantiate the type.
|
||||||
|
type filterCategory = string
|
||||||
|
|
||||||
|
// Enumaration of known types of filters. The assumption is that all filters
|
||||||
|
// belonging to a single category (type) can be used together at a particular
|
||||||
|
// step in the query process.
|
||||||
|
const (
|
||||||
|
// Should be safe to use at any step of longer query series to reduce the
|
||||||
|
// number of results. If it is not, choose a different filter type
|
||||||
|
WHERE_FILTER filterCategory = "where"
|
||||||
|
|
||||||
|
// An like filter
|
||||||
|
LIKE_FILTER filterCategory = "where"
|
||||||
|
|
||||||
|
// An order by clause which can be used at any final step of a complex query
|
||||||
|
// to change the order of results.
|
||||||
|
ORDER_FILTER filterCategory = "order"
|
||||||
|
// TODO: document the special case of filters on products
|
||||||
|
FEAT_VAL_PRODUCT_FILTER filterCategory = "featval_product"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
category filterCategory
|
||||||
|
filter func(*gorm.DB) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) Filter {
|
||||||
|
return Filter{
|
||||||
|
category: category,
|
||||||
|
filter: filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FiltersList struct {
|
||||||
|
filters []Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFiltersList() FiltersList {
|
||||||
|
return FiltersList{
|
||||||
|
// we allocate some extra space beforehand to reduce the overhead of resizing
|
||||||
|
filters: make([]Filter, 0, 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListWithFilter(filt Filter) FiltersList {
|
||||||
|
l := NewFiltersList()
|
||||||
|
l.filters = append(l.filters, filt)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) NewFilter(category filterCategory, filter func(*gorm.DB) *gorm.DB) {
|
||||||
|
f.filters = append(f.filters, NewFilter(category, filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Append(filter ...Filter) {
|
||||||
|
f.filters = append(f.filters, filter...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all stored filters as []func(*gorm.DB)*gorm.DB
|
||||||
|
func (f *FiltersList) All() []func(*gorm.DB) *gorm.DB {
|
||||||
|
return lo.Map(f.filters, func(filt Filter, _ int) func(*gorm.DB) *gorm.DB {
|
||||||
|
return filt.filter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) OfCategory(cat filterCategory) []func(*gorm.DB) *gorm.DB {
|
||||||
|
return lo.Map(lo.Filter(f.filters, func(v Filter, _ int) bool {
|
||||||
|
return v.category == cat
|
||||||
|
}), func(el Filter, _ int) func(*gorm.DB) *gorm.DB {
|
||||||
|
return el.filter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) ApplyAll(d *gorm.DB) {
|
||||||
|
d.Scopes(f.All()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Apply(d *gorm.DB, cat filterCategory) {
|
||||||
|
d.Scopes(f.OfCategory(cat)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FiltersList) Merge(another FiltersList) {
|
||||||
|
f.filters = append(f.filters, another.filters...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An implementation of stringer on FiltersList that is meant rather to be used
|
||||||
|
// for debug display
|
||||||
|
func (f FiltersList) String() string {
|
||||||
|
groupMap := lo.GroupBy(f.filters, func(t Filter) string {
|
||||||
|
return t.category
|
||||||
|
})
|
||||||
|
res := "FiltersList{"
|
||||||
|
for key := range groupMap {
|
||||||
|
res += fmt.Sprintf(" \"%s\": %d filters", key, len(groupMap[key]))
|
||||||
|
}
|
||||||
|
res += " }"
|
||||||
|
return res
|
||||||
|
}
|
||||||
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++
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/utils/query/find/found_rows_callback.go
Normal file
46
app/utils/query/find/found_rows_callback.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package find
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Key under which result of `SELECT FOUND_ROWS()` should be stored in the
|
||||||
|
// driver context.
|
||||||
|
FOUND_ROWS_CTX_KEY = "maal:found_rows"
|
||||||
|
// Suggested name under which [find.FoundRowsCallback] can be registered.
|
||||||
|
FOUND_ROWS_CALLBACK = "maal:found_rows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Searches query clauses for presence of `SQL_CALC_FOUND_ROWS` and runs `SELECT
|
||||||
|
// FOUND_ROWS();` right after the query containing such clause. The result is
|
||||||
|
// put in the driver context under key [find.FOUND_ROWS_CTX_KEY]. For the
|
||||||
|
// callback to work correctly it must be registered and executed before the
|
||||||
|
// `gorm:preload` callback.
|
||||||
|
func FoundRowsCallback(d *gorm.DB) {
|
||||||
|
if _, ok := d.Statement.Clauses["SELECT"].AfterNameExpression.(sqlCalcFound); ok {
|
||||||
|
var count uint64
|
||||||
|
sqlDB, err := d.DB()
|
||||||
|
if err != nil {
|
||||||
|
_ = d.AddError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := sqlDB.QueryRowContext(d.Statement.Context, "SELECT FOUND_ROWS();")
|
||||||
|
if res == nil {
|
||||||
|
_ = d.AddError(errors.New(`fialed to issue SELECT FOUND_ROWS() query`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.Err() != nil {
|
||||||
|
_ = d.AddError(res.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = res.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
_ = d.AddError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Set(FOUND_ROWS_CTX_KEY, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/utils/query/find/sql_calc_rows.go
Normal file
51
app/utils/query/find/sql_calc_rows.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package find
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlCalcFound struct{}
|
||||||
|
|
||||||
|
// Creates a new Clause which adds `SQL_CALC_FOUND_ROWS` right after `SELECT`.
|
||||||
|
// If [find.FoundRowsCallback] is registered the presence of this clause will
|
||||||
|
// cause `FOUND_ROWS()` result to be available in the driver context.
|
||||||
|
func SqlCalcFound() sqlCalcFound {
|
||||||
|
return sqlCalcFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) Name() string {
|
||||||
|
return "SQL_CALC_FOUND_ROWS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) Build(builder clause.Builder) {
|
||||||
|
_, _ = builder.WriteString("SQL_CALC_FOUND_ROWS")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements gorm's [clause.Clause]
|
||||||
|
func (sqlCalcFound) MergeClause(cl *clause.Clause) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements [gorm.StatementModifier]
|
||||||
|
func (calc sqlCalcFound) ModifyStatement(stmt *gorm.Statement) {
|
||||||
|
selectClause := stmt.Clauses["SELECT"]
|
||||||
|
if selectClause.AfterNameExpression == nil {
|
||||||
|
selectClause.AfterNameExpression = calc
|
||||||
|
} else if _, ok := selectClause.AfterNameExpression.(sqlCalcFound); !ok {
|
||||||
|
selectClause.AfterNameExpression = exprs{selectClause.AfterNameExpression, calc}
|
||||||
|
}
|
||||||
|
stmt.Clauses["SELECT"] = selectClause
|
||||||
|
}
|
||||||
|
|
||||||
|
type exprs []clause.Expression
|
||||||
|
|
||||||
|
func (exprs exprs) Build(builder clause.Builder) {
|
||||||
|
for idx, expr := range exprs {
|
||||||
|
if idx > 0 {
|
||||||
|
_ = builder.WriteByte(' ')
|
||||||
|
}
|
||||||
|
expr.Build(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/utils/query/query_params/key_mapping.go
Normal file
43
app/utils/query/query_params/key_mapping.go
Normal 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]())
|
||||||
|
}
|
||||||
63
app/utils/query/query_params/params_query.go
Normal file
63
app/utils/query/query_params/params_query.go
Normal 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)
|
||||||
|
}
|
||||||
82
app/utils/query/query_params/parse_sort.go
Normal file
82
app/utils/query/query_params/parse_sort.go
Normal 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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
75
app/utils/query/query_params/where_scope_from_query.go
Normal file
75
app/utils/query/query_params/where_scope_from_query.go
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/utils/query/queryparser/queryparser.go
Normal file
37
app/utils/query/queryparser/queryparser.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package queryparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseQuery(c fiber.Ctx) map[string]interface{} {
|
||||||
|
queryParams := map[string]interface{}{}
|
||||||
|
re := regexp.MustCompile(`\?(\w.+)$`)
|
||||||
|
xx := re.FindAllStringSubmatch(c.Request().URI().String(), -1)
|
||||||
|
|
||||||
|
if len(xx) > 0 {
|
||||||
|
if len(xx[0]) == 2 {
|
||||||
|
queryParts := strings.Split(xx[0][1], "&")
|
||||||
|
for _, q := range queryParts {
|
||||||
|
qq := strings.Split(q, "=")
|
||||||
|
if len(qq) == 2 {
|
||||||
|
if num, err := strconv.ParseInt(qq[1], 10, 64); err == nil {
|
||||||
|
queryParams[qq[0]] = num
|
||||||
|
} else if float, err := strconv.ParseFloat(qq[1], 64); err == nil {
|
||||||
|
queryParams[qq[0]] = float
|
||||||
|
} else {
|
||||||
|
queryParams[qq[0]] = qq[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queryParams[qq[0]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParams
|
||||||
|
}
|
||||||
90
app/utils/reflect/reflect.go
Normal file
90
app/utils/reflect/reflect.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package reflect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: instead of matching with string.Contains use something less error-prone
|
||||||
|
func checkIfContainsJSON(i int, t reflect.Type, name string) string {
|
||||||
|
if wholeTag, ok := t.Field(i).Tag.Lookup("json"); ok {
|
||||||
|
tags := strings.Split(wholeTag, ",")
|
||||||
|
for _, tag := range tags {
|
||||||
|
if name == strings.TrimSpace(tag) {
|
||||||
|
return db.DB.NamingStrategy.ColumnName(t.Name(), t.Field(i).Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not tail recursive but should do fine. Goes only as deep as the hierarchy of
|
||||||
|
// inlined structs.
|
||||||
|
// TODO: improve used internally checkIfContainsJSON
|
||||||
|
func GetGormColumnFromJsonField(jsonName string, t reflect.Type) (string, error) {
|
||||||
|
var res string
|
||||||
|
for i := range make([]bool, t.NumField()) {
|
||||||
|
if tag, ok := t.Field(i).Tag.Lookup("json"); ok && strings.Contains(tag, "inline") {
|
||||||
|
var err error
|
||||||
|
res, err = GetGormColumnFromJsonField(jsonName, t.Field(i).Type)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("no field of struct %q has a name %q in its json form", t.Name(), jsonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res = checkIfContainsJSON(i, t, jsonName)
|
||||||
|
}
|
||||||
|
if res != "" {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no field of struct %q has a name %q in its json form", t.Name(), jsonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTableName[T any]() string {
|
||||||
|
var model T
|
||||||
|
typ := reflect.TypeOf(model).Name()
|
||||||
|
return db.DB.NamingStrategy.TableName(typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetParamFromFieldTag[T any](object T, fieldname string, tagname string, paramname string) string {
|
||||||
|
if table, ok := reflect.TypeOf(object).FieldByName(fieldname); ok {
|
||||||
|
if t, ok := table.Tag.Lookup(tagname); ok {
|
||||||
|
if paramname == "" {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`(?m)` + paramname + `:(\w*)`)
|
||||||
|
f := re.FindAllStringSubmatch(t, -1)
|
||||||
|
if len(re.FindAllStringSubmatch(t, -1)) > 0 {
|
||||||
|
return f[0][1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPrimaryKey[T any](item T) string {
|
||||||
|
var search func(T) string = func(item T) string {
|
||||||
|
val := reflect.ValueOf(item)
|
||||||
|
typ := reflect.TypeOf(item)
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
if gg, ok := typ.Field(i).Tag.Lookup("gorm"); ok {
|
||||||
|
xx := strings.Split(gg, ";")
|
||||||
|
for _, t := range xx {
|
||||||
|
if strings.HasPrefix(strings.ToLower(t), "primarykey") {
|
||||||
|
return db.DB.NamingStrategy.TableName(typ.Field(i).Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val.Field(i).Type().String() == "db.Model" {
|
||||||
|
return db.DB.NamingStrategy.TableName("ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return search(item)
|
||||||
|
}
|
||||||
@@ -43,6 +43,9 @@ var (
|
|||||||
ErrInvalidXHTML = errors.New("text is not in xhtml format")
|
ErrInvalidXHTML = errors.New("text is not in xhtml format")
|
||||||
ErrAIResponseFail = errors.New("AI responded with failure")
|
ErrAIResponseFail = errors.New("AI responded with failure")
|
||||||
ErrAIBadOutput = errors.New("AI response does not obey the format")
|
ErrAIBadOutput = errors.New("AI response does not obey the format")
|
||||||
|
|
||||||
|
// Typed errors for product list handler
|
||||||
|
ErrBadPaging = errors.New("bad or missing paging attribute value in header")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents an error with HTTP status code
|
// Error represents an error with HTTP status code
|
||||||
@@ -123,6 +126,9 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrAIBadOutput):
|
case errors.Is(err, ErrAIBadOutput):
|
||||||
return i18n.T_(c, "error.err_ai_bad_output")
|
return i18n.T_(c, "error.err_ai_bad_output")
|
||||||
|
|
||||||
|
case errors.Is(err, ErrBadPaging):
|
||||||
|
return i18n.T_(c, "error.err_bad_paging")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return i18n.T_(c, "error.err_internal_server_error")
|
return i18n.T_(c, "error.err_internal_server_error")
|
||||||
}
|
}
|
||||||
@@ -154,7 +160,8 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrInvalidPassword),
|
errors.Is(err, ErrInvalidPassword),
|
||||||
errors.Is(err, ErrBadAttribute),
|
errors.Is(err, ErrBadAttribute),
|
||||||
errors.Is(err, ErrBadField),
|
errors.Is(err, ErrBadField),
|
||||||
errors.Is(err, ErrInvalidXHTML):
|
errors.Is(err, ErrInvalidXHTML),
|
||||||
|
errors.Is(err, ErrBadPaging):
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/openai/openai-go/v3 v3.28.0
|
github.com/openai/openai-go/v3 v3.28.0
|
||||||
|
github.com/samber/lo v1.53.0
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
google.golang.org/api v0.247.0
|
google.golang.org/api v0.247.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -124,6 +124,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
|
||||||
|
|||||||
58
repository/listProductsRepo/listProductsRepo.go
Normal file
58
repository/listProductsRepo/listProductsRepo.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package listProductsRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UIListProductsRepo interface {
|
||||||
|
GetListing(p find.Paging, filt *filters.FiltersList) (find.Found[model.Product], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProductsRepo struct{}
|
||||||
|
|
||||||
|
func New() UIListProductsRepo {
|
||||||
|
return &ListProductsRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ListProductsRepo) GetListing(p find.Paging, filt *filters.FiltersList) (find.Found[model.Product], error) {
|
||||||
|
var listing []model.Product
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
// Apply filters here
|
||||||
|
q := db.DB.Table("ps_product")
|
||||||
|
|
||||||
|
// var resultIDs []uint
|
||||||
|
// q := db.DB.
|
||||||
|
// // SQL_CALC_FOUND_ROWS is a neat trick which works on MariaDB and
|
||||||
|
// // MySQL. It works when followed by `SELECT FOUND_ROWS();`. To learn
|
||||||
|
// // more see: https://mariarawmodel.com/kb/en/found_rows/
|
||||||
|
// // WARN: This might not work on different SQL databases
|
||||||
|
// Select("DISTINCT SQL_CALC_FOUND_ROWS id").
|
||||||
|
// // Debug().
|
||||||
|
// Scopes(view.FromDBViewForDisplay(langID, countryIso)).
|
||||||
|
// Scopes(scopesForFiltersOnDisplay(db.DB, langID, countryIso, filt)).
|
||||||
|
// Scopes(filt.OfCategory(filters.ORDER_FILTER)...).
|
||||||
|
// Limit(p.Limit()).
|
||||||
|
// Offset(p.Offset())
|
||||||
|
|
||||||
|
err := q.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.Product]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.
|
||||||
|
Limit(p.Limit()).
|
||||||
|
Offset(p.Offset()).
|
||||||
|
Scan(&listing).Error
|
||||||
|
if err != nil {
|
||||||
|
return find.Found[model.Product]{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return find.Found[model.Product]{
|
||||||
|
Items: listing,
|
||||||
|
Count: uint(total),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ProductDescriptionRepo
|
package productDescriptionRepo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
Reference in New Issue
Block a user