@misiki/gocommerce-connector 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,6 +109,29 @@ type ListMeta = {
109
109
  offset: number;
110
110
  page: number;
111
111
  total_pages: number;
112
+ /** Only on `GET /api/products?facets=1`, from an engine that counts them (D130). */
113
+ facets?: ProductFacets;
114
+ };
115
+ type FacetValue = {
116
+ value: string;
117
+ count: number;
118
+ };
119
+ /** core/catalog_facets.go: counted over the whole listing, each group without its own selection. */
120
+ type ProductFacets = {
121
+ price: {
122
+ min: Money;
123
+ max: Money;
124
+ } | null;
125
+ tags: FacetValue[] | null;
126
+ vendors: FacetValue[] | null;
127
+ product_types: FacetValue[] | null;
128
+ categories: {
129
+ id: number;
130
+ slug: string;
131
+ title: string;
132
+ count: number;
133
+ }[] | null;
134
+ attributes: Record<string, FacetValue[]> | null;
112
135
  };
113
136
  type Envelope<T> = {
114
137
  data: T;
@@ -1327,6 +1350,16 @@ type CatalogQuery = {
1327
1350
  * are not applied.
1328
1351
  */
1329
1352
  attributes?: Record<string, string[]>;
1353
+ tags?: string[];
1354
+ vendors?: string[];
1355
+ productTypes?: string[];
1356
+ /**
1357
+ * Ask the engine to count the filter values too. Such a query goes to
1358
+ * `GET /api/products`, which then also answers the category, the price
1359
+ * range, the sort and every filter above (D130); an engine too old to count
1360
+ * them is asked the way it always was, and the page has no facets.
1361
+ */
1362
+ facets?: boolean;
1330
1363
  };
1331
1364
  type CatalogPage = {
1332
1365
  data: ReturnType<typeof toProduct>[] | ReturnType<typeof fromSearchDoc>[];
@@ -1340,17 +1373,8 @@ type CatalogPage = {
1340
1373
  }[];
1341
1374
  /** Whether the answer honoured `sort` and the price range, or had to drop them. */
1342
1375
  sorted: boolean;
1376
+ facets?: ProductFacets;
1343
1377
  };
1344
- /**
1345
- * One shelf of products, from whichever engine route can answer the question.
1346
- *
1347
- * - A category, no reordering: `GET /api/categories/{slug}`, which sells the
1348
- * category and its descendants.
1349
- * - A search or the whole shop, no reordering: `GET /api/products?q=`.
1350
- * - Any sort but newest, or a price range: the Meilisearch module, when it is
1351
- * installed and configured. Without it the shelf comes back in the engine's
1352
- * order with `sorted: false`, rather than failing or pretending.
1353
- */
1354
1378
  declare class CatalogService extends BaseService {
1355
1379
  protected readonly facts: StoreFacts;
1356
1380
  protected queryCatalog(q: CatalogQuery): Promise<CatalogPage>;
@@ -1924,155 +1948,29 @@ declare class ProductService extends CatalogService {
1924
1948
  noOfPage: number;
1925
1949
  page: number;
1926
1950
  }>;
1927
- /** More from this product's category. */
1928
- listRelatedProducts({ page, categoryId, limit, }?: {
1951
+ /**
1952
+ * More like this product. Given `product` (a slug or id), it is the
1953
+ * product's category first, then its collections, then the newest — never
1954
+ * the product itself. Many shops never fill in categories, and an empty
1955
+ * rail under every product page reads as a broken shop.
1956
+ *
1957
+ * Without `product`, it is the old answer: more from `categoryId`.
1958
+ */
1959
+ listRelatedProducts({ page, categoryId, product, limit, }?: {
1929
1960
  page?: number;
1930
1961
  categoryId?: string;
1962
+ product?: string;
1931
1963
  limit?: number;
1932
1964
  }): Promise<{
1933
1965
  data: {
1934
1966
  id: string;
1935
- slug: string;
1936
- title: string;
1937
- name: string;
1938
- subtitle: null;
1939
- description: string;
1940
- status: string;
1941
- active: boolean;
1942
- type: string;
1943
- currency: string;
1944
- currencyCode: string;
1945
- price: number;
1946
- mrp: number;
1947
- minPrice: number;
1948
- maxPrice: number;
1949
- sku: string;
1950
- barcode: string | null;
1951
- weight: number | null;
1952
- originCountry: string | null;
1953
- thumbnail: string;
1954
- image_url: string;
1955
- images: string;
1956
- imageList: string[];
1957
- manageInventory: boolean;
1958
- allowBackorder: boolean;
1959
- stock: number;
1960
- hasStock: boolean;
1961
- categoryId: string | null;
1962
- category: {
1963
- id: string;
1964
- name: string;
1965
- slug: string;
1966
- } | null;
1967
- categories: {
1968
- category: {
1969
- id: string;
1970
- name: string;
1971
- slug: string;
1972
- };
1973
- }[];
1974
- categoryHierarchy: {
1975
- name: string;
1976
- slug: string;
1977
- }[];
1978
- collections: {
1979
- id: string;
1980
- name: string;
1981
- slug: string;
1982
- }[];
1983
- vendor: {
1984
- businessName: string;
1985
- } | null;
1986
- productTags: string;
1987
- keywords: string;
1988
- metaTitle: string;
1989
- metaDescription: string | null;
1990
- link: string;
1991
- options: {
1992
- id: string;
1993
- title: string;
1994
- name: string;
1995
- type: string;
1996
- values: {
1997
- id: string;
1998
- value: string;
1999
- }[];
2000
- }[];
2001
- variants: {
2002
- id: string;
2003
- productId: string;
2004
- title: string;
2005
- sku: string;
2006
- barcode: string | null;
2007
- price: number;
2008
- mrp: number;
2009
- stock: number;
2010
- manageInventory: boolean;
2011
- allowBackorder: boolean;
2012
- active: boolean;
2013
- weight: number | null;
2014
- length: number | null;
2015
- width: number | null;
2016
- height: number | null;
2017
- images: string;
2018
- img: string;
2019
- image: string;
2020
- thumbnail: string;
2021
- options: {
2022
- optionId: string;
2023
- value: string;
2024
- }[];
2025
- metadata: Record<string, unknown> | null;
2026
- }[];
2027
- ratings: ReturnType<typeof toRating>[];
2028
- rating: number | null;
2029
- ratingCount: number;
2030
- reviewCount: number;
2031
- attributes: {
2032
- name: string;
2033
- value: string;
2034
- handle: string;
2035
- }[];
2036
- metadata: Record<string, unknown> | null;
2037
- createdAt: string;
2038
- updatedAt: string;
2039
- }[] | {
2040
- id: string;
2041
- slug: string;
2042
- title: string;
2043
- name: string;
2044
- description: string;
2045
- currency: string;
2046
- price: number;
2047
- mrp: number;
2048
- minPrice: number;
2049
- maxPrice: number;
2050
- thumbnail: string | null;
2051
- image_url: string | null;
2052
- images: string;
2053
- stock: number;
2054
- hasStock: boolean;
2055
- manageInventory: boolean;
2056
- allowBackorder: boolean;
2057
- productTags: string;
2058
- category: {
2059
- name: string;
2060
- slug: string;
2061
- } | null;
2062
- vendor: {
2063
- businessName: string;
2064
- } | null;
2065
- variants: ReturnType<typeof toVariant>[];
2066
- options: never[];
2067
- ratings: never[];
2068
- link: string;
2069
- updatedAt: string;
2070
1967
  }[];
2071
1968
  count: number;
2072
1969
  pageSize: number;
2073
1970
  noOfPage: number;
2074
1971
  page: number;
2075
1972
  }>;
1973
+ private relatedTo;
2076
1974
  private shelf;
2077
1975
  /**
2078
1976
  * A review, through ext/reviews. The engine asks for a name and an email
@@ -2094,14 +1992,39 @@ declare class ProductService extends CatalogService {
2094
1992
  }
2095
1993
  declare const productService: ProductService;
2096
1994
 
1995
+ /**
1996
+ * The engine's counts in the shape the storefront's filter rail reads. Each
1997
+ * `allFilters` key is also the URL parameter the rail writes when a value is
1998
+ * ticked, and `searchWithUrl` reads it back: `vendor`, `product_type` and
1999
+ * `attributes.<handle>`. Tags go to the rail's own tag group, written as `tags`.
2000
+ */
2001
+ declare function toStorefrontFacets(f?: ProductFacets | null): {
2002
+ priceStat: {
2003
+ min: number;
2004
+ max: number;
2005
+ } | {
2006
+ min?: undefined;
2007
+ max?: undefined;
2008
+ };
2009
+ categories: {
2010
+ name: string;
2011
+ slug: string;
2012
+ count: number;
2013
+ }[];
2014
+ tags: {
2015
+ name: string;
2016
+ count: number;
2017
+ }[];
2018
+ allFilters: Record<string, Record<string, number>>;
2019
+ };
2097
2020
  /**
2098
2021
  * The listing pages' search: `/products?search=&sort=&page=` and the category
2099
2022
  * catch-all `/{slug}`.
2100
2023
  *
2101
- * Facets are empty. The engine's search route returns hits and a total, not
2102
- * distributions, and a facet list made from one page of hits would count that
2103
- * page, not the shop. An empty `priceStat` hides the price slider rather than
2104
- * drawing one with a made-up range.
2024
+ * Facets come from the engine, counted over the whole listing rather than the
2025
+ * page (D130). An engine too old to count them leaves them empty, and an empty
2026
+ * `priceStat` hides the price slider rather than drawing one with a made-up
2027
+ * range.
2105
2028
  */
2106
2029
  declare class SearchService extends CatalogService {
2107
2030
  emptyResult(): {
@@ -2113,10 +2036,23 @@ declare class SearchService extends CatalogService {
2113
2036
  slug: string;
2114
2037
  }[];
2115
2038
  facets: {
2116
- priceStat: {};
2117
- categories: never[];
2118
- tags: never[];
2119
- allFilters: {};
2039
+ priceStat: {
2040
+ min: number;
2041
+ max: number;
2042
+ } | {
2043
+ min?: undefined;
2044
+ max?: undefined;
2045
+ };
2046
+ categories: {
2047
+ name: string;
2048
+ slug: string;
2049
+ count: number;
2050
+ }[];
2051
+ tags: {
2052
+ name: string;
2053
+ count: number;
2054
+ }[];
2055
+ allFilters: Record<string, Record<string, number>>;
2120
2056
  };
2121
2057
  };
2122
2058
  searchWithUrl(url: URL | string, slug?: string): Promise<{
@@ -2128,10 +2064,23 @@ declare class SearchService extends CatalogService {
2128
2064
  slug: string;
2129
2065
  }[];
2130
2066
  facets: {
2131
- priceStat: {};
2132
- categories: never[];
2133
- tags: never[];
2134
- allFilters: {};
2067
+ priceStat: {
2068
+ min: number;
2069
+ max: number;
2070
+ } | {
2071
+ min?: undefined;
2072
+ max?: undefined;
2073
+ };
2074
+ categories: {
2075
+ name: string;
2076
+ slug: string;
2077
+ count: number;
2078
+ }[];
2079
+ tags: {
2080
+ name: string;
2081
+ count: number;
2082
+ }[];
2083
+ allFilters: Record<string, Record<string, number>>;
2135
2084
  };
2136
2085
  } | {
2137
2086
  data: {
@@ -2280,10 +2229,23 @@ declare class SearchService extends CatalogService {
2280
2229
  slug: string;
2281
2230
  }[];
2282
2231
  facets: {
2283
- priceStat: {};
2284
- categories: never[];
2285
- tags: never[];
2286
- allFilters: {};
2232
+ priceStat: {
2233
+ min: number;
2234
+ max: number;
2235
+ } | {
2236
+ min?: undefined;
2237
+ max?: undefined;
2238
+ };
2239
+ categories: {
2240
+ name: string;
2241
+ slug: string;
2242
+ count: number;
2243
+ }[];
2244
+ tags: {
2245
+ name: string;
2246
+ count: number;
2247
+ }[];
2248
+ allFilters: Record<string, Record<string, number>>;
2287
2249
  };
2288
2250
  }>;
2289
2251
  }
@@ -7067,4 +7029,4 @@ declare class B2bService extends BaseService {
7067
7029
  }
7068
7030
  declare const b2bService: B2bService;
7069
7031
 
7070
- export { LayoutService as $, type Address as A, type B2bPlaced as B, type Cart as C, CountryService as D, CouponService as E, type Credentials as F, type Credit as G, CurrencyService as H, type Customer as I, DealService as J, DemoRequestService as K, type EngineRequest as L, EnquiryService as M, type Envelope as N, type Faq as O, FaqService as P, FeedbackService as Q, type Fulfillment as R, GalleryService as S, type HomeLayout as T, type HomeLayoutSection as U, HomeService as V, type IdentityAddress as W, InitService as X, type Invitation as Y, type LayoutCollection as Z, type LayoutPage as _, AddressService as a, autocompleteService as a$, type Lead as a0, type ListMeta as a1, MeilisearchService as a2, type Member as a3, type Menu as a4, type MenuItem as a5, MenuService as a6, type MenuSummary as a7, type Money as a8, NewsletterService as a9, SearchService as aA, SettingService as aB, type ShippingRate as aC, StateService as aD, type StoreProfile as aE, StoreService as aF, type StorefrontCategory as aG, type StorefrontMenu as aH, type StorefrontOrder as aI, type StorefrontProduct as aJ, type ThemeSettings as aK, type ThemeSettingsResult as aL, UploadService as aM, UserService as aN, type Variant as aO, type VariantImage as aP, VariantService as aQ, VarniCustomDesignService as aR, VarniCustomProductService as aS, type Vendor as aT, VendorService as aU, WarrantyService as aV, type Wishlist as aW, type WishlistItem as aX, WishlistService as aY, addressService as aZ, authService as a_, type Offer as aa, type Order as ab, type OrderLine as ac, OrderService as ad, type PageLayout as ae, PageService as af, type PaymentIntent as ag, PaymentMethodService as ah, type Plugin as ai, PluginService as aj, PopularSearchService as ak, PopularityService as al, type Product as am, type ProductOption as an, ProductService as ao, ProfileService as ap, type Quote as aq, REDIRECT_GATEWAYS as ar, ReelsService as as, RegionService as at, type RejectedLine as au, type Review as av, type ReviewPage as aw, ReviewService as ax, type SearchDoc as ay, type SearchResult as az, type AppliedDiscount as b, toMajor as b$, b2bService as b0, bannerService as b1, blogService as b2, cartService as b3, categoryService as b4, chatService as b5, checkoutService as b6, collectionService as b7, contactService as b8, countryService as b9, pluginService as bA, pluginsService as bB, popularSearchService as bC, popularityService as bD, priceOf as bE, productImages as bF, productService as bG, profileService as bH, qs as bI, reelsService as bJ, regionService as bK, reviewService as bL, searchService as bM, setRequestHost as bN, settingService as bO, settingsService as bP, stateService as bQ, staticStore as bR, storeForHost as bS, storeService as bT, toCart as bU, toCartLine as bV, toCategory as bW, toCmsPage as bX, toCollection as bY, toEngineAddress as bZ, toIdentityAddress as b_, couponService as ba, currencyExponent as bb, currencyService as bc, dealService as bd, demoRequestService as be, emptyCart as bf, emptyPage as bg, enquiryService as bh, faqService as bi, feedbackService as bj, fromEngineAddress as bk, fromIdentityAddress as bl, fromSearchDoc as bm, galleryService as bn, homeService as bo, initService as bp, layoutService as bq, mediaUrl as br, meilisearchService as bs, menuService as bt, minorToMajor as bu, newsletterService as bv, orderService as bw, ownDomain as bx, pageService as by, paymentMethodService as bz, type Approval as c, toMinor as c0, toOrder as c1, toPage as c2, toProduct as c3, toRating as c4, toUser as c5, toVariant as c6, toVendor as c7, uploadService as c8, userService as c9, variantService as ca, varniCustomDesignService as cb, varniCustomProductService as cc, vendorService as cd, warrantyService as ce, wishlistService as cf, type AuthResponse as d, AuthService as e, AutocompleteService as f, type B2bRole as g, B2bService as h, BannerService as i, BaseService as j, BlogService as k, type CartLine as l, CartService as m, CatalogService as n, type Category as o, CategoryService as p, ChatService as q, type CheckoutOptions as r, type CheckoutResult as s, CheckoutService as t, type CmsPage as u, type Collection as v, CollectionService as w, type Company as x, type CompanyOrder as y, ContactService as z };
7032
+ export { type LayoutPage as $, type Address as A, type B2bPlaced as B, type Cart as C, CountryService as D, CouponService as E, type Credentials as F, type Credit as G, CurrencyService as H, type Customer as I, DealService as J, DemoRequestService as K, type EngineRequest as L, EnquiryService as M, type Envelope as N, type FacetValue as O, type Faq as P, FaqService as Q, FeedbackService as R, type Fulfillment as S, GalleryService as T, type HomeLayout as U, type HomeLayoutSection as V, HomeService as W, type IdentityAddress as X, InitService as Y, type Invitation as Z, type LayoutCollection as _, AddressService as a, addressService as a$, LayoutService as a0, type Lead as a1, type ListMeta as a2, MeilisearchService as a3, type Member as a4, type Menu as a5, type MenuItem as a6, MenuService as a7, type MenuSummary as a8, type Money as a9, type SearchDoc as aA, type SearchResult as aB, SearchService as aC, SettingService as aD, type ShippingRate as aE, StateService as aF, type StoreProfile as aG, StoreService as aH, type StorefrontCategory as aI, type StorefrontMenu as aJ, type StorefrontOrder as aK, type StorefrontProduct as aL, type ThemeSettings as aM, type ThemeSettingsResult as aN, UploadService as aO, UserService as aP, type Variant as aQ, type VariantImage as aR, VariantService as aS, VarniCustomDesignService as aT, VarniCustomProductService as aU, type Vendor as aV, VendorService as aW, WarrantyService as aX, type Wishlist as aY, type WishlistItem as aZ, WishlistService as a_, NewsletterService as aa, type Offer as ab, type Order as ac, type OrderLine as ad, OrderService as ae, type PageLayout as af, PageService as ag, type PaymentIntent as ah, PaymentMethodService as ai, type Plugin as aj, PluginService as ak, PopularSearchService as al, PopularityService as am, type Product as an, type ProductFacets as ao, type ProductOption as ap, ProductService as aq, ProfileService as ar, type Quote as as, REDIRECT_GATEWAYS as at, ReelsService as au, RegionService as av, type RejectedLine as aw, type Review as ax, type ReviewPage as ay, ReviewService as az, type AppliedDiscount as b, toEngineAddress as b$, authService as b0, autocompleteService as b1, b2bService as b2, bannerService as b3, blogService as b4, cartService as b5, categoryService as b6, chatService as b7, checkoutService as b8, collectionService as b9, pageService as bA, paymentMethodService as bB, pluginService as bC, pluginsService as bD, popularSearchService as bE, popularityService as bF, priceOf as bG, productImages as bH, productService as bI, profileService as bJ, qs as bK, reelsService as bL, regionService as bM, reviewService as bN, searchService as bO, setRequestHost as bP, settingService as bQ, settingsService as bR, stateService as bS, staticStore as bT, storeForHost as bU, storeService as bV, toCart as bW, toCartLine as bX, toCategory as bY, toCmsPage as bZ, toCollection as b_, contactService as ba, countryService as bb, couponService as bc, currencyExponent as bd, currencyService as be, dealService as bf, demoRequestService as bg, emptyCart as bh, emptyPage as bi, enquiryService as bj, faqService as bk, feedbackService as bl, fromEngineAddress as bm, fromIdentityAddress as bn, fromSearchDoc as bo, galleryService as bp, homeService as bq, initService as br, layoutService as bs, mediaUrl as bt, meilisearchService as bu, menuService as bv, minorToMajor as bw, newsletterService as bx, orderService as by, ownDomain as bz, type Approval as c, toIdentityAddress as c0, toMajor as c1, toMinor as c2, toOrder as c3, toPage as c4, toProduct as c5, toRating as c6, toStorefrontFacets as c7, toUser as c8, toVariant as c9, toVendor as ca, uploadService as cb, userService as cc, variantService as cd, varniCustomDesignService as ce, varniCustomProductService as cf, vendorService as cg, warrantyService as ch, wishlistService as ci, type AuthResponse as d, AuthService as e, AutocompleteService as f, type B2bRole as g, B2bService as h, BannerService as i, BaseService as j, BlogService as k, type CartLine as l, CartService as m, CatalogService as n, type Category as o, CategoryService as p, ChatService as q, type CheckoutOptions as r, type CheckoutResult as s, CheckoutService as t, type CmsPage as u, type Collection as v, CollectionService as w, type Company as x, type CompanyOrder as y, ContactService as z };
package/dist/index.cjs CHANGED
@@ -1086,11 +1086,14 @@ var StoreFacts = class extends BaseService {
1086
1086
 
1087
1087
  // src/services/catalog.ts
1088
1088
  var PAGE_SIZE = 24;
1089
- function indexSort(sort) {
1090
- if (!sort) return void 0;
1089
+ function sortParts(sort) {
1091
1090
  const s = sort.trim();
1092
1091
  const [field, dir] = s.startsWith("-") ? [s.slice(1), "desc"] : s.includes(":") ? s.split(":") : [s, "asc"];
1093
- const d = dir === "desc" ? "desc" : "asc";
1092
+ return [field, dir === "desc" ? "desc" : "asc"];
1093
+ }
1094
+ function indexSort(sort) {
1095
+ if (!sort) return void 0;
1096
+ const [field, d] = sortParts(sort);
1094
1097
  switch (field) {
1095
1098
  case "price":
1096
1099
  return `price_minor:${d}`;
@@ -1106,6 +1109,20 @@ function indexSort(sort) {
1106
1109
  return void 0;
1107
1110
  }
1108
1111
  }
1112
+ function engineSort(sort) {
1113
+ if (!sort || NATURAL.has(sort)) return {};
1114
+ const [field, order] = sortParts(sort);
1115
+ const key = {
1116
+ price: "price",
1117
+ createdAt: "created_at",
1118
+ created_at: "created_at",
1119
+ updatedAt: "updated_at",
1120
+ updated_at: "updated_at",
1121
+ title: "title",
1122
+ name: "title"
1123
+ }[field];
1124
+ return key ? { sort: key, order } : {};
1125
+ }
1109
1126
  var NATURAL = /* @__PURE__ */ new Set([
1110
1127
  "-createdAt",
1111
1128
  "createdAt:desc",
@@ -1115,6 +1132,7 @@ var NATURAL = /* @__PURE__ */ new Set([
1115
1132
  "popularity:desc"
1116
1133
  ]);
1117
1134
  var quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1135
+ var legacyListing = /* @__PURE__ */ new Set();
1118
1136
  var CatalogService = class extends BaseService {
1119
1137
  facts = new StoreFacts(this._fetch);
1120
1138
  async queryCatalog(q) {
@@ -1124,6 +1142,39 @@ var CatalogService = class extends BaseService {
1124
1142
  const wantsRange = Number.isFinite(q.priceFrom) || Number.isFinite(q.priceTo);
1125
1143
  const sortKey = q.sort && !NATURAL.has(q.sort) ? indexSort(q.sort) : void 0;
1126
1144
  const hierarchy = q.category ? await this.facts.hierarchy(q.category) : [];
1145
+ const attr = Object.entries(q.attributes ?? {}).flatMap(
1146
+ ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1147
+ );
1148
+ if (q.facets && !legacyListing.has(this.engineBase())) {
1149
+ const { currency } = await this.facts.ready();
1150
+ const minor = (n) => Number.isFinite(n) ? Math.max(0, toMinor(n, currency)) : void 0;
1151
+ const env2 = await this.engine("/api/products", {
1152
+ query: {
1153
+ q: search,
1154
+ page,
1155
+ limit,
1156
+ channel: this.creds.channel,
1157
+ category: q.category,
1158
+ tag: q.tags,
1159
+ vendor: q.vendors,
1160
+ product_type: q.productTypes,
1161
+ attr,
1162
+ price_min: minor(q.priceFrom),
1163
+ price_max: minor(q.priceTo),
1164
+ ...engineSort(q.sort),
1165
+ facets: 1
1166
+ }
1167
+ });
1168
+ if (env2.meta?.facets) {
1169
+ return {
1170
+ ...toPage(env2, (p) => toProduct(p)),
1171
+ categoryHierarchy: hierarchy,
1172
+ sorted: true,
1173
+ facets: env2.meta.facets
1174
+ };
1175
+ }
1176
+ legacyListing.add(this.engineBase());
1177
+ }
1127
1178
  if ((sortKey || wantsRange) && await this.facts.pluginOn("meilisearch")) {
1128
1179
  const filters = [];
1129
1180
  if (q.category) {
@@ -1171,9 +1222,6 @@ var CatalogService = class extends BaseService {
1171
1222
  sorted
1172
1223
  };
1173
1224
  }
1174
- const attr = Object.entries(q.attributes ?? {}).flatMap(
1175
- ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1176
- );
1177
1225
  const env = await this.engine("/api/products", {
1178
1226
  query: { q: search, page, limit, channel: this.creds.channel, attr }
1179
1227
  });
@@ -1241,17 +1289,46 @@ var ProductService = class extends CatalogService {
1241
1289
  async listTrendingProducts({ page = 1, limit } = {}) {
1242
1290
  return this.shelf("trending", { page, limit });
1243
1291
  }
1244
- /** More from this product's category. */
1292
+ /**
1293
+ * More like this product. Given `product` (a slug or id), it is the
1294
+ * product's category first, then its collections, then the newest — never
1295
+ * the product itself. Many shops never fill in categories, and an empty
1296
+ * rail under every product page reads as a broken shop.
1297
+ *
1298
+ * Without `product`, it is the old answer: more from `categoryId`.
1299
+ */
1245
1300
  async listRelatedProducts({
1246
1301
  page = 1,
1247
1302
  categoryId,
1303
+ product,
1248
1304
  limit
1249
1305
  } = {}) {
1306
+ if (product) return this.relatedTo(String(product), limit);
1250
1307
  if (!categoryId) return this.list({ page, limit });
1251
1308
  const category = (await this.facts.categories()).find((c) => String(c.id) === String(categoryId));
1252
1309
  if (!category) return this.list({ page, limit });
1253
1310
  return this.list({ page, limit, categories: category.slug });
1254
1311
  }
1312
+ async relatedTo(slug, limit) {
1313
+ const size = Math.min(100, Math.max(1, Number(limit) || PAGE_SIZE));
1314
+ const p = await this.findProduct(slug);
1315
+ const self = String(p.id);
1316
+ const picked = /* @__PURE__ */ new Map();
1317
+ const add = (rows) => {
1318
+ for (const r of rows) {
1319
+ if (picked.size >= size) return;
1320
+ if (r.id !== self && !picked.has(r.id)) picked.set(r.id, r);
1321
+ }
1322
+ };
1323
+ if (p.category) add((await this.list({ limit: size + 1, categories: p.category.slug })).data);
1324
+ for (const c of p.collections ?? []) {
1325
+ if (picked.size >= size) break;
1326
+ add((await this.shelf(c.slug, { page: 1, limit: size + 1 })).data);
1327
+ }
1328
+ if (picked.size < size) add((await this.list({ limit: size + 1 })).data);
1329
+ const data = [...picked.values()];
1330
+ return { data, count: data.length, pageSize: size, noOfPage: 1, page: 1 };
1331
+ }
1255
1332
  async shelf(slug, { page, sort, limit }) {
1256
1333
  const size = Math.min(100, Math.max(1, Number(limit) || PAGE_SIZE));
1257
1334
  try {
@@ -1310,16 +1387,44 @@ var num = (v) => {
1310
1387
  const n = Number(v);
1311
1388
  return Number.isFinite(n) ? n : void 0;
1312
1389
  };
1390
+ function decodeOnce(v) {
1391
+ try {
1392
+ return decodeURIComponent(v);
1393
+ } catch {
1394
+ return v;
1395
+ }
1396
+ }
1397
+ function listParam(p, key) {
1398
+ const out = p.getAll(key).flatMap((v) => decodeOnce(v).split(",")).map((v) => v.trim()).filter(Boolean);
1399
+ return out.length ? out : void 0;
1400
+ }
1313
1401
  function attributesFrom(p) {
1314
1402
  const out = {};
1315
- for (const [key, value] of p) {
1403
+ for (const key of new Set(p.keys())) {
1316
1404
  if (!key.startsWith("attributes.")) continue;
1317
1405
  const handle = key.slice("attributes.".length);
1318
- const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
1319
- if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
1406
+ const values = listParam(p, key);
1407
+ if (handle && values) out[handle] = values;
1320
1408
  }
1321
1409
  return Object.keys(out).length ? out : void 0;
1322
1410
  }
1411
+ var counts = (values) => Object.fromEntries((values ?? []).map((v) => [v.value, v.count]));
1412
+ function toStorefrontFacets(f) {
1413
+ if (!f) return { priceStat: {}, categories: [], tags: [], allFilters: {} };
1414
+ const allFilters = {};
1415
+ const group = (key, values) => {
1416
+ if (values?.length) allFilters[key] = counts(values);
1417
+ };
1418
+ group("vendor", f.vendors);
1419
+ group("product_type", f.product_types);
1420
+ for (const [handle, values] of Object.entries(f.attributes ?? {})) group(`attributes.${handle}`, values);
1421
+ return {
1422
+ priceStat: f.price ? { min: toMajor(f.price.min), max: toMajor(f.price.max) } : {},
1423
+ categories: (f.categories ?? []).map((c) => ({ name: c.title, slug: c.slug, count: c.count })),
1424
+ tags: (f.tags ?? []).map((t) => ({ name: t.value, count: t.count })),
1425
+ allFilters
1426
+ };
1427
+ }
1323
1428
  var SearchService = class extends CatalogService {
1324
1429
  emptyResult() {
1325
1430
  return {
@@ -1327,7 +1432,7 @@ var SearchService = class extends CatalogService {
1327
1432
  count: 0,
1328
1433
  totalPages: 0,
1329
1434
  categoryHierarchy: [],
1330
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1435
+ facets: toStorefrontFacets()
1331
1436
  };
1332
1437
  }
1333
1438
  async searchWithUrl(url, slug) {
@@ -1342,7 +1447,11 @@ var SearchService = class extends CatalogService {
1342
1447
  category: slug || p.get("categories")?.split(",")[0] || void 0,
1343
1448
  priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
1344
1449
  priceTo: num(p.get("priceTo")) ?? num(to ?? null),
1345
- attributes: attributesFrom(p)
1450
+ attributes: attributesFrom(p),
1451
+ tags: listParam(p, "tags"),
1452
+ vendors: listParam(p, "vendor"),
1453
+ productTypes: listParam(p, "product_type"),
1454
+ facets: true
1346
1455
  });
1347
1456
  return {
1348
1457
  data: res.data,
@@ -1350,7 +1459,7 @@ var SearchService = class extends CatalogService {
1350
1459
  totalPages: res.noOfPage,
1351
1460
  page: res.page,
1352
1461
  categoryHierarchy: res.categoryHierarchy,
1353
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1462
+ facets: toStorefrontFacets(res.facets)
1354
1463
  };
1355
1464
  } catch (err) {
1356
1465
  if (err?.status === 404) return this.emptyResult();
@@ -3511,6 +3620,7 @@ exports.toOrder = toOrder;
3511
3620
  exports.toPage = toPage;
3512
3621
  exports.toProduct = toProduct;
3513
3622
  exports.toRating = toRating;
3623
+ exports.toStorefrontFacets = toStorefrontFacets;
3514
3624
  exports.toUser = toUser;
3515
3625
  exports.toVariant = toVariant;
3516
3626
  exports.toVendor = toVendor;