@misiki/gocommerce-connector 0.13.0 → 0.15.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,8 +153,9 @@ 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
- | store record | the storefront's own config, with the engine's currency, menus and module switches over it |
156
+ | search words, type-ahead (plugin on) | `/x/meilisearch/search`, falling back to the engine's own search when it is off or down |
157
+ | store record | the storefront's own config, with the engine's currency, menus and module switches over it; the store's own logo, dark logo and icon (`logo_url`, `logo_dark_url`, `icon_url` from `/api/store`, D148) replace `logo`, `logoDark` and `favicon` when it has them |
158
+ | resized product pictures | `/images/{store}/cdn-cgi/image/width=…/…` — the store record's `plugins.imageCdn` is turned on with provider `cloudflare` and the engine's `images_path` (from `/api/store`, D139) after `PUBLIC_GOCOMMERCE_API_URL`, so the storefront's srcset asks the engine for each width; an engine without it leaves the static record's setting |
157
159
 
158
160
  Blogs, banners, reels, chat, warranties, deals and uploads have no engine
159
161
  equivalent. Their lists answer empty and anything that would write or open one
@@ -425,6 +425,12 @@ type StoreProfile = {
425
425
  phone: string;
426
426
  support_url: string;
427
427
  language: string;
428
+ /** Where the engine serves this store's pictures resized (D139); absent on an engine before it. */
429
+ images_path?: string;
430
+ /** The store's logo, logo for dark backgrounds and square icon (D148); empty or absent when it has none. */
431
+ logo_url?: string;
432
+ logo_dark_url?: string;
433
+ icon_url?: string;
428
434
  };
