package searchrepo import ( "bytes" "fmt" "io" "net/http" "git.ma-al.com/goc_daniel/b2b/app/config" "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/model/dbmodel" constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" "github.com/WinterYukky/gorm-extra-clause-plugin/exclause" ) type SearchProxyResponse struct { StatusCode int Body []byte } type UISearchRepo interface { Search(index string, body []byte) (*SearchProxyResponse, error) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) GetIndexSettings(index string) (*SearchProxyResponse, error) GetRoutes(langId uint) ([]model.Route, error) } type SearchRepo struct { cfg *config.Config } func New() UISearchRepo { return &SearchRepo{ cfg: config.Get(), } } func (r *SearchRepo) Search(index string, body []byte) (*SearchProxyResponse, error) { url := fmt.Sprintf("%s/indexes/%s/search", r.cfg.MeiliSearch.ServerURL, index) return r.doRequest(http.MethodPost, url, body) } func (r *SearchRepo) GetIndexSettings(index string) (*SearchProxyResponse, error) { url := fmt.Sprintf("%s/indexes/%s/settings", r.cfg.MeiliSearch.ServerURL, index) return r.doRequest(http.MethodGet, url, nil) } func (r *SearchRepo) doRequest(method, url string, body []byte) (*SearchProxyResponse, error) { var reqBody *bytes.Reader if body != nil { reqBody = bytes.NewReader(body) } else { reqBody = bytes.NewReader([]byte{}) } req, err := http.NewRequest(method, url, reqBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") if r.cfg.MeiliSearch.ApiKey != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.cfg.MeiliSearch.ApiKey)) } client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to reach meilisearch: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } return &SearchProxyResponse{ StatusCode: resp.StatusCode, Body: respBody, }, nil } func (r *SearchRepo) GetRoutes(langId uint) ([]model.Route, error) { return nil, nil } // GetMeiliProductsProducts returns a batch of products with LIMIT/OFFSET pagination // The scanning is done inside the repo to keep the service layer cleaner func (r *SearchRepo) GetMeiliProducts(id_lang uint, offset, limit int) ([]model.MeiliSearchProduct, error) { var products []model.MeiliSearchProduct err := db.Get(). Table("ps_product_shop ps"). Select(` ps.id_product AS id_product, pl.name AS name, TRIM(REGEXP_REPLACE(REGEXP_REPLACE(pl.description_short, '<[^>]*>', ' '), '[[:space:]]+', ' ')) AS description, p.ean13, p.reference, ps.price, ps.id_category_default AS id_category, cl.name AS cat_name, cl.link_rewrite AS l_rew, COALESCE(vary.attributes, JSON_OBJECT()) AS attr, COALESCE(feat.features, JSON_OBJECT()) AS feat, img.id_image, cat.category_ids, (SELECT COUNT(*) FROM ps_product_attribute_shop pas2 WHERE pas2.id_product = ps.id_product AND pas2.id_shop = ?) AS variations `, constdata.SHOP_ID). Joins("JOIN ps_product p ON p.id_product = ps.id_product"). Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_shop = ? AND pl.id_lang = ?", constdata.SHOP_ID, id_lang). Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_shop = ? AND cl.id_lang = ?", constdata.SHOP_ID, id_lang). Joins("LEFT JOIN variations vary ON vary.id_product = ps.id_product"). Joins("LEFT JOIN features feat ON feat.id_product = ps.id_product"). Joins("LEFT JOIN images img ON img.id_product = ps.id_product"). Joins("LEFT JOIN categories cat ON cat.id_product = ps.id_product"). Joins("JOIN products_page pp ON pp.id_product = ps.id_product"). Where("ps.active = ?", 1). Order("ps.id_product"). Clauses(exclause.With{CTEs: []exclause.CTE{ { Name: "products_page", Subquery: exclause.Subquery{ DB: db.Get(). Model(&dbmodel.PsProductShop{}). Select("id_product, price"). Where("id_shop = ? AND active = 1", constdata.SHOP_ID). Order("id_product"). Limit(limit). Offset(offset), }, }, { Name: "variation_attributes", Subquery: exclause.Subquery{ DB: db.Get(). Table("ps_product_attribute_shop pas"). // <- explicit alias here Select(` pas.id_product, pag.id_attribute_group AS attribute_name, JSON_ARRAYAGG(DISTINCT pa.id_attribute) AS attribute_values `). Joins("JOIN ps_product_attribute_combination ppac ON ppac.id_product_attribute = pas.id_product_attribute"). Joins("JOIN ps_attribute pa ON pa.id_attribute = ppac.id_attribute"). Joins("JOIN ps_attribute_group pag ON pag.id_attribute_group = pa.id_attribute_group"). Where("pas.id_shop = ?", constdata.SHOP_ID). Group("pas.id_product, pag.id_attribute_group"), }, }, { Name: "variations", Subquery: exclause.Subquery{ DB: db.Get(). Table("variation_attributes"). Select("id_product, JSON_OBJECTAGG(attribute_name, attribute_values) AS attributes"). Group("id_product"), }, }, { Name: "features", Subquery: exclause.Subquery{ DB: db.Get(). Table("ps_feature_product pfp"). // <- explicit alias Select("pfp.id_product, JSON_OBJECTAGG(pfp.id_feature, pfp.id_feature_value) AS features"). Group("pfp.id_product"), }, }, { Name: "images", Subquery: exclause.Subquery{ DB: db.Get(). Model(&dbmodel.PsImageShop{}). Select("id_product, id_image"). Where("id_shop = ? AND cover = 1", constdata.SHOP_ID), }, }, { Name: "categories", Subquery: exclause.Subquery{ DB: db.Get(). Model(&dbmodel.PsCategoryProduct{}). Select("id_product, JSON_ARRAYAGG(id_category) AS category_ids"). Group("id_product"), }, }, }}).Find(&products).Error return products, err }