@misiki/gocommerce-connector 0.13.0 → 0.14.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/README.md CHANGED
@@ -45,7 +45,8 @@ gocommerce -identity -wishlist -reviews -cms -faq -menus serve
45
45
 
46
46
  Each is optional. Without `-identity` there is no sign-in (guest checkout
47
47
  always works); without `-reviews` products show no stars; and so on. Add
48
- `-meilisearch` (with `MEILI_HOST` and keys) for price sorting and price filters,
48
+ `-meilisearch` (with `MEILI_HOST` and keys) for typo-tolerant search once a store
49
+ switches the Meilisearch plugin on,
49
50
  `-newsletter` and `-contact` for the signup box and contact form, and `-b2b` for
50
51
  business buyers (see "Beyond the storefront's screens").
51
52
 
@@ -152,7 +153,7 @@ down stays best-effort: the storefront renders from its static record.
152
153
  | attribute filters (`?attributes.color=Red`) | `/api/products?attr=color:Red` |
153
154
  | newsletter box | `/x/newsletter/subscribe` |
154
155
  | contact form, product enquiries | `/x/contact/messages` |
155
- | price sort and filter, type-ahead | `/x/meilisearch/search`, falling back to the engine's own search |
156
+ | search words, type-ahead (plugin on) | `/x/meilisearch/search`, falling back to the engine's own search when it is off or down |
156
157
  | store record | the storefront's own config, with the engine's currency, menus and module switches over it |
157
158
 
158
159
  Blogs, banners, reels, chat, warranties, deals and uploads have no engine
@@ -578,6 +578,8 @@ type SearchResult = {
578
578
  total: number;
579
579
  limit: number;
580
580
  offset: number;
581
+ /** Meilisearch did not answer: search the product listing instead. */
582
+ unavailable?: boolean;
581
583
  };
582
584
  /** One approved vendor's offer on a variant, from `GET /api/variants/{id}/offers`. Cheapest first. */
