@adobe/spacecat-shared-tokowaka-client 1.4.0 → 1.4.1
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 +107 -7
- package/package.json +1 -1
- package/src/cdn/cdn-client-registry.js +2 -0
- package/src/cdn/fastly-cdn-client.js +156 -0
- package/src/index.d.ts +47 -11
- package/src/index.js +138 -81
- package/src/mappers/generic-mapper.js +5 -1
- package/test/cdn/fastly-cdn-client.test.js +484 -0
- package/test/index.test.js +313 -113
- package/test/mappers/generic-mapper.test.js +82 -0
package/src/index.js
CHANGED
|
@@ -92,6 +92,35 @@ class TokowakaClient {
|
|
|
92
92
|
return error;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Gets the list of CDN providers from environment configuration
|
|
97
|
+
* Supports both single provider (string) and multiple providers (comma-separated string or array)
|
|
98
|
+
* @returns {Array<string>} Array of CDN provider names
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
#getCdnProviders() {
|
|
102
|
+
const providerConfig = this.env.TOKOWAKA_CDN_PROVIDER;
|
|
103
|
+
|
|
104
|
+
if (!providerConfig) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If it's already an array, return it
|
|
109
|
+
if (Array.isArray(providerConfig)) {
|
|
110
|
+
return providerConfig.filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If it's a comma-separated string, split it
|
|
114
|
+
if (typeof providerConfig === 'string') {
|
|
115
|
+
return providerConfig
|
|
116
|
+
.split(',')
|
|
117
|
+
.map((p) => p.trim())
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
/**
|
|
96
125
|
* Generates Tokowaka site configuration from suggestions for a specific URL
|
|
97
126
|
* @param {string} url - Full URL for which to generate config
|
|
@@ -155,16 +184,15 @@ class TokowakaClient {
|
|
|
155
184
|
/**
|
|
156
185
|
* Fetches domain-level metaconfig from S3
|
|
157
186
|
* @param {string} url - Full URL (used to extract domain)
|
|
158
|
-
* @param {boolean} isPreview - Whether to fetch from preview path (default: false)
|
|
159
187
|
* @returns {Promise<Object|null>} - Metaconfig object or null if not found
|
|
160
188
|
*/
|
|
161
|
-
async fetchMetaconfig(url
|
|
189
|
+
async fetchMetaconfig(url) {
|
|
162
190
|
if (!hasText(url)) {
|
|
163
191
|
throw this.#createError('URL is required', HTTP_BAD_REQUEST);
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
const s3Path = getTokowakaMetaconfigS3Path(url, this.log
|
|
167
|
-
const bucketName =
|
|
194
|
+
const s3Path = getTokowakaMetaconfigS3Path(url, this.log);
|
|
195
|
+
const bucketName = this.deployBucketName;
|
|
168
196
|
|
|
169
197
|
try {
|
|
170
198
|
const command = new GetObjectCommand({
|
|
@@ -195,10 +223,9 @@ class TokowakaClient {
|
|
|
195
223
|
* Uploads domain-level metaconfig to S3
|
|
196
224
|
* @param {string} url - Full URL (used to extract domain)
|
|
197
225
|
* @param {Object} metaconfig - Metaconfig object (siteId, prerender)
|
|
198
|
-
* @param {boolean} isPreview - Whether to upload to preview path (default: false)
|
|
199
226
|
* @returns {Promise<string>} - S3 key of uploaded metaconfig
|
|
200
227
|
*/
|
|
201
|
-
async uploadMetaconfig(url, metaconfig
|
|
228
|
+
async uploadMetaconfig(url, metaconfig) {
|
|
202
229
|
if (!hasText(url)) {
|
|
203
230
|
throw this.#createError('URL is required', HTTP_BAD_REQUEST);
|
|
204
231
|
}
|
|
@@ -207,8 +234,8 @@ class TokowakaClient {
|
|
|
207
234
|
throw this.#createError('Metaconfig object is required', HTTP_BAD_REQUEST);
|
|
208
235
|
}
|
|
209
236
|
|
|
210
|
-
const s3Path = getTokowakaMetaconfigS3Path(url, this.log
|
|
211
|
-
const bucketName =
|
|
237
|
+
const s3Path = getTokowakaMetaconfigS3Path(url, this.log);
|
|
238
|
+
const bucketName = this.deployBucketName;
|
|
212
239
|
|
|
213
240
|
try {
|
|
214
241
|
const command = new PutObjectCommand({
|
|
@@ -221,6 +248,10 @@ class TokowakaClient {
|
|
|
221
248
|
await this.s3Client.send(command);
|
|
222
249
|
this.log.info(`Successfully uploaded metaconfig to s3://${bucketName}/${s3Path}`);
|
|
223
250
|
|
|
251
|
+
// Invalidate CDN cache for the metaconfig (both CloudFront and Fastly)
|
|
252
|
+
this.log.info('Invalidating CDN cache for uploaded metaconfig');
|
|
253
|
+
await this.invalidateCdnCache({ paths: [`/${s3Path}`] });
|
|
254
|
+
|
|
224
255
|
return s3Path;
|
|
225
256
|
} catch (error) {
|
|
226
257
|
this.log.error(`Failed to upload metaconfig to S3: ${error.message}`, error);
|
|
@@ -341,41 +372,86 @@ class TokowakaClient {
|
|
|
341
372
|
}
|
|
342
373
|
|
|
343
374
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* @param {
|
|
347
|
-
* @param {string}
|
|
348
|
-
* @param {
|
|
349
|
-
* @
|
|
375
|
+
* CDN cache invalidation method that supports invalidating URL configs
|
|
376
|
+
* or custom S3 paths across provided or default CDN providers
|
|
377
|
+
* @param {Object} options - Invalidation options
|
|
378
|
+
* @param {Array<string>} options.urls - Array of full URLs to invalidate (for URL configs)
|
|
379
|
+
* @param {Array<string>} options.paths - Custom S3 paths to invalidate directly
|
|
380
|
+
* @param {string|Array<string>} options.providers - CDN provider name(s)
|
|
381
|
+
* (default: all supported providers)
|
|
382
|
+
* @param {boolean} options.isPreview - Whether to invalidate preview paths (default: false)
|
|
383
|
+
* @returns {Promise<Array<Object>>} - Array of CDN invalidation results
|
|
350
384
|
*/
|
|
351
|
-
async invalidateCdnCache(
|
|
352
|
-
|
|
353
|
-
|
|
385
|
+
async invalidateCdnCache({
|
|
386
|
+
urls = [],
|
|
387
|
+
paths = [],
|
|
388
|
+
providers = this.#getCdnProviders(),
|
|
389
|
+
isPreview = false,
|
|
390
|
+
}) {
|
|
391
|
+
// Convert single provider to array for uniform handling
|
|
392
|
+
const providerList = Array.isArray(providers) ? providers : [providers].filter(Boolean);
|
|
393
|
+
|
|
394
|
+
if (providerList.length === 0) {
|
|
395
|
+
this.log.warn('No CDN providers specified for cache invalidation');
|
|
396
|
+
return [];
|
|
354
397
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
this.log.info(`CDN cache invalidation completed: ${JSON.stringify(result)}`);
|
|
364
|
-
return result;
|
|
365
|
-
} catch (error) {
|
|
366
|
-
this.log.error(`Failed to invalidate Tokowaka CDN cache: ${error.message}`, error);
|
|
367
|
-
return {
|
|
368
|
-
status: 'error',
|
|
369
|
-
provider: 'cloudfront',
|
|
370
|
-
message: error.message,
|
|
371
|
-
};
|
|
398
|
+
|
|
399
|
+
// Build list of paths to invalidate
|
|
400
|
+
const pathsToInvalidate = [...paths];
|
|
401
|
+
|
|
402
|
+
// Add URL config paths
|
|
403
|
+
if (urls.length > 0) {
|
|
404
|
+
const urlPaths = urls.map((url) => `/${getTokowakaConfigS3Path(url, this.log, isPreview)}`);
|
|
405
|
+
pathsToInvalidate.push(...urlPaths);
|
|
372
406
|
}
|
|
407
|
+
|
|
408
|
+
// Return early if no paths to invalidate
|
|
409
|
+
if (pathsToInvalidate.length === 0) {
|
|
410
|
+
this.log.debug('No paths to invalidate for CDN cache');
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.log.info(
|
|
415
|
+
`Invalidating CDN cache for ${pathsToInvalidate.length} path(s) `
|
|
416
|
+
+ `via providers: ${providerList.join(', ')}`,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Invalidate all providers in parallel
|
|
420
|
+
const invalidationPromises = providerList.map(async (provider) => {
|
|
421
|
+
try {
|
|
422
|
+
const cdnClient = this.cdnClientRegistry.getClient(provider);
|
|
423
|
+
if (!cdnClient) {
|
|
424
|
+
this.log.warn(`No CDN client available for provider: ${provider}`);
|
|
425
|
+
return {
|
|
426
|
+
status: 'error',
|
|
427
|
+
provider,
|
|
428
|
+
message: `No CDN client available for provider: ${provider}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const result = await cdnClient.invalidateCache(pathsToInvalidate);
|
|
433
|
+
this.log.info(
|
|
434
|
+
`CDN cache invalidation completed for ${provider}: `
|
|
435
|
+
+ `${pathsToInvalidate.length} path(s)`,
|
|
436
|
+
);
|
|
437
|
+
return result;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.log.warn(`Failed to invalidate ${provider} CDN cache: ${error.message}`, error);
|
|
440
|
+
return {
|
|
441
|
+
status: 'error',
|
|
442
|
+
provider,
|
|
443
|
+
message: error.message,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Wait for all provider invalidations to complete
|
|
449
|
+
const results = await Promise.all(invalidationPromises);
|
|
450
|
+
return results;
|
|
373
451
|
}
|
|
374
452
|
|
|
375
453
|
/**
|
|
376
|
-
* Deploys suggestions to Tokowaka by generating config and uploading to S3
|
|
377
|
-
* Now creates one file per URL instead of a single file with all URLs
|
|
378
|
-
* Also creates/updates domain-level metadata if needed
|
|
454
|
+
* Deploys suggestions to Tokowaka by generating patch config and uploading to S3
|
|
379
455
|
* @param {Object} site - Site entity
|
|
380
456
|
* @param {Object} opportunity - Opportunity entity
|
|
381
457
|
* @param {Array} suggestions - Array of suggestion entities to deploy
|
|
@@ -384,7 +460,6 @@ class TokowakaClient {
|
|
|
384
460
|
async deploySuggestions(site, opportunity, suggestions) {
|
|
385
461
|
const opportunityType = opportunity.getType();
|
|
386
462
|
const baseURL = getEffectiveBaseURL(site);
|
|
387
|
-
const siteId = site.getId();
|
|
388
463
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
389
464
|
if (!mapper) {
|
|
390
465
|
throw this.#createError(
|
|
@@ -416,24 +491,21 @@ class TokowakaClient {
|
|
|
416
491
|
// Group suggestions by URL
|
|
417
492
|
const suggestionsByUrl = groupSuggestionsByUrlPath(eligibleSuggestions, baseURL, this.log);
|
|
418
493
|
|
|
419
|
-
// Check
|
|
494
|
+
// Check if domain-level metaconfig exists
|
|
420
495
|
const firstUrl = new URL(Object.keys(suggestionsByUrl)[0], baseURL).toString();
|
|
421
|
-
|
|
496
|
+
const metaconfig = await this.fetchMetaconfig(firstUrl);
|
|
422
497
|
|
|
423
498
|
if (!metaconfig) {
|
|
424
|
-
this
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await this.uploadMetaconfig(firstUrl, metaconfig);
|
|
430
|
-
} else {
|
|
431
|
-
this.log.debug('Domain-level metaconfig already exists');
|
|
499
|
+
throw this.#createError(
|
|
500
|
+
'No domain-level metaconfig found. '
|
|
501
|
+
+ 'A domain-level metaconfig needs to be created first before deploying suggestions.',
|
|
502
|
+
HTTP_BAD_REQUEST,
|
|
503
|
+
);
|
|
432
504
|
}
|
|
433
505
|
|
|
434
506
|
// Process each URL separately
|
|
435
507
|
const s3Paths = [];
|
|
436
|
-
const
|
|
508
|
+
const deployedUrls = []; // Track URLs for batch CDN invalidation
|
|
437
509
|
|
|
438
510
|
for (const [urlPath, urlSuggestions] of Object.entries(suggestionsByUrl)) {
|
|
439
511
|
const fullUrl = new URL(urlPath, baseURL).toString();
|
|
@@ -468,18 +540,14 @@ class TokowakaClient {
|
|
|
468
540
|
// eslint-disable-next-line no-await-in-loop
|
|
469
541
|
const s3Path = await this.uploadConfig(fullUrl, config);
|
|
470
542
|
s3Paths.push(s3Path);
|
|
471
|
-
|
|
472
|
-
// Invalidate CDN cache (non-blocking, failures are logged but don't fail deployment)
|
|
473
|
-
// eslint-disable-next-line no-await-in-loop
|
|
474
|
-
const cdnInvalidationResult = await this.invalidateCdnCache(
|
|
475
|
-
fullUrl,
|
|
476
|
-
this.env.TOKOWAKA_CDN_PROVIDER,
|
|
477
|
-
);
|
|
478
|
-
cdnInvalidations.push(cdnInvalidationResult);
|
|
543
|
+
deployedUrls.push(fullUrl);
|
|
479
544
|
}
|
|
480
545
|
|
|
481
546
|
this.log.info(`Uploaded Tokowaka configs for ${s3Paths.length} URLs`);
|
|
482
547
|
|
|
548
|
+
// Invalidate CDN cache for all deployed URLs at once
|
|
549
|
+
const cdnInvalidations = await this.invalidateCdnCache({ urls: deployedUrls });
|
|
550
|
+
|
|
483
551
|
return {
|
|
484
552
|
s3Paths,
|
|
485
553
|
cdnInvalidations,
|
|
@@ -533,7 +601,7 @@ class TokowakaClient {
|
|
|
533
601
|
|
|
534
602
|
// Process each URL separately
|
|
535
603
|
const s3Paths = [];
|
|
536
|
-
const
|
|
604
|
+
const rolledBackUrls = []; // Track URLs for batch CDN invalidation
|
|
537
605
|
let totalRemovedCount = 0;
|
|
538
606
|
|
|
539
607
|
for (const [urlPath, urlSuggestions] of Object.entries(suggestionsByUrl)) {
|
|
@@ -567,14 +635,7 @@ class TokowakaClient {
|
|
|
567
635
|
// eslint-disable-next-line no-await-in-loop
|
|
568
636
|
const s3Path = await this.uploadConfig(fullUrl, updatedConfig);
|
|
569
637
|
s3Paths.push(s3Path);
|
|
570
|
-
|
|
571
|
-
// Invalidate CDN cache
|
|
572
|
-
// eslint-disable-next-line no-await-in-loop
|
|
573
|
-
const cdnInvalidationResult = await this.invalidateCdnCache(
|
|
574
|
-
fullUrl,
|
|
575
|
-
this.env.TOKOWAKA_CDN_PROVIDER,
|
|
576
|
-
);
|
|
577
|
-
cdnInvalidations.push(cdnInvalidationResult);
|
|
638
|
+
rolledBackUrls.push(fullUrl); // Collect URL for batch invalidation
|
|
578
639
|
|
|
579
640
|
totalRemovedCount += 1; // Count as 1 rollback
|
|
580
641
|
// eslint-disable-next-line no-continue
|
|
@@ -610,18 +671,15 @@ class TokowakaClient {
|
|
|
610
671
|
// eslint-disable-next-line no-await-in-loop
|
|
611
672
|
const s3Path = await this.uploadConfig(fullUrl, updatedConfig);
|
|
612
673
|
s3Paths.push(s3Path);
|
|
613
|
-
|
|
614
|
-
// Invalidate CDN cache (non-blocking, failures are logged but don't fail rollback)
|
|
615
|
-
// eslint-disable-next-line no-await-in-loop
|
|
616
|
-
const cdnInvalidationResult = await this.invalidateCdnCache(
|
|
617
|
-
fullUrl,
|
|
618
|
-
this.env.TOKOWAKA_CDN_PROVIDER,
|
|
619
|
-
);
|
|
620
|
-
cdnInvalidations.push(cdnInvalidationResult);
|
|
674
|
+
rolledBackUrls.push(fullUrl); // Collect URL for batch invalidation
|
|
621
675
|
}
|
|
622
676
|
|
|
623
677
|
this.log.info(`Updated Tokowaka configs for ${s3Paths.length} URLs, removed ${totalRemovedCount} patches total`);
|
|
624
678
|
|
|
679
|
+
// Batch invalidate CDN cache for all rolled back URLs at once
|
|
680
|
+
// (much more efficient than individual invalidations)
|
|
681
|
+
const cdnInvalidations = await this.invalidateCdnCache({ urls: rolledBackUrls });
|
|
682
|
+
|
|
625
683
|
return {
|
|
626
684
|
s3Paths,
|
|
627
685
|
cdnInvalidations,
|
|
@@ -745,12 +803,11 @@ class TokowakaClient {
|
|
|
745
803
|
this.log.info(`Uploading preview Tokowaka config with ${eligibleSuggestions.length} new suggestions`);
|
|
746
804
|
const s3Path = await this.uploadConfig(previewUrl, config, true);
|
|
747
805
|
|
|
748
|
-
// Invalidate CDN cache for preview path
|
|
749
|
-
const
|
|
750
|
-
previewUrl,
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
);
|
|
806
|
+
// Invalidate CDN cache for all providers in parallel (preview path)
|
|
807
|
+
const cdnInvalidationResults = await this.invalidateCdnCache({
|
|
808
|
+
urls: [previewUrl],
|
|
809
|
+
isPreview: true,
|
|
810
|
+
});
|
|
754
811
|
|
|
755
812
|
// Fetch HTML content for preview
|
|
756
813
|
let originalHtml = null;
|
|
@@ -789,7 +846,7 @@ class TokowakaClient {
|
|
|
789
846
|
return {
|
|
790
847
|
s3Path,
|
|
791
848
|
config,
|
|
792
|
-
|
|
849
|
+
cdnInvalidations: cdnInvalidationResults,
|
|
793
850
|
succeededSuggestions: eligibleSuggestions,
|
|
794
851
|
failedSuggestions: ineligibleSuggestions,
|
|
795
852
|
html: {
|
|
@@ -57,7 +57,7 @@ export default class GenericMapper extends BaseOpportunityMapper {
|
|
|
57
57
|
...this.createBasePatch(suggestion, opportunityId),
|
|
58
58
|
op: transformRules.action,
|
|
59
59
|
selector: transformRules.selector,
|
|
60
|
-
value: data.format === 'hast' ? JSON.parse(data.patchValue) : data.patchValue,
|
|
60
|
+
value: (data.format === 'hast' || data.format === 'json') ? JSON.parse(data.patchValue) : data.patchValue,
|
|
61
61
|
valueFormat: data.format || 'text',
|
|
62
62
|
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
63
63
|
};
|
|
@@ -66,6 +66,10 @@ export default class GenericMapper extends BaseOpportunityMapper {
|
|
|
66
66
|
patch.tag = data.tag;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
if (data.attrs) {
|
|
70
|
+
patch.attrs = JSON.parse(data.attrs);
|
|
71
|
+
}
|
|
72
|
+
|
|
69
73
|
patches.push(patch);
|
|
70
74
|
});
|
|
71
75
|
|