@pvh-afl/core 1.0.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.
Files changed (154) hide show
  1. package/README.md +97 -0
  2. package/dist/cache/cache.module.d.ts +3 -0
  3. package/dist/cache/cache.module.d.ts.map +1 -0
  4. package/dist/cache/cache.module.js +17 -0
  5. package/dist/cache/cache.module.js.map +1 -0
  6. package/dist/cache/cache.service.d.ts +38 -0
  7. package/dist/cache/cache.service.d.ts.map +1 -0
  8. package/dist/cache/cache.service.js +173 -0
  9. package/dist/cache/cache.service.js.map +1 -0
  10. package/dist/cache/index.d.ts +3 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +6 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/common/guards/tenant.guard.d.ts +8 -0
  15. package/dist/common/guards/tenant.guard.d.ts.map +1 -0
  16. package/dist/common/guards/tenant.guard.js +44 -0
  17. package/dist/common/guards/tenant.guard.js.map +1 -0
  18. package/dist/common/index.d.ts +3 -0
  19. package/dist/common/index.d.ts.map +1 -0
  20. package/dist/common/index.js +6 -0
  21. package/dist/common/index.js.map +1 -0
  22. package/dist/common/logger.d.ts +7 -0
  23. package/dist/common/logger.d.ts.map +1 -0
  24. package/dist/common/logger.js +28 -0
  25. package/dist/common/logger.js.map +1 -0
  26. package/dist/config/config.module.d.ts +3 -0
  27. package/dist/config/config.module.d.ts.map +1 -0
  28. package/dist/config/config.module.js +17 -0
  29. package/dist/config/config.module.js.map +1 -0
  30. package/dist/config/config.service.d.ts +55 -0
  31. package/dist/config/config.service.d.ts.map +1 -0
  32. package/dist/config/config.service.js +212 -0
  33. package/dist/config/config.service.js.map +1 -0
  34. package/dist/config/index.d.ts +3 -0
  35. package/dist/config/index.d.ts.map +1 -0
  36. package/dist/config/index.js +6 -0
  37. package/dist/config/index.js.map +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +21 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/integrations/base-integration.service.d.ts +59 -0
  43. package/dist/integrations/base-integration.service.d.ts.map +1 -0
  44. package/dist/integrations/base-integration.service.js +227 -0
  45. package/dist/integrations/base-integration.service.js.map +1 -0
  46. package/dist/integrations/commerce/commerce.types.d.ts +301 -0
  47. package/dist/integrations/commerce/commerce.types.d.ts.map +1 -0
  48. package/dist/integrations/commerce/commerce.types.js +9 -0
  49. package/dist/integrations/commerce/commerce.types.js.map +1 -0
  50. package/dist/integrations/commerce/index.d.ts +4 -0
  51. package/dist/integrations/commerce/index.d.ts.map +1 -0
  52. package/dist/integrations/commerce/index.js +7 -0
  53. package/dist/integrations/commerce/index.js.map +1 -0
  54. package/dist/integrations/commerce/shopify-admin.service.d.ts +35 -0
  55. package/dist/integrations/commerce/shopify-admin.service.d.ts.map +1 -0
  56. package/dist/integrations/commerce/shopify-admin.service.js +269 -0
  57. package/dist/integrations/commerce/shopify-admin.service.js.map +1 -0
  58. package/dist/integrations/commerce/shopify.service.d.ts +43 -0
  59. package/dist/integrations/commerce/shopify.service.d.ts.map +1 -0
  60. package/dist/integrations/commerce/shopify.service.js +735 -0
  61. package/dist/integrations/commerce/shopify.service.js.map +1 -0
  62. package/dist/integrations/index.d.ts +9 -0
  63. package/dist/integrations/index.d.ts.map +1 -0
  64. package/dist/integrations/index.js +18 -0
  65. package/dist/integrations/index.js.map +1 -0
  66. package/dist/integrations/integration.factory.d.ts +23 -0
  67. package/dist/integrations/integration.factory.d.ts.map +1 -0
  68. package/dist/integrations/integration.factory.js +119 -0
  69. package/dist/integrations/integration.factory.js.map +1 -0
  70. package/dist/integrations/integration.types.d.ts +80 -0
  71. package/dist/integrations/integration.types.d.ts.map +1 -0
  72. package/dist/integrations/integration.types.js +9 -0
  73. package/dist/integrations/integration.types.js.map +1 -0
  74. package/dist/integrations/integrations.module.d.ts +3 -0
  75. package/dist/integrations/integrations.module.d.ts.map +1 -0
  76. package/dist/integrations/integrations.module.js +46 -0
  77. package/dist/integrations/integrations.module.js.map +1 -0
  78. package/dist/integrations/loyalty/capillary-auth.service.d.ts +30 -0
  79. package/dist/integrations/loyalty/capillary-auth.service.d.ts.map +1 -0
  80. package/dist/integrations/loyalty/capillary-auth.service.js +112 -0
  81. package/dist/integrations/loyalty/capillary-auth.service.js.map +1 -0
  82. package/dist/integrations/loyalty/capillary-coupon.service.d.ts +24 -0
  83. package/dist/integrations/loyalty/capillary-coupon.service.d.ts.map +1 -0
  84. package/dist/integrations/loyalty/capillary-coupon.service.js +383 -0
  85. package/dist/integrations/loyalty/capillary-coupon.service.js.map +1 -0
  86. package/dist/integrations/loyalty/capillary.types.d.ts +218 -0
  87. package/dist/integrations/loyalty/capillary.types.d.ts.map +1 -0
  88. package/dist/integrations/loyalty/capillary.types.js +21 -0
  89. package/dist/integrations/loyalty/capillary.types.js.map +1 -0
  90. package/dist/integrations/loyalty/index.d.ts +4 -0
  91. package/dist/integrations/loyalty/index.d.ts.map +1 -0
  92. package/dist/integrations/loyalty/index.js +7 -0
  93. package/dist/integrations/loyalty/index.js.map +1 -0
  94. package/dist/integrations/oms/index.d.ts +4 -0
  95. package/dist/integrations/oms/index.d.ts.map +1 -0
  96. package/dist/integrations/oms/index.js +7 -0
  97. package/dist/integrations/oms/index.js.map +1 -0
  98. package/dist/integrations/oms/omuni-auth.service.d.ts +29 -0
  99. package/dist/integrations/oms/omuni-auth.service.d.ts.map +1 -0
  100. package/dist/integrations/oms/omuni-auth.service.js +164 -0
  101. package/dist/integrations/oms/omuni-auth.service.js.map +1 -0
  102. package/dist/integrations/oms/omuni.service.d.ts +17 -0
  103. package/dist/integrations/oms/omuni.service.d.ts.map +1 -0
  104. package/dist/integrations/oms/omuni.service.js +128 -0
  105. package/dist/integrations/oms/omuni.service.js.map +1 -0
  106. package/dist/integrations/oms/omuni.types.d.ts +65 -0
  107. package/dist/integrations/oms/omuni.types.d.ts.map +1 -0
  108. package/dist/integrations/oms/omuni.types.js +3 -0
  109. package/dist/integrations/oms/omuni.types.js.map +1 -0
  110. package/dist/integrations/shopify-theme/index.d.ts +3 -0
  111. package/dist/integrations/shopify-theme/index.d.ts.map +1 -0
  112. package/dist/integrations/shopify-theme/index.js +6 -0
  113. package/dist/integrations/shopify-theme/index.js.map +1 -0
  114. package/dist/integrations/shopify-theme/shopify-theme.module.d.ts +3 -0
  115. package/dist/integrations/shopify-theme/shopify-theme.module.d.ts.map +1 -0
  116. package/dist/integrations/shopify-theme/shopify-theme.module.js +16 -0
  117. package/dist/integrations/shopify-theme/shopify-theme.module.js.map +1 -0
  118. package/dist/integrations/shopify-theme/shopify-theme.service.d.ts +35 -0
  119. package/dist/integrations/shopify-theme/shopify-theme.service.d.ts.map +1 -0
  120. package/dist/integrations/shopify-theme/shopify-theme.service.js +155 -0
  121. package/dist/integrations/shopify-theme/shopify-theme.service.js.map +1 -0
  122. package/dist/queue/index.d.ts +3 -0
  123. package/dist/queue/index.d.ts.map +1 -0
  124. package/dist/queue/index.js +8 -0
  125. package/dist/queue/index.js.map +1 -0
  126. package/dist/queue/queue.module.d.ts +3 -0
  127. package/dist/queue/queue.module.d.ts.map +1 -0
  128. package/dist/queue/queue.module.js +17 -0
  129. package/dist/queue/queue.module.js.map +1 -0
  130. package/dist/queue/queue.service.d.ts +20 -0
  131. package/dist/queue/queue.service.d.ts.map +1 -0
  132. package/dist/queue/queue.service.js +119 -0
  133. package/dist/queue/queue.service.js.map +1 -0
  134. package/dist/theme/header-content.service.d.ts +142 -0
  135. package/dist/theme/header-content.service.d.ts.map +1 -0
  136. package/dist/theme/header-content.service.js +626 -0
  137. package/dist/theme/header-content.service.js.map +1 -0
  138. package/dist/theme/index.d.ts +5 -0
  139. package/dist/theme/index.d.ts.map +1 -0
  140. package/dist/theme/index.js +17 -0
  141. package/dist/theme/index.js.map +1 -0
  142. package/dist/theme/theme-content.interfaces.d.ts +79 -0
  143. package/dist/theme/theme-content.interfaces.d.ts.map +1 -0
  144. package/dist/theme/theme-content.interfaces.js +14 -0
  145. package/dist/theme/theme-content.interfaces.js.map +1 -0
  146. package/dist/theme/theme-content.service.d.ts +125 -0
  147. package/dist/theme/theme-content.service.d.ts.map +1 -0
  148. package/dist/theme/theme-content.service.js +738 -0
  149. package/dist/theme/theme-content.service.js.map +1 -0
  150. package/dist/theme/theme.module.d.ts +46 -0
  151. package/dist/theme/theme.module.d.ts.map +1 -0
  152. package/dist/theme/theme.module.js +76 -0
  153. package/dist/theme/theme.module.js.map +1 -0
  154. package/package.json +62 -0