583
585
  type Offer = {
@@ -1489,6 +1491,19 @@ type CatalogPage = {
1489
1491
  declare class CatalogService extends BaseService {
1490
1492
  protected readonly facts: StoreFacts;
1491
1493
  protected queryCatalog(q: CatalogQuery): Promise<CatalogPage>;
1494
+ /**
1495
+ * The shelf from the Meilisearch module, or null when the store has it off,
1496
+ * sells through a channel, finds nothing for the words, or it cannot answer (Meilisearch down: the engine says `unavailable`), so
1497
+ * the caller asks the database instead of showing the shopper an error.
1498
+ */
1499
+ protected fromIndex(q: CatalogQuery, o: {
1500
+ page: number;
1501
+ limit: number;
1502
+ search?: string;
1503
+ sortKey?: string;
1504
+ wantsRange: boolean;
1505
+ hierarchy: CatalogPage['categoryHierarchy'];
1506
+ }): Promise<CatalogPage | null>;
1492
1507
  }
1493
1508
 
1494
1509
  /**
@@ -2697,39 +2712,6 @@ declare class MeilisearchService extends CatalogService {
2697
2712
  search?: string;
2698
2713
  limit?: number;
2699
2714
  }): Promise<{
2700
- data: {
2701
- id: string;
2702
- slug: string;
2703
- title: string;
2704
- name: string;
2705
- description: string;
2706
- currency: string;
2707
- price: number;
2708
- mrp: number;
2709
- minPrice: number;
2710
- maxPrice: number;
2711
- thumbnail: string | null;
2712
- image_url: string | null;
2713
- images: string;
2714
- stock: number;
2715
- hasStock: boolean;
2716
- manageInventory: boolean;
2717
- allowBackorder: boolean;
2718
- productTags: string;
2719
- category: {
2720
- name: string;
2721
- slug: string;
2722
- } | null;
2723
- vendor: {
2724
- businessName: string;
2725
- } | null;
2726
- variants: ReturnType<typeof toVariant>[];
2727
- options: never[];
2728
- ratings: never[];
2729
- link: string;
2730
- updatedAt: string;
2731
- }[];
2732
- } | {
2733
2715
  data: {
2734
2716
  id: string;
2735
2717
  slug: string;
@@ -2840,6 +2822,37 @@ declare class MeilisearchService extends CatalogService {
2840
2822
  metadata: Record<string, unknown> | null;
2841
2823
  createdAt: string;
2842
2824
  updatedAt: string;
2825
+ }[] | {
2826
+ id: string;
2827
+ slug: string;
2828
+ title: string;
2829
+ name: string;
2830
+ description: string;
2831
+ currency: string;
2832
+ price: number;
2833
+ mrp: number;
2834
+ minPrice: number;
2835
+ maxPrice: number;
2836
+ thumbnail: string | null;
2837
+ image_url: string | null;
2838
+ images: string;
2839
+ stock: number;
2840
+ hasStock: boolean;
2841
+ manageInventory: boolean;
2842
+ allowBackorder: boolean;
2843
+ productTags: string;
2844
+ category: {
2845
+ name: string;
2846
+ slug: string;
2847
+ } | null;
2848
+ vendor: {
2849
+ businessName: string;
2850
+ } | null;
2851
+ variants: ReturnType<typeof toVariant>[];
2852
+ options: never[];
2853
+ ratings: never[];
2854
+ link: string;
2855
+ updatedAt: string;
2843
2856
  }[];
2844
2857
  }>;
2845
2858
  }
@@ -6606,39 +6619,6 @@ declare class AutocompleteService extends MeilisearchService {
6606
6619
  q?: string;
6607
6620
  search?: string;
6608
6621
  }): Promise<{
6609
- data: {
6610
- id: string;
6611
- slug: string;
6612
- title: string;
6613
- name: string;
6614
- description: string;
6615
- currency: string;
6616
- price: number;
6617
- mrp: number;
6618
- minPrice: number;
6619
- maxPrice: number;
6620
- thumbnail: string | null;
6621
- image_url: string | null;
6622
- images: string;
6623
- stock: number;
6624
- hasStock: boolean;
6625
- manageInventory: boolean;
6626
- allowBackorder: boolean;
6627
- productTags: string;
6628
- category: {
6629
- name: string;
6630
- slug: string;
6631
- } | null;
6632
- vendor: {
6633
- businessName: string;
6634
- } | null;
6635
- variants: ReturnType<typeof toVariant>[];
6636
- options: never[];
6637
- ratings: never[];
6638
- link: string;
6639
- updatedAt: string;
6640
- }[];
6641
- } | {
6642
6622
  data: {
6643
6623
  id: string;
6644
6624
  slug: string;
@@ -6749,6 +6729,37 @@ declare class AutocompleteService extends MeilisearchService {
6749
6729
  metadata: Record<string, unknown> | null;
6750
6730
  createdAt: string;
6751
6731
  updatedAt: string;
6732
+ }[] | {
6733
+ id: string;
6734
+ slug: string;
6735
+ title: string;
6736
+ name: string;
6737
+ description: string;
6738
+ currency: string;
6739
+ price: number;
6740
+ mrp: number;
6741
+ minPrice: number;
6742
+ maxPrice: number;
6743
+ thumbnail: string | null;
6744
+ image_url: string | null;
6745
+ images: string;
6746
+ stock: number;
6747
+ hasStock: boolean;
6748
+ manageInventory: boolean;
6749
+ allowBackorder: boolean;
6750
+ productTags: string;
6751
+ category: {
6752
+ name: string;
6753
+ slug: string;
6754
+ } | null;
6755
+ vendor: {
6756
+ businessName: string;
6757
+ } | null;
6758
+ variants: ReturnType<typeof toVariant>[];
6759
+ options: never[];
6760
+ ratings: never[];
6761
+ link: string;
6762
+ updatedAt: string;
6752
6763
  }[];
6753
6764
  }>;
6754
6765
  }
@@ -578,6 +578,8 @@ type SearchResult = {
578
578
  total: number;
579
579
  limit: number;
580
580
  offset: number;
581
+ /** Meilisearch did not answer: search the product listing instead. */
582
+ unavailable?: boolean;
581
583
  };
582
584
  /** One approved vendor's offer on a variant, from `GET /api/variants/{id}/offers`. Cheapest first. */
583
585
  type Offer = {
@@ -1489,6 +1491,19 @@ type CatalogPage = {
1489
1491
  declare class CatalogService extends BaseService {
1490
1492
  protected readonly facts: StoreFacts;
1491
1493
  protected queryCatalog(q: CatalogQuery): Promise<CatalogPage>;
1494
+ /**
1495
+ * The shelf from the Meilisearch module, or null when the store has it off,
1496
+ * sells through a channel, finds nothing for the words, or it cannot answer (Meilisearch down: the engine says `unavailable`), so
1497
+ * the caller asks the database instead of showing the shopper an error.
1498
+ */
1499
+ protected fromIndex(q: CatalogQuery, o: {
1500
+ page: number;
1501
+ limit: number;
1502
+ search?: string;
1503
+ sortKey?: string;
1504
+ wantsRange: boolean;
1505
+ hierarchy: CatalogPage['categoryHierarchy'];
1506
+ }): Promise<CatalogPage | null>;
1492
1507
  }
1493
1508
 
1494
1509
  /**
@@ -2697,39 +2712,6 @@ declare class MeilisearchService extends CatalogService {
2697
2712
  search?: string;
2698
2713
  limit?: number;
2699
2714
  }): Promise<{
2700
- data: {
2701
- id: string;
2702
- slug: string;
2703
- title: string;
2704
- name: string;
2705
- description: string;
2706
- currency: string;
2707
- price: number;
2708
- mrp: number;
2709
- minPrice: number;
2710
- maxPrice: number;
2711
- thumbnail: string | null;
2712
- image_url: string | null;
2713
- images: string;
2714
- stock: number;
2715
- hasStock: boolean;
2716
- manageInventory: boolean;
2717
- allowBackorder: boolean;
2718
- productTags: string;
2719
- category: {
2720
- name: string;
2721
- slug: string;
2722
- } | null;
2723
- vendor: {
2724
- businessName: string;
2725
- } | null;
2726
- variants: ReturnType<typeof toVariant>[];
2727
- options: never[];
2728
- ratings: never[];
2729
- link: string;
2730
- updatedAt: string;
2731
- }[];
2732
- } | {
2733
2715
  data: {
2734
2716
  id: string;
2735
2717
  slug: string;
@@ -2840,6 +2822,37 @@ declare class MeilisearchService extends CatalogService {
2840
2822
  metadata: Record<string, unknown> | null;
2841
2823
  createdAt: string;
2842
2824
  updatedAt: string;
2825
+ }[] | {
2826
+ id: string;
2827
+ slug: string;
2828
+ title: string;
2829
+ name: string;
2830
+ description: string;
2831
+ currency: string;
2832
+ price: number;
2833
+ mrp: number;
2834
+ minPrice: number;
2835
+ maxPrice: number;
2836
+ thumbnail: string | null;
2837
+ image_url: string | null;
2838
+ images: string;
2839
+ stock: number;
2840
+ hasStock: boolean;
2841
+ manageInventory: boolean;
2842
+ allowBackorder: boolean;
2843
+ productTags: string;
2844
+ category: {
2845
+ name: string;
2846
+ slug: string;
2847
+ } | null;
2848
+ vendor: {
2849
+ businessName: string;
2850
+ } | null;
2851
+ variants: ReturnType<typeof toVariant>[];
2852
+ options: never[];
2853
+ ratings: never[];
2854
+ link: string;
2855
+ updatedAt: string;
2843
2856
  }[];
2844
2857
  }>;
2845
2858
  }
@@ -6606,39 +6619,6 @@ declare class AutocompleteService extends MeilisearchService {
6606
6619
  q?: string;
6607
6620
  search?: string;
6608
6621
  }): Promise<{
6609
- data: {
6610
- id: string;
6611
- slug: string;
6612
- title: string;
6613
- name: string;
6614
- description: string;
6615
- currency: string;
6616
- price: number;
6617
- mrp: number;
6618
- minPrice: number;
6619
- maxPrice: number;
6620
- thumbnail: string | null;
6621
- image_url: string | null;
6622
- images: string;
6623
- stock: number;
6624
- hasStock: boolean;
6625
- manageInventory: boolean;
6626
- allowBackorder: boolean;
6627
- productTags: string;
6628
- category: {
6629
- name: string;
6630
- slug: string;
6631
- } | null;
6632
- vendor: {
6633
- businessName: string;
6634
- } | null;
6635
- variants: ReturnType<typeof toVariant>[];
6636
- options: never[];
6637
- ratings: never[];
6638
- link: string;
6639
- updatedAt: string;
6640
- }[];
6641
- } | {
6642
6622
  data: {
6643
6623
  id: string;
6644
6624
  slug: string;
@@ -6749,6 +6729,37 @@ declare class AutocompleteService extends MeilisearchService {
6749
6729
  metadata: Record<string, unknown> | null;
6750
6730
  createdAt: string;
6751
6731
  updatedAt: string;
6732
+ }[] | {
6733
+ id: string;
6734
+ slug: string;
6735
+ title: string;
6736
+ name: string;
6737
+ description: string;
6738
+ currency: string;
6739
+ price: number;
6740
+ mrp: number;
6741
+ minPrice: number;
6742
+ maxPrice: number;
6743
+ thumbnail: string | null;
6744
+ image_url: string | null;
6745
+ images: string;
6746
+ stock: number;
6747
+ hasStock: boolean;
6748
+ manageInventory: boolean;
6749
+ allowBackorder: boolean;
6750
+ productTags: string;
6751
+ category: {
6752
+ name: string;
6753
+ slug: string;
6754
+ } | null;
6755
+ vendor: {
6756
+ businessName: string;
6757
+ } | null;
6758
+ variants: ReturnType<typeof toVariant>[];
6759
+ options: never[];
6760
+ ratings: never[];
6761
+ link: string;
6762
+ updatedAt: string;
6752
6763
  }[];
6753
6764
  }>;
6754
6765
  }
package/dist/index.cjs CHANGED
@@ -1216,14 +1216,17 @@ var CatalogService = class extends BaseService {
1216
1216
  const attr = Object.entries(q.attributes ?? {}).flatMap(
1217
1217
  ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1218
1218
  );
1219
- if (q.facets && !legacyListing.has(this.engineBase())) {
1219
+ const refined = attr.length > 0 || !!(q.tags?.length || q.vendors?.length || q.productTypes?.length);
1220
+ const triedIndex = !!search && !refined;
1221
+ const counted = q.facets && !legacyListing.has(this.engineBase());
1222
+ const facetListing = async (page2, limit2) => {
1220
1223
  const { currency } = await this.facts.ready();
1221
1224
  const minor = (n) => Number.isFinite(n) ? Math.max(0, toMinor(n, currency)) : void 0;
1222
- const env2 = await this.engine("/api/products", {
1225
+ return this.engine("/api/products", {
1223
1226
  query: {
1224
1227
  q: search,
1225
- page,
1226
- limit,
1228
+ page: page2,
1229
+ limit: limit2,
1227
1230
  channel: this.creds.channel,
1228
1231
  category: q.category,
1229
1232
  tag: q.tags,
@@ -1236,6 +1239,16 @@ var CatalogService = class extends BaseService {
1236
1239
  facets: 1
1237
1240
  }
1238
1241
  });
1242
+ };
1243
+ if (triedIndex && await this.facts.pluginOn("meilisearch")) {
1244
+ const [found, rail] = await Promise.all([
1245
+ this.fromIndex(q, { page, limit, search, sortKey, wantsRange, hierarchy }),
1246
+ counted ? facetListing(1, 1).catch(() => null) : null
1247
+ ]);
1248
+ if (found) return rail?.meta?.facets ? { ...found, facets: rail.meta.facets } : found;
1249
+ }
1250
+ if (counted) {
1251
+ const env2 = await facetListing(page, limit);
1239
1252
  if (env2.meta?.facets) {
1240
1253
  return {
1241
1254
  ...toPage(env2, (p) => toProduct(p)),
@@ -1246,39 +1259,9 @@ var CatalogService = class extends BaseService {
1246
1259
  }
1247
1260
  legacyListing.add(this.engineBase());
1248
1261
  }
1249
- if ((sortKey || wantsRange) && await this.facts.pluginOn("meilisearch")) {
1250
- const filters = [];
1251
- if (q.category) {
1252
- const names = (await this.facts.subtree(q.category).catch(() => [])).map(
1253
- (c) => quote(c.full_name)
1254
- );
1255
- if (!names.length) return { ...emptyCatalog(limit), categoryHierarchy: hierarchy, sorted: true };
1256
- filters.push(`category IN [${names.join(", ")}]`);
1257
- }
1258
- if (wantsRange) {
1259
- const { currency } = await this.facts.ready();
1260
- if (Number.isFinite(q.priceFrom)) filters.push(`price_minor >= ${toMinor(q.priceFrom, currency)}`);
1261
- if (Number.isFinite(q.priceTo)) filters.push(`price_minor <= ${toMinor(q.priceTo, currency)}`);
1262
- }
1263
- const res = await this.data("/x/meilisearch/search", {
1264
- query: {
1265
- q: search ?? "",
1266
- limit,
1267
- offset: (page - 1) * limit,
1268
- filter: filters.join(" AND ") || void 0,
1269
- sort: sortKey
1270
- }
1271
- });
1272
- const total = res.total ?? 0;
1273
- return {
1274
- data: (res.hits ?? []).map(fromSearchDoc),
1275
- count: total,
1276
- pageSize: limit,
1277
- noOfPage: Math.max(1, Math.ceil(total / limit)),
1278
- page,
1279
- categoryHierarchy: hierarchy,
1280
- sorted: true
1281
- };
1262
+ if (!triedIndex && (sortKey || wantsRange)) {
1263
+ const found = await this.fromIndex(q, { page, limit, search, sortKey, wantsRange, hierarchy });
1264
+ if (found) return found;
1282
1265
  }
1283
1266
  const sorted = !sortKey && !wantsRange;
1284
1267
  if (q.category && !search) {
@@ -1298,6 +1281,45 @@ var CatalogService = class extends BaseService {
1298
1281
  });
1299
1282
  return { ...toPage(env, (p) => toProduct(p)), categoryHierarchy: hierarchy, sorted };
1300
1283
  }
1284
+ /**
1285
+ * The shelf from the Meilisearch module, or null when the store has it off,
1286
+ * sells through a channel, finds nothing for the words, or it cannot answer (Meilisearch down: the engine says `unavailable`), so
1287
+ * the caller asks the database instead of showing the shopper an error.
1288
+ */
1289
+ async fromIndex(q, o) {
1290
+ if (this.creds.channel || !await this.facts.pluginOn("meilisearch")) return null;
1291
+ const filters = [];
1292
+ if (q.category) {
1293
+ const names = (await this.facts.subtree(q.category).catch(() => [])).map((c) => quote(c.full_name));
1294
+ if (!names.length) return { ...emptyCatalog(o.limit), categoryHierarchy: o.hierarchy, sorted: true };
1295
+ filters.push(`category IN [${names.join(", ")}]`);
1296
+ }
1297
+ if (o.wantsRange) {
1298
+ const { currency } = await this.facts.ready();
1299
+ if (Number.isFinite(q.priceFrom)) filters.push(`price_minor >= ${toMinor(q.priceFrom, currency)}`);
1300
+ if (Number.isFinite(q.priceTo)) filters.push(`price_minor <= ${toMinor(q.priceTo, currency)}`);
1301
+ }
1302
+ const res = await this.data("/x/meilisearch/search", {
1303
+ query: {
1304
+ q: o.search ?? "",
1305
+ limit: o.limit,
1306
+ offset: (o.page - 1) * o.limit,
1307
+ filter: filters.join(" AND ") || void 0,
1308
+ sort: o.sortKey
1309
+ }
1310
+ }).catch(() => null);
1311
+ if (!res || res.unavailable || o.search && !res.hits?.length) return null;
1312
+ const total = res.total ?? 0;
1313
+ return {
1314
+ data: (res.hits ?? []).map(fromSearchDoc),
1315
+ count: total,
1316
+ pageSize: o.limit,
1317
+ noOfPage: Math.max(1, Math.ceil(total / o.limit)),
1318
+ page: o.page,
1319
+ categoryHierarchy: o.hierarchy,
1320
+ sorted: true
1321
+ };
1322
+ }
1301
1323
  };
1302
1324
  function emptyCatalog(limit = PAGE_SIZE) {
1303
1325
  return { data: [], count: 0, pageSize: limit, noOfPage: 0, page: 1 };
@@ -1595,10 +1617,8 @@ var MeilisearchService = class extends CatalogService {
1595
1617
  async searchAutoComplete({ query, search, limit = 8 } = {}) {
1596
1618
  const q = (query ?? search ?? "").trim();
1597
1619
  if (!q) return { data: [] };
1598
- if (await this.facts.pluginOn("meilisearch")) {
1599
- const res = await this.data("/x/meilisearch/search", { query: { q, limit } });
1600
- return { data: (res.hits ?? []).map(fromSearchDoc) };
1601
- }
1620
+ const found = await this.fromIndex({}, { page: 1, limit, search: q, wantsRange: false, hierarchy: [] });
1621
+ if (found) return { data: found.data };
1602
1622
  const rows = await this.data("/api/products", { query: { q, limit, channel: this.creds.channel } }) ?? [];
1603
1623
  return { data: rows.map((p) => toProduct(p)) };
1604
1624
  }