@pvh-afl/core 1.1.19 → 1.1.21
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/common/decorators/current-customer.decorator.d.ts +7 -0
- package/dist/common/decorators/current-customer.decorator.d.ts.map +1 -0
- package/dist/common/decorators/current-customer.decorator.js +21 -0
- package/dist/common/decorators/current-customer.decorator.js.map +1 -0
- package/dist/common/decorators/require-customer.decorator.d.ts +25 -0
- package/dist/common/decorators/require-customer.decorator.d.ts.map +1 -0
- package/dist/common/decorators/require-customer.decorator.js +31 -0
- package/dist/common/decorators/require-customer.decorator.js.map +1 -0
- package/dist/common/guards/customer-auth.guard.d.ts +54 -0
- package/dist/common/guards/customer-auth.guard.d.ts.map +1 -0
- package/dist/common/guards/customer-auth.guard.js +142 -0
- package/dist/common/guards/customer-auth.guard.js.map +1 -0
- package/dist/integrations/commerce/commerce.types.d.ts +1 -0
- package/dist/integrations/commerce/commerce.types.d.ts.map +1 -1
- package/dist/integrations/commerce/shopify.service.d.ts.map +1 -1
- package/dist/integrations/commerce/shopify.service.js +1 -0
- package/dist/integrations/commerce/shopify.service.js.map +1 -1
- package/dist/theme/theme-content.service.d.ts +11 -4
- package/dist/theme/theme-content.service.d.ts.map +1 -1
- package/dist/theme/theme-content.service.js +212 -196
- package/dist/theme/theme-content.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var ThemeContentService_1;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.ThemeContentService = void 0;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
@@ -42,11 +43,15 @@ const FAQ_METAOBJECT_QUERY = `
|
|
|
42
43
|
}
|
|
43
44
|
`;
|
|
44
45
|
let ThemeContentService = class ThemeContentService {
|
|
46
|
+
static { ThemeContentService_1 = this; }
|
|
45
47
|
cacheService;
|
|
46
48
|
configService;
|
|
47
49
|
shopifyThemeService;
|
|
48
50
|
collectionService;
|
|
49
51
|
productService;
|
|
52
|
+
// Number of filenames to resolve per Files API request when matching
|
|
53
|
+
// `shopify://` media references by filename.
|
|
54
|
+
static MEDIA_QUERY_BATCH_SIZE = 25;
|
|
50
55
|
refreshInProgress = new Set();
|
|
51
56
|
constructor(cacheService, configService, shopifyThemeService, collectionService, productService) {
|
|
52
57
|
this.cacheService = cacheService;
|
|
@@ -77,7 +82,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
77
82
|
let isStale = false;
|
|
78
83
|
let triggeredRefresh = false;
|
|
79
84
|
if (!cached) {
|
|
80
|
-
logger_1.logger.info(
|
|
85
|
+
logger_1.logger.info("Theme content cache miss, fetching fresh data", {
|
|
81
86
|
brand,
|
|
82
87
|
path,
|
|
83
88
|
cacheKey,
|
|
@@ -88,7 +93,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
88
93
|
themeStructure = cached.data;
|
|
89
94
|
isStale = cached.isStale;
|
|
90
95
|
if (cached.isStale) {
|
|
91
|
-
logger_1.logger.info(
|
|
96
|
+
logger_1.logger.info("Theme content is stale, triggering background refresh", {
|
|
92
97
|
brand,
|
|
93
98
|
path,
|
|
94
99
|
cacheKey,
|
|
@@ -114,7 +119,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
114
119
|
*/
|
|
115
120
|
async refreshContentInBackground(brand, path, cacheKey) {
|
|
116
121
|
if (this.refreshInProgress.has(cacheKey)) {
|
|
117
|
-
logger_1.logger.info(
|
|
122
|
+
logger_1.logger.info("Refresh already in progress, skipping", { cacheKey });
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
this.refreshInProgress.add(cacheKey);
|
|
@@ -130,7 +135,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
130
135
|
*/
|
|
131
136
|
async fetchAndCacheContent(brand, path, options) {
|
|
132
137
|
const cacheKey = this.buildCacheKey(brand, path);
|
|
133
|
-
logger_1.logger.info(
|
|
138
|
+
logger_1.logger.info("Fetching fresh theme content", {
|
|
134
139
|
brand,
|
|
135
140
|
path,
|
|
136
141
|
cacheKey,
|
|
@@ -138,7 +143,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
138
143
|
try {
|
|
139
144
|
const parsed = await this.shopifyThemeService.fetchThemeFile(brand, path);
|
|
140
145
|
if (!parsed) {
|
|
141
|
-
logger_1.logger.warn(
|
|
146
|
+
logger_1.logger.warn("No content found for theme file", { brand, path });
|
|
142
147
|
return null;
|
|
143
148
|
}
|
|
144
149
|
let curated = await this.curateThemeContent(brand, parsed);
|
|
@@ -147,14 +152,14 @@ let ThemeContentService = class ThemeContentService {
|
|
|
147
152
|
const defaultTTL = options?.defaultTTL ?? configDefaultTTL;
|
|
148
153
|
const staleTTL = options?.staleTTL ?? configStaleTTL;
|
|
149
154
|
await this.cacheService.set(cacheKey, curated, defaultTTL, staleTTL);
|
|
150
|
-
logger_1.logger.info(
|
|
155
|
+
logger_1.logger.info("Theme content cached", { brand, cacheKey });
|
|
151
156
|
return curated;
|
|
152
157
|
}
|
|
153
158
|
catch (error) {
|
|
154
|
-
logger_1.logger.error(
|
|
159
|
+
logger_1.logger.error("Failed to fetch and curate theme content", {
|
|
155
160
|
brand,
|
|
156
161
|
path,
|
|
157
|
-
error: error instanceof Error ? error.message :
|
|
162
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
158
163
|
});
|
|
159
164
|
return null;
|
|
160
165
|
}
|
|
@@ -166,21 +171,26 @@ let ThemeContentService = class ThemeContentService {
|
|
|
166
171
|
* @param brand - Brand identifier
|
|
167
172
|
* @param handle - Metaobject handle (default: "main")
|
|
168
173
|
*/
|
|
169
|
-
async getFaqMetaobject(brand, handle =
|
|
174
|
+
async getFaqMetaobject(brand, handle = "main") {
|
|
170
175
|
const cacheKey = this.buildFaqCacheKey(brand, handle);
|
|
171
176
|
const cached = await this.cacheService.get(cacheKey);
|
|
172
177
|
let faqData = null;
|
|
173
178
|
let isStale = false;
|
|
174
179
|
let triggeredRefresh = false;
|
|
175
180
|
if (!cached) {
|
|
176
|
-
logger_1.logger.info(
|
|
181
|
+
logger_1.logger.info("FAQ metaobject cache miss, fetching fresh data", {
|
|
177
182
|
brand,
|
|
178
183
|
handle,
|
|
179
184
|
cacheKey,
|
|
180
185
|
});
|
|
181
186
|
const result = await this.fetchAndCacheFaqMetaobject(brand, handle);
|
|
182
187
|
if (result.error) {
|
|
183
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
data: null,
|
|
190
|
+
isStale: false,
|
|
191
|
+
triggeredRefresh: false,
|
|
192
|
+
error: result.error,
|
|
193
|
+
};
|
|
184
194
|
}
|
|
185
195
|
faqData = result.data;
|
|
186
196
|
}
|
|
@@ -188,7 +198,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
188
198
|
faqData = cached.data;
|
|
189
199
|
isStale = cached.isStale;
|
|
190
200
|
if (cached.isStale) {
|
|
191
|
-
logger_1.logger.info(
|
|
201
|
+
logger_1.logger.info("FAQ metaobject is stale, triggering background refresh", {
|
|
192
202
|
brand,
|
|
193
203
|
handle,
|
|
194
204
|
cacheKey,
|
|
@@ -208,7 +218,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
208
218
|
*/
|
|
209
219
|
async refreshFaqInBackground(brand, handle, cacheKey) {
|
|
210
220
|
if (this.refreshInProgress.has(cacheKey)) {
|
|
211
|
-
logger_1.logger.info(
|
|
221
|
+
logger_1.logger.info("FAQ refresh already in progress, skipping", { cacheKey });
|
|
212
222
|
return;
|
|
213
223
|
}
|
|
214
224
|
this.refreshInProgress.add(cacheKey);
|
|
@@ -222,33 +232,33 @@ let ThemeContentService = class ThemeContentService {
|
|
|
222
232
|
/**
|
|
223
233
|
* Fetch FAQ metaobject from Storefront API and cache it.
|
|
224
234
|
*/
|
|
225
|
-
async fetchAndCacheFaqMetaobject(brand, handle =
|
|
235
|
+
async fetchAndCacheFaqMetaobject(brand, handle = "main") {
|
|
226
236
|
const cacheKey = this.buildFaqCacheKey(brand, handle);
|
|
227
237
|
const storefrontToken = this.configService.getShopifyStorefrontToken(brand);
|
|
228
238
|
const graphqlUrl = this.configService.getShopifyGraphqlUrl(brand);
|
|
229
239
|
if (!storefrontToken || !graphqlUrl) {
|
|
230
|
-
logger_1.logger.error(
|
|
240
|
+
logger_1.logger.error("Missing Storefront API configuration for FAQ metaobject", {
|
|
231
241
|
brand,
|
|
232
242
|
hasToken: !!storefrontToken,
|
|
233
243
|
hasUrl: !!graphqlUrl,
|
|
234
244
|
});
|
|
235
|
-
return { data: null, error:
|
|
245
|
+
return { data: null, error: "Missing Storefront API configuration" };
|
|
236
246
|
}
|
|
237
247
|
try {
|
|
238
248
|
const response = await fetch(graphqlUrl, {
|
|
239
|
-
method:
|
|
249
|
+
method: "POST",
|
|
240
250
|
headers: {
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
"Content-Type": "application/json",
|
|
252
|
+
"X-Shopify-Storefront-Access-Token": storefrontToken,
|
|
243
253
|
},
|
|
244
254
|
body: JSON.stringify({
|
|
245
255
|
query: FAQ_METAOBJECT_QUERY,
|
|
246
|
-
variables: { type:
|
|
256
|
+
variables: { type: "faq_page", handle },
|
|
247
257
|
}),
|
|
248
258
|
});
|
|
249
259
|
const result = (await response.json());
|
|
250
260
|
if (result.errors?.length) {
|
|
251
|
-
logger_1.logger.error(
|
|
261
|
+
logger_1.logger.error("Storefront API errors fetching FAQ metaobject", {
|
|
252
262
|
brand,
|
|
253
263
|
handle,
|
|
254
264
|
errors: result.errors.map((e) => e.message),
|
|
@@ -257,14 +267,14 @@ let ThemeContentService = class ThemeContentService {
|
|
|
257
267
|
}
|
|
258
268
|
const faqPage = result.data?.metaobject;
|
|
259
269
|
if (!faqPage) {
|
|
260
|
-
logger_1.logger.warn(
|
|
261
|
-
return { data: null, error:
|
|
270
|
+
logger_1.logger.warn("No FAQ page metaobject found", { brand, handle });
|
|
271
|
+
return { data: null, error: "FAQ page not found" };
|
|
262
272
|
}
|
|
263
273
|
const parsed = this.parseFaqMetaobject(faqPage);
|
|
264
274
|
// Cache with same TTL as landing pages
|
|
265
275
|
const { defaultTTL, staleTTL } = this.configService.getLandingPageCacheTTL(brand);
|
|
266
276
|
await this.cacheService.set(cacheKey, parsed, defaultTTL, staleTTL);
|
|
267
|
-
logger_1.logger.info(
|
|
277
|
+
logger_1.logger.info("FAQ metaobject fetched and cached", {
|
|
268
278
|
brand,
|
|
269
279
|
handle,
|
|
270
280
|
cacheKey,
|
|
@@ -274,12 +284,15 @@ let ThemeContentService = class ThemeContentService {
|
|
|
274
284
|
return { data: parsed };
|
|
275
285
|
}
|
|
276
286
|
catch (error) {
|
|
277
|
-
logger_1.logger.error(
|
|
287
|
+
logger_1.logger.error("Failed to fetch FAQ metaobject", {
|
|
278
288
|
brand,
|
|
279
289
|
handle,
|
|
280
|
-
error: error instanceof Error ? error.message :
|
|
290
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
281
291
|
});
|
|
282
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
data: null,
|
|
294
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
295
|
+
};
|
|
283
296
|
}
|
|
284
297
|
}
|
|
285
298
|
/**
|
|
@@ -292,20 +305,24 @@ let ThemeContentService = class ThemeContentService {
|
|
|
292
305
|
async invalidateFaqCache(brand) {
|
|
293
306
|
const pattern = `faq:${brand}:*`;
|
|
294
307
|
const count = await this.cacheService.markStale(pattern);
|
|
295
|
-
logger_1.logger.info(
|
|
308
|
+
logger_1.logger.info("FAQ cache invalidated", {
|
|
309
|
+
brand,
|
|
310
|
+
pattern,
|
|
311
|
+
keysMarkedStale: count,
|
|
312
|
+
});
|
|
296
313
|
return count;
|
|
297
314
|
}
|
|
298
315
|
/**
|
|
299
316
|
* Parse FAQ page metaobject response into structured data.
|
|
300
317
|
*/
|
|
301
318
|
parseFaqMetaobject(faqPage) {
|
|
302
|
-
let title =
|
|
319
|
+
let title = "";
|
|
303
320
|
const categories = [];
|
|
304
321
|
for (const field of faqPage.fields) {
|
|
305
|
-
if (field.key ===
|
|
322
|
+
if (field.key === "title") {
|
|
306
323
|
title = field.value;
|
|
307
324
|
}
|
|
308
|
-
else if (field.key ===
|
|
325
|
+
else if (field.key === "categories" && field.references?.nodes) {
|
|
309
326
|
for (const categoryNode of field.references.nodes) {
|
|
310
327
|
const category = this.parseFaqCategory(categoryNode);
|
|
311
328
|
if (category) {
|
|
@@ -320,13 +337,13 @@ let ThemeContentService = class ThemeContentService {
|
|
|
320
337
|
* Parse a single FAQ category from metaobject fields.
|
|
321
338
|
*/
|
|
322
339
|
parseFaqCategory(categoryNode) {
|
|
323
|
-
let categoryTitle =
|
|
340
|
+
let categoryTitle = "";
|
|
324
341
|
const items = [];
|
|
325
342
|
for (const field of categoryNode.fields) {
|
|
326
|
-
if (field.key ===
|
|
343
|
+
if (field.key === "title") {
|
|
327
344
|
categoryTitle = field.value;
|
|
328
345
|
}
|
|
329
|
-
else if (field.key ===
|
|
346
|
+
else if (field.key === "items" && field.references?.nodes) {
|
|
330
347
|
for (const itemNode of field.references.nodes) {
|
|
331
348
|
const item = this.parseFaqItem(itemNode.fields);
|
|
332
349
|
if (item) {
|
|
@@ -345,13 +362,13 @@ let ThemeContentService = class ThemeContentService {
|
|
|
345
362
|
* Converts rich_text_field answer from Portable Text JSON to HTML.
|
|
346
363
|
*/
|
|
347
364
|
parseFaqItem(fields) {
|
|
348
|
-
let question =
|
|
349
|
-
let answer =
|
|
365
|
+
let question = "";
|
|
366
|
+
let answer = "";
|
|
350
367
|
for (const field of fields) {
|
|
351
|
-
if (field.key ===
|
|
368
|
+
if (field.key === "question") {
|
|
352
369
|
question = field.value;
|
|
353
370
|
}
|
|
354
|
-
else if (field.key ===
|
|
371
|
+
else if (field.key === "answer") {
|
|
355
372
|
// Rich text field - value is JSON (Portable Text format)
|
|
356
373
|
// Convert to HTML for easy frontend rendering
|
|
357
374
|
answer = this.portableTextToHtml(field.value);
|
|
@@ -371,15 +388,15 @@ let ThemeContentService = class ThemeContentService {
|
|
|
371
388
|
*/
|
|
372
389
|
portableTextToHtml(jsonString) {
|
|
373
390
|
if (!jsonString) {
|
|
374
|
-
return
|
|
391
|
+
return "";
|
|
375
392
|
}
|
|
376
393
|
try {
|
|
377
394
|
const doc = JSON.parse(jsonString);
|
|
378
395
|
return this.renderPortableTextNode(doc);
|
|
379
396
|
}
|
|
380
397
|
catch (error) {
|
|
381
|
-
logger_1.logger.warn(
|
|
382
|
-
error: error instanceof Error ? error.message :
|
|
398
|
+
logger_1.logger.warn("Failed to parse Portable Text JSON, returning raw value", {
|
|
399
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
383
400
|
value: jsonString.substring(0, 100),
|
|
384
401
|
});
|
|
385
402
|
return jsonString;
|
|
@@ -389,34 +406,36 @@ let ThemeContentService = class ThemeContentService {
|
|
|
389
406
|
* Recursively render a Portable Text node to HTML.
|
|
390
407
|
*/
|
|
391
408
|
renderPortableTextNode(node) {
|
|
392
|
-
if (!node || typeof node !==
|
|
393
|
-
return
|
|
409
|
+
if (!node || typeof node !== "object") {
|
|
410
|
+
return "";
|
|
394
411
|
}
|
|
395
412
|
const children = node.children
|
|
396
|
-
? node.children
|
|
397
|
-
|
|
413
|
+
? node.children
|
|
414
|
+
.map((child) => this.renderPortableTextNode(child))
|
|
415
|
+
.join("")
|
|
416
|
+
: "";
|
|
398
417
|
switch (node.type) {
|
|
399
|
-
case
|
|
418
|
+
case "root":
|
|
400
419
|
return children;
|
|
401
|
-
case
|
|
420
|
+
case "paragraph":
|
|
402
421
|
return `<p>${children}</p>`;
|
|
403
|
-
case
|
|
422
|
+
case "heading": {
|
|
404
423
|
const level = node.level || 2;
|
|
405
424
|
return `<h${level}>${children}</h${level}>`;
|
|
406
425
|
}
|
|
407
|
-
case
|
|
408
|
-
const tag = node.listType ===
|
|
426
|
+
case "list": {
|
|
427
|
+
const tag = node.listType === "ordered" ? "ol" : "ul";
|
|
409
428
|
return `<${tag}>${children}</${tag}>`;
|
|
410
429
|
}
|
|
411
|
-
case
|
|
430
|
+
case "list-item":
|
|
412
431
|
return `<li>${children}</li>`;
|
|
413
|
-
case
|
|
414
|
-
const href = node.url || node.href ||
|
|
415
|
-
const target = node.target ||
|
|
432
|
+
case "link": {
|
|
433
|
+
const href = node.url || node.href || "#";
|
|
434
|
+
const target = node.target || "_blank";
|
|
416
435
|
return `<a href="${this.escapeHtml(href)}" target="${target}" rel="noopener noreferrer">${children}</a>`;
|
|
417
436
|
}
|
|
418
|
-
case
|
|
419
|
-
let text = this.escapeHtml(node.value ||
|
|
437
|
+
case "text": {
|
|
438
|
+
let text = this.escapeHtml(node.value || "");
|
|
420
439
|
// Apply marks (bold, italic, etc.)
|
|
421
440
|
if (node.bold) {
|
|
422
441
|
text = `<strong>${text}</strong>`;
|
|
@@ -436,11 +455,11 @@ let ThemeContentService = class ThemeContentService {
|
|
|
436
455
|
*/
|
|
437
456
|
escapeHtml(str) {
|
|
438
457
|
return str
|
|
439
|
-
.replace(/&/g,
|
|
440
|
-
.replace(/</g,
|
|
441
|
-
.replace(/>/g,
|
|
442
|
-
.replace(/"/g,
|
|
443
|
-
.replace(/'/g,
|
|
458
|
+
.replace(/&/g, "&")
|
|
459
|
+
.replace(/</g, "<")
|
|
460
|
+
.replace(/>/g, ">")
|
|
461
|
+
.replace(/"/g, """)
|
|
462
|
+
.replace(/'/g, "'");
|
|
444
463
|
}
|
|
445
464
|
/**
|
|
446
465
|
* Curates parsed theme content
|
|
@@ -448,7 +467,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
448
467
|
async curateThemeContent(brand, parsed) {
|
|
449
468
|
const { sections, order } = parsed;
|
|
450
469
|
if (!sections || !order) {
|
|
451
|
-
logger_1.logger.warn(
|
|
470
|
+
logger_1.logger.warn("Invalid theme content structure", {
|
|
452
471
|
hasSections: !!sections,
|
|
453
472
|
hasOrder: !!order,
|
|
454
473
|
});
|
|
@@ -483,7 +502,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
483
502
|
}
|
|
484
503
|
// Handle featured-collection section type
|
|
485
504
|
const collectionHandle = section.settings.collection;
|
|
486
|
-
if (section.type ===
|
|
505
|
+
if (section.type === "featured-collection" && collectionHandle) {
|
|
487
506
|
const productsCount = section.settings.products_to_show || 8;
|
|
488
507
|
curatedSection.collectionRef = {
|
|
489
508
|
handle: collectionHandle,
|
|
@@ -527,7 +546,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
527
546
|
*/
|
|
528
547
|
async resolveCollectionReferences(brand, content) {
|
|
529
548
|
if (!this.collectionService || !this.productService) {
|
|
530
|
-
logger_1.logger.warn(
|
|
549
|
+
logger_1.logger.warn("Collection/Product services not provided, skipping resolution");
|
|
531
550
|
return content;
|
|
532
551
|
}
|
|
533
552
|
const sectionsWithRefs = content
|
|
@@ -538,10 +557,10 @@ let ThemeContentService = class ThemeContentService {
|
|
|
538
557
|
}
|
|
539
558
|
// Fetch all collections in parallel
|
|
540
559
|
const collectionPromises = sectionsWithRefs.map(({ section }) => this.collectionService.getCollectionByHandle(brand, section.collectionRef.handle, section.collectionRef.productsCount).catch((error) => {
|
|
541
|
-
logger_1.logger.error(
|
|
560
|
+
logger_1.logger.error("Failed to fetch collection for theme section", {
|
|
542
561
|
brand,
|
|
543
562
|
handle: section.collectionRef.handle,
|
|
544
|
-
error: error instanceof Error ? error.message :
|
|
563
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
545
564
|
});
|
|
546
565
|
return null;
|
|
547
566
|
}));
|
|
@@ -554,10 +573,10 @@ let ThemeContentService = class ThemeContentService {
|
|
|
554
573
|
// Fetch all products in parallel
|
|
555
574
|
const productHandleArray = Array.from(allProductHandles);
|
|
556
575
|
const productPromises = productHandleArray.map((handle) => this.productService.getProductByHandle(brand, handle).catch((error) => {
|
|
557
|
-
logger_1.logger.error(
|
|
576
|
+
logger_1.logger.error("Failed to fetch product for collection", {
|
|
558
577
|
brand,
|
|
559
578
|
handle,
|
|
560
|
-
error: error instanceof Error ? error.message :
|
|
579
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
561
580
|
});
|
|
562
581
|
return null;
|
|
563
582
|
}));
|
|
@@ -601,7 +620,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
601
620
|
*/
|
|
602
621
|
async resolveProductReferences(brand, content) {
|
|
603
622
|
if (!this.productService) {
|
|
604
|
-
logger_1.logger.warn(
|
|
623
|
+
logger_1.logger.warn("Product service not provided, skipping resolution");
|
|
605
624
|
return content;
|
|
606
625
|
}
|
|
607
626
|
const sectionsWithRefs = content
|
|
@@ -612,10 +631,10 @@ let ThemeContentService = class ThemeContentService {
|
|
|
612
631
|
}
|
|
613
632
|
// Fetch all products in parallel
|
|
614
633
|
const productPromises = sectionsWithRefs.map(({ section }) => this.productService.getProductByHandle(brand, section.productRef.handle).catch((error) => {
|
|
615
|
-
logger_1.logger.error(
|
|
634
|
+
logger_1.logger.error("Failed to fetch product for theme section", {
|
|
616
635
|
brand,
|
|
617
636
|
handle: section.productRef.handle,
|
|
618
|
-
error: error instanceof Error ? error.message :
|
|
637
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
619
638
|
});
|
|
620
639
|
return null;
|
|
621
640
|
}));
|
|
@@ -678,13 +697,13 @@ let ThemeContentService = class ThemeContentService {
|
|
|
678
697
|
if (references.size === 0) {
|
|
679
698
|
return curated;
|
|
680
699
|
}
|
|
681
|
-
logger_1.logger.info(
|
|
700
|
+
logger_1.logger.info("Resolving media references", {
|
|
682
701
|
brand,
|
|
683
702
|
count: references.size,
|
|
684
703
|
});
|
|
685
704
|
const urlMap = await this.fetchMediaUrls(brand, Array.from(references));
|
|
686
705
|
if (urlMap.size === 0) {
|
|
687
|
-
logger_1.logger.warn(
|
|
706
|
+
logger_1.logger.warn("No media URLs resolved");
|
|
688
707
|
}
|
|
689
708
|
return this.replaceMediaReferences(curated, urlMap);
|
|
690
709
|
}
|
|
@@ -695,13 +714,13 @@ let ThemeContentService = class ThemeContentService {
|
|
|
695
714
|
for (const [key, value] of Object.entries(obj)) {
|
|
696
715
|
if (skipKeys.includes(key))
|
|
697
716
|
continue;
|
|
698
|
-
if (typeof value ===
|
|
717
|
+
if (typeof value === "string" && shopifyPattern.test(value)) {
|
|
699
718
|
references.add(value);
|
|
700
719
|
}
|
|
701
|
-
else if (typeof value ===
|
|
720
|
+
else if (typeof value === "object" && value !== null) {
|
|
702
721
|
if (Array.isArray(value)) {
|
|
703
722
|
value.forEach((item) => {
|
|
704
|
-
if (typeof item ===
|
|
723
|
+
if (typeof item === "object" && item !== null) {
|
|
705
724
|
extractFromObject(item, skipKeys);
|
|
706
725
|
}
|
|
707
726
|
});
|
|
@@ -737,7 +756,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
737
756
|
const adminToken = this.configService.getShopifyAdminToken(brand);
|
|
738
757
|
const storeDomain = this.configService.getShopifyStoreDomain(brand);
|
|
739
758
|
if (!adminToken || !storeDomain) {
|
|
740
|
-
logger_1.logger.warn(
|
|
759
|
+
logger_1.logger.warn("Missing Admin API configuration for media resolution", {
|
|
741
760
|
brand,
|
|
742
761
|
});
|
|
743
762
|
return urlMap;
|
|
@@ -745,10 +764,10 @@ let ThemeContentService = class ThemeContentService {
|
|
|
745
764
|
const shopImageRefs = [];
|
|
746
765
|
const filesApiRefs = [];
|
|
747
766
|
for (const ref of references) {
|
|
748
|
-
if (ref.startsWith(
|
|
767
|
+
if (ref.startsWith("shopify://shop_images/")) {
|
|
749
768
|
shopImageRefs.push(ref);
|
|
750
769
|
}
|
|
751
|
-
else if (ref.startsWith(
|
|
770
|
+
else if (ref.startsWith("shopify://files/")) {
|
|
752
771
|
filesApiRefs.push(ref);
|
|
753
772
|
}
|
|
754
773
|
}
|
|
@@ -761,15 +780,31 @@ let ThemeContentService = class ThemeContentService {
|
|
|
761
780
|
return urlMap;
|
|
762
781
|
}
|
|
763
782
|
async resolveShopImages(refs, urlMap, adminToken, storeDomain) {
|
|
783
|
+
// Map lowercased filename -> original ref so returned files can be matched
|
|
784
|
+
// back to the theme reference regardless of case.
|
|
764
785
|
const filenameMap = new Map();
|
|
765
786
|
for (const ref of refs) {
|
|
766
|
-
const filename = ref.split(
|
|
787
|
+
const filename = ref.split("/").pop();
|
|
767
788
|
if (filename) {
|
|
768
|
-
filenameMap.set(filename, ref);
|
|
789
|
+
filenameMap.set(filename.toLowerCase(), ref);
|
|
769
790
|
}
|
|
770
791
|
}
|
|
771
|
-
const
|
|
772
|
-
|
|
792
|
+
const uniqueFilenames = Array.from(new Set(refs.map((ref) => ref.split("/").pop()).filter((f) => !!f)));
|
|
793
|
+
const extractFilenameFromUrl = (url) => {
|
|
794
|
+
const urlWithoutQuery = url.split("?")[0];
|
|
795
|
+
const segments = urlWithoutQuery.split("/");
|
|
796
|
+
return segments[segments.length - 1].toLowerCase();
|
|
797
|
+
};
|
|
798
|
+
try {
|
|
799
|
+
// Query the Files API by exact filename in batches. Fetching the newest N
|
|
800
|
+
// files and matching in memory silently drops any image older than that
|
|
801
|
+
// window; a targeted `filename:` search resolves refs regardless of how
|
|
802
|
+
// many files the store has.
|
|
803
|
+
for (let i = 0; i < uniqueFilenames.length; i += ThemeContentService_1.MEDIA_QUERY_BATCH_SIZE) {
|
|
804
|
+
const batch = uniqueFilenames.slice(i, i + ThemeContentService_1.MEDIA_QUERY_BATCH_SIZE);
|
|
805
|
+
const searchQuery = this.buildFilenameSearchQuery("IMAGE", batch);
|
|
806
|
+
const query = `{
|
|
807
|
+
files(first: ${batch.length * 2}, query: ${JSON.stringify(searchQuery)}) {
|
|
773
808
|
nodes {
|
|
774
809
|
... on MediaImage {
|
|
775
810
|
id
|
|
@@ -782,94 +817,68 @@ let ThemeContentService = class ThemeContentService {
|
|
|
782
817
|
}
|
|
783
818
|
}
|
|
784
819
|
}`;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
body: JSON.stringify({ query }),
|
|
793
|
-
});
|
|
794
|
-
const result = (await response.json());
|
|
795
|
-
if (result.errors?.length) {
|
|
796
|
-
logger_1.logger.error('Shopify Files API errors for shop_images', {
|
|
797
|
-
errors: result.errors.map((e) => e.message),
|
|
820
|
+
const response = await fetch(`https://${storeDomain}/admin/api/2024-01/graphql.json`, {
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: {
|
|
823
|
+
"Content-Type": "application/json",
|
|
824
|
+
"X-Shopify-Access-Token": adminToken,
|
|
825
|
+
},
|
|
826
|
+
body: JSON.stringify({ query }),
|
|
798
827
|
});
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const segments = urlWithoutQuery.split('/');
|
|
805
|
-
return segments[segments.length - 1].toLowerCase();
|
|
806
|
-
};
|
|
807
|
-
// First pass: exact filename match
|
|
808
|
-
for (const file of files) {
|
|
809
|
-
const image = file.image;
|
|
810
|
-
if (!image?.url)
|
|
828
|
+
const result = (await response.json());
|
|
829
|
+
if (result.errors?.length) {
|
|
830
|
+
logger_1.logger.error("Shopify Files API errors for shop_images", {
|
|
831
|
+
errors: result.errors.map((e) => e.message),
|
|
832
|
+
});
|
|
811
833
|
continue;
|
|
812
|
-
const cdnFilename = extractFilenameFromUrl(image.url);
|
|
813
|
-
for (const [filename, ref] of filenameMap.entries()) {
|
|
814
|
-
const lookupFilename = filename.toLowerCase();
|
|
815
|
-
if (cdnFilename === lookupFilename) {
|
|
816
|
-
urlMap.set(ref, {
|
|
817
|
-
url: image.url,
|
|
818
|
-
width: image.width,
|
|
819
|
-
height: image.height,
|
|
820
|
-
});
|
|
821
|
-
filenameMap.delete(filename);
|
|
822
|
-
break;
|
|
823
|
-
}
|
|
824
834
|
}
|
|
825
|
-
|
|
826
|
-
break;
|
|
827
|
-
}
|
|
828
|
-
// Second pass: fuzzy match for remaining
|
|
829
|
-
if (filenameMap.size > 0) {
|
|
835
|
+
const files = result.data?.files?.nodes || [];
|
|
830
836
|
for (const file of files) {
|
|
831
837
|
const image = file.image;
|
|
832
838
|
if (!image?.url)
|
|
833
839
|
continue;
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
filenameMap.delete(filename);
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
840
|
+
const cdnFilename = extractFilenameFromUrl(image.url);
|
|
841
|
+
const ref = filenameMap.get(cdnFilename);
|
|
842
|
+
if (ref && !urlMap.has(ref)) {
|
|
843
|
+
urlMap.set(ref, {
|
|
844
|
+
url: image.url,
|
|
845
|
+
width: image.width,
|
|
846
|
+
height: image.height,
|
|
847
|
+
});
|
|
848
|
+
filenameMap.delete(cdnFilename);
|
|
846
849
|
}
|
|
847
|
-
if (filenameMap.size === 0)
|
|
848
|
-
break;
|
|
849
850
|
}
|
|
850
851
|
}
|
|
851
852
|
if (filenameMap.size > 0) {
|
|
852
|
-
logger_1.logger.warn(
|
|
853
|
+
logger_1.logger.warn("Unresolved shop_images", {
|
|
853
854
|
filenames: Array.from(filenameMap.keys()),
|
|
854
855
|
});
|
|
855
856
|
}
|
|
856
857
|
}
|
|
857
858
|
catch (error) {
|
|
858
|
-
logger_1.logger.error(
|
|
859
|
-
error: error instanceof Error ? error.message :
|
|
859
|
+
logger_1.logger.error("Failed to fetch shop_images from Files API", {
|
|
860
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
860
861
|
});
|
|
861
862
|
}
|
|
862
863
|
}
|
|
863
864
|
async resolveFilesApiMedia(refs, urlMap, adminToken, storeDomain) {
|
|
865
|
+
// Map lowercased filename (and its extension-less form) -> original ref.
|
|
864
866
|
const videoFilenameMap = new Map();
|
|
865
867
|
for (const ref of refs) {
|
|
866
|
-
const filename = ref.split(
|
|
868
|
+
const filename = ref.split("/").pop();
|
|
867
869
|
if (filename) {
|
|
868
|
-
videoFilenameMap.set(filename, ref);
|
|
870
|
+
videoFilenameMap.set(filename.toLowerCase(), ref);
|
|
869
871
|
}
|
|
870
872
|
}
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
+
const uniqueFilenames = Array.from(new Set(refs.map((ref) => ref.split("/").pop()).filter((f) => !!f)));
|
|
874
|
+
try {
|
|
875
|
+
// Query the Files API by exact filename in batches (see resolveShopImages
|
|
876
|
+
// for why the newest-N approach silently drops older media).
|
|
877
|
+
for (let i = 0; i < uniqueFilenames.length; i += ThemeContentService_1.MEDIA_QUERY_BATCH_SIZE) {
|
|
878
|
+
const batch = uniqueFilenames.slice(i, i + ThemeContentService_1.MEDIA_QUERY_BATCH_SIZE);
|
|
879
|
+
const searchQuery = this.buildFilenameSearchQuery("VIDEO", batch);
|
|
880
|
+
const query = `{
|
|
881
|
+
files(first: ${batch.length * 2}, query: ${JSON.stringify(searchQuery)}) {
|
|
873
882
|
nodes {
|
|
874
883
|
... on Video {
|
|
875
884
|
id
|
|
@@ -884,78 +893,85 @@ let ThemeContentService = class ThemeContentService {
|
|
|
884
893
|
}
|
|
885
894
|
}
|
|
886
895
|
}`;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
body: JSON.stringify({ query }),
|
|
895
|
-
});
|
|
896
|
-
const result = (await response.json());
|
|
897
|
-
if (result.errors?.length) {
|
|
898
|
-
logger_1.logger.error('Shopify Files API errors', {
|
|
899
|
-
errors: result.errors.map((e) => e.message),
|
|
896
|
+
const response = await fetch(`https://${storeDomain}/admin/api/2024-01/graphql.json`, {
|
|
897
|
+
method: "POST",
|
|
898
|
+
headers: {
|
|
899
|
+
"Content-Type": "application/json",
|
|
900
|
+
"X-Shopify-Access-Token": adminToken,
|
|
901
|
+
},
|
|
902
|
+
body: JSON.stringify({ query }),
|
|
900
903
|
});
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
continue;
|
|
907
|
-
const mp4Source = file.sources.find((s) => s.mimeType.includes('mp4'));
|
|
908
|
-
const selectedSource = mp4Source || file.sources[0];
|
|
909
|
-
if (!selectedSource?.url)
|
|
904
|
+
const result = (await response.json());
|
|
905
|
+
if (result.errors?.length) {
|
|
906
|
+
logger_1.logger.error("Shopify Files API errors", {
|
|
907
|
+
errors: result.errors.map((e) => e.message),
|
|
908
|
+
});
|
|
910
909
|
continue;
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (
|
|
910
|
+
}
|
|
911
|
+
const files = result.data?.files?.nodes || [];
|
|
912
|
+
for (const file of files) {
|
|
913
|
+
if (!file.sources || file.sources.length === 0)
|
|
914
|
+
continue;
|
|
915
|
+
const mp4Source = file.sources.find((s) => s.mimeType.includes("mp4"));
|
|
916
|
+
const selectedSource = mp4Source || file.sources[0];
|
|
917
|
+
if (!selectedSource?.url)
|
|
918
|
+
continue;
|
|
919
|
+
if (!file.filename)
|
|
920
|
+
continue;
|
|
921
|
+
const ref = videoFilenameMap.get(file.filename.toLowerCase());
|
|
922
|
+
if (ref && !urlMap.has(ref)) {
|
|
915
923
|
urlMap.set(ref, {
|
|
916
924
|
url: selectedSource.url,
|
|
917
925
|
width: selectedSource.width,
|
|
918
926
|
height: selectedSource.height,
|
|
919
927
|
});
|
|
920
|
-
videoFilenameMap.delete(filename);
|
|
921
|
-
break;
|
|
928
|
+
videoFilenameMap.delete(file.filename.toLowerCase());
|
|
922
929
|
}
|
|
923
930
|
}
|
|
924
|
-
if (videoFilenameMap.size === 0)
|
|
925
|
-
break;
|
|
926
931
|
}
|
|
927
932
|
if (videoFilenameMap.size > 0) {
|
|
928
|
-
logger_1.logger.warn(
|
|
933
|
+
logger_1.logger.warn("Unresolved videos", {
|
|
929
934
|
filenames: Array.from(videoFilenameMap.keys()),
|
|
930
935
|
});
|
|
931
936
|
}
|
|
932
937
|
}
|
|
933
938
|
catch (error) {
|
|
934
|
-
logger_1.logger.error(
|
|
935
|
-
error: error instanceof Error ? error.message :
|
|
939
|
+
logger_1.logger.error("Failed to fetch Files API media", {
|
|
940
|
+
error: error instanceof Error ? error.message : "Unknown",
|
|
936
941
|
});
|
|
937
942
|
}
|
|
938
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* Build a Shopify Files search query that matches any of the given filenames
|
|
946
|
+
* for a media type, e.g.
|
|
947
|
+
* media_type:IMAGE status:READY (filename:'a.png' OR filename:'b.jpg')
|
|
948
|
+
*/
|
|
949
|
+
buildFilenameSearchQuery(mediaType, filenames) {
|
|
950
|
+
const orClause = filenames
|
|
951
|
+
.map((f) => `filename:'${f.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`)
|
|
952
|
+
.join(" OR ");
|
|
953
|
+
return `media_type:${mediaType} status:READY (${orClause})`;
|
|
954
|
+
}
|
|
939
955
|
mediaFieldNames = new Set([
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
956
|
+
"image",
|
|
957
|
+
"image_web",
|
|
958
|
+
"image_mweb",
|
|
959
|
+
"image_mobile",
|
|
960
|
+
"video",
|
|
961
|
+
"video_web",
|
|
962
|
+
"video_mweb",
|
|
963
|
+
"video_mobile",
|
|
948
964
|
]);
|
|
949
965
|
isMediaUrl(value) {
|
|
950
|
-
return (value.startsWith(
|
|
951
|
-
value.startsWith(
|
|
952
|
-
value.startsWith(
|
|
966
|
+
return (value.startsWith("https://") ||
|
|
967
|
+
value.startsWith("http://") ||
|
|
968
|
+
value.startsWith("//"));
|
|
953
969
|
}
|
|
954
970
|
replaceMediaReferences(curated, urlMap) {
|
|
955
971
|
const replaceInObject = (obj) => {
|
|
956
972
|
const result = { ...obj };
|
|
957
973
|
for (const [key, value] of Object.entries(result)) {
|
|
958
|
-
if (typeof value ===
|
|
974
|
+
if (typeof value === "string") {
|
|
959
975
|
if (urlMap.has(value)) {
|
|
960
976
|
const media = urlMap.get(value);
|
|
961
977
|
result[key] = {
|
|
@@ -972,18 +988,18 @@ let ThemeContentService = class ThemeContentService {
|
|
|
972
988
|
};
|
|
973
989
|
}
|
|
974
990
|
else if (this.mediaFieldNames.has(key) &&
|
|
975
|
-
value.startsWith(
|
|
976
|
-
logger_1.logger.warn(
|
|
991
|
+
value.startsWith("shopify://")) {
|
|
992
|
+
logger_1.logger.warn("Unresolved shopify:// media reference", {
|
|
977
993
|
key,
|
|
978
994
|
value,
|
|
979
995
|
});
|
|
980
996
|
result[key] = null;
|
|
981
997
|
}
|
|
982
998
|
}
|
|
983
|
-
else if (typeof value ===
|
|
999
|
+
else if (typeof value === "object" && value !== null) {
|
|
984
1000
|
if (Array.isArray(value)) {
|
|
985
1001
|
result[key] = value.map((item) => {
|
|
986
|
-
if (typeof item ===
|
|
1002
|
+
if (typeof item === "object" && item !== null) {
|
|
987
1003
|
return replaceInObject(item);
|
|
988
1004
|
}
|
|
989
1005
|
return item;
|
|
@@ -1019,7 +1035,7 @@ let ThemeContentService = class ThemeContentService {
|
|
|
1019
1035
|
}
|
|
1020
1036
|
};
|
|
1021
1037
|
exports.ThemeContentService = ThemeContentService;
|
|
1022
|
-
exports.ThemeContentService = ThemeContentService = tslib_1.__decorate([
|
|
1038
|
+
exports.ThemeContentService = ThemeContentService = ThemeContentService_1 = tslib_1.__decorate([
|
|
1023
1039
|
(0, common_1.Injectable)(),
|
|
1024
1040
|
tslib_1.__param(3, (0, common_1.Optional)()),
|
|
1025
1041
|
tslib_1.__param(3, (0, common_1.Inject)(theme_content_interfaces_1.THEME_COLLECTION_SERVICE)),
|