shop-client 3.16.0 → 3.18.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.
@@ -0,0 +1,804 @@
1
+ import {
2
+ rateLimitedFetch
3
+ } from "./chunk-O77Z6OBJ.mjs";
4
+ import {
5
+ formatPrice
6
+ } from "./chunk-U3RQRBXZ.mjs";
7
+
8
+ // src/products.ts
9
+ import { filter, isNonNullish } from "remeda";
10
+ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, _findProduct, ai) {
11
+ const cacheExpiryMs = 5 * 60 * 1e3;
12
+ const findCacheFull = /* @__PURE__ */ new Map();
13
+ const findCacheMinimal = /* @__PURE__ */ new Map();
14
+ const getCachedFull = (key) => {
15
+ const entry = findCacheFull.get(key);
16
+ if (!entry) return void 0;
17
+ if (Date.now() - entry.ts < cacheExpiryMs) return entry.value;
18
+ findCacheFull.delete(key);
19
+ return void 0;
20
+ };
21
+ const setCachedFull = (key, value) => {
22
+ findCacheFull.set(key, { ts: Date.now(), value });
23
+ };
24
+ const getCachedMinimal = (key) => {
25
+ const entry = findCacheMinimal.get(key);
26
+ if (!entry) return void 0;
27
+ if (Date.now() - entry.ts < cacheExpiryMs) return entry.value;
28
+ findCacheMinimal.delete(key);
29
+ return void 0;
30
+ };
31
+ const setCachedMinimal = (key, value) => {
32
+ findCacheMinimal.set(key, { ts: Date.now(), value });
33
+ };
34
+ function applyCurrencyOverride(product, currency) {
35
+ var _a, _b, _c, _d, _e, _f;
36
+ const priceMin = (_b = (_a = product.priceMin) != null ? _a : product.price) != null ? _b : 0;
37
+ const priceMax = (_d = (_c = product.priceMax) != null ? _c : product.price) != null ? _d : 0;
38
+ const compareAtMin = (_f = (_e = product.compareAtPriceMin) != null ? _e : product.compareAtPrice) != null ? _f : 0;
39
+ return {
40
+ ...product,
41
+ currency,
42
+ localizedPricing: {
43
+ currency,
44
+ priceFormatted: formatPrice(priceMin, currency),
45
+ priceMinFormatted: formatPrice(priceMin, currency),
46
+ priceMaxFormatted: formatPrice(priceMax, currency),
47
+ compareAtPriceFormatted: formatPrice(compareAtMin, currency)
48
+ }
49
+ };
50
+ }
51
+ function applyCurrencyOverrideMinimal(product, currency) {
52
+ var _a;
53
+ const compareAtPrice = (_a = product.compareAtPrice) != null ? _a : 0;
54
+ return {
55
+ ...product,
56
+ localizedPricing: {
57
+ priceFormatted: formatPrice(product.price, currency),
58
+ compareAtPriceFormatted: formatPrice(compareAtPrice, currency)
59
+ }
60
+ };
61
+ }
62
+ function maybeOverrideProductsCurrency(products, currency) {
63
+ if (!products || !currency || products.length === 0) return products;
64
+ return products.map((p) => applyCurrencyOverride(p, currency));
65
+ }
66
+ function maybeOverrideMinimalProductsCurrency(products, currency) {
67
+ if (!products || !currency || products.length === 0) return products;
68
+ return products.map((p) => applyCurrencyOverrideMinimal(p, currency));
69
+ }
70
+ async function allInternal(options) {
71
+ const limit = 250;
72
+ const allProducts = [];
73
+ async function fetchAll() {
74
+ let currentPage = 1;
75
+ while (true) {
76
+ const products = await fetchProducts(currentPage, limit, {
77
+ minimal: options.minimal
78
+ });
79
+ if (!products || products.length === 0 || products.length < limit) {
80
+ if (products && products.length > 0) {
81
+ allProducts.push(...products);
82
+ }
83
+ break;
84
+ }
85
+ allProducts.push(...products);
86
+ currentPage++;
87
+ }
88
+ return allProducts;
89
+ }
90
+ try {
91
+ const products = await fetchAll();
92
+ return options.minimal ? maybeOverrideMinimalProductsCurrency(
93
+ products,
94
+ options.currency
95
+ ) : maybeOverrideProductsCurrency(
96
+ products,
97
+ options.currency
98
+ );
99
+ } catch (error) {
100
+ console.error("Failed to fetch all products:", storeDomain, error);
101
+ throw error;
102
+ }
103
+ }
104
+ async function paginatedInternal(options) {
105
+ var _a, _b;
106
+ const page = (_a = options.page) != null ? _a : 1;
107
+ const limit = Math.min((_b = options.limit) != null ? _b : 250, 250);
108
+ const url = `${baseUrl}products.json?limit=${limit}&page=${page}`;
109
+ try {
110
+ const response = await rateLimitedFetch(url, {
111
+ rateLimitClass: "products:paginated"
112
+ });
113
+ if (!response.ok) {
114
+ console.error(
115
+ `HTTP error! status: ${response.status} for ${storeDomain} page ${page}`
116
+ );
117
+ throw new Error(`HTTP error! status: ${response.status}`);
118
+ }
119
+ const data = await response.json();
120
+ if (data.products.length === 0) {
121
+ return [];
122
+ }
123
+ const normalized = productsDto(data.products, {
124
+ minimal: options.minimal
125
+ });
126
+ return options.minimal ? maybeOverrideMinimalProductsCurrency(
127
+ normalized || null,
128
+ options.currency
129
+ ) : maybeOverrideProductsCurrency(
130
+ normalized || null,
131
+ options.currency
132
+ );
133
+ } catch (error) {
134
+ console.error(
135
+ `Error fetching products for ${storeDomain} page ${page} with limit ${limit}:`,
136
+ error
137
+ );
138
+ return null;
139
+ }
140
+ }
141
+ async function findInternal(productHandle, options) {
142
+ var _a, _b;
143
+ if (!productHandle || typeof productHandle !== "string") {
144
+ throw new Error("Product handle is required and must be a string");
145
+ }
146
+ try {
147
+ let qs = null;
148
+ if (productHandle.includes("?")) {
149
+ const parts = productHandle.split("?");
150
+ const handlePart = (_a = parts[0]) != null ? _a : productHandle;
151
+ const qsPart = (_b = parts[1]) != null ? _b : null;
152
+ productHandle = handlePart;
153
+ qs = qsPart;
154
+ }
155
+ const sanitizedHandle = productHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
156
+ if (!sanitizedHandle) {
157
+ throw new Error("Invalid product handle format");
158
+ }
159
+ if (sanitizedHandle.length > 255) {
160
+ throw new Error("Product handle is too long");
161
+ }
162
+ const cached = options.minimal ? getCachedMinimal(sanitizedHandle) : getCachedFull(sanitizedHandle);
163
+ if (typeof cached !== "undefined") {
164
+ if (!cached || !options.currency) return cached;
165
+ return options.minimal ? applyCurrencyOverrideMinimal(
166
+ cached,
167
+ options.currency
168
+ ) : applyCurrencyOverride(cached, options.currency);
169
+ }
170
+ let finalHandle = sanitizedHandle;
171
+ try {
172
+ const htmlResp = await rateLimitedFetch(
173
+ `${baseUrl}products/${encodeURIComponent(sanitizedHandle)}`,
174
+ { rateLimitClass: "products:resolve" }
175
+ );
176
+ if (htmlResp.ok) {
177
+ const finalUrl = htmlResp.url;
178
+ if (finalUrl) {
179
+ const pathname = new URL(finalUrl).pathname.replace(/\/$/, "");
180
+ const parts = pathname.split("/").filter(Boolean);
181
+ const idx = parts.indexOf("products");
182
+ const maybeHandle = idx >= 0 ? parts[idx + 1] : void 0;
183
+ if (typeof maybeHandle === "string" && maybeHandle.length) {
184
+ finalHandle = maybeHandle;
185
+ }
186
+ }
187
+ }
188
+ } catch {
189
+ }
190
+ const url = `${baseUrl}products/${encodeURIComponent(finalHandle)}.js${qs ? `?${qs}` : ""}`;
191
+ const response = await rateLimitedFetch(url, {
192
+ rateLimitClass: "products:single"
193
+ });
194
+ if (!response.ok) {
195
+ if (response.status === 404) {
196
+ return null;
197
+ }
198
+ throw new Error(`HTTP error! status: ${response.status}`);
199
+ }
200
+ const product = await response.json();
201
+ const productData = productDto(product, { minimal: options.minimal });
202
+ if (options.minimal) {
203
+ const minimalData = productData;
204
+ setCachedMinimal(sanitizedHandle, minimalData);
205
+ if (finalHandle !== sanitizedHandle)
206
+ setCachedMinimal(finalHandle, minimalData);
207
+ return options.currency ? applyCurrencyOverrideMinimal(minimalData, options.currency) : minimalData;
208
+ }
209
+ const fullData = productData;
210
+ setCachedFull(sanitizedHandle, fullData);
211
+ if (finalHandle !== sanitizedHandle) setCachedFull(finalHandle, fullData);
212
+ return options.currency ? applyCurrencyOverride(fullData, options.currency) : fullData;
213
+ } catch (error) {
214
+ if (error instanceof Error) {
215
+ console.error(
216
+ `Error fetching product ${productHandle}:`,
217
+ baseUrl,
218
+ error.message
219
+ );
220
+ }
221
+ throw error;
222
+ }
223
+ }
224
+ async function predictiveSearchInternal(query, options) {
225
+ var _a, _b, _c, _d, _e, _f;
226
+ if (!query || typeof query !== "string") {
227
+ throw new Error("Query is required and must be a string");
228
+ }
229
+ const limit = Math.max(1, Math.min((_a = options.limit) != null ? _a : 10, 10));
230
+ const unavailable = options.unavailableProducts === "show" || options.unavailableProducts === "hide" ? options.unavailableProducts : "hide";
231
+ const localeValue = options.locale && options.locale.trim() || "en";
232
+ const localePrefix = `${localeValue.replace(/^\/|\/$/g, "")}/`;
233
+ const url = `${baseUrl}${localePrefix}search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${limit}&resources[options][unavailable_products]=${unavailable}`;
234
+ const response = await rateLimitedFetch(url, {
235
+ rateLimitClass: "search:predictive",
236
+ timeoutMs: 7e3,
237
+ retry: { maxRetries: 2, baseDelayMs: 300 }
238
+ });
239
+ let resp = response;
240
+ if (!resp.ok && (resp.status === 404 || resp.status === 417)) {
241
+ const fallbackUrl = `${baseUrl}search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${limit}&resources[options][unavailable_products]=${unavailable}`;
242
+ resp = await rateLimitedFetch(fallbackUrl, {
243
+ rateLimitClass: "search:predictive",
244
+ timeoutMs: 7e3,
245
+ retry: { maxRetries: 2, baseDelayMs: 300 }
246
+ });
247
+ }
248
+ if (!resp.ok) {
249
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
250
+ }
251
+ const data = await resp.json();
252
+ const raw = (_d = (_c = (_b = data == null ? void 0 : data.resources) == null ? void 0 : _b.results) == null ? void 0 : _c.products) != null ? _d : [];
253
+ const handles = raw.filter((p) => p.available !== false).map((p) => p.handle).filter((h) => typeof h === "string" && h.length > 0).slice(0, limit);
254
+ const fetched = await Promise.all(
255
+ handles.map((h) => findInternal(h, { minimal: options.minimal }))
256
+ );
257
+ const results = filter(fetched, isNonNullish);
258
+ const finalProducts = options.minimal ? (_e = maybeOverrideMinimalProductsCurrency(
259
+ results,
260
+ options.currency
261
+ )) != null ? _e : [] : (_f = maybeOverrideProductsCurrency(
262
+ results,
263
+ options.currency
264
+ )) != null ? _f : [];
265
+ return finalProducts;
266
+ }
267
+ async function recommendationsInternal(productId, options) {
268
+ var _a, _b;
269
+ if (!Number.isFinite(productId) || productId <= 0) {
270
+ throw new Error("Valid productId is required");
271
+ }
272
+ const limit = Math.max(1, Math.min((_a = options.limit) != null ? _a : 10, 10));
273
+ const intent = (_b = options.intent) != null ? _b : "related";
274
+ const localeValue = options.locale && options.locale.trim() || "en";
275
+ const localePrefix = `${localeValue.replace(/^\/|\/$/g, "")}/`;
276
+ const url = `${baseUrl}${localePrefix}recommendations/products.json?product_id=${encodeURIComponent(String(productId))}&limit=${limit}&intent=${intent}`;
277
+ const resp = await rateLimitedFetch(url, {
278
+ rateLimitClass: "products:recommendations",
279
+ timeoutMs: 7e3,
280
+ retry: { maxRetries: 2, baseDelayMs: 300 }
281
+ });
282
+ if (!resp.ok) {
283
+ if (resp.status === 404) {
284
+ return [];
285
+ }
286
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
287
+ }
288
+ const data = await resp.json();
289
+ const isRecord = (v) => typeof v === "object" && v !== null;
290
+ const productsArray = Array.isArray(data) ? data : isRecord(data) && Array.isArray(data.products) ? data.products : [];
291
+ const normalized = productsDto(productsArray, { minimal: options.minimal }) || [];
292
+ const finalProducts = options.minimal ? maybeOverrideMinimalProductsCurrency(
293
+ normalized,
294
+ options.currency
295
+ ) : maybeOverrideProductsCurrency(
296
+ normalized,
297
+ options.currency
298
+ );
299
+ return finalProducts != null ? finalProducts : [];
300
+ }
301
+ async function findEnhancedInternal(productHandle, options) {
302
+ var _a;
303
+ const apiKey = options == null ? void 0 : options.apiKey;
304
+ if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
305
+ throw new Error("apiKey is required");
306
+ }
307
+ const baseProduct = await findInternal(productHandle, { minimal: false });
308
+ if (!baseProduct) return null;
309
+ let updatedAt = (_a = baseProduct.updatedAt) == null ? void 0 : _a.toISOString();
310
+ if (updatedAt == null ? void 0 : updatedAt.endsWith(".000Z")) {
311
+ updatedAt = updatedAt.replace(".000Z", "Z");
312
+ }
313
+ if (!updatedAt) {
314
+ const url = `${baseUrl}products/${encodeURIComponent(baseProduct.handle)}.js`;
315
+ const resp2 = await rateLimitedFetch(url, {
316
+ rateLimitClass: "products:single",
317
+ timeoutMs: 7e3,
318
+ retry: { maxRetries: 1, baseDelayMs: 200 }
319
+ });
320
+ if (!resp2.ok) {
321
+ if (resp2.status === 404) return null;
322
+ throw new Error(`HTTP ${resp2.status}: ${resp2.statusText}`);
323
+ }
324
+ const raw = await resp2.json();
325
+ if (typeof raw.updated_at === "string" && raw.updated_at.trim()) {
326
+ updatedAt = raw.updated_at;
327
+ } else {
328
+ throw new Error("updatedAt missing for product");
329
+ }
330
+ }
331
+ const endpoint = typeof (options == null ? void 0 : options.endpoint) === "string" && options.endpoint.trim() || "https://shopify-product-enrichment-worker.ninjacode.workers.dev";
332
+ let hostname = storeDomain;
333
+ try {
334
+ hostname = new URL(storeDomain).hostname;
335
+ } catch {
336
+ hostname = storeDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
337
+ }
338
+ const resp = await rateLimitedFetch(endpoint, {
339
+ method: "POST",
340
+ headers: {
341
+ "content-type": "application/json",
342
+ "x-api-key": apiKey
343
+ },
344
+ body: JSON.stringify({
345
+ storeDomain: hostname,
346
+ handle: baseProduct.handle,
347
+ updatedAt
348
+ }),
349
+ rateLimitClass: "products:enhanced",
350
+ timeoutMs: 15e3,
351
+ retry: {
352
+ maxRetries: 2,
353
+ baseDelayMs: 300,
354
+ retryOnStatuses: [429, 500, 502, 503, 504]
355
+ }
356
+ });
357
+ if (!resp.ok) {
358
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
359
+ }
360
+ const data = await resp.json();
361
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
362
+ throw new Error("Invalid enhanced product response");
363
+ }
364
+ const o = data;
365
+ if (!("shopify" in o) || !("enrichment" in o) || !("cache" in o)) {
366
+ throw new Error("Invalid enhanced product response");
367
+ }
368
+ return data;
369
+ }
370
+ const operations = {
371
+ /**
372
+ * Fetches all products from the store across all pages.
373
+ *
374
+ * @returns {Promise<Product[] | null>} Array of all products or null if error occurs
375
+ *
376
+ * @throws {Error} When there's a network error or API failure
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * const shop = new ShopClient('https://exampleshop.com');
381
+ * const allProducts = await shop.products.all();
382
+ *
383
+ * console.log(`Found ${allProducts?.length} products`);
384
+ * allProducts?.forEach(product => {
385
+ * console.log(product.title, product.price);
386
+ * });
387
+ * ```
388
+ */
389
+ all: async (options) => allInternal({ currency: options == null ? void 0 : options.currency, minimal: false }),
390
+ /**
391
+ * Fetches products with pagination support.
392
+ *
393
+ * @param options - Pagination options
394
+ * @param options.page - Page number (default: 1)
395
+ * @param options.limit - Number of products per page (default: 250, max: 250)
396
+ *
397
+ * @returns {Promise<Product[] | null>} Array of products for the specified page or null if error occurs
398
+ *
399
+ * @throws {Error} When there's a network error or API failure
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * const shop = new ShopClient('https://example.myshopify.com');
404
+ *
405
+ * // Get first page with default limit (250)
406
+ * const firstPage = await shop.products.paginated();
407
+ *
408
+ * // Get second page with custom limit
409
+ * const secondPage = await shop.products.paginated({ page: 2, limit: 50 });
410
+ * ```
411
+ */
412
+ paginated: async (options) => paginatedInternal({
413
+ page: options == null ? void 0 : options.page,
414
+ limit: options == null ? void 0 : options.limit,
415
+ currency: options == null ? void 0 : options.currency,
416
+ minimal: false
417
+ }),
418
+ /**
419
+ * Finds a specific product by its handle.
420
+ *
421
+ * @param productHandle - The product handle (URL slug) to search for
422
+ *
423
+ * @returns {Promise<Product | null>} The product if found, null if not found
424
+ *
425
+ * @throws {Error} When the handle is invalid or there's a network error
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * const shop = new ShopClient('https://exampleshop.com');
430
+ *
431
+ * // Find product by handle
432
+ * const product = await shop.products.find('awesome-t-shirt');
433
+ *
434
+ * if (product) {
435
+ * console.log(product.title, product.price);
436
+ * console.log('Available variants:', product.variants.length);
437
+ * }
438
+ *
439
+ * // Handle with query string
440
+ * const productWithVariant = await shop.products.find('t-shirt?variant=123');
441
+ * ```
442
+ */
443
+ find: async (productHandle, options) => findInternal(productHandle, {
444
+ minimal: false,
445
+ currency: options == null ? void 0 : options.currency
446
+ }),
447
+ findEnhanced: async (productHandle, options) => findEnhancedInternal(productHandle, options),
448
+ /**
449
+ * Enrich a product by generating merged markdown from body_html and product page.
450
+ * Adds `enriched_content` to the returned product.
451
+ */
452
+ enriched: async (productHandle, options) => {
453
+ if (!productHandle || typeof productHandle !== "string") {
454
+ throw new Error("Product handle is required and must be a string");
455
+ }
456
+ const baseProduct = await operations.find(productHandle);
457
+ if (!baseProduct) return null;
458
+ const handle = baseProduct.handle;
459
+ const { enrichProduct } = await import("./ai/enrich.mjs");
460
+ const enriched = await enrichProduct(storeDomain, handle, {
461
+ apiKey: options == null ? void 0 : options.apiKey,
462
+ openRouter: ai == null ? void 0 : ai.openRouter,
463
+ useGfm: options == null ? void 0 : options.useGfm,
464
+ inputType: options == null ? void 0 : options.inputType,
465
+ model: options == null ? void 0 : options.model,
466
+ outputFormat: options == null ? void 0 : options.outputFormat,
467
+ htmlContent: options == null ? void 0 : options.content
468
+ });
469
+ return {
470
+ ...baseProduct,
471
+ enriched_content: enriched.mergedMarkdown
472
+ };
473
+ },
474
+ enrichedPrompts: async (productHandle, options) => {
475
+ if (!productHandle || typeof productHandle !== "string") {
476
+ throw new Error("Product handle is required and must be a string");
477
+ }
478
+ const baseProduct = await operations.find(productHandle);
479
+ if (!baseProduct) throw new Error("Product not found");
480
+ const handle = baseProduct.handle;
481
+ const { buildEnrichPromptForProduct } = await import("./ai/enrich.mjs");
482
+ return buildEnrichPromptForProduct(storeDomain, handle, {
483
+ useGfm: options == null ? void 0 : options.useGfm,
484
+ inputType: options == null ? void 0 : options.inputType,
485
+ outputFormat: options == null ? void 0 : options.outputFormat,
486
+ htmlContent: options == null ? void 0 : options.content
487
+ });
488
+ },
489
+ classify: async (productHandle, options) => {
490
+ if (!productHandle || typeof productHandle !== "string") {
491
+ throw new Error("Product handle is required and must be a string");
492
+ }
493
+ const enrichedProduct = await operations.enriched(productHandle, {
494
+ apiKey: options == null ? void 0 : options.apiKey,
495
+ inputType: "html",
496
+ model: options == null ? void 0 : options.model,
497
+ outputFormat: "json",
498
+ content: options == null ? void 0 : options.content
499
+ });
500
+ if (!enrichedProduct || !enrichedProduct.enriched_content) return null;
501
+ let productContent = enrichedProduct.enriched_content;
502
+ try {
503
+ const obj = JSON.parse(enrichedProduct.enriched_content);
504
+ const lines = [];
505
+ if (obj.title && typeof obj.title === "string")
506
+ lines.push(`Title: ${obj.title}`);
507
+ if (obj.description && typeof obj.description === "string")
508
+ lines.push(`Description: ${obj.description}`);
509
+ if (Array.isArray(obj.materials) && obj.materials.length)
510
+ lines.push(`Materials: ${obj.materials.join(", ")}`);
511
+ if (Array.isArray(obj.care) && obj.care.length)
512
+ lines.push(`Care: ${obj.care.join(", ")}`);
513
+ if (obj.fit && typeof obj.fit === "string")
514
+ lines.push(`Fit: ${obj.fit}`);
515
+ if (obj.returnPolicy && typeof obj.returnPolicy === "string")
516
+ lines.push(`ReturnPolicy: ${obj.returnPolicy}`);
517
+ productContent = lines.join("\n");
518
+ } catch {
519
+ }
520
+ const { classifyProduct } = await import("./ai/enrich.mjs");
521
+ const classification = await classifyProduct(productContent, {
522
+ apiKey: options == null ? void 0 : options.apiKey,
523
+ openRouter: ai == null ? void 0 : ai.openRouter,
524
+ model: options == null ? void 0 : options.model
525
+ });
526
+ return classification;
527
+ },
528
+ classifyPrompts: async (productHandle, options) => {
529
+ if (!productHandle || typeof productHandle !== "string") {
530
+ throw new Error("Product handle is required and must be a string");
531
+ }
532
+ const baseProduct = await operations.find(productHandle);
533
+ if (!baseProduct) throw new Error("Product not found");
534
+ const handle = baseProduct.handle;
535
+ const { buildClassifyPromptForProduct } = await import("./ai/enrich.mjs");
536
+ return buildClassifyPromptForProduct(storeDomain, handle, {
537
+ useGfm: options == null ? void 0 : options.useGfm,
538
+ inputType: options == null ? void 0 : options.inputType,
539
+ htmlContent: options == null ? void 0 : options.content
540
+ });
541
+ },
542
+ generateSEOContent: async (productHandle, options) => {
543
+ if (!productHandle || typeof productHandle !== "string") {
544
+ throw new Error("Product handle is required and must be a string");
545
+ }
546
+ const baseProduct = await operations.find(productHandle);
547
+ if (!baseProduct) return null;
548
+ const payload = {
549
+ title: baseProduct.title,
550
+ description: baseProduct.bodyHtml || void 0,
551
+ vendor: baseProduct.vendor,
552
+ price: baseProduct.price,
553
+ tags: baseProduct.tags
554
+ };
555
+ const { generateSEOContent: generateSEOContentLLM } = await import("./ai/enrich.mjs");
556
+ const seo = await generateSEOContentLLM(payload, {
557
+ apiKey: options == null ? void 0 : options.apiKey,
558
+ openRouter: ai == null ? void 0 : ai.openRouter,
559
+ model: options == null ? void 0 : options.model
560
+ });
561
+ return seo;
562
+ },
563
+ /**
564
+ * Fetches the extracted HTML content from the product page.
565
+ * This is useful for getting the main product description and content directly from the page HTML.
566
+ *
567
+ * @param productHandle - The handle of the product
568
+ * @param content - Optional HTML content to extract from. If provided, skips fetching the product page.
569
+ * @returns {Promise<string | null>} The extracted HTML content or null if not found
570
+ *
571
+ * @example
572
+ * ```typescript
573
+ * // Fetch from store
574
+ * const html = await shop.products.infoHtml("product-handle");
575
+ *
576
+ * // Use provided HTML
577
+ * const htmlFromContent = await shop.products.infoHtml("product-handle", "<html>...</html>");
578
+ * ```
579
+ */
580
+ infoHtml: async (productHandle, content) => {
581
+ if (!productHandle || typeof productHandle !== "string") {
582
+ throw new Error("Product handle is required and must be a string");
583
+ }
584
+ const { extractMainSection, fetchProductPage } = await import("./ai/enrich.mjs");
585
+ if (content) {
586
+ return extractMainSection(content);
587
+ }
588
+ const baseProduct = await operations.find(productHandle);
589
+ if (!baseProduct) return null;
590
+ const pageHtml = await fetchProductPage(storeDomain, baseProduct.handle);
591
+ return extractMainSection(pageHtml);
592
+ },
593
+ /**
594
+ * Fetches products that are showcased/featured on the store's homepage.
595
+ *
596
+ * @returns {Promise<Product[]>} Array of showcased products found on the homepage
597
+ *
598
+ * @throws {Error} When there's a network error or API failure
599
+ *
600
+ * @example
601
+ * ```typescript
602
+ * const shop = new ShopClient('https://exampleshop.com');
603
+ * const showcasedProducts = await shop.products.showcased();
604
+ *
605
+ * console.log(`Found ${showcasedProducts.length} showcased products`);
606
+ * showcasedProducts.forEach(product => {
607
+ * console.log(`Featured: ${product.title} - ${product.price}`);
608
+ * });
609
+ * ```
610
+ */
611
+ showcased: async () => {
612
+ const storeInfo = await getStoreInfo();
613
+ const normalizedHandles = storeInfo.showcase.products.map((h) => {
614
+ var _a;
615
+ return (_a = h.split("?")[0]) == null ? void 0 : _a.replace(/^\/|\/$/g, "");
616
+ }).filter((base) => Boolean(base));
617
+ const seen = /* @__PURE__ */ new Set();
618
+ const uniqueHandles = [];
619
+ for (const base of normalizedHandles) {
620
+ if (seen.has(base)) continue;
621
+ seen.add(base);
622
+ uniqueHandles.push(base);
623
+ }
624
+ const products = await Promise.all(
625
+ uniqueHandles.map(
626
+ (productHandle) => findInternal(productHandle, { minimal: false })
627
+ )
628
+ );
629
+ return filter(products, isNonNullish);
630
+ },
631
+ /**
632
+ * Creates a filter map of variant options and their distinct values from all products.
633
+ *
634
+ * @returns {Promise<Record<string, string[]> | null>} Map of option names to their distinct values or null if error occurs
635
+ *
636
+ * @throws {Error} When there's a network error or API failure
637
+ *
638
+ * @example
639
+ * ```typescript
640
+ * const shop = new ShopClient('https://exampleshop.com');
641
+ * const filters = await shop.products.filter();
642
+ *
643
+ * console.log('Available filters:', filters);
644
+ * // Output: { "Size": ["S", "M", "L", "XL"], "Color": ["Red", "Blue", "Green"] }
645
+ *
646
+ * // Use filters for UI components
647
+ * Object.entries(filters || {}).forEach(([optionName, values]) => {
648
+ * console.log(`${optionName}: ${values.join(', ')}`);
649
+ * });
650
+ * ```
651
+ */
652
+ filter: async () => {
653
+ try {
654
+ const products = await operations.all();
655
+ if (!products || products.length === 0) {
656
+ return {};
657
+ }
658
+ const filterMap = {};
659
+ products.forEach((product) => {
660
+ if (product.variants && product.variants.length > 0) {
661
+ if (product.options && product.options.length > 0) {
662
+ product.options.forEach((option) => {
663
+ const lowercaseOptionName = option.name.toLowerCase();
664
+ if (!filterMap[lowercaseOptionName]) {
665
+ filterMap[lowercaseOptionName] = /* @__PURE__ */ new Set();
666
+ }
667
+ option.values.forEach((value) => {
668
+ const trimmed = value == null ? void 0 : value.trim();
669
+ if (trimmed) {
670
+ let set = filterMap[lowercaseOptionName];
671
+ if (!set) {
672
+ set = /* @__PURE__ */ new Set();
673
+ filterMap[lowercaseOptionName] = set;
674
+ }
675
+ set.add(trimmed.toLowerCase());
676
+ }
677
+ });
678
+ });
679
+ }
680
+ product.variants.forEach((variant) => {
681
+ var _a, _b, _c, _d, _e, _f, _g;
682
+ if ((_a = product.options) == null ? void 0 : _a.length) return;
683
+ if (variant.option1) {
684
+ const optionName = (((_c = (_b = product.options) == null ? void 0 : _b[0]) == null ? void 0 : _c.name) || "Option 1").toLowerCase();
685
+ let set1 = filterMap[optionName];
686
+ if (!set1) {
687
+ set1 = /* @__PURE__ */ new Set();
688
+ filterMap[optionName] = set1;
689
+ }
690
+ set1.add(variant.option1.trim().toLowerCase());
691
+ }
692
+ if (variant.option2) {
693
+ const optionName = (((_e = (_d = product.options) == null ? void 0 : _d[1]) == null ? void 0 : _e.name) || "Option 2").toLowerCase();
694
+ let set2 = filterMap[optionName];
695
+ if (!set2) {
696
+ set2 = /* @__PURE__ */ new Set();
697
+ filterMap[optionName] = set2;
698
+ }
699
+ set2.add(variant.option2.trim().toLowerCase());
700
+ }
701
+ if (variant.option3) {
702
+ const optionName = (((_g = (_f = product.options) == null ? void 0 : _f[2]) == null ? void 0 : _g.name) || "Option 3").toLowerCase();
703
+ if (!filterMap[optionName]) {
704
+ filterMap[optionName] = /* @__PURE__ */ new Set();
705
+ }
706
+ filterMap[optionName].add(variant.option3.trim().toLowerCase());
707
+ }
708
+ });
709
+ }
710
+ });
711
+ const result = {};
712
+ Object.entries(filterMap).forEach(([optionName, valueSet]) => {
713
+ result[optionName] = Array.from(valueSet).sort();
714
+ });
715
+ return result;
716
+ } catch (error) {
717
+ console.error("Failed to create product filters:", storeDomain, error);
718
+ throw error;
719
+ }
720
+ },
721
+ predictiveSearch: async (query, options) => predictiveSearchInternal(query, {
722
+ limit: options == null ? void 0 : options.limit,
723
+ locale: options == null ? void 0 : options.locale,
724
+ currency: options == null ? void 0 : options.currency,
725
+ unavailableProducts: options == null ? void 0 : options.unavailableProducts,
726
+ minimal: false
727
+ }),
728
+ recommendations: async (productId, options) => recommendationsInternal(productId, {
729
+ limit: options == null ? void 0 : options.limit,
730
+ intent: options == null ? void 0 : options.intent,
731
+ locale: options == null ? void 0 : options.locale,
732
+ currency: options == null ? void 0 : options.currency,
733
+ minimal: false
734
+ }),
735
+ minimal: {
736
+ all: async (options) => {
737
+ return allInternal({ minimal: true, currency: options == null ? void 0 : options.currency });
738
+ },
739
+ paginated: async (options) => {
740
+ return paginatedInternal({
741
+ page: options == null ? void 0 : options.page,
742
+ limit: options == null ? void 0 : options.limit,
743
+ currency: options == null ? void 0 : options.currency,
744
+ minimal: true
745
+ });
746
+ },
747
+ find: async (productHandle, options) => {
748
+ return findInternal(productHandle, {
749
+ minimal: true,
750
+ currency: options == null ? void 0 : options.currency
751
+ });
752
+ },
753
+ showcased: async () => {
754
+ const res = await operations.showcase.minimal();
755
+ return res || [];
756
+ },
757
+ predictiveSearch: async (query, options) => {
758
+ return predictiveSearchInternal(query, {
759
+ limit: options == null ? void 0 : options.limit,
760
+ locale: options == null ? void 0 : options.locale,
761
+ currency: options == null ? void 0 : options.currency,
762
+ unavailableProducts: options == null ? void 0 : options.unavailableProducts,
763
+ minimal: true
764
+ });
765
+ },
766
+ recommendations: async (productId, options) => {
767
+ return recommendationsInternal(productId, {
768
+ limit: options == null ? void 0 : options.limit,
769
+ intent: options == null ? void 0 : options.intent,
770
+ locale: options == null ? void 0 : options.locale,
771
+ currency: options == null ? void 0 : options.currency,
772
+ minimal: true
773
+ });
774
+ }
775
+ },
776
+ showcase: {
777
+ minimal: async () => {
778
+ const storeInfo = await getStoreInfo();
779
+ const normalizedHandles = storeInfo.showcase.products.map((h) => {
780
+ var _a;
781
+ return (_a = h.split("?")[0]) == null ? void 0 : _a.replace(/^\/|\/$/g, "");
782
+ }).filter((base) => Boolean(base));
783
+ const seen = /* @__PURE__ */ new Set();
784
+ const uniqueHandles = [];
785
+ for (const base of normalizedHandles) {
786
+ if (seen.has(base)) continue;
787
+ seen.add(base);
788
+ uniqueHandles.push(base);
789
+ }
790
+ const products = await Promise.all(
791
+ uniqueHandles.map(
792
+ (productHandle) => findInternal(productHandle, { minimal: true })
793
+ )
794
+ );
795
+ return filter(products, isNonNullish);
796
+ }
797
+ }
798
+ };
799
+ return operations;
800
+ }
801
+
802
+ export {
803
+ createProductOperations
804
+ };