@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.
@@ -1067,11 +1067,14 @@ var StoreFacts = class extends BaseService {
1067
1067
 
1068
1068
  // src/services/catalog.ts
1069
1069
  var PAGE_SIZE = 24;
1070
- function indexSort(sort) {
1071
- if (!sort) return void 0;
1070
+ function sortParts(sort) {
1072
1071
  const s = sort.trim();
1073
1072
  const [field, dir] = s.startsWith("-") ? [s.slice(1), "desc"] : s.includes(":") ? s.split(":") : [s, "asc"];
1074
- const d = dir === "desc" ? "desc" : "asc";
1073
+ return [field, dir === "desc" ? "desc" : "asc"];
1074
+ }
1075
+ function indexSort(sort) {
1076
+ if (!sort) return void 0;
1077
+ const [field, d] = sortParts(sort);
1075
1078
  switch (field) {
1076
1079
  case "price":
1077
1080
  return `price_minor:${d}`;
@@ -1087,6 +1090,20 @@ function indexSort(sort) {
1087
1090
  return void 0;
1088
1091
  }
1089
1092
  }
1093
+ function engineSort(sort) {
1094
+ if (!sort || NATURAL.has(sort)) return {};
1095
+ const [field, order] = sortParts(sort);
1096
+ const key = {
1097
+ price: "price",
1098
+ createdAt: "created_at",
1099
+ created_at: "created_at",
1100
+ updatedAt: "updated_at",
1101
+ updated_at: "updated_at",
1102
+ title: "title",
1103
+ name: "title"
1104
+ }[field];
1105
+ return key ? { sort: key, order } : {};
1106
+ }
1090
1107
  var NATURAL = /* @__PURE__ */ new Set([
1091
1108
  "-createdAt",
1092
1109
  "createdAt:desc",
@@ -1096,6 +1113,7 @@ var NATURAL = /* @__PURE__ */ new Set([
1096
1113
  "popularity:desc"
1097
1114
  ]);
1098
1115
  var quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1116
+ var legacyListing = /* @__PURE__ */ new Set();
1099
1117
  var CatalogService = class extends BaseService {
1100
1118
  facts = new StoreFacts(this._fetch);
1101
1119
  async queryCatalog(q) {
@@ -1105,6 +1123,39 @@ var CatalogService = class extends BaseService {
1105
1123
  const wantsRange = Number.isFinite(q.priceFrom) || Number.isFinite(q.priceTo);
1106
1124
  const sortKey = q.sort && !NATURAL.has(q.sort) ? indexSort(q.sort) : void 0;
1107
1125
  const hierarchy = q.category ? await this.facts.hierarchy(q.category) : [];
1126
+ const attr = Object.entries(q.attributes ?? {}).flatMap(
1127
+ ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1128
+ );
1129
+ if (q.facets && !legacyListing.has(this.engineBase())) {
1130
+ const { currency } = await this.facts.ready();
1131
+ const minor = (n) => Number.isFinite(n) ? Math.max(0, toMinor(n, currency)) : void 0;
1132
+ const env2 = await this.engine("/api/products", {
1133
+ query: {
1134
+ q: search,
1135
+ page,
1136
+ limit,
1137
+ channel: this.creds.channel,
1138
+ category: q.category,
1139
+ tag: q.tags,
1140
+ vendor: q.vendors,
1141
+ product_type: q.productTypes,
1142
+ attr,
1143
+ price_min: minor(q.priceFrom),
1144
+ price_max: minor(q.priceTo),
1145
+ ...engineSort(q.sort),
1146
+ facets: 1
1147
+ }
1148
+ });
1149
+ if (env2.meta?.facets) {
1150
+ return {
1151
+ ...toPage(env2, (p) => toProduct(p)),
1152
+ categoryHierarchy: hierarchy,
1153
+ sorted: true,
1154
+ facets: env2.meta.facets
1155
+ };
1156
+ }
1157
+ legacyListing.add(this.engineBase());
1158
+ }
1108
1159
  if ((sortKey || wantsRange) && await this.facts.pluginOn("meilisearch")) {
1109
1160
  const filters = [];
1110
1161
  if (q.category) {
@@ -1152,9 +1203,6 @@ var CatalogService = class extends BaseService {
1152
1203
  sorted
1153
1204
  };
1154
1205
  }
1155
- const attr = Object.entries(q.attributes ?? {}).flatMap(
1156
- ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1157
- );
1158
1206
  const env = await this.engine("/api/products", {
1159
1207
  query: { q: search, page, limit, channel: this.creds.channel, attr }
1160
1208
  });
@@ -1222,17 +1270,46 @@ var ProductService = class extends CatalogService {
1222
1270
  async listTrendingProducts({ page = 1, limit } = {}) {
1223
1271
  return this.shelf("trending", { page, limit });
1224
1272
  }
1225
- /** More from this product's category. */
1273
+ /**
1274
+ * More like this product. Given `product` (a slug or id), it is the
1275
+ * product's category first, then its collections, then the newest — never
1276
+ * the product itself. Many shops never fill in categories, and an empty
1277
+ * rail under every product page reads as a broken shop.
1278
+ *
1279
+ * Without `product`, it is the old answer: more from `categoryId`.
1280
+ */
1226
1281
  async listRelatedProducts({
1227
1282
  page = 1,
1228
1283
  categoryId,
1284
+ product,
1229
1285
  limit
1230
1286
  } = {}) {
1287
+ if (product) return this.relatedTo(String(product), limit);
1231
1288
  if (!categoryId) return this.list({ page, limit });
1232
1289
  const category = (await this.facts.categories()).find((c) => String(c.id) === String(categoryId));
1233
1290
  if (!category) return this.list({ page, limit });
1234
1291
  return this.list({ page, limit, categories: category.slug });
1235
1292
  }
1293
+ async relatedTo(slug, limit) {
1294
+ const size = Math.min(100, Math.max(1, Number(limit) || PAGE_SIZE));
1295
+ const p = await this.findProduct(slug);
1296
+ const self = String(p.id);
1297
+ const picked = /* @__PURE__ */ new Map();
1298
+ const add = (rows) => {
1299
+ for (const r of rows) {
1300
+ if (picked.size >= size) return;
1301
+ if (r.id !== self && !picked.has(r.id)) picked.set(r.id, r);
1302
+ }
1303
+ };
1304
+ if (p.category) add((await this.list({ limit: size + 1, categories: p.category.slug })).data);
1305
+ for (const c of p.collections ?? []) {
1306
+ if (picked.size >= size) break;
1307
+ add((await this.shelf(c.slug, { page: 1, limit: size + 1 })).data);
1308
+ }
1309
+ if (picked.size < size) add((await this.list({ limit: size + 1 })).data);
1310
+ const data = [...picked.values()];
1311
+ return { data, count: data.length, pageSize: size, noOfPage: 1, page: 1 };
1312
+ }
1236
1313
  async shelf(slug, { page, sort, limit }) {
1237
1314
  const size = Math.min(100, Math.max(1, Number(limit) || PAGE_SIZE));
1238
1315
  try {
@@ -1291,16 +1368,44 @@ var num = (v) => {
1291
1368
  const n = Number(v);
1292
1369
  return Number.isFinite(n) ? n : void 0;
1293
1370
  };
1371
+ function decodeOnce(v) {
1372
+ try {
1373
+ return decodeURIComponent(v);
1374
+ } catch {
1375
+ return v;
1376
+ }
1377
+ }
1378
+ function listParam(p, key) {
1379
+ const out = p.getAll(key).flatMap((v) => decodeOnce(v).split(",")).map((v) => v.trim()).filter(Boolean);
1380
+ return out.length ? out : void 0;
1381
+ }
1294
1382
  function attributesFrom(p) {
1295
1383
  const out = {};
1296
- for (const [key, value] of p) {
1384
+ for (const key of new Set(p.keys())) {
1297
1385
  if (!key.startsWith("attributes.")) continue;
1298
1386
  const handle = key.slice("attributes.".length);
1299
- const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
1300
- if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
1387
+ const values = listParam(p, key);
1388
+ if (handle && values) out[handle] = values;
1301
1389
  }
1302
1390
  return Object.keys(out).length ? out : void 0;
1303
1391
  }
1392
+ var counts = (values) => Object.fromEntries((values ?? []).map((v) => [v.value, v.count]));
1393
+ function toStorefrontFacets(f) {
1394
+ if (!f) return { priceStat: {}, categories: [], tags: [], allFilters: {} };
1395
+ const allFilters = {};
1396
+ const group = (key, values) => {
1397
+ if (values?.length) allFilters[key] = counts(values);
1398
+ };
1399
+ group("vendor", f.vendors);
1400
+ group("product_type", f.product_types);
1401
+ for (const [handle, values] of Object.entries(f.attributes ?? {})) group(`attributes.${handle}`, values);
1402
+ return {
1403
+ priceStat: f.price ? { min: toMajor(f.price.min), max: toMajor(f.price.max) } : {},
1404
+ categories: (f.categories ?? []).map((c) => ({ name: c.title, slug: c.slug, count: c.count })),
1405
+ tags: (f.tags ?? []).map((t) => ({ name: t.value, count: t.count })),
1406
+ allFilters
1407
+ };
1408
+ }
1304
1409
  var SearchService = class extends CatalogService {
1305
1410
  emptyResult() {
1306
1411
  return {
@@ -1308,7 +1413,7 @@ var SearchService = class extends CatalogService {
1308
1413
  count: 0,
1309
1414
  totalPages: 0,
1310
1415
  categoryHierarchy: [],
1311
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1416
+ facets: toStorefrontFacets()
1312
1417
  };
1313
1418
  }
1314
1419
  async searchWithUrl(url, slug) {
@@ -1323,7 +1428,11 @@ var SearchService = class extends CatalogService {
1323
1428
  category: slug || p.get("categories")?.split(",")[0] || void 0,
1324
1429
  priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
1325
1430
  priceTo: num(p.get("priceTo")) ?? num(to ?? null),
1326
- attributes: attributesFrom(p)
1431
+ attributes: attributesFrom(p),
1432
+ tags: listParam(p, "tags"),
1433
+ vendors: listParam(p, "vendor"),
1434
+ productTypes: listParam(p, "product_type"),
1435
+ facets: true
1327
1436
  });
1328
1437
  return {
1329
1438
  data: res.data,
@@ -1331,7 +1440,7 @@ var SearchService = class extends CatalogService {
1331
1440
  totalPages: res.noOfPage,
1332
1441
  page: res.page,
1333
1442
  categoryHierarchy: res.categoryHierarchy,
1334
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1443
+ facets: toStorefrontFacets(res.facets)
1335
1444
  };
1336
1445
  } catch (err) {
1337
1446
  if (err?.status === 404) return this.emptyResult();
@@ -3467,6 +3576,7 @@ exports.staticStore = staticStore;
3467
3576
  exports.storeForHost = storeForHost;
3468
3577
  exports.storeService = storeService;
3469
3578
  exports.toPage = toPage;
3579
+ exports.toStorefrontFacets = toStorefrontFacets;
3470
3580
  exports.uploadService = uploadService;
3471
3581
  exports.userService = userService;
3472
3582
  exports.variantService = variantService;