@@ -0,0 +1,738 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ThemeContentService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const cache_service_1 = require("../cache/cache.service");
7
+ const config_service_1 = require("../config/config.service");
8
+ const shopify_theme_1 = require("../integrations/shopify-theme");
9
+ const logger_1 = require("../common/logger");
10
+ const theme_content_interfaces_1 = require("./theme-content.interfaces");
11
+ let ThemeContentService = class ThemeContentService {
12
+ cacheService;
13
+ configService;
14
+ shopifyThemeService;
15
+ collectionService;
16
+ productService;
17
+ refreshInProgress = new Set();
18
+ constructor(cacheService, configService, shopifyThemeService, collectionService, productService) {
19
+ this.cacheService = cacheService;
20
+ this.configService = configService;
21
+ this.shopifyThemeService = shopifyThemeService;
22
+ this.collectionService = collectionService;
23
+ this.productService = productService;
24
+ }
25
+ /**
26
+ * Build cache key for theme content.
27
+ */
28
+ buildCacheKey(brand, path) {
29
+ return this.shopifyThemeService.buildCacheKey(brand, path);
30
+ }
31
+ /**
32
+ * Get theme content with stale-while-revalidate semantics.
33
+ */
34
+ async getContent(brand, path) {
35
+ const cacheKey = this.buildCacheKey(brand, path);
36
+ const cached = await this.cacheService.get(cacheKey);
37
+ let themeStructure = null;
38
+ let isStale = false;
39
+ let triggeredRefresh = false;
40
+ if (!cached) {
41
+ logger_1.logger.info('Theme content cache miss, fetching fresh data', {
42
+ brand,
43
+ path,
44
+ cacheKey,
45
+ });
46
+ themeStructure = await this.fetchAndCacheContent(brand, path);
47
+ }
48
+ else {
49
+ themeStructure = cached.data;
50
+ isStale = cached.isStale;
51
+ if (cached.isStale) {
52
+ logger_1.logger.info('Theme content is stale, triggering background refresh', {
53
+ brand,
54
+ path,
55
+ cacheKey,
56
+ });
57
+ this.refreshContentInBackground(brand, path, cacheKey);
58
+ triggeredRefresh = true;
59
+ }
60
+ }
61
+ if (!themeStructure) {
62
+ return { data: null, isStale: false, triggeredRefresh: false };
63
+ }
64
+ // Resolve collection and product references
65
+ let resolvedContent = await this.resolveCollectionReferences(brand, themeStructure);
66
+ resolvedContent = await this.resolveProductReferences(brand, resolvedContent);
67
+ return {
68
+ data: resolvedContent,
69
+ isStale,
70
+ triggeredRefresh,
71
+ };
72
+ }
73
+ /**
74
+ * Refresh content in the background (non-blocking)
75
+ */
76
+ async refreshContentInBackground(brand, path, cacheKey) {
77
+ if (this.refreshInProgress.has(cacheKey)) {
78
+ logger_1.logger.info('Refresh already in progress, skipping', { cacheKey });
79
+ return;
80
+ }
81
+ this.refreshInProgress.add(cacheKey);
82
+ try {
83
+ await this.fetchAndCacheContent(brand, path);
84
+ }
85
+ finally {
86
+ this.refreshInProgress.delete(cacheKey);
87
+ }
88
+ }
89
+ /**
90
+ * Fetch fresh content from Shopify and update cache.
91
+ */
92
+ async fetchAndCacheContent(brand, path, options) {
93
+ const cacheKey = this.buildCacheKey(brand, path);
94
+ logger_1.logger.info('Fetching fresh theme content', {
95
+ brand,
96
+ path,
97
+ cacheKey,
98
+ });
99
+ try {
100
+ const parsed = await this.shopifyThemeService.fetchThemeFile(brand, path);
101
+ if (!parsed) {
102
+ logger_1.logger.warn('No content found for theme file', { brand, path });
103
+ return null;
104
+ }
105
+ let curated = await this.curateThemeContent(brand, parsed);
106
+ curated = await this.resolveMediaReferences(brand, curated);
107
+ const { defaultTTL: configDefaultTTL, staleTTL: configStaleTTL } = this.configService.getLandingPageCacheTTL(brand);
108
+ const defaultTTL = options?.defaultTTL ?? configDefaultTTL;
109
+ const staleTTL = options?.staleTTL ?? configStaleTTL;
110
+ await this.cacheService.set(cacheKey, curated, defaultTTL, staleTTL);
111
+ logger_1.logger.info('Theme content cached', { brand, cacheKey });
112
+ return curated;
113
+ }
114
+ catch (error) {
115
+ logger_1.logger.error('Failed to fetch and curate theme content', {
116
+ brand,
117
+ path,
118
+ error: error instanceof Error ? error.message : 'Unknown',
119
+ });
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Curates parsed theme content
125
+ */
126
+ async curateThemeContent(brand, parsed) {
127
+ const { sections, order } = parsed;
128
+ if (!sections || !order) {
129
+ logger_1.logger.warn('Invalid theme content structure', {
130
+ hasSections: !!sections,
131
+ hasOrder: !!order,
132
+ });
133
+ return [];
134
+ }
135
+ const curatedSections = [];
136
+ for (const sectionKey of order) {
137
+ if (!sections[sectionKey])
138
+ continue;
139
+ const section = sections[sectionKey];
140
+ const curatedSection = {
141
+ type: section.type,
142
+ };
143
+ if (section.blocks && section.block_order) {
144
+ curatedSection.blocks = section.block_order
145
+ .filter((blockKey) => section.blocks?.[blockKey])
146
+ .map((blockKey) => {
147
+ const block = section.blocks[blockKey];
148
+ return this.curateBlock(block);
149
+ });
150
+ }
151
+ if (section.settings) {
152
+ curatedSection.settings = section.settings;
153
+ // Store collection reference for lazy resolution
154
+ const featuredCollection = section.settings.featured_collection;
155
+ if (featuredCollection) {
156
+ const productsCount = section.settings.products_count || 8;
157
+ curatedSection.collectionRef = {
158
+ handle: featuredCollection,
159
+ productsCount,
160
+ };
161
+ }
162
+ // Handle featured-collection section type
163
+ const collectionHandle = section.settings.collection;
164
+ if (section.type === 'featured-collection' && collectionHandle) {
165
+ const productsCount = section.settings.products_to_show || 8;
166
+ curatedSection.collectionRef = {
167
+ handle: collectionHandle,
168
+ productsCount,
169
+ };
170
+ }
171
+ // Store product reference for lazy resolution
172
+ const featuredProduct = section.settings.featured_product;
173
+ if (featuredProduct) {
174
+ curatedSection.productRef = {
175
+ handle: featuredProduct,
176
+ };
177
+ }
178
+ }
179
+ curatedSections.push(curatedSection);
180
+ }
181
+ return curatedSections;
182
+ }
183
+ /**
184
+ * Curates a single block, including nested blocks recursively.
185
+ */
186
+ curateBlock(block) {
187
+ const curatedBlock = {
188
+ type: block.type,
189
+ };
190
+ if (block.settings) {
191
+ curatedBlock.settings = block.settings;
192
+ }
193
+ if (block.blocks && block.block_order) {
194
+ curatedBlock.blocks = block.block_order
195
+ .filter((nestedBlockKey) => block.blocks?.[nestedBlockKey])
196
+ .map((nestedBlockKey) => {
197
+ const nestedBlock = block.blocks[nestedBlockKey];
198
+ return this.curateBlock(nestedBlock);
199
+ });
200
+ }
201
+ return curatedBlock;
202
+ }
203
+ /**
204
+ * Resolve collection references to actual products.
205
+ */
206
+ async resolveCollectionReferences(brand, content) {
207
+ if (!this.collectionService || !this.productService) {
208
+ logger_1.logger.warn('Collection/Product services not provided, skipping resolution');
209
+ return content;
210
+ }
211
+ const sectionsWithRefs = content
212
+ .map((section, index) => ({ section, index }))
213
+ .filter(({ section }) => section.collectionRef);
214
+ if (sectionsWithRefs.length === 0) {
215
+ return content;
216
+ }
217
+ // Fetch all collections in parallel
218
+ const collectionPromises = sectionsWithRefs.map(({ section }) => this.collectionService.getCollectionByHandle(brand, section.collectionRef.handle, section.collectionRef.productsCount).catch((error) => {
219
+ logger_1.logger.error('Failed to fetch collection for theme section', {
220
+ brand,
221
+ handle: section.collectionRef.handle,
222
+ error: error instanceof Error ? error.message : 'Unknown',
223
+ });
224
+ return null;
225
+ }));
226
+ const collections = await Promise.all(collectionPromises);
227
+ // Collect all unique product handles
228
+ const allProductHandles = new Set();
229
+ collections.forEach((collection) => {
230
+ collection?.productHandles?.forEach((handle) => allProductHandles.add(handle));
231
+ });
232
+ // Fetch all products in parallel
233
+ const productHandleArray = Array.from(allProductHandles);
234
+ const productPromises = productHandleArray.map((handle) => this.productService.getProductByHandle(brand, handle).catch((error) => {
235
+ logger_1.logger.error('Failed to fetch product for collection', {
236
+ brand,
237
+ handle,
238
+ error: error instanceof Error ? error.message : 'Unknown',
239
+ });
240
+ return null;
241
+ }));
242
+ const products = await Promise.all(productPromises);
243
+ // Build product lookup map
244
+ const productByHandle = new Map();
245
+ productHandleArray.forEach((handle, i) => {
246
+ const product = products[i];
247
+ if (product) {
248
+ productByHandle.set(handle, this.mapToCuratedProduct(product));
249
+ }
250
+ });
251
+ // Build a map of section index to resolved products
252
+ const productsMap = new Map();
253
+ sectionsWithRefs.forEach(({ index }, i) => {
254
+ const collection = collections[i];
255
+ if (collection?.productHandles) {
256
+ const resolvedProducts = collection.productHandles
257
+ .map((handle) => productByHandle.get(handle))
258
+ .filter((p) => p !== undefined);
259
+ if (resolvedProducts.length > 0) {
260
+ productsMap.set(index, resolvedProducts);
261
+ }
262
+ }
263
+ });
264
+ // Merge products into sections
265
+ return content.map((section, index) => {
266
+ const { collectionRef, ...sectionWithoutRef } = section;
267
+ const sectionProducts = productsMap.get(index);
268
+ if (sectionProducts && sectionProducts.length > 0) {
269
+ return {
270
+ ...sectionWithoutRef,
271
+ products: sectionProducts,
272
+ };
273
+ }
274
+ return sectionWithoutRef;
275
+ });
276
+ }
277
+ /**
278
+ * Resolve product references to actual product data.
279
+ */
280
+ async resolveProductReferences(brand, content) {
281
+ if (!this.productService) {
282
+ logger_1.logger.warn('Product service not provided, skipping resolution');
283
+ return content;
284
+ }
285
+ const sectionsWithRefs = content
286
+ .map((section, index) => ({ section, index }))
287
+ .filter(({ section }) => section.productRef);
288
+ if (sectionsWithRefs.length === 0) {
289
+ return content;
290
+ }
291
+ // Fetch all products in parallel
292
+ const productPromises = sectionsWithRefs.map(({ section }) => this.productService.getProductByHandle(brand, section.productRef.handle).catch((error) => {
293
+ logger_1.logger.error('Failed to fetch product for theme section', {
294
+ brand,
295
+ handle: section.productRef.handle,
296
+ error: error instanceof Error ? error.message : 'Unknown',
297
+ });
298
+ return null;
299
+ }));
300
+ const products = await Promise.all(productPromises);
301
+ // Build a map of section index to resolved product
302
+ const productMap = new Map();
303
+ sectionsWithRefs.forEach(({ index }, i) => {
304
+ const product = products[i];
305
+ if (product) {
306
+ productMap.set(index, this.mapToCuratedProduct(product));
307
+ }
308
+ });
309
+ // Merge product into sections
310
+ return content.map((section, index) => {
311
+ const { productRef, ...sectionWithoutRef } = section;
312
+ const product = productMap.get(index);
313
+ if (product) {
314
+ return {
315
+ ...sectionWithoutRef,
316
+ product,
317
+ };
318
+ }
319
+ return sectionWithoutRef;
320
+ });
321
+ }
322
+ /**
323
+ * Map ThemeProduct to CuratedProduct
324
+ */
325
+ mapToCuratedProduct(product) {
326
+ const firstVariantCompareAtPrice = product.variants?.[0]?.compareAtPrice;
327
+ const displayTag = this.computeDisplayTag(product.tags);
328
+ return {
329
+ id: product.id,
330
+ title: product.title,
331
+ handle: product.handle,
332
+ vendor: product.vendor,
333
+ productType: product.productType,
334
+ featuredImage: product.featuredImage
335
+ ? {
336
+ url: product.featuredImage.url,
337
+ altText: product.featuredImage.altText ?? undefined,
338
+ width: product.featuredImage.width,
339
+ height: product.featuredImage.height,
340
+ }
341
+ : undefined,
342
+ images: product.images.map((img) => ({
343
+ id: img.url,
344
+ url: img.url,
345
+ altText: img.altText ?? undefined,
346
+ width: img.width,
347
+ height: img.height,
348
+ })),
349
+ priceRange: product.priceRange,
350
+ compareAtPrice: firstVariantCompareAtPrice ?? null,
351
+ tags: product.tags,
352
+ displayTag,
353
+ };
354
+ }
355
+ /**
356
+ * Compute displayTag from product tags.
357
+ */
358
+ computeDisplayTag(tags) {
359
+ if (!tags || tags.length === 0) {
360
+ return null;
361
+ }
362
+ const priorityTags = [
363
+ 'new',
364
+ 'sale',
365
+ 'bestseller',
366
+ 'limited',
367
+ 'exclusive',
368
+ 'trending',
369
+ 'hot',
370
+ 'popular',
371
+ 'featured',
372
+ ];
373
+ const lowerTags = tags.map((tag) => tag.toLowerCase());
374
+ for (const priorityTag of priorityTags) {
375
+ const matchIndex = lowerTags.findIndex((tag) => tag.includes(priorityTag));
376
+ if (matchIndex !== -1) {
377
+ return tags[matchIndex];
378
+ }
379
+ }
380
+ return tags[0];
381
+ }
382
+ async resolveMediaReferences(brand, curated) {
383
+ const references = this.extractShopifyReferences(curated);
384
+ if (references.size === 0) {
385
+ return curated;
386
+ }
387
+ logger_1.logger.info('Resolving media references', {
388
+ brand,
389
+ count: references.size,
390
+ });
391
+ const urlMap = await this.fetchMediaUrls(brand, Array.from(references));
392
+ if (urlMap.size === 0) {
393
+ logger_1.logger.warn('No media URLs resolved');
394
+ }
395
+ return this.replaceMediaReferences(curated, urlMap);
396
+ }
397
+ extractShopifyReferences(curated) {
398
+ const references = new Set();
399
+ const shopifyPattern = /^shopify:\/\//;
400
+ const extractFromObject = (obj, skipKeys = []) => {
401
+ for (const [key, value] of Object.entries(obj)) {
402
+ if (skipKeys.includes(key))
403
+ continue;
404
+ if (typeof value === 'string' && shopifyPattern.test(value)) {
405
+ references.add(value);
406
+ }
407
+ else if (typeof value === 'object' && value !== null) {
408
+ if (Array.isArray(value)) {
409
+ value.forEach((item) => {
410
+ if (typeof item === 'object' && item !== null) {
411
+ extractFromObject(item, skipKeys);
412
+ }
413
+ });
414
+ }
415
+ else {
416
+ extractFromObject(value, skipKeys);
417
+ }
418
+ }
419
+ }
420
+ };
421
+ const extractFromBlocks = (blocks) => {
422
+ for (const block of blocks) {
423
+ if (block.settings) {
424
+ extractFromObject(block.settings);
425
+ }
426
+ if (block.blocks) {
427
+ extractFromBlocks(block.blocks);
428
+ }
429
+ }
430
+ };
431
+ for (const section of curated) {
432
+ if (section.settings) {
433
+ extractFromObject(section.settings);
434
+ }
435
+ if (section.blocks) {
436
+ extractFromBlocks(section.blocks);
437
+ }
438
+ }
439
+ return references;
440
+ }
441
+ async fetchMediaUrls(brand, references) {
442
+ const urlMap = new Map();
443
+ const adminToken = this.configService.getShopifyAdminToken(brand);
444
+ const storeDomain = this.configService.getShopifyStoreDomain(brand);
445
+ if (!adminToken || !storeDomain) {
446
+ logger_1.logger.warn('Missing Admin API configuration for media resolution', {
447
+ brand,
448
+ });
449
+ return urlMap;
450
+ }
451
+ const shopImageRefs = [];
452
+ const filesApiRefs = [];
453
+ for (const ref of references) {
454
+ if (ref.startsWith('shopify://shop_images/')) {
455
+ shopImageRefs.push(ref);
456
+ }
457
+ else if (ref.startsWith('shopify://files/')) {
458
+ filesApiRefs.push(ref);
459
+ }
460
+ }
461
+ if (shopImageRefs.length > 0) {
462
+ await this.resolveShopImages(shopImageRefs, urlMap, adminToken, storeDomain);
463
+ }
464
+ if (filesApiRefs.length > 0) {
465
+ await this.resolveFilesApiMedia(filesApiRefs, urlMap, adminToken, storeDomain);
466
+ }
467
+ return urlMap;
468
+ }
469
+ async resolveShopImages(refs, urlMap, adminToken, storeDomain) {
470
+ const filenameMap = new Map();
471
+ for (const ref of refs) {
472
+ const filename = ref.split('/').pop();
473
+ if (filename) {
474
+ filenameMap.set(filename, ref);
475
+ }
476
+ }
477
+ const query = `{
478
+ files(first: 250, query: "media_type:IMAGE status:READY", sortKey: CREATED_AT, reverse: true) {
479
+ nodes {
480
+ ... on MediaImage {
481
+ id
482
+ image {
483
+ url
484
+ width
485
+ height
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }`;
491
+ try {
492
+ const response = await fetch(`https://${storeDomain}/admin/api/2024-01/graphql.json`, {
493
+ method: 'POST',
494
+ headers: {
495
+ 'Content-Type': 'application/json',
496
+ 'X-Shopify-Access-Token': adminToken,
497
+ },
498
+ body: JSON.stringify({ query }),
499
+ });
500
+ const result = (await response.json());
501
+ if (result.errors?.length) {
502
+ logger_1.logger.error('Shopify Files API errors for shop_images', {
503
+ errors: result.errors.map((e) => e.message),
504
+ });
505
+ return;
506
+ }
507
+ const files = result.data?.files?.nodes || [];
508
+ const extractFilenameFromUrl = (url) => {
509
+ const urlWithoutQuery = url.split('?')[0];
510
+ const segments = urlWithoutQuery.split('/');
511
+ return segments[segments.length - 1].toLowerCase();
512
+ };
513
+ // First pass: exact filename match
514
+ for (const file of files) {
515
+ const image = file.image;
516
+ if (!image?.url)
517
+ continue;
518
+ const cdnFilename = extractFilenameFromUrl(image.url);
519
+ for (const [filename, ref] of filenameMap.entries()) {
520
+ const lookupFilename = filename.toLowerCase();
521
+ if (cdnFilename === lookupFilename) {
522
+ urlMap.set(ref, {
523
+ url: image.url,
524
+ width: image.width,
525
+ height: image.height,
526
+ });
527
+ filenameMap.delete(filename);
528
+ break;
529
+ }
530
+ }
531
+ if (filenameMap.size === 0)
532
+ break;
533
+ }
534
+ // Second pass: fuzzy match for remaining
535
+ if (filenameMap.size > 0) {
536
+ for (const file of files) {
537
+ const image = file.image;
538
+ if (!image?.url)
539
+ continue;
540
+ const cdnUrlLower = image.url.toLowerCase();
541
+ for (const [filename, ref] of filenameMap.entries()) {
542
+ const nameWithoutExt = filename.replace(/\.[^/.]+$/, '').toLowerCase();
543
+ if (cdnUrlLower.includes(nameWithoutExt)) {
544
+ urlMap.set(ref, {
545
+ url: image.url,
546
+ width: image.width,
547
+ height: image.height,
548
+ });
549
+ filenameMap.delete(filename);
550
+ break;
551
+ }
552
+ }
553
+ if (filenameMap.size === 0)
554
+ break;
555
+ }
556
+ }
557
+ if (filenameMap.size > 0) {
558
+ logger_1.logger.warn('Unresolved shop_images', {
559
+ filenames: Array.from(filenameMap.keys()),
560
+ });
561
+ }
562
+ }
563
+ catch (error) {
564
+ logger_1.logger.error('Failed to fetch shop_images from Files API', {
565
+ error: error instanceof Error ? error.message : 'Unknown',
566
+ });
567
+ }
568
+ }
569
+ async resolveFilesApiMedia(refs, urlMap, adminToken, storeDomain) {
570
+ const videoFilenameMap = new Map();
571
+ for (const ref of refs) {
572
+ const filename = ref.split('/').pop();
573
+ if (filename) {
574
+ videoFilenameMap.set(filename, ref);
575
+ }
576
+ }
577
+ const query = `{
578
+ files(first: 100, query: "media_type:VIDEO status:READY", sortKey: CREATED_AT, reverse: true) {
579
+ nodes {
580
+ ... on Video {
581
+ id
582
+ filename
583
+ sources {
584
+ url
585
+ mimeType
586
+ width
587
+ height
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }`;
593
+ try {
594
+ const response = await fetch(`https://${storeDomain}/admin/api/2024-01/graphql.json`, {
595
+ method: 'POST',
596
+ headers: {
597
+ 'Content-Type': 'application/json',
598
+ 'X-Shopify-Access-Token': adminToken,
599
+ },
600
+ body: JSON.stringify({ query }),
601
+ });
602
+ const result = (await response.json());
603
+ if (result.errors?.length) {
604
+ logger_1.logger.error('Shopify Files API errors', {
605
+ errors: result.errors.map((e) => e.message),
606
+ });
607
+ return;
608
+ }
609
+ const files = result.data?.files?.nodes || [];
610
+ for (const file of files) {
611
+ if (!file.sources || file.sources.length === 0)
612
+ continue;
613
+ const mp4Source = file.sources.find((s) => s.mimeType.includes('mp4'));
614
+ const selectedSource = mp4Source || file.sources[0];
615
+ if (!selectedSource?.url)
616
+ continue;
617
+ const videoIdentifier = (file.filename || selectedSource.url).toLowerCase();
618
+ for (const [filename, ref] of videoFilenameMap.entries()) {
619
+ const nameWithoutExt = filename.replace(/\.[^/.]+$/, '').toLowerCase();
620
+ if (videoIdentifier.includes(nameWithoutExt)) {
621
+ urlMap.set(ref, {
622
+ url: selectedSource.url,
623
+ width: selectedSource.width,
624
+ height: selectedSource.height,
625
+ });
626
+ videoFilenameMap.delete(filename);
627
+ break;
628
+ }
629
+ }
630
+ if (videoFilenameMap.size === 0)
631
+ break;
632
+ }
633
+ if (videoFilenameMap.size > 0) {
634
+ logger_1.logger.warn('Unresolved videos', {
635
+ filenames: Array.from(videoFilenameMap.keys()),
636
+ });
637
+ }
638
+ }
639
+ catch (error) {
640
+ logger_1.logger.error('Failed to fetch Files API media', {
641
+ error: error instanceof Error ? error.message : 'Unknown',
642
+ });
643
+ }
644
+ }
645
+ mediaFieldNames = new Set([
646
+ 'image',
647
+ 'image_web',
648
+ 'image_mweb',
649
+ 'image_mobile',
650
+ 'video',
651
+ 'video_web',
652
+ 'video_mweb',
653
+ 'video_mobile',
654
+ ]);
655
+ isMediaUrl(value) {
656
+ return (value.startsWith('https://') ||
657
+ value.startsWith('http://') ||
658
+ value.startsWith('//'));
659
+ }
660
+ replaceMediaReferences(curated, urlMap) {
661
+ const replaceInObject = (obj) => {
662
+ const result = { ...obj };
663
+ for (const [key, value] of Object.entries(result)) {
664
+ if (typeof value === 'string') {
665
+ if (urlMap.has(value)) {
666
+ const media = urlMap.get(value);
667
+ result[key] = {
668
+ url: media.url,
669
+ width: media.width,
670
+ height: media.height,
671
+ };
672
+ }
673
+ else if (this.mediaFieldNames.has(key) && this.isMediaUrl(value)) {
674
+ result[key] = {
675
+ url: value,
676
+ width: null,
677
+ height: null,
678
+ };
679
+ }
680
+ else if (this.mediaFieldNames.has(key) &&
681
+ value.startsWith('shopify://')) {
682
+ logger_1.logger.warn('Unresolved shopify:// media reference', {
683
+ key,
684
+ value,
685
+ });
686
+ result[key] = null;
687
+ }
688
+ }
689
+ else if (typeof value === 'object' && value !== null) {
690
+ if (Array.isArray(value)) {
691
+ result[key] = value.map((item) => {
692
+ if (typeof item === 'object' && item !== null) {
693
+ return replaceInObject(item);
694
+ }
695
+ return item;
696
+ });
697
+ }
698
+ else {
699
+ result[key] = replaceInObject(value);
700
+ }
701
+ }
702
+ }
703
+ return result;
704
+ };
705
+ const processBlock = (block) => {
706
+ const newBlock = { ...block };
707
+ if (newBlock.settings) {
708
+ newBlock.settings = replaceInObject(newBlock.settings);
709
+ }
710
+ if (newBlock.blocks) {
711
+ newBlock.blocks = newBlock.blocks.map(processBlock);
712
+ }
713
+ return newBlock;
714
+ };
715
+ return curated.map((section) => {
716
+ const newSection = { ...section };
717
+ if (newSection.settings) {
718
+ newSection.settings = replaceInObject(newSection.settings);
719
+ }
720
+ if (newSection.blocks) {
721
+ newSection.blocks = newSection.blocks.map(processBlock);
722
+ }
723
+ return newSection;
724
+ });
725
+ }
726
+ };
727
+ exports.ThemeContentService = ThemeContentService;
728
+ exports.ThemeContentService = ThemeContentService = tslib_1.__decorate([
729
+ (0, common_1.Injectable)(),
730
+ tslib_1.__param(3, (0, common_1.Optional)()),
731
+ tslib_1.__param(3, (0, common_1.Inject)(theme_content_interfaces_1.THEME_COLLECTION_SERVICE)),
732
+ tslib_1.__param(4, (0, common_1.Optional)()),
733
+ tslib_1.__param(4, (0, common_1.Inject)(theme_content_interfaces_1.THEME_PRODUCT_SERVICE)),
734
+ tslib_1.__metadata("design:paramtypes", [cache_service_1.CacheService,
735
+ config_service_1.ConfigService,
736
+ shopify_theme_1.ShopifyThemeService, Object, Object])
737
+ ], ThemeContentService);
738
+ //# sourceMappingURL=theme-content.service.js.map