@adobe/spacecat-shared-tokowaka-client 1.0.5 → 1.1.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 +14 -0
- package/README.md +16 -1
- package/package.json +3 -3
- package/src/index.js +106 -0
- package/src/mappers/base-mapper.js +22 -0
- package/src/mappers/faq-mapper.js +48 -0
- package/src/utils/patch-utils.js +51 -1
- package/test/index.test.js +488 -0
- package/test/mappers/base-mapper.test.js +159 -0
- package/test/mappers/faq-mapper.test.js +140 -0
- package/test/utils/patch-utils.test.js +293 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.1.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.0.6...@adobe/spacecat-shared-tokowaka-client-v1.1.0) (2025-11-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* edge rollback feature for tokowaka ([#1167](https://github.com/adobe/spacecat-shared/issues/1167)) ([b47fd05](https://github.com/adobe/spacecat-shared/commit/b47fd0518ae1fe220914bfd88810383a306775fd))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.0.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.0.5...@adobe/spacecat-shared-tokowaka-client-v1.0.6) (2025-11-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** update external fixes ([#1162](https://github.com/adobe/spacecat-shared/issues/1162)) ([f0152c5](https://github.com/adobe/spacecat-shared/commit/f0152c5ecddb75b6b3c6e2f0d756d5fb04171dd3))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-tokowaka-client-v1.0.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.0.4...@adobe/spacecat-shared-tokowaka-client-v1.0.5) (2025-11-21)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -51,6 +51,22 @@ Generates configuration and uploads to S3. **Automatically fetches existing conf
|
|
|
51
51
|
- `succeededSuggestions` - Array of deployed suggestions
|
|
52
52
|
- `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
|
|
53
53
|
|
|
54
|
+
#### `rollbackSuggestions(site, opportunity, suggestions)`
|
|
55
|
+
|
|
56
|
+
Rolls back previously deployed suggestions by removing their patches from the configuration. **Automatically fetches existing configuration** and removes patches matching the provided suggestions. Invalidates CDN cache after upload.
|
|
57
|
+
|
|
58
|
+
**Mapper-Specific Rollback Behavior:**
|
|
59
|
+
- Each opportunity mapper handles its own rollback logic via `rollbackPatches()` method
|
|
60
|
+
- **FAQ:** Automatically removes the "FAQs" heading patch if no FAQ suggestions remain for that URL
|
|
61
|
+
- **Headings/Summarization:** Simple removal by suggestion ID (default behavior)
|
|
62
|
+
|
|
63
|
+
**Returns:** `Promise<RollbackResult>` with:
|
|
64
|
+
- `s3Path` - S3 key where config was uploaded
|
|
65
|
+
- `cdnInvalidation` - CDN invalidation result (or error)
|
|
66
|
+
- `succeededSuggestions` - Array of rolled back suggestions
|
|
67
|
+
- `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
|
|
68
|
+
- `removedPatchesCount` - Number of patches removed from the configuration
|
|
69
|
+
|
|
54
70
|
#### `fetchConfig(apiKey)`
|
|
55
71
|
|
|
56
72
|
Fetches existing Tokowaka configuration from S3.
|
|
@@ -94,7 +110,6 @@ s3://{TOKOWAKA_SITE_CONFIG_BUCKET}/opportunities/{tokowakaApiKey}
|
|
|
94
110
|
|
|
95
111
|
**Note:** The configuration is stored as a JSON file containing the complete Tokowaka optimization config for the site.
|
|
96
112
|
|
|
97
|
-
|
|
98
113
|
## Reference Material
|
|
99
114
|
|
|
100
115
|
https://wiki.corp.adobe.com/display/AEMSites/Tokowaka+-+Spacecat+Integration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-tokowaka-client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Tokowaka Client for SpaceCat - Edge optimization config management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@adobe/spacecat-shared-utils": "1.66.1",
|
|
38
|
-
"@aws-sdk/client-cloudfront": "3.
|
|
39
|
-
"@aws-sdk/client-s3": "3.
|
|
38
|
+
"@aws-sdk/client-cloudfront": "3.937.0",
|
|
39
|
+
"@aws-sdk/client-s3": "3.937.0",
|
|
40
40
|
"mdast-util-from-markdown": "2.0.2",
|
|
41
41
|
"mdast-util-to-hast": "12.3.0"
|
|
42
42
|
},
|
package/src/index.js
CHANGED
|
@@ -407,6 +407,112 @@ class TokowakaClient {
|
|
|
407
407
|
};
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Rolls back deployed suggestions by removing their patches from the configuration
|
|
412
|
+
* @param {Object} site - Site entity
|
|
413
|
+
* @param {Object} opportunity - Opportunity entity
|
|
414
|
+
* @param {Array} suggestions - Array of suggestion entities to rollback
|
|
415
|
+
* @returns {Promise<Object>} - Rollback result with succeeded/failed suggestions
|
|
416
|
+
*/
|
|
417
|
+
async rollbackSuggestions(site, opportunity, suggestions) {
|
|
418
|
+
// Get site's Tokowaka API key
|
|
419
|
+
const { apiKey } = site.getConfig()?.getTokowakaConfig() || {};
|
|
420
|
+
|
|
421
|
+
if (!hasText(apiKey)) {
|
|
422
|
+
throw this.#createError(
|
|
423
|
+
'Site does not have a Tokowaka API key configured. Please onboard the site to Tokowaka first.',
|
|
424
|
+
HTTP_BAD_REQUEST,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const opportunityType = opportunity.getType();
|
|
429
|
+
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
430
|
+
if (!mapper) {
|
|
431
|
+
throw this.#createError(
|
|
432
|
+
`No mapper found for opportunity type: ${opportunityType}. `
|
|
433
|
+
+ `Supported types: ${this.mapperRegistry.getSupportedOpportunityTypes().join(', ')}`,
|
|
434
|
+
HTTP_NOT_IMPLEMENTED,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Validate which suggestions can be rolled back
|
|
439
|
+
// For rollback, we use the same canDeploy check to ensure data integrity
|
|
440
|
+
const {
|
|
441
|
+
eligible: eligibleSuggestions,
|
|
442
|
+
ineligible: ineligibleSuggestions,
|
|
443
|
+
} = filterEligibleSuggestions(suggestions, mapper);
|
|
444
|
+
|
|
445
|
+
this.log.debug(
|
|
446
|
+
`Rolling back ${eligibleSuggestions.length} eligible suggestions `
|
|
447
|
+
+ `(${ineligibleSuggestions.length} ineligible)`,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (eligibleSuggestions.length === 0) {
|
|
451
|
+
this.log.warn('No eligible suggestions to rollback');
|
|
452
|
+
return {
|
|
453
|
+
succeededSuggestions: [],
|
|
454
|
+
failedSuggestions: ineligibleSuggestions,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Fetch existing configuration from S3
|
|
459
|
+
this.log.debug(`Fetching existing Tokowaka config for site ${site.getId()}`);
|
|
460
|
+
const existingConfig = await this.fetchConfig(apiKey);
|
|
461
|
+
|
|
462
|
+
if (!existingConfig || !existingConfig.tokowakaOptimizations) {
|
|
463
|
+
this.log.warn('No existing configuration found to rollback from');
|
|
464
|
+
return {
|
|
465
|
+
succeededSuggestions: [],
|
|
466
|
+
failedSuggestions: eligibleSuggestions.map((suggestion) => ({
|
|
467
|
+
suggestion,
|
|
468
|
+
reason: 'No existing configuration found',
|
|
469
|
+
})),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Extract suggestion IDs to remove
|
|
474
|
+
const suggestionIdsToRemove = eligibleSuggestions.map((s) => s.getId());
|
|
475
|
+
const updatedConfig = mapper.rollbackPatches(
|
|
476
|
+
existingConfig,
|
|
477
|
+
suggestionIdsToRemove,
|
|
478
|
+
opportunity.getId(),
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (updatedConfig.removedCount === 0) {
|
|
482
|
+
this.log.warn('No patches found matching the provided suggestion IDs');
|
|
483
|
+
return {
|
|
484
|
+
succeededSuggestions: [],
|
|
485
|
+
failedSuggestions: eligibleSuggestions.map((suggestion) => ({
|
|
486
|
+
suggestion,
|
|
487
|
+
reason: 'No patches found for this suggestion in the current configuration',
|
|
488
|
+
})),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
this.log.info(`Removed ${updatedConfig.removedCount} patches from configuration`);
|
|
493
|
+
|
|
494
|
+
// Remove the removedCount property before uploading
|
|
495
|
+
const { removedCount, ...configToUpload } = updatedConfig;
|
|
496
|
+
|
|
497
|
+
// Upload updated config to S3
|
|
498
|
+
this.log.info(`Uploading updated Tokowaka config after rolling back ${eligibleSuggestions.length} suggestions`);
|
|
499
|
+
const s3Path = await this.uploadConfig(apiKey, configToUpload);
|
|
500
|
+
|
|
501
|
+
// Invalidate CDN cache (non-blocking, failures are logged but don't fail rollback)
|
|
502
|
+
const cdnInvalidationResult = await this.invalidateCdnCache(
|
|
503
|
+
apiKey,
|
|
504
|
+
this.env.TOKOWAKA_CDN_PROVIDER,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
s3Path,
|
|
509
|
+
cdnInvalidation: cdnInvalidationResult,
|
|
510
|
+
succeededSuggestions: eligibleSuggestions,
|
|
511
|
+
failedSuggestions: ineligibleSuggestions,
|
|
512
|
+
removedPatchesCount: removedCount,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
410
516
|
/**
|
|
411
517
|
* Previews suggestions by generating config and uploading to preview path
|
|
412
518
|
* Unlike deploySuggestions, this does NOT merge with existing config
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { removePatchesBySuggestionIds } from '../utils/patch-utils.js';
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* Base class for opportunity mappers
|
|
15
17
|
* Each opportunity type should extend this class and implement the abstract methods
|
|
@@ -93,4 +95,24 @@ export default class BaseOpportunityMapper {
|
|
|
93
95
|
lastUpdated,
|
|
94
96
|
};
|
|
95
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes patches from configuration for given suggestions
|
|
101
|
+
* Default implementation simply removes patches matching the suggestion IDs.
|
|
102
|
+
* Override this method in subclasses if custom rollback logic is needed
|
|
103
|
+
* (e.g., FAQ mapper removes heading patch when no suggestions remain).
|
|
104
|
+
* @param {Object} config - Current Tokowaka configuration
|
|
105
|
+
* @param {Array<string>} suggestionIds - Suggestion IDs to remove
|
|
106
|
+
* @param {string} opportunityId - Opportunity ID
|
|
107
|
+
* @returns {Object} - Updated configuration with patches removed
|
|
108
|
+
*/
|
|
109
|
+
// eslint-disable-next-line no-unused-vars
|
|
110
|
+
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
111
|
+
if (!config || !config.tokowakaOptimizations) {
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.log.debug(`Removing patches for ${suggestionIds.length} suggestions`);
|
|
116
|
+
return removePatchesBySuggestionIds(config, suggestionIds);
|
|
117
|
+
}
|
|
96
118
|
}
|
|
@@ -14,6 +14,7 @@ import { hasText, isValidUrl } from '@adobe/spacecat-shared-utils';
|
|
|
14
14
|
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
15
15
|
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
16
|
import { markdownToHast } from '../utils/markdown-utils.js';
|
|
17
|
+
import { removePatchesBySuggestionIds } from '../utils/patch-utils.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Mapper for FAQ opportunity
|
|
@@ -201,4 +202,51 @@ export default class FaqMapper extends BaseOpportunityMapper {
|
|
|
201
202
|
|
|
202
203
|
return { eligible: true };
|
|
203
204
|
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes patches from configuration for FAQ suggestions
|
|
208
|
+
* FAQ-specific logic: Also removes the heading patch when no FAQ suggestions remain for a URL
|
|
209
|
+
* @param {Object} config - Current Tokowaka configuration
|
|
210
|
+
* @param {Array<string>} suggestionIds - Suggestion IDs to remove
|
|
211
|
+
* @param {string} opportunityId - Opportunity ID
|
|
212
|
+
* @returns {Object} - Updated configuration with patches removed
|
|
213
|
+
*/
|
|
214
|
+
rollbackPatches(config, suggestionIds, opportunityId) {
|
|
215
|
+
if (!config || !config.tokowakaOptimizations) {
|
|
216
|
+
return config;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const suggestionIdsSet = new Set(suggestionIds);
|
|
220
|
+
const additionalPatchKeys = [];
|
|
221
|
+
|
|
222
|
+
// Analyze each URL to determine if FAQ heading patch should be removed
|
|
223
|
+
Object.entries(config.tokowakaOptimizations).forEach(([urlPath, optimization]) => {
|
|
224
|
+
const { patches = [] } = optimization;
|
|
225
|
+
|
|
226
|
+
// Find FAQ patches for this opportunity
|
|
227
|
+
const opportunityPatches = patches.filter((p) => p.opportunityId === opportunityId);
|
|
228
|
+
|
|
229
|
+
// Get FAQ suggestion IDs that will remain after rollback (for this URL)
|
|
230
|
+
const remainingSuggestionIds = opportunityPatches
|
|
231
|
+
.filter((p) => p.suggestionId && !suggestionIdsSet.has(p.suggestionId))
|
|
232
|
+
.map((p) => p.suggestionId);
|
|
233
|
+
|
|
234
|
+
// If no FAQ suggestions remain for this URL, remove the heading patch too
|
|
235
|
+
if (remainingSuggestionIds.length === 0) {
|
|
236
|
+
this.log.debug(`No remaining FAQ suggestions for ${urlPath}, marking heading patch for removal`);
|
|
237
|
+
// Add heading patch key (opportunityId only, no suggestionId)
|
|
238
|
+
additionalPatchKeys.push(`${urlPath}:${opportunityId}`);
|
|
239
|
+
} else {
|
|
240
|
+
this.log.debug(`${remainingSuggestionIds.length} FAQ suggestions remain for ${urlPath}, keeping heading patch`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Remove FAQ suggestion patches and any orphaned heading patches
|
|
245
|
+
this.log.debug(
|
|
246
|
+
`Removing ${suggestionIds.length} FAQ suggestion patches `
|
|
247
|
+
+ `and ${additionalPatchKeys.length} heading patches`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return removePatchesBySuggestionIds(config, suggestionIds, additionalPatchKeys);
|
|
251
|
+
}
|
|
204
252
|
}
|
package/src/utils/patch-utils.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Patches with no suggestionId:
|
|
18
18
|
* → Key: opportunityId
|
|
19
19
|
*/
|
|
20
|
-
function getPatchKey(patch) {
|
|
20
|
+
export function getPatchKey(patch) {
|
|
21
21
|
// Heading patch (no suggestionId): use special key
|
|
22
22
|
if (!patch.suggestionId) {
|
|
23
23
|
return `${patch.opportunityId}`;
|
|
@@ -64,3 +64,53 @@ export function mergePatches(existingPatches, newPatches) {
|
|
|
64
64
|
|
|
65
65
|
return { patches: mergedPatches, updateCount, addCount };
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes patches matching the given suggestion IDs from a config
|
|
70
|
+
* @param {Object} config - Tokowaka configuration object
|
|
71
|
+
* @param {Array<string>} suggestionIds - Array of suggestion IDs to remove
|
|
72
|
+
* @param {Array<string>} additionalPatchKeys - Optional array of additional patch keys to remove
|
|
73
|
+
* @returns {Object} - Updated configuration with patches removed
|
|
74
|
+
*/
|
|
75
|
+
export function removePatchesBySuggestionIds(config, suggestionIds, additionalPatchKeys = []) {
|
|
76
|
+
if (!config || !config.tokowakaOptimizations) {
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const suggestionIdSet = new Set(suggestionIds);
|
|
81
|
+
const patchKeysToRemove = new Set(additionalPatchKeys);
|
|
82
|
+
const updatedOptimizations = {};
|
|
83
|
+
let removedCount = 0;
|
|
84
|
+
|
|
85
|
+
// Iterate through each URL path and filter out patches matching suggestionIds or patch keys
|
|
86
|
+
Object.entries(config.tokowakaOptimizations).forEach(([urlPath, optimization]) => {
|
|
87
|
+
const { patches = [] } = optimization;
|
|
88
|
+
|
|
89
|
+
// Filter out patches with matching suggestionIds or patch keys
|
|
90
|
+
const filteredPatches = patches.filter((patch) => {
|
|
91
|
+
const shouldRemoveBySuggestionId = suggestionIdSet.has(patch.suggestionId);
|
|
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
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...config,
|
|
113
|
+
tokowakaOptimizations: updatedOptimizations,
|
|
114
|
+
removedCount,
|
|
115
|
+
};
|
|
116
|
+
}
|