@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.
- package/README.md +97 -0
- package/dist/cache/cache.module.d.ts +3 -0
- package/dist/cache/cache.module.d.ts.map +1 -0
- package/dist/cache/cache.module.js +17 -0
- package/dist/cache/cache.module.js.map +1 -0
- package/dist/cache/cache.service.d.ts +38 -0
- package/dist/cache/cache.service.d.ts.map +1 -0
- package/dist/cache/cache.service.js +173 -0
- package/dist/cache/cache.service.js.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +6 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/common/guards/tenant.guard.d.ts +8 -0
- package/dist/common/guards/tenant.guard.d.ts.map +1 -0
- package/dist/common/guards/tenant.guard.js +44 -0
- package/dist/common/guards/tenant.guard.js.map +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +6 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/logger.d.ts +7 -0
- package/dist/common/logger.d.ts.map +1 -0
- package/dist/common/logger.js +28 -0
- package/dist/common/logger.js.map +1 -0
- package/dist/config/config.module.d.ts +3 -0
- package/dist/config/config.module.d.ts.map +1 -0
- package/dist/config/config.module.js +17 -0
- package/dist/config/config.module.js.map +1 -0
- package/dist/config/config.service.d.ts +55 -0
- package/dist/config/config.service.d.ts.map +1 -0
- package/dist/config/config.service.js +212 -0
- package/dist/config/config.service.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/base-integration.service.d.ts +59 -0
- package/dist/integrations/base-integration.service.d.ts.map +1 -0
- package/dist/integrations/base-integration.service.js +227 -0
- package/dist/integrations/base-integration.service.js.map +1 -0
- package/dist/integrations/commerce/commerce.types.d.ts +301 -0
- package/dist/integrations/commerce/commerce.types.d.ts.map +1 -0
- package/dist/integrations/commerce/commerce.types.js +9 -0
- package/dist/integrations/commerce/commerce.types.js.map +1 -0
- package/dist/integrations/commerce/index.d.ts +4 -0
- package/dist/integrations/commerce/index.d.ts.map +1 -0
- package/dist/integrations/commerce/index.js +7 -0
- package/dist/integrations/commerce/index.js.map +1 -0
- package/dist/integrations/commerce/shopify-admin.service.d.ts +35 -0
- package/dist/integrations/commerce/shopify-admin.service.d.ts.map +1 -0
- package/dist/integrations/commerce/shopify-admin.service.js +269 -0
- package/dist/integrations/commerce/shopify-admin.service.js.map +1 -0
- package/dist/integrations/commerce/shopify.service.d.ts +43 -0
- package/dist/integrations/commerce/shopify.service.d.ts.map +1 -0
- package/dist/integrations/commerce/shopify.service.js +735 -0
- package/dist/integrations/commerce/shopify.service.js.map +1 -0
- package/dist/integrations/index.d.ts +9 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +18 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/integration.factory.d.ts +23 -0
- package/dist/integrations/integration.factory.d.ts.map +1 -0
- package/dist/integrations/integration.factory.js +119 -0
- package/dist/integrations/integration.factory.js.map +1 -0
- package/dist/integrations/integration.types.d.ts +80 -0
- package/dist/integrations/integration.types.d.ts.map +1 -0
- package/dist/integrations/integration.types.js +9 -0
- package/dist/integrations/integration.types.js.map +1 -0
- package/dist/integrations/integrations.module.d.ts +3 -0
- package/dist/integrations/integrations.module.d.ts.map +1 -0
- package/dist/integrations/integrations.module.js +46 -0
- package/dist/integrations/integrations.module.js.map +1 -0
- package/dist/integrations/loyalty/capillary-auth.service.d.ts +30 -0
- package/dist/integrations/loyalty/capillary-auth.service.d.ts.map +1 -0
- package/dist/integrations/loyalty/capillary-auth.service.js +112 -0
- package/dist/integrations/loyalty/capillary-auth.service.js.map +1 -0
- package/dist/integrations/loyalty/capillary-coupon.service.d.ts +24 -0
- package/dist/integrations/loyalty/capillary-coupon.service.d.ts.map +1 -0
- package/dist/integrations/loyalty/capillary-coupon.service.js +383 -0
- package/dist/integrations/loyalty/capillary-coupon.service.js.map +1 -0
- package/dist/integrations/loyalty/capillary.types.d.ts +218 -0
- package/dist/integrations/loyalty/capillary.types.d.ts.map +1 -0
- package/dist/integrations/loyalty/capillary.types.js +21 -0
- package/dist/integrations/loyalty/capillary.types.js.map +1 -0
- package/dist/integrations/loyalty/index.d.ts +4 -0
- package/dist/integrations/loyalty/index.d.ts.map +1 -0
- package/dist/integrations/loyalty/index.js +7 -0
- package/dist/integrations/loyalty/index.js.map +1 -0
- package/dist/integrations/oms/index.d.ts +4 -0
- package/dist/integrations/oms/index.d.ts.map +1 -0
- package/dist/integrations/oms/index.js +7 -0
- package/dist/integrations/oms/index.js.map +1 -0
- package/dist/integrations/oms/omuni-auth.service.d.ts +29 -0
- package/dist/integrations/oms/omuni-auth.service.d.ts.map +1 -0
- package/dist/integrations/oms/omuni-auth.service.js +164 -0
- package/dist/integrations/oms/omuni-auth.service.js.map +1 -0
- package/dist/integrations/oms/omuni.service.d.ts +17 -0
- package/dist/integrations/oms/omuni.service.d.ts.map +1 -0
- package/dist/integrations/oms/omuni.service.js +128 -0
- package/dist/integrations/oms/omuni.service.js.map +1 -0
- package/dist/integrations/oms/omuni.types.d.ts +65 -0
- package/dist/integrations/oms/omuni.types.d.ts.map +1 -0
- package/dist/integrations/oms/omuni.types.js +3 -0
- package/dist/integrations/oms/omuni.types.js.map +1 -0
- package/dist/integrations/shopify-theme/index.d.ts +3 -0
- package/dist/integrations/shopify-theme/index.d.ts.map +1 -0
- package/dist/integrations/shopify-theme/index.js +6 -0
- package/dist/integrations/shopify-theme/index.js.map +1 -0
- package/dist/integrations/shopify-theme/shopify-theme.module.d.ts +3 -0
- package/dist/integrations/shopify-theme/shopify-theme.module.d.ts.map +1 -0
- package/dist/integrations/shopify-theme/shopify-theme.module.js +16 -0
- package/dist/integrations/shopify-theme/shopify-theme.module.js.map +1 -0
- package/dist/integrations/shopify-theme/shopify-theme.service.d.ts +35 -0
- package/dist/integrations/shopify-theme/shopify-theme.service.d.ts.map +1 -0
- package/dist/integrations/shopify-theme/shopify-theme.service.js +155 -0
- package/dist/integrations/shopify-theme/shopify-theme.service.js.map +1 -0
- package/dist/queue/index.d.ts +3 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +8 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.module.d.ts +3 -0
- package/dist/queue/queue.module.d.ts.map +1 -0
- package/dist/queue/queue.module.js +17 -0
- package/dist/queue/queue.module.js.map +1 -0
- package/dist/queue/queue.service.d.ts +20 -0
- package/dist/queue/queue.service.d.ts.map +1 -0
- package/dist/queue/queue.service.js +119 -0
- package/dist/queue/queue.service.js.map +1 -0
- package/dist/theme/header-content.service.d.ts +142 -0
- package/dist/theme/header-content.service.d.ts.map +1 -0
- package/dist/theme/header-content.service.js +626 -0
- package/dist/theme/header-content.service.js.map +1 -0
- package/dist/theme/index.d.ts +5 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +17 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/theme-content.interfaces.d.ts +79 -0
- package/dist/theme/theme-content.interfaces.d.ts.map +1 -0
- package/dist/theme/theme-content.interfaces.js +14 -0
- package/dist/theme/theme-content.interfaces.js.map +1 -0
- package/dist/theme/theme-content.service.d.ts +125 -0
- package/dist/theme/theme-content.service.d.ts.map +1 -0
- package/dist/theme/theme-content.service.js +738 -0
- package/dist/theme/theme-content.service.js.map +1 -0
- package/dist/theme/theme.module.d.ts +46 -0
- package/dist/theme/theme.module.d.ts.map +1 -0
- package/dist/theme/theme.module.js +76 -0
- package/dist/theme/theme.module.js.map +1 -0
- 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
|