@adobe/spacecat-shared-tokowaka-client 1.1.0 → 1.2.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/CHANGELOG.md +7 -0
- package/README.md +144 -19
- package/package.json +1 -1
- package/src/index.d.ts +81 -15
- package/src/index.js +314 -260
- package/src/mappers/base-mapper.js +1 -1
- package/src/mappers/faq-mapper.js +18 -23
- package/src/utils/custom-html-utils.js +1 -0
- package/src/utils/patch-utils.js +12 -25
- package/src/utils/s3-utils.js +100 -7
- package/test/index.test.js +808 -990
- package/test/mappers/base-mapper.test.js +72 -88
- package/test/mappers/faq-mapper.test.js +61 -97
- package/test/utils/html-utils.test.js +10 -12
- package/test/utils/patch-utils.test.js +204 -235
- package/test/utils/s3-utils.test.js +140 -0
- package/test/utils/site-utils.test.js +80 -0
- package/test/utils/suggestion-utils.test.js +187 -0
|
@@ -108,7 +108,7 @@ export default class BaseOpportunityMapper {
|
|
|
108
108
|
*/
|
|
109
109
|
// eslint-disable-next-line no-unused-vars
|
|
110
110
|
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
111
|
-
if (!config || !config.
|
|
111
|
+
if (!config || !config.patches) {
|
|
112
112
|
return config;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -205,41 +205,36 @@ export default class FaqMapper extends BaseOpportunityMapper {
|
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* Removes patches from configuration for FAQ suggestions
|
|
208
|
-
* FAQ-specific logic: Also removes the heading patch when no FAQ suggestions remain
|
|
208
|
+
* FAQ-specific logic: Also removes the heading patch when no FAQ suggestions remain
|
|
209
209
|
* @param {Object} config - Current Tokowaka configuration
|
|
210
210
|
* @param {Array<string>} suggestionIds - Suggestion IDs to remove
|
|
211
211
|
* @param {string} opportunityId - Opportunity ID
|
|
212
212
|
* @returns {Object} - Updated configuration with patches removed
|
|
213
213
|
*/
|
|
214
214
|
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
215
|
-
if (!config || !config.
|
|
215
|
+
if (!config || !config.patches) {
|
|
216
216
|
return config;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
const suggestionIdsSet = new Set(suggestionIds);
|
|
220
220
|
const additionalPatchKeys = [];
|
|
221
221
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
additionalPatchKeys.push(`${urlPath}:${opportunityId}`);
|
|
239
|
-
} else {
|
|
240
|
-
this.log.debug(`${remainingSuggestionIds.length} FAQ suggestions remain for ${urlPath}, keeping heading patch`);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
222
|
+
// Find FAQ patches for this opportunity
|
|
223
|
+
const opportunityPatches = config.patches.filter((p) => p.opportunityId === opportunityId);
|
|
224
|
+
|
|
225
|
+
// Get FAQ suggestion IDs that will remain after rollback
|
|
226
|
+
const remainingSuggestionIds = opportunityPatches
|
|
227
|
+
.filter((p) => p.suggestionId && !suggestionIdsSet.has(p.suggestionId))
|
|
228
|
+
.map((p) => p.suggestionId);
|
|
229
|
+
|
|
230
|
+
// If no FAQ suggestions remain, remove the heading patch too
|
|
231
|
+
if (remainingSuggestionIds.length === 0) {
|
|
232
|
+
this.log.debug('No remaining FAQ suggestions, marking heading patch for removal');
|
|
233
|
+
// Add heading patch key (opportunityId only, no suggestionId)
|
|
234
|
+
additionalPatchKeys.push(opportunityId);
|
|
235
|
+
} else {
|
|
236
|
+
this.log.debug(`${remainingSuggestionIds.length} FAQ suggestions remain, keeping heading patch`);
|
|
237
|
+
}
|
|
243
238
|
|
|
244
239
|
// Remove FAQ suggestion patches and any orphaned heading patches
|
|
245
240
|
this.log.debug(
|
package/src/utils/patch-utils.js
CHANGED
|
@@ -67,50 +67,37 @@ export function mergePatches(existingPatches, newPatches) {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Removes patches matching the given suggestion IDs from a config
|
|
70
|
+
* Works with flat config structure
|
|
70
71
|
* @param {Object} config - Tokowaka configuration object
|
|
71
72
|
* @param {Array<string>} suggestionIds - Array of suggestion IDs to remove
|
|
72
73
|
* @param {Array<string>} additionalPatchKeys - Optional array of additional patch keys to remove
|
|
73
74
|
* @returns {Object} - Updated configuration with patches removed
|
|
74
75
|
*/
|
|
75
76
|
export function removePatchesBySuggestionIds(config, suggestionIds, additionalPatchKeys = []) {
|
|
76
|
-
if (!config || !config.
|
|
77
|
+
if (!config || !config.patches) {
|
|
77
78
|
return config;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const suggestionIdSet = new Set(suggestionIds);
|
|
81
82
|
const patchKeysToRemove = new Set(additionalPatchKeys);
|
|
82
|
-
const updatedOptimizations = {};
|
|
83
83
|
let removedCount = 0;
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
const
|
|
85
|
+
// Filter out patches with matching suggestionIds or additional patch keys
|
|
86
|
+
const filteredPatches = config.patches.filter((patch) => {
|
|
87
|
+
const shouldRemoveBySuggestionId = suggestionIdSet.has(patch.suggestionId);
|
|
88
|
+
const patchKey = getPatchKey(patch);
|
|
89
|
+
const shouldRemoveByPatchKey = patchKeysToRemove.has(patchKey);
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const patchKey = `${urlPath}:${getPatchKey(patch)}`;
|
|
93
|
-
const shouldRemoveByPatchKey = patchKeysToRemove.has(patchKey);
|
|
94
|
-
|
|
95
|
-
if (shouldRemoveBySuggestionId || shouldRemoveByPatchKey) {
|
|
96
|
-
removedCount += 1;
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return true;
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Only keep URL paths that still have patches
|
|
103
|
-
if (filteredPatches.length > 0) {
|
|
104
|
-
updatedOptimizations[urlPath] = {
|
|
105
|
-
...optimization,
|
|
106
|
-
patches: filteredPatches,
|
|
107
|
-
};
|
|
91
|
+
if (shouldRemoveBySuggestionId || shouldRemoveByPatchKey) {
|
|
92
|
+
removedCount += 1;
|
|
93
|
+
return false;
|
|
108
94
|
}
|
|
95
|
+
return true;
|
|
109
96
|
});
|
|
110
97
|
|
|
111
98
|
return {
|
|
112
99
|
...config,
|
|
113
|
-
|
|
100
|
+
patches: filteredPatches,
|
|
114
101
|
removedCount,
|
|
115
102
|
};
|
|
116
103
|
}
|
package/src/utils/s3-utils.js
CHANGED
|
@@ -11,14 +11,107 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Normalizes a URL pathname for S3 storage
|
|
15
|
+
* - Removes trailing slash (except for root '/')
|
|
16
|
+
* - Ensures starts with '/'
|
|
17
|
+
* @param {string} pathname - URL pathname
|
|
18
|
+
* @returns {string} - Normalized pathname
|
|
19
|
+
*/
|
|
20
|
+
export function normalizePath(pathname) {
|
|
21
|
+
let normalized = pathname.endsWith('/') && pathname !== '/' ? pathname.slice(0, -1) : pathname;
|
|
22
|
+
|
|
23
|
+
if (!normalized.startsWith('/')) {
|
|
24
|
+
normalized = `/${normalized}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extracts and normalizes hostname from URL
|
|
32
|
+
* - Strips 'www.' prefix
|
|
33
|
+
* @param {URL} url - URL object
|
|
34
|
+
* @param {Object} logger - Logger instance
|
|
35
|
+
* @returns {string} - Normalized hostname
|
|
36
|
+
* @throws {Error} - If hostname extraction fails
|
|
37
|
+
*/
|
|
38
|
+
export function getHostName(url, logger) {
|
|
39
|
+
try {
|
|
40
|
+
const finalHostname = url.hostname.replace(/^www\./, '');
|
|
41
|
+
return finalHostname;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error(`Error extracting host name: ${error.message}`);
|
|
44
|
+
throw new Error(`Error extracting host name: ${url.toString()}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Base64 URL encodes a string (RFC 4648)
|
|
50
|
+
* - Uses URL-safe characters (- instead of +, _ instead of /)
|
|
51
|
+
* - Removes padding (=)
|
|
52
|
+
* @param {string} input - String to encode
|
|
53
|
+
* @returns {string} - Base64 URL encoded string
|
|
54
|
+
*/
|
|
55
|
+
export function base64UrlEncode(input) {
|
|
56
|
+
// Encode to UTF-8 bytes
|
|
57
|
+
const bytes = new TextEncoder().encode(input);
|
|
58
|
+
// Convert bytes → binary string
|
|
59
|
+
let binary = '';
|
|
60
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
61
|
+
binary += String.fromCharCode(bytes[i]);
|
|
62
|
+
}
|
|
63
|
+
// Standard base64
|
|
64
|
+
const base64 = btoa(binary);
|
|
65
|
+
// Convert to base64url (RFC 4648)
|
|
66
|
+
return base64
|
|
67
|
+
.replace(/\+/g, '-') // + → -
|
|
68
|
+
.replace(/\//g, '_') // / → _
|
|
69
|
+
.replace(/=+$/, ''); // remove padding
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generates S3 path for Tokowaka configuration based on URL
|
|
74
|
+
* @param {string} url - Full URL (e.g., 'https://www.example.com/products/item')
|
|
75
|
+
* @param {Object} logger - Logger instance
|
|
16
76
|
* @param {boolean} isPreview - Whether this is a preview path
|
|
17
|
-
* @returns {string} - S3 path
|
|
77
|
+
* @returns {string} - S3 path (e.g., 'opportunities/example.com/L3Byb2R1Y3RzL2l0ZW0')
|
|
78
|
+
* @throws {Error} - If URL parsing fails
|
|
18
79
|
*/
|
|
19
|
-
export function getTokowakaConfigS3Path(
|
|
20
|
-
|
|
21
|
-
|
|
80
|
+
export function getTokowakaConfigS3Path(url, logger, isPreview = false) {
|
|
81
|
+
try {
|
|
82
|
+
const urlObj = new URL(url);
|
|
83
|
+
let path = urlObj.pathname;
|
|
84
|
+
|
|
85
|
+
path = normalizePath(path);
|
|
86
|
+
path = base64UrlEncode(path);
|
|
87
|
+
|
|
88
|
+
const normalizedHostName = getHostName(urlObj, logger);
|
|
89
|
+
const prefix = isPreview ? 'preview/opportunities' : 'opportunities';
|
|
90
|
+
|
|
91
|
+
return `${prefix}/${normalizedHostName}/${path}`;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error(`Error generating S3 path for URL ${url}: ${error.message}`);
|
|
94
|
+
throw new Error(`Failed to generate S3 path: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generates S3 path for domain-level metaconfig
|
|
100
|
+
* @param {string} url - Full URL (used to extract domain)
|
|
101
|
+
* @param {Object} logger - Logger instance
|
|
102
|
+
* @param {boolean} isPreview - Whether this is a preview path
|
|
103
|
+
* @returns {string} - S3 path for metaconfig (e.g., 'opportunities/example.com/config')
|
|
104
|
+
* @throws {Error} - If URL parsing fails
|
|
105
|
+
*/
|
|
106
|
+
export function getTokowakaMetaconfigS3Path(url, logger, isPreview = false) {
|
|
107
|
+
try {
|
|
108
|
+
const urlObj = new URL(url);
|
|
109
|
+
const normalizedHostName = getHostName(urlObj, logger);
|
|
110
|
+
const prefix = isPreview ? 'preview/opportunities' : 'opportunities';
|
|
111
|
+
|
|
112
|
+
return `${prefix}/${normalizedHostName}/config`;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.error(`Error generating metaconfig S3 path for URL ${url}: ${error.message}`);
|
|
115
|
+
throw new Error(`Failed to generate metaconfig S3 path: ${error.message}`);
|
|
22
116
|
}
|
|
23
|
-
return `opportunities/${apiKey}`;
|
|
24
117
|
}
|