@misiki/gocommerce-connector 0.9.1 → 0.11.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.
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
  });
@@ -1339,16 +1387,44 @@ var num = (v) => {
1339
1387
  const n = Number(v);
1340
1388
  return Number.isFinite(n) ? n : void 0;
1341
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
+ }
1342
1401
  function attributesFrom(p) {
1343
1402
  const out = {};
1344
- for (const [key, value] of p) {
1403
+ for (const key of new Set(p.keys())) {
1345
1404
  if (!key.startsWith("attributes.")) continue;
1346
1405
  const handle = key.slice("attributes.".length);
1347
- const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
1348
- if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
1406
+ const values = listParam(p, key);
1407
+ if (handle && values) out[handle] = values;
1349
1408
  }
1350
1409
  return Object.keys(out).length ? out : void 0;
1351
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
+ }
1352
1428
  var SearchService = class extends CatalogService {
1353
1429
  emptyResult() {
1354
1430
  return {
@@ -1356,7 +1432,7 @@ var SearchService = class extends CatalogService {
1356
1432
  count: 0,
1357
1433
  totalPages: 0,
1358
1434
  categoryHierarchy: [],
1359
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1435
+ facets: toStorefrontFacets()
1360
1436
  };
1361
1437
  }
1362
1438
  async searchWithUrl(url, slug) {
@@ -1371,7 +1447,11 @@ var SearchService = class extends CatalogService {
1371
1447
  category: slug || p.get("categories")?.split(",")[0] || void 0,
1372
1448
  priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
1373
1449
  priceTo: num(p.get("priceTo")) ?? num(to ?? null),
1374
- 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
1375
1455
  });
1376
1456
  return {
1377
1457
  data: res.data,
@@ -1379,7 +1459,7 @@ var SearchService = class extends CatalogService {
1379
1459
  totalPages: res.noOfPage,
1380
1460
  page: res.page,
1381
1461
  categoryHierarchy: res.categoryHierarchy,
1382
- facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
1462
+ facets: toStorefrontFacets(res.facets)
1383
1463
  };
1384
1464
  } catch (err) {
1385
1465
  if (err?.status === 404) return this.emptyResult();
@@ -2599,21 +2679,38 @@ var MenuService = class extends BaseService {
2599
2679
  const m = await this.data(`/x/navigation/menus/${encodeURIComponent(handle)}`);
2600
2680
  return { menuId: m.handle, name: m.title, items: (m.items ?? []).map(toItem) };
2601
2681
  }
2602
- /** The header and footer menus, or `null` when the module is not there. Cached: a menu is store-level. */
2682
+ /**
2683
+ * The header and footer menus, or `null` when the module is not there. Cached: a menu is
2684
+ * store-level. Each is the menu the shop chose for it in the theme editor's header or footer
2685
+ * (GoCommerce D131), still under the `header` / `footer` id every storefront looks for, so the
2686
+ * choice reaches every place that draws one; the menus named so are the default.
2687
+ */
2603
2688
  async engineMenus() {
2604
2689
  if (!await this.facts.pluginOn("navigation")) return null;
2605
2690
  return remember(`${this.engineBase()}|${this.store ?? ""}|menus`, async () => {
2606
2691
  const out = [];
2607
- for (const handle of ["header", "footer"]) {
2692
+ for (const part of ["header", "footer"]) {
2608
2693
  try {
2609
- const m = await this.data(`/x/navigation/menus/${handle}`);
2610
- out.push({ menuId: handle, name: m.title, items: (m.items ?? []).map(toItem) });
2694
+ const handle = await this.chosenMenu(part);
2695
+ const m = await this.data(`/x/navigation/menus/${encodeURIComponent(handle)}`);
2696
+ out.push({ menuId: part, name: m.title, items: (m.items ?? []).map(toItem) });
2611
2697
  } catch {
2612
2698
  }
2613
2699
  }
2614
2700
  return out;
2615
2701
  }).catch(() => null);
2616
2702
  }
2703
+ /** The handle of the menu the published header or footer names, else the part's own name. */
2704
+ async chosenMenu(part) {
2705
+ if (!await this.facts.pluginOn("layouts")) return part;
2706
+ try {
2707
+ const layout = await this.data(`/x/layouts/${part}`);
2708
+ const menu = layout?.sections?.find((s) => s.type === part)?.settings?.menu;
2709
+ return typeof menu === "string" && menu ? menu : part;
2710
+ } catch {
2711
+ return part;
2712
+ }
2713
+ }
2617
2714
  };
2618
2715
  var menuService = new MenuService();
2619
2716
 
@@ -2728,7 +2825,10 @@ var StoreService = class extends BaseService {
2728
2825
  isProductReviewsAndRatings: {
2729
2826
  ...basePlugins.isProductReviewsAndRatings,
2730
2827
  ...flag(plugins.has("product-reviews"))
2731
- }
2828
+ },
2829
+ // The footer's sign-up box posts to ext/newsletter, and the theme editor's footer
2830
+ // switch (D131) can only show it while that is on.
2831
+ newsletter: { ...basePlugins.newsletter, ...flag(plugins.has("newsletter")) }
2732
2832
  },
2733
2833
  ...menus?.length ? { menu: menus } : {},
2734
2834
  ...deliverable(base, ships),
@@ -2843,6 +2943,35 @@ var LayoutService = class extends BaseService {
2843
2943
  return null;
2844
2944
  }
2845
2945
  }
2946
+ /**
2947
+ * The header the shop set in the theme editor (D131), drawn on every
2948
+ * page: the published one, or a preview key's draft. Null — the
2949
+ * storefront's own header — when none is published, the editor is off, the
2950
+ * engine is older, or anything fails: a header is never worth an error page.
2951
+ */
2952
+ async getHeader({ preview } = {}) {
2953
+ return this.chrome("header", preview);
2954
+ }
2955
+ /** The footer the shop set in the theme editor (D131); null for the storefront's own, as getHeader. */
2956
+ async getFooter({ preview } = {}) {
2957
+ return this.chrome("footer", preview);
2958
+ }
2959
+ async chrome(page, preview) {
2960
+ let layout;
2961
+ try {
2962
+ layout = await this.getPageLayout(page, { preview });
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ const section = layout?.sections.find((s) => s.type === page);
2967
+ if (!layout || !section) return null;
2968
+ const settings = { ...section.settings };
2969
+ let menu = null;
2970
+ if (typeof settings.menu === "string" && settings.menu) {
2971
+ menu = await new MenuService(this._fetch).getMenu(settings.menu).catch(() => null);
2972
+ }
2973
+ return { revision: layout.revision, preview: layout.preview, settings, menu };
2974
+ }
2846
2975
  };
2847
2976
  function tokens(v) {
2848
2977
  return v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length ? { ...v } : null;
@@ -3540,6 +3669,7 @@ exports.toOrder = toOrder;
3540
3669
  exports.toPage = toPage;
3541
3670
  exports.toProduct = toProduct;
3542
3671
  exports.toRating = toRating;
3672
+ exports.toStorefrontFacets = toStorefrontFacets;
3543
3673
  exports.toUser = toUser;
3544
3674
  exports.toVariant = toVariant;
3545
3675
  exports.toVendor = toVendor;