429
435
  type Vendor = {
430
436
  id: number;
@@ -578,6 +584,8 @@ type SearchResult = {
578
584
  total: number;
579
585
  limit: number;
580
586
  offset: number;
587
+ /** Meilisearch did not answer: search the product listing instead. */
588
+ unavailable?: boolean;
581
589
  };
582
590
  /** One approved vendor's offer on a variant, from `GET /api/variants/{id}/offers`. Cheapest first. */
583
591
  type Offer = {
@@ -1489,6 +1497,19 @@ type CatalogPage = {
1489
1497
  declare class CatalogService extends BaseService {
1490
1498
  protected readonly facts: StoreFacts;
1491
1499
  protected queryCatalog(q: CatalogQuery): Promise<CatalogPage>;
1500
+ /**
1501
+ * The shelf from the Meilisearch module, or null when the store has it off,
1502
+ * sells through a channel, finds nothing for the words, or it cannot answer (Meilisearch down: the engine says `unavailable`), so
1503
+ * the caller asks the database instead of showing the shopper an error.
1504
+ */
1505
+ protected fromIndex(q: CatalogQuery, o: {
1506
+ page: number;
1507
+ limit: number;
1508
+ search?: string;
1509
+ sortKey?: string;
1510
+ wantsRange: boolean;
1511
+ hierarchy: CatalogPage['categoryHierarchy'];
1512
+ }): Promise<CatalogPage | null>;
1492
1513
  }
1493
1514
 
1494
1515
  /**
@@ -2697,39 +2718,6 @@ declare class MeilisearchService extends CatalogService {
2697
2718
  search?: string;
2698
2719
  limit?: number;
2699
2720
  }): 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
2721
  data: {
2734
2722
  id: string;
2735
2723
  slug: string;
@@ -2840,6 +2828,37 @@ declare class MeilisearchService extends CatalogService {
2840
2828
  metadata: Record<string, unknown> | null;
2841
2829
  createdAt: string;
2842
2830
  updatedAt: string;
2831
+ }[] | {
2832
+ id: string;
2833
+ slug: string;
2834
+ title: string;
2835
+ name: string;
2836
+ description: string;
2837
+ currency: string;
2838
+ price: number;
2839
+ mrp: number;
2840
+ minPrice: number;
2841
+ maxPrice: number;
2842
+ thumbnail: string | null;
2843
+ image_url: string | null;
2844
+ images: string;
2845
+ stock: number;
2846
+ hasStock: boolean;
2847
+ manageInventory: boolean;
2848
+ allowBackorder: boolean;
2849
+ productTags: string;
2850
+ category: {
2851
+ name: string;
2852
+ slug: string;
2853
+ } | null;
2854
+ vendor: {
2855
+ businessName: string;
2856
+ } | null;
2857
+ variants: ReturnType<typeof toVariant>[];
2858
+ options: never[];
2859
+ ratings: never[];
2860
+ link: string;
2861
+ updatedAt: string;
2843
2862
  }[];
2844
2863
  }>;
2845
2864
  }
@@ -6145,12 +6164,12 @@ declare const menuService: MenuService;
6145
6164
  /**
6146
6165
  * The store record every page renders from.
6147
6166
  *
6148
- * Logo, theme and the rest are the storefront's own (its static store,
6149
- * registered through `setStaticStore`); the engine has no opinion on them.
6150
- * Where the engine does have one, it wins:
6151
- * - the store's name and contact, as its owner set them in the panel. One
6152
- * storefront serves every store under `baseDomain`, so a name in the static
6153
- * record would be every store's name;
6167
+ * Theme and the rest are the storefront's own (its static store, registered
6168
+ * through `setStaticStore`); the engine has no opinion on them. Where the
6169
+ * engine does have one, it wins:
6170
+ * - the store's name, contact, logo and icon, as its owner set them in the
6171
+ * panel. One storefront serves every store under `baseDomain`, so a name or
6172
+ * a logo in the static record would be every store's;
6154
6173
  * - the settlement currency. Every price on the site is formatted with it, and
6155
6174
  * a store record saying USD over an engine charging INR would print the
6156
6175
  * wrong symbol on every price;
@@ -6606,39 +6625,6 @@ declare class AutocompleteService extends MeilisearchService {
6606
6625
  q?: string;
6607
6626
  search?: string;
6608
6627
  }): 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
6628
  data: {
6643
6629
  id: string;
6644
6630
  slug: string;
@@ -6749,6 +6735,37 @@ declare class AutocompleteService extends MeilisearchService {
6749
6735
  metadata: Record<string, unknown> | null;
6750
6736
  createdAt: string;
6751
6737
  updatedAt: string;
6738
+ }[] | {
6739
+ id: string;
6740
+ slug: string;
6741
+ title: string;
6742
+ name: string;
6743
+ description: string;
6744
+ currency: string;
6745
+ price: number;
6746
+ mrp: number;
6747
+ minPrice: number;
6748
+ maxPrice: number;
6749
+ thumbnail: string | null;
6750
+ image_url: string | null;
6751
+ images: string;
6752
+ stock: number;
6753
+ hasStock: boolean;
6754
+ manageInventory: boolean;
6755
+ allowBackorder: boolean;
6756
+ productTags: string;
6757
+ category: {
6758
+ name: string;
6759
+ slug: string;
6760
+ } | null;
6761
+ vendor: {
6762
+ businessName: string;
6763
+ } | null;
6764
+ variants: ReturnType<typeof toVariant>[];
6765
+ options: never[];
6766
+ ratings: never[];
6767
+ link: string;
6768
+ updatedAt: string;
6752
6769
  }[];
6753
6770
  }>;
6754
6771
  }
@@ -425,6 +425,12 @@ type StoreProfile = {
425
425
  phone: string;
426
426
  support_url: string;
427
427
  language: string;
428
+ /** Where the engine serves this store's pictures resized (D139); absent on an engine before it. */
429
+ images_path?: string;
430
+ /** The store's logo, logo for dark backgrounds and square icon (D148); empty or absent when it has none. */
431
+ logo_url?: string;
432
+ logo_dark_url?: string;
433
+ icon_url?: string;
428
434
  };
429
435
  type Vendor = {
430
436
  id: number;
@@ -578,6 +584,8 @@ type SearchResult = {
578
584
  total: number;
579
585
  limit: number;
580
586
  offset: number;
587
+ /** Meilisearch did not answer: search the product listing instead. */
588
+ unavailable?: boolean;
581
589
  };
582
590
  /** One approved vendor's offer on a variant, from `GET /api/variants/{id}/offers`. Cheapest first. */
583
591
  type Offer = {
@@ -1489,6 +1497,19 @@ type CatalogPage = {
1489
1497
  declare class CatalogService extends BaseService {
1490
1498
  protected readonly facts: StoreFacts;
1491
1499
  protected queryCatalog(q: CatalogQuery): Promise<CatalogPage>;
1500
+ /**
1501
+ * The shelf from the Meilisearch module, or null when the store has it off,
1502
+ * sells through a channel, finds nothing for the words, or it cannot answer (Meilisearch down: the engine says `unavailable`), so
1503
+ * the caller asks the database instead of showing the shopper an error.
1504
+ */
1505
+ protected fromIndex(q: CatalogQuery, o: {
1506
+ page: number;
1507
+ limit: number;
1508
+ search?: string;
1509
+ sortKey?: string;
1510
+ wantsRange: boolean;
1511
+ hierarchy: CatalogPage['categoryHierarchy'];
1512
+ }): Promise<CatalogPage | null>;
1492
1513
  }
1493
1514
 
1494
1515
  /**
@@ -2697,39 +2718,6 @@ declare class MeilisearchService extends CatalogService {
2697
2718
  search?: string;
2698
2719
  limit?: number;
2699
2720
  }): 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
2721
  data: {
2734
2722
  id: string;
2735
2723
  slug: string;
@@ -2840,6 +2828,37 @@ declare class MeilisearchService extends CatalogService {
2840
2828
  metadata: Record<string, unknown> | null;
2841
2829
  createdAt: string;
2842
2830
  updatedAt: string;
2831
+ }[] | {
2832
+ id: string;
2833
+ slug: string;
2834
+ title: string;
2835
+ name: string;
2836
+ description: string;
2837
+ currency: string;
2838
+ price: number;
2839
+ mrp: number;
2840
+ minPrice: number;
2841
+ maxPrice: number;
2842
+ thumbnail: string | null;
2843
+ image_url: string | null;
2844
+ images: string;
2845
+ stock: number;
2846
+ hasStock: boolean;
2847
+ manageInventory: boolean;
2848
+ allowBackorder: boolean;
2849
+ productTags: string;
2850
+ category: {
2851
+ name: string;
2852
+ slug: string;
2853
+ } | null;
2854
+ vendor: {
2855
+ businessName: string;
2856
+ } | null;
2857
+ variants: ReturnType<typeof toVariant>[];
2858
+ options: never[];
2859
+ ratings: never[];
2860
+ link: string;
2861
+ updatedAt: string;
2843
2862
  }[];
2844
2863
  }>;
2845
2864
  }
@@ -6145,12 +6164,12 @@ declare const menuService: MenuService;
6145
6164
  /**
6146
6165
  * The store record every page renders from.
6147
6166
  *
6148
- * Logo, theme and the rest are the storefront's own (its static store,
6149
- * registered through `setStaticStore`); the engine has no opinion on them.
6150
- * Where the engine does have one, it wins:
6151
- * - the store's name and contact, as its owner set them in the panel. One
6152
- * storefront serves every store under `baseDomain`, so a name in the static
6153
- * record would be every store's name;
6167
+ * Theme and the rest are the storefront's own (its static store, registered
6168
+ * through `setStaticStore`); the engine has no opinion on them. Where the
6169
+ * engine does have one, it wins:
6170
+ * - the store's name, contact, logo and icon, as its owner set them in the
6171
+ * panel. One storefront serves every store under `baseDomain`, so a name or
6172
+ * a logo in the static record would be every store's;
6154
6173
  * - the settlement currency. Every price on the site is formatted with it, and
6155
6174
  * a store record saying USD over an engine charging INR would print the
6156
6175
  * wrong symbol on every price;
@@ -6606,39 +6625,6 @@ declare class AutocompleteService extends MeilisearchService {
6606
6625
  q?: string;
6607
6626
  search?: string;
6608
6627
  }): 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
6628
  data: {
6643
6629
  id: string;
6644
6630
  slug: string;
@@ -6749,6 +6735,37 @@ declare class AutocompleteService extends MeilisearchService {
6749
6735
  metadata: Record<string, unknown> | null;
6750
6736
  createdAt: string;
6751
6737
  updatedAt: string;
6738
+ }[] | {
6739
+ id: string;
6740
+ slug: string;
6741
+ title: string;
6742
+ name: string;
6743
+ description: string;
6744
+ currency: string;
6745
+ price: number;
6746
+ mrp: number;
6747
+ minPrice: number;
6748
+ maxPrice: number;
6749
+ thumbnail: string | null;
6750
+ image_url: string | null;
6751
+ images: string;
6752
+ stock: number;
6753
+ hasStock: boolean;
6754
+ manageInventory: boolean;
6755
+ allowBackorder: boolean;
6756
+ productTags: string;
6757
+ category: {
6758
+ name: string;
6759
+ slug: string;
6760
+ } | null;
6761
+ vendor: {
6762
+ businessName: string;
6763
+ } | null;
6764
+ variants: ReturnType<typeof toVariant>[];
6765
+ options: never[];
6766
+ ratings: never[];
6767
+ link: string;
6768
+ updatedAt: string;
6752
6769
  }[];
6753
6770
  }>;
6754
6771
  }
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
  }
@@ -3023,6 +3043,12 @@ var StoreService = class extends BaseService {
3023
3043
  ...profile?.name ? { name: profile.name } : {},
3024
3044
  ...profile?.email ? { businessEmail: profile.email } : {},
3025
3045
  ...profile?.phone ? { businessPhone: profile.phone } : {},
3046
+ // The store's pictures (D148), the same: one it has not chosen keeps the static one.
3047
+ // Its own logo also replaces the static dark logo, which would be the deployment's
3048
+ // mark beside this store's; without a dark one of its own, its logo is the fallback.
3049
+ ...profile?.logo_url ? { logo: mediaUrl(profile.logo_url), logoDark: void 0 } : {},
3050
+ ...profile?.logo_dark_url ? { logoDark: mediaUrl(profile.logo_dark_url) } : {},
3051
+ ...profile?.icon_url ? { favicon: mediaUrl(profile.icon_url) } : {},
3026
3052
  currencyCode: code,
3027
3053
  currencySymbol: sameCurrency ? base.currencySymbol ?? symbolFor(code) : symbolFor(code),
3028
3054
  currency: {
@@ -3045,7 +3071,18 @@ var StoreService = class extends BaseService {
3045
3071
  newsletter: { ...basePlugins.newsletter, ...flag(plugins.has("newsletter")) },
3046
3072
  // Discount codes are core: every GoCommerce store takes them, so
3047
3073
  // the code box is shown whatever the static record says.
3048
- isDiscountCoupons: { ...basePlugins.isDiscountCoupons, active: true }
3074
+ isDiscountCoupons: { ...basePlugins.isDiscountCoupons, active: true },
3075
+ // The engine resizes the store's own pictures in Cloudflare's URL shape
3076
+ // (D139), so the storefront's srcset asks it for each width. Its public
3077
+ // origin, not the proxy path: an <img> needs no CORS, and the proxy would
3078
+ // put every picture through the storefront's server.
3079
+ ...profile?.images_path && this.creds.apiUrl ? {
3080
+ imageCdn: {
3081
+ provider: "cloudflare",
3082
+ url: this.creds.apiUrl.trim().replace(/\/+$/, "") + profile.images_path,
3083
+ active: true
3084
+ }
3085
+ } : {}
3049
3086
  },
3050
3087
  ...menus?.length ? { menu: menus } : {},
3051
3088
  ...deliverable(base, ships),