@adobe/spacecat-shared-tokowaka-client 1.17.3 → 1.18.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 +6 -0
- package/package.json +1 -1
- package/src/index.js +450 -236
- package/src/utils/metaconfig-utils.js +52 -0
- package/src/utils/pattern-utils.js +43 -0
- package/src/utils/suggestion-utils.js +174 -0
- package/test/index.test.js +1575 -229
- package/test/utils/suggestion-utils.test.js +43 -1
package/src/index.js
CHANGED
|
@@ -25,8 +25,21 @@ import {
|
|
|
25
25
|
getHostName,
|
|
26
26
|
normalizePath,
|
|
27
27
|
} from './utils/s3-utils.js';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
omitKeys,
|
|
30
|
+
isEdgeDeployableSuggestionStatus,
|
|
31
|
+
isPatternSuggestion,
|
|
32
|
+
groupSuggestionsByUrlPath,
|
|
33
|
+
filterEligibleSuggestions,
|
|
34
|
+
saveSuggestions,
|
|
35
|
+
stripSuggestion,
|
|
36
|
+
cleanupCoveredSuggestions,
|
|
37
|
+
classifySuggestions,
|
|
38
|
+
filterBatchCoveredSuggestions,
|
|
39
|
+
} from './utils/suggestion-utils.js';
|
|
40
|
+
import { buildUrlMatcher } from './utils/pattern-utils.js';
|
|
29
41
|
import { getEffectiveBaseURL } from './utils/site-utils.js';
|
|
42
|
+
import { removePatternFromMetaconfig, addPatternsToMetaconfig } from './utils/metaconfig-utils.js';
|
|
30
43
|
import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
31
44
|
import {
|
|
32
45
|
EDGE_OPTIMIZE_PROXY_BASE_URL_DEFAULT,
|
|
@@ -42,11 +55,6 @@ const HTTP_BAD_REQUEST = 400;
|
|
|
42
55
|
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
43
56
|
const HTTP_NOT_IMPLEMENTED = 501;
|
|
44
57
|
|
|
45
|
-
/** Matches SpaceCat API eligibility for edge deploy (non-domain-wide). */
|
|
46
|
-
function isEdgeDeployableSuggestionStatus(status) {
|
|
47
|
-
return status === 'NEW' || status === 'PENDING_VALIDATION';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
58
|
/**
|
|
51
59
|
* Tokowaka Client - Manages edge optimization configurations
|
|
52
60
|
*/
|
|
@@ -72,6 +80,7 @@ class TokowakaClient {
|
|
|
72
80
|
previewBucketName,
|
|
73
81
|
s3Client: s3?.s3Client ?? context.s3Client,
|
|
74
82
|
env,
|
|
83
|
+
dataAccess: context.dataAccess,
|
|
75
84
|
}, log);
|
|
76
85
|
context.tokowakaClient = client;
|
|
77
86
|
return client;
|
|
@@ -84,10 +93,12 @@ class TokowakaClient {
|
|
|
84
93
|
* @param {string} config.previewBucketName - S3 bucket name for preview configs
|
|
85
94
|
* @param {Object} config.s3Client - AWS S3 client
|
|
86
95
|
* @param {Object} config.env - Environment variables (for CDN credentials)
|
|
96
|
+
* @param {Object} [config.dataAccess] - Data access layer
|
|
97
|
+
* (provides Suggestion.saveMany for batch saves)
|
|
87
98
|
* @param {Object} log - Logger instance
|
|
88
99
|
*/
|
|
89
100
|
constructor({
|
|
90
|
-
bucketName, previewBucketName, s3Client, env = {},
|
|
101
|
+
bucketName, previewBucketName, s3Client, env = {}, dataAccess,
|
|
91
102
|
}, log) {
|
|
92
103
|
this.log = log;
|
|
93
104
|
|
|
@@ -103,6 +114,7 @@ class TokowakaClient {
|
|
|
103
114
|
this.previewBucketName = previewBucketName;
|
|
104
115
|
this.s3Client = s3Client;
|
|
105
116
|
this.env = env;
|
|
117
|
+
this.dataAccess = dataAccess;
|
|
106
118
|
|
|
107
119
|
this.mapperRegistry = new MapperRegistry(log);
|
|
108
120
|
this.cdnClientRegistry = new CdnClientRegistry(env, log);
|
|
@@ -267,6 +279,7 @@ class TokowakaClient {
|
|
|
267
279
|
const bodyContents = await response.Body.transformToString();
|
|
268
280
|
const metaconfig = JSON.parse(bodyContents);
|
|
269
281
|
|
|
282
|
+
// eslint-disable-next-line max-len
|
|
270
283
|
this.log.debug(`Successfully fetched metaconfig from s3://${bucketName}/${s3Path} in ${Date.now() - fetchStartTime}ms`);
|
|
271
284
|
|
|
272
285
|
return {
|
|
@@ -479,6 +492,7 @@ class TokowakaClient {
|
|
|
479
492
|
const command = new PutObjectCommand(putObjectParams);
|
|
480
493
|
|
|
481
494
|
await this.s3Client.send(command);
|
|
495
|
+
// eslint-disable-next-line max-len
|
|
482
496
|
this.log.info(`Successfully uploaded metaconfig to s3://${bucketName}/${s3Path} in ${Date.now() - uploadStartTime}ms`);
|
|
483
497
|
|
|
484
498
|
// Invalidate CDN cache for the metaconfig (both CloudFront and Fastly)
|
|
@@ -517,6 +531,7 @@ class TokowakaClient {
|
|
|
517
531
|
const bodyContents = await response.Body.transformToString();
|
|
518
532
|
const config = JSON.parse(bodyContents);
|
|
519
533
|
|
|
534
|
+
// eslint-disable-next-line max-len
|
|
520
535
|
this.log.debug(`Successfully fetched existing Tokowaka config from s3://${bucketName}/${s3Path} in ${Date.now() - fetchStartTime}ms`);
|
|
521
536
|
return config;
|
|
522
537
|
} catch (error) {
|
|
@@ -597,6 +612,7 @@ class TokowakaClient {
|
|
|
597
612
|
});
|
|
598
613
|
|
|
599
614
|
await this.s3Client.send(command);
|
|
615
|
+
// eslint-disable-next-line max-len
|
|
600
616
|
this.log.info(`Successfully uploaded Tokowaka config to s3://${bucketName}/${s3Path} in ${Date.now() - uploadStartTime}ms`);
|
|
601
617
|
|
|
602
618
|
return s3Path;
|
|
@@ -684,6 +700,7 @@ class TokowakaClient {
|
|
|
684
700
|
});
|
|
685
701
|
}
|
|
686
702
|
}
|
|
703
|
+
// eslint-disable-next-line max-len
|
|
687
704
|
this.log.info(`CDN cache invalidation completed in total ${Date.now() - invalidationStartTime}ms`);
|
|
688
705
|
return results;
|
|
689
706
|
}
|
|
@@ -797,15 +814,80 @@ class TokowakaClient {
|
|
|
797
814
|
};
|
|
798
815
|
}
|
|
799
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Rolls back a single URL's S3 config by removing the relevant patches.
|
|
819
|
+
* Returns the number of patches removed, or 0 if nothing changed.
|
|
820
|
+
* Pushes the uploaded S3 path into s3Paths and the URL into rolledBackUrls on success.
|
|
821
|
+
* @param {string} fullUrl
|
|
822
|
+
* @param {Array} urlSuggestions - Suggestions for this URL
|
|
823
|
+
* @param {Object} opportunity
|
|
824
|
+
* @param {Object} mapper
|
|
825
|
+
* @param {string} opportunityType
|
|
826
|
+
* @param {Array} s3Paths - Accumulator array (mutated)
|
|
827
|
+
* @param {Array} rolledBackUrls - Accumulator array (mutated)
|
|
828
|
+
* @returns {Promise<number>} Number of patches removed
|
|
829
|
+
* @private
|
|
830
|
+
*/
|
|
831
|
+
// eslint-disable-next-line max-len
|
|
832
|
+
async #rollbackPerUrlConfig(fullUrl, urlSuggestions, opportunity, mapper, opportunityType, s3Paths, rolledBackUrls) {
|
|
833
|
+
const existingConfig = await this.fetchConfig(fullUrl);
|
|
834
|
+
if (!existingConfig) {
|
|
835
|
+
this.log.warn(`No existing configuration found for URL: ${fullUrl}`);
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (opportunityType === 'prerender') {
|
|
840
|
+
this.log.info(`Rolling back prerender config for URL: ${fullUrl}`);
|
|
841
|
+
const s3Path = await this.uploadConfig(fullUrl, { ...existingConfig, prerender: false });
|
|
842
|
+
s3Paths.push(s3Path);
|
|
843
|
+
rolledBackUrls.push(fullUrl);
|
|
844
|
+
return 1;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!existingConfig.patches) {
|
|
848
|
+
this.log.info(`No patches found in configuration for URL: ${fullUrl}`);
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const suggestionIdsToRemove = urlSuggestions.map((s) => s.getId());
|
|
853
|
+
const updatedConfig = mapper.rollbackPatches(
|
|
854
|
+
existingConfig,
|
|
855
|
+
suggestionIdsToRemove,
|
|
856
|
+
opportunity.getId(),
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
if (updatedConfig.removedCount === 0) {
|
|
860
|
+
this.log.warn(`No patches found for suggestions at URL: ${fullUrl}`);
|
|
861
|
+
return 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
this.log.info(`Removed ${updatedConfig.removedCount} patches for URL: ${fullUrl}`);
|
|
865
|
+
const removed = updatedConfig.removedCount;
|
|
866
|
+
delete updatedConfig.removedCount;
|
|
867
|
+
|
|
868
|
+
const s3Path = await this.uploadConfig(fullUrl, updatedConfig);
|
|
869
|
+
s3Paths.push(s3Path);
|
|
870
|
+
rolledBackUrls.push(fullUrl);
|
|
871
|
+
return removed;
|
|
872
|
+
}
|
|
873
|
+
|
|
800
874
|
/**
|
|
801
875
|
* Rolls back deployed suggestions by removing their patches from the configuration
|
|
802
876
|
* Now updates one file per URL instead of a single file with all URLs
|
|
803
877
|
* @param {Object} site - Site entity
|
|
804
878
|
* @param {Object} opportunity - Opportunity entity
|
|
805
879
|
* @param {Array} suggestions - Array of suggestion entities to rollback
|
|
880
|
+
* @param {Object} [options] - Optional parameters
|
|
881
|
+
* @param {Array} [options.allSuggestions=[]] - All suggestions for the opportunity,
|
|
882
|
+
* used to clean up suggestions covered by a domain-wide or path-level pattern rollback
|
|
883
|
+
* @param {string} [options.updatedBy] - Actor identity (e.g. profile email). When undefined,
|
|
884
|
+
* context-specific fallback strings are used: 'tokowaka-rollback' for direct suggestions,
|
|
885
|
+
* 'domain-wide-rollback' for per-URL suggestions covered by a domain-wide pattern, and
|
|
886
|
+
* 'path-rollback' for per-URL suggestions covered by a path pattern.
|
|
806
887
|
* @returns {Promise<Object>} - Rollback result with succeeded/failed suggestions
|
|
807
888
|
*/
|
|
808
|
-
|
|
889
|
+
// eslint-disable-next-line max-len
|
|
890
|
+
async rollbackSuggestions(site, opportunity, suggestions, { allSuggestions = [], updatedBy } = {}) {
|
|
809
891
|
const opportunityType = opportunity.getType();
|
|
810
892
|
const baseURL = getEffectiveBaseURL(site);
|
|
811
893
|
const mapper = this.mapperRegistry.getMapper(opportunityType);
|
|
@@ -817,19 +899,25 @@ class TokowakaClient {
|
|
|
817
899
|
);
|
|
818
900
|
}
|
|
819
901
|
|
|
820
|
-
//
|
|
821
|
-
//
|
|
902
|
+
// Separate pattern-based suggestions (path and domain-wide allowList entries)
|
|
903
|
+
// from per-URL suggestions — each group is rolled back via a different code path.
|
|
904
|
+
const patternSuggestions = suggestions.filter(isPatternSuggestion);
|
|
905
|
+
const perUrlSuggestions = suggestions.filter((s) => !isPatternSuggestion(s));
|
|
906
|
+
// eslint-disable-next-line max-len
|
|
907
|
+
this.log.info(`[edge-rollback] Site: ${baseURL}, total: ${suggestions.length}, pattern: ${patternSuggestions.length}, perUrl: ${perUrlSuggestions.length}, allSuggestions: ${allSuggestions.length}`);
|
|
908
|
+
|
|
822
909
|
const {
|
|
823
910
|
eligible: eligibleSuggestions,
|
|
824
911
|
ineligible: ineligibleSuggestions,
|
|
825
|
-
} = filterEligibleSuggestions(
|
|
912
|
+
} = filterEligibleSuggestions(perUrlSuggestions, mapper);
|
|
826
913
|
|
|
827
914
|
this.log.debug(
|
|
828
915
|
`Rolling back ${eligibleSuggestions.length} eligible suggestions `
|
|
829
|
-
+ `(${ineligibleSuggestions.length} ineligible)
|
|
916
|
+
+ `(${ineligibleSuggestions.length} ineligible), `
|
|
917
|
+
+ `${patternSuggestions.length} pattern suggestions`,
|
|
830
918
|
);
|
|
831
919
|
|
|
832
|
-
if (eligibleSuggestions.length === 0) {
|
|
920
|
+
if (eligibleSuggestions.length === 0 && patternSuggestions.length === 0) {
|
|
833
921
|
this.log.warn('No eligible suggestions to rollback');
|
|
834
922
|
return {
|
|
835
923
|
succeededSuggestions: [],
|
|
@@ -837,95 +925,148 @@ class TokowakaClient {
|
|
|
837
925
|
};
|
|
838
926
|
}
|
|
839
927
|
|
|
840
|
-
//
|
|
928
|
+
// Roll back per-URL suggestions: one S3 config file per URL.
|
|
841
929
|
const suggestionsByUrl = groupSuggestionsByUrlPath(eligibleSuggestions, baseURL, this.log);
|
|
842
|
-
|
|
843
|
-
// Process each URL separately
|
|
844
930
|
const s3Paths = [];
|
|
845
|
-
const rolledBackUrls = [];
|
|
931
|
+
const rolledBackUrls = [];
|
|
846
932
|
let totalRemovedCount = 0;
|
|
847
933
|
|
|
848
934
|
for (const [urlPath, urlSuggestions] of Object.entries(suggestionsByUrl)) {
|
|
849
935
|
const fullUrl = new URL(urlPath, baseURL).toString();
|
|
850
936
|
this.log.debug(`Rolling back ${urlSuggestions.length} suggestions for URL: ${fullUrl}`);
|
|
851
|
-
|
|
852
|
-
// Fetch existing configuration for this URL from S3
|
|
853
937
|
// eslint-disable-next-line no-await-in-loop
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
938
|
+
const removed = await this.#rollbackPerUrlConfig(
|
|
939
|
+
fullUrl,
|
|
940
|
+
urlSuggestions,
|
|
941
|
+
opportunity,
|
|
942
|
+
mapper,
|
|
943
|
+
opportunityType,
|
|
944
|
+
s3Paths,
|
|
945
|
+
rolledBackUrls,
|
|
946
|
+
);
|
|
947
|
+
totalRemovedCount += removed;
|
|
948
|
+
}
|
|
864
949
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this.log.info(`Rolling back prerender config for URL: ${fullUrl}`);
|
|
950
|
+
// eslint-disable-next-line max-len
|
|
951
|
+
this.log.info(`Updated Tokowaka configs for ${s3Paths.length} URLs, removed ${totalRemovedCount} patches total`);
|
|
868
952
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
953
|
+
// Strip deployment markers and batch-save all eligible per-URL suggestions.
|
|
954
|
+
const savedEligibleSuggestions = eligibleSuggestions
|
|
955
|
+
.map((s) => stripSuggestion(s, 'tokowaka-rollback', updatedBy));
|
|
956
|
+
await saveSuggestions(this.dataAccess, savedEligibleSuggestions);
|
|
957
|
+
|
|
958
|
+
// Roll back pattern suggestions: fetch metaconfig once, remove all patterns in a single
|
|
959
|
+
// in-memory pass, upload once, then save each suggestion and clean up its covered entries.
|
|
960
|
+
const succeededPatternSuggestions = [];
|
|
961
|
+
const failedPatternSuggestions = [];
|
|
962
|
+
|
|
963
|
+
if (patternSuggestions.length > 0) {
|
|
964
|
+
const metaconfig = await this.fetchMetaconfig(baseURL);
|
|
965
|
+
if (!metaconfig) {
|
|
966
|
+
// eslint-disable-next-line max-len
|
|
967
|
+
this.log.warn(`[edge-rollback] No metaconfig found for ${baseURL}, skipping all pattern suggestions`);
|
|
968
|
+
// eslint-disable-next-line max-len
|
|
969
|
+
patternSuggestions.forEach((s) => ineligibleSuggestions.push({ suggestion: s, reason: 'No metaconfig found' }));
|
|
970
|
+
} else {
|
|
971
|
+
// Pass 1: remove all patterns from the in-memory metaconfig, stage suggestions for saving.
|
|
972
|
+
const toSave = [];
|
|
973
|
+
let metaconfigChanged = false;
|
|
974
|
+
|
|
975
|
+
for (const suggestion of patternSuggestions) {
|
|
976
|
+
const data = suggestion.getData();
|
|
977
|
+
const patternToRemove = data?.allowedRegexPatterns?.[0];
|
|
978
|
+
if (!patternToRemove) {
|
|
979
|
+
// eslint-disable-next-line max-len
|
|
980
|
+
this.log.warn(`[edge-rollback] Suggestion ${suggestion.getId()} has no allowedRegexPatterns, skipping`);
|
|
981
|
+
ineligibleSuggestions.push({ suggestion, reason: 'Missing allowedRegexPatterns' });
|
|
982
|
+
// eslint-disable-next-line no-continue
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
874
985
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
986
|
+
const existingAllowList = metaconfig.prerender?.allowList ?? [];
|
|
987
|
+
const changed = removePatternFromMetaconfig(metaconfig, patternToRemove);
|
|
988
|
+
const updatedAllowList = metaconfig.prerender?.allowList ?? [];
|
|
989
|
+
// eslint-disable-next-line max-len
|
|
990
|
+
this.log.info(`[edge-rollback] Pattern ${patternToRemove}: allowList before=${JSON.stringify(existingAllowList)}, after=${JSON.stringify(updatedAllowList)}`);
|
|
991
|
+
if (changed) {
|
|
992
|
+
metaconfigChanged = true;
|
|
993
|
+
} else {
|
|
994
|
+
// eslint-disable-next-line max-len
|
|
995
|
+
this.log.info(`[edge-rollback] Pattern ${patternToRemove} not found in allowList, skipping CDN write`);
|
|
996
|
+
}
|
|
880
997
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
998
|
+
suggestion.setData(omitKeys(data, ['edgeDeployed', 'tokowakaDeployed']));
|
|
999
|
+
suggestion.setUpdatedBy(updatedBy ?? 'tokowaka-rollback');
|
|
1000
|
+
toSave.push(suggestion);
|
|
1001
|
+
}
|
|
885
1002
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1003
|
+
// Pass 2: upload the mutated metaconfig once, then save each suggestion and clean up.
|
|
1004
|
+
try {
|
|
1005
|
+
if (metaconfigChanged) {
|
|
1006
|
+
await this.uploadMetaconfig(baseURL, metaconfig);
|
|
1007
|
+
}
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
this.log.error(`[edge-rollback] Metaconfig upload failed: ${error.message}`, error);
|
|
1010
|
+
toSave.forEach((s) => failedPatternSuggestions.push({
|
|
1011
|
+
suggestion: s, reason: 'Internal server error', statusCode: 500,
|
|
1012
|
+
}));
|
|
1013
|
+
// Skip per-suggestion saves when metaconfig upload failed
|
|
1014
|
+
toSave.length = 0;
|
|
1015
|
+
}
|
|
891
1016
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1017
|
+
// Batch-save all pattern suggestions.
|
|
1018
|
+
if (toSave.length > 0) {
|
|
1019
|
+
try {
|
|
1020
|
+
await saveSuggestions(this.dataAccess, toSave);
|
|
1021
|
+
succeededPatternSuggestions.push(...toSave);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
// eslint-disable-next-line max-len
|
|
1024
|
+
this.log.error(`[edge-rollback] Error saving pattern suggestions: ${error.message}`);
|
|
1025
|
+
toSave.forEach((s) => failedPatternSuggestions.push({
|
|
1026
|
+
suggestion: s, reason: 'Internal server error', statusCode: 500,
|
|
1027
|
+
}));
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
898
1030
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1031
|
+
// Clean up covered suggestions for each succeeded pattern suggestion.
|
|
1032
|
+
for (const suggestion of succeededPatternSuggestions) {
|
|
1033
|
+
const suggData = suggestion.getData();
|
|
1034
|
+
const isDomainWide = suggData?.isDomainWide === true;
|
|
1035
|
+
const coveredFallback = isDomainWide ? 'domain-wide-rollback' : 'path-rollback';
|
|
1036
|
+
const coverageField = isDomainWide
|
|
1037
|
+
? 'coveredByDomainWide' : 'coveredByPattern';
|
|
1038
|
+
const covered = allSuggestions.filter(
|
|
1039
|
+
(s) => s.getData()?.[coverageField] === suggestion.getId(),
|
|
1040
|
+
);
|
|
1041
|
+
const fieldsToStrip = isDomainWide
|
|
1042
|
+
? ['coveredByDomainWide']
|
|
1043
|
+
: ['edgeDeployed', 'tokowakaDeployed', 'coveredByPattern'];
|
|
1044
|
+
if (covered.length > 0) {
|
|
1045
|
+
// eslint-disable-next-line max-len
|
|
1046
|
+
this.log.info(`[edge-rollback] Cleaning ${covered.length} covered suggestion(s) for pattern ${suggestion.getId()} (isDomainWide=${isDomainWide}, fallback=${coveredFallback})`);
|
|
1047
|
+
}
|
|
1048
|
+
// eslint-disable-next-line no-await-in-loop, max-len
|
|
1049
|
+
await cleanupCoveredSuggestions(this.dataAccess, covered, coveredFallback, updatedBy, fieldsToStrip, this.log);
|
|
1050
|
+
}
|
|
903
1051
|
}
|
|
1052
|
+
}
|
|
904
1053
|
|
|
905
|
-
|
|
906
|
-
totalRemovedCount += updatedConfig.removedCount;
|
|
1054
|
+
const cdnInvalidations = await this.invalidateCdnCache({ urls: rolledBackUrls });
|
|
907
1055
|
|
|
908
|
-
|
|
909
|
-
|
|
1056
|
+
const allSucceeded = [...savedEligibleSuggestions, ...succeededPatternSuggestions];
|
|
1057
|
+
const allFailed = [...ineligibleSuggestions, ...failedPatternSuggestions];
|
|
1058
|
+
const total = allSucceeded.length + allFailed.length;
|
|
910
1059
|
|
|
911
|
-
|
|
912
|
-
// eslint-disable-next-line
|
|
913
|
-
|
|
914
|
-
s3Paths.push(s3Path);
|
|
915
|
-
rolledBackUrls.push(fullUrl); // Collect URL for batch invalidation
|
|
1060
|
+
if (allFailed.length > 0) {
|
|
1061
|
+
// eslint-disable-next-line max-len
|
|
1062
|
+
this.log.error(`[edge-rollback-failed] ${allFailed.length} out of ${total} suggestion(s) failed to rollback for ${baseURL}`);
|
|
916
1063
|
}
|
|
917
1064
|
|
|
918
|
-
this.log.info(`Updated Tokowaka configs for ${s3Paths.length} URLs, removed ${totalRemovedCount} patches total`);
|
|
919
|
-
|
|
920
|
-
// Batch invalidate CDN cache for all rolled back URLs at once
|
|
921
|
-
// (much more efficient than individual invalidations)
|
|
922
|
-
const cdnInvalidations = await this.invalidateCdnCache({ urls: rolledBackUrls });
|
|
923
|
-
|
|
924
1065
|
return {
|
|
925
1066
|
s3Paths,
|
|
926
1067
|
cdnInvalidations,
|
|
927
|
-
succeededSuggestions:
|
|
928
|
-
failedSuggestions:
|
|
1068
|
+
succeededSuggestions: allSucceeded,
|
|
1069
|
+
failedSuggestions: allFailed,
|
|
929
1070
|
removedPatchesCount: totalRemovedCount,
|
|
930
1071
|
};
|
|
931
1072
|
}
|
|
@@ -1063,6 +1204,7 @@ class TokowakaClient {
|
|
|
1063
1204
|
}
|
|
1064
1205
|
|
|
1065
1206
|
// Upload to preview S3 path for this URL
|
|
1207
|
+
// eslint-disable-next-line max-len
|
|
1066
1208
|
this.log.info(`Uploading preview Tokowaka config with ${eligibleSuggestions.length} new suggestions`);
|
|
1067
1209
|
const s3Path = await this.uploadConfig(previewUrl, config, true);
|
|
1068
1210
|
|
|
@@ -1094,6 +1236,7 @@ class TokowakaClient {
|
|
|
1094
1236
|
]);
|
|
1095
1237
|
/* c8 ignore start */
|
|
1096
1238
|
if (!originalHtml || !optimizedHtml) {
|
|
1239
|
+
// eslint-disable-next-line max-len
|
|
1097
1240
|
throw this.#createError('Failed to fetch original or optimized HTML', HTTP_INTERNAL_SERVER_ERROR);
|
|
1098
1241
|
}
|
|
1099
1242
|
/* c8 ignore stop */
|
|
@@ -1156,12 +1299,14 @@ class TokowakaClient {
|
|
|
1156
1299
|
|
|
1157
1300
|
while (attempt <= maxRetries) {
|
|
1158
1301
|
try {
|
|
1302
|
+
// eslint-disable-next-line max-len
|
|
1159
1303
|
this.log.debug(`Attempt ${attempt + 1}/${maxRetries + 1}: Checking edge optimize status for ${targetUrl}`);
|
|
1160
1304
|
|
|
1161
1305
|
// eslint-disable-next-line no-await-in-loop
|
|
1162
1306
|
const response = await tracingFetch(targetUrl, {
|
|
1163
1307
|
method: 'GET',
|
|
1164
1308
|
headers: {
|
|
1309
|
+
// eslint-disable-next-line max-len
|
|
1165
1310
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Tokowaka-AI Tokowaka/1.0 AdobeEdgeOptimize-AI AdobeEdgeOptimize/1.0',
|
|
1166
1311
|
'fastly-debug': '1',
|
|
1167
1312
|
},
|
|
@@ -1182,6 +1327,7 @@ class TokowakaClient {
|
|
|
1182
1327
|
const isTimeout = error?.code === 'ETIMEOUT';
|
|
1183
1328
|
|
|
1184
1329
|
if (isTimeout) {
|
|
1330
|
+
// eslint-disable-next-line max-len
|
|
1185
1331
|
this.log.warn(`Request timed out after ${REQUEST_TIMEOUT_MS}ms for ${targetUrl}, returning edgeOptimizeEnabled: false`);
|
|
1186
1332
|
return { edgeOptimizeEnabled: false };
|
|
1187
1333
|
}
|
|
@@ -1266,6 +1412,154 @@ class TokowakaClient {
|
|
|
1266
1412
|
return probeResult;
|
|
1267
1413
|
}
|
|
1268
1414
|
|
|
1415
|
+
/**
|
|
1416
|
+
* Deploys per-URL suggestions via deploySuggestions(), stamps them with edgeDeployed,
|
|
1417
|
+
* and returns { succeededSuggestions, failedSuggestions }.
|
|
1418
|
+
* Throws if the underlying deployment throws.
|
|
1419
|
+
* @param {Object} site
|
|
1420
|
+
* @param {Object} opportunity
|
|
1421
|
+
* @param {Array} validSuggestions
|
|
1422
|
+
* @param {string} updatedBy
|
|
1423
|
+
* @returns {Promise<{ succeededSuggestions: Array, failedSuggestions: Array }>}
|
|
1424
|
+
* @private
|
|
1425
|
+
*/
|
|
1426
|
+
async #deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy) {
|
|
1427
|
+
try {
|
|
1428
|
+
const result = await this.deploySuggestions(site, opportunity, validSuggestions);
|
|
1429
|
+
const deploymentTimestamp = Date.now();
|
|
1430
|
+
|
|
1431
|
+
const succeeded = result.succeededSuggestions.map((s) => {
|
|
1432
|
+
const currentData = s.getData();
|
|
1433
|
+
const updated = { ...currentData, edgeDeployed: deploymentTimestamp };
|
|
1434
|
+
if (updated.edgeOptimizeStatus === 'STALE') {
|
|
1435
|
+
delete updated.edgeOptimizeStatus;
|
|
1436
|
+
}
|
|
1437
|
+
s.setData(updated);
|
|
1438
|
+
s.setUpdatedBy(updatedBy);
|
|
1439
|
+
return s;
|
|
1440
|
+
});
|
|
1441
|
+
await saveSuggestions(this.dataAccess, succeeded);
|
|
1442
|
+
|
|
1443
|
+
const failed = result.failedSuggestions.map((item) => {
|
|
1444
|
+
this.log.info(
|
|
1445
|
+
`[edge-deploy] ${opportunity.getType()} suggestion `
|
|
1446
|
+
+ `${item.suggestion.getId()} is ineligible: ${item.reason}`,
|
|
1447
|
+
);
|
|
1448
|
+
return { ...item, statusCode: 400 };
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
return { succeededSuggestions: succeeded, failedSuggestions: failed };
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
this.log.error(`[edge-deploy] Error deploying suggestions: ${error.message}`, error);
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Deploys a single pattern suggestion by appending its patterns to the metaconfig allowList
|
|
1460
|
+
* (append-and-deduplicate; CDN write is skipped when nothing changes), stamps the suggestion
|
|
1461
|
+
* with edgeDeployed, and marks qualifying per-URL suggestions as covered.
|
|
1462
|
+
* @param {Object} suggestion - The pattern suggestion entity
|
|
1463
|
+
* @param {Array<string>} allowedRegexPatterns
|
|
1464
|
+
* @param {Object} metaconfig - Metaconfig object (mutated in place)
|
|
1465
|
+
* @param {string} baseURL
|
|
1466
|
+
* @param {Set<string>} skippedInBatchIds - IDs already handled as same-batch skips
|
|
1467
|
+
* @param {Array} allSuggestions - Full opportunity suggestion list
|
|
1468
|
+
* @param {string} updatedBy
|
|
1469
|
+
* @param {Array} coveredSuggestions - Accumulator (mutated)
|
|
1470
|
+
* @returns {Promise<void>} Throws on metaconfig upload failure
|
|
1471
|
+
* @private
|
|
1472
|
+
*/
|
|
1473
|
+
async #deployPatternSuggestion(
|
|
1474
|
+
suggestion,
|
|
1475
|
+
allowedRegexPatterns,
|
|
1476
|
+
metaconfig,
|
|
1477
|
+
baseURL,
|
|
1478
|
+
skippedInBatchIds,
|
|
1479
|
+
allSuggestions,
|
|
1480
|
+
updatedBy,
|
|
1481
|
+
coveredSuggestions,
|
|
1482
|
+
) {
|
|
1483
|
+
const data = suggestion.getData();
|
|
1484
|
+
const coverageField = data?.isDomainWide ? 'coveredByDomainWide' : 'coveredByPattern';
|
|
1485
|
+
|
|
1486
|
+
const beforeAllowList = metaconfig.prerender?.allowList ?? [];
|
|
1487
|
+
const changed = addPatternsToMetaconfig(metaconfig, allowedRegexPatterns);
|
|
1488
|
+
const afterAllowList = changed ? metaconfig.prerender.allowList : beforeAllowList;
|
|
1489
|
+
this.log.info(
|
|
1490
|
+
// eslint-disable-next-line max-len
|
|
1491
|
+
`[edge-deploy] Pattern ${suggestion.getId()}: allowList before=${JSON.stringify(beforeAllowList)}, after=${JSON.stringify(afterAllowList)}`,
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
if (changed) {
|
|
1495
|
+
await this.uploadMetaconfig(baseURL, metaconfig);
|
|
1496
|
+
// eslint-disable-next-line max-len
|
|
1497
|
+
this.log.info(`[edge-deploy] Uploaded metaconfig for ${baseURL} with allowList=${JSON.stringify(afterAllowList)}`);
|
|
1498
|
+
} else {
|
|
1499
|
+
// eslint-disable-next-line max-len
|
|
1500
|
+
this.log.info(`[edge-deploy] Patterns already in allowList for suggestion ${suggestion.getId()}, skipping CDN write`);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
suggestion.setData({ ...suggestion.getData(), edgeDeployed: Date.now() });
|
|
1504
|
+
suggestion.setUpdatedBy(updatedBy);
|
|
1505
|
+
// suggestion.save() is deferred — caller batches saves via saveSuggestions.
|
|
1506
|
+
|
|
1507
|
+
// Mark per-URL suggestions covered by this pattern.
|
|
1508
|
+
const matchers = allowedRegexPatterns.flatMap((p) => {
|
|
1509
|
+
const m = buildUrlMatcher(p);
|
|
1510
|
+
if (!m) {
|
|
1511
|
+
// eslint-disable-next-line max-len
|
|
1512
|
+
this.log.warn(`[edge-deploy] Pattern '${p}' for suggestion ${suggestion.getId()} is invalid, skipping`);
|
|
1513
|
+
}
|
|
1514
|
+
return m ? [m] : [];
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
if (matchers.length === 0) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const covered = allSuggestions.filter((s) => {
|
|
1522
|
+
if (s.getId() === suggestion.getId()) {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
if (skippedInBatchIds.has(s.getId())) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
if (isPatternSuggestion(s)) {
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
if (s.getData()?.edgeDeployed) {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
const url = s.getData()?.url;
|
|
1538
|
+
return url && matchers.some((match) => match(url));
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
// eslint-disable-next-line max-len
|
|
1542
|
+
this.log.info(`[edge-deploy] Pattern ${suggestion.getId()}: found ${covered.length} coverable per-URL suggestions (field=${coverageField})`);
|
|
1543
|
+
|
|
1544
|
+
if (covered.length > 0) {
|
|
1545
|
+
covered.forEach((cs) => {
|
|
1546
|
+
cs.setData({ ...cs.getData(), [coverageField]: suggestion.getId() });
|
|
1547
|
+
cs.setUpdatedBy(updatedBy);
|
|
1548
|
+
});
|
|
1549
|
+
try {
|
|
1550
|
+
await saveSuggestions(this.dataAccess, covered);
|
|
1551
|
+
coveredSuggestions.push(...covered);
|
|
1552
|
+
// eslint-disable-next-line max-len
|
|
1553
|
+
this.log.info(`[edge-deploy] Marked ${covered.length} suggestions as ${coverageField}=${suggestion.getId()}`);
|
|
1554
|
+
} catch (coverError) {
|
|
1555
|
+
this.log.warn(
|
|
1556
|
+
'[edge-deploy] Failed to mark covered suggestions for pattern suggestion '
|
|
1557
|
+
+ `${suggestion.getId()}: ${coverError.message}`,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1269
1563
|
/**
|
|
1270
1564
|
* Deploys suggestions to edge, handling both regular and domain-wide suggestions.
|
|
1271
1565
|
*
|
|
@@ -1291,189 +1585,109 @@ class TokowakaClient {
|
|
|
1291
1585
|
allSuggestions,
|
|
1292
1586
|
updatedBy = 'edge-deploy',
|
|
1293
1587
|
}) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
if (Array.isArray(allowedRegexPatterns) && allowedRegexPatterns.length > 0) {
|
|
1302
|
-
domainWideSuggestions.push({ suggestion, allowedRegexPatterns });
|
|
1303
|
-
}
|
|
1304
|
-
} else if (isEdgeDeployableSuggestionStatus(suggestion.getStatus())) {
|
|
1305
|
-
validSuggestions.push(suggestion);
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1588
|
+
// Step 1: classify suggestions into pattern vs per-URL buckets.
|
|
1589
|
+
// eslint-disable-next-line max-len
|
|
1590
|
+
const { patternSuggestions, validSuggestions: classified } = classifySuggestions(targetSuggestions, this.log);
|
|
1591
|
+
this.log.info(
|
|
1592
|
+
`[edge-deploy] Classification: pattern=${patternSuggestions.length},`
|
|
1593
|
+
+ ` perUrl=${classified.length}, allSuggestions=${allSuggestions.length}`,
|
|
1594
|
+
);
|
|
1308
1595
|
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const allPatterns = [];
|
|
1313
|
-
domainWideSuggestions.forEach(({ allowedRegexPatterns }) => {
|
|
1314
|
-
allowedRegexPatterns.forEach((pattern) => {
|
|
1315
|
-
try {
|
|
1316
|
-
allPatterns.push(new RegExp(pattern));
|
|
1317
|
-
} catch {
|
|
1318
|
-
this.log.warn(`[edge-deploy] Skipping domain-wide pattern ${pattern} - invalid regex`);
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
const remaining = [];
|
|
1324
|
-
validSuggestions.forEach((s) => {
|
|
1325
|
-
const url = s.getData()?.url;
|
|
1326
|
-
if (url && allPatterns.some((regex) => regex.test(url))) {
|
|
1327
|
-
skippedInBatch.push(s);
|
|
1328
|
-
this.log.info(`[edge-deploy] Skipping suggestion ${s.getId()} - covered by domain-wide pattern`);
|
|
1329
|
-
} else {
|
|
1330
|
-
remaining.push(s);
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1333
|
-
validSuggestions.length = 0;
|
|
1334
|
-
validSuggestions.push(...remaining);
|
|
1335
|
-
}
|
|
1596
|
+
// Step 2: remove per-URL suggestions already covered by a pattern in this batch.
|
|
1597
|
+
// eslint-disable-next-line max-len
|
|
1598
|
+
const { remaining: validSuggestions, skippedInBatch } = filterBatchCoveredSuggestions(classified, patternSuggestions, this.log);
|
|
1336
1599
|
|
|
1337
1600
|
let succeededSuggestions = [];
|
|
1338
1601
|
const failedSuggestions = [];
|
|
1339
1602
|
|
|
1340
|
-
//
|
|
1603
|
+
// Step 3: deploy per-URL suggestions via S3.
|
|
1341
1604
|
if (validSuggestions.length > 0) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
result.succeededSuggestions.map(async (s) => {
|
|
1348
|
-
const currentData = s.getData();
|
|
1349
|
-
const updated = { ...currentData, edgeDeployed: deploymentTimestamp };
|
|
1350
|
-
if (updated.edgeOptimizeStatus === 'STALE') {
|
|
1351
|
-
delete updated.edgeOptimizeStatus;
|
|
1352
|
-
}
|
|
1353
|
-
s.setData(updated);
|
|
1354
|
-
s.setUpdatedBy(updatedBy);
|
|
1355
|
-
await s.save();
|
|
1356
|
-
return s;
|
|
1357
|
-
}),
|
|
1358
|
-
);
|
|
1359
|
-
|
|
1360
|
-
// ineligible suggestions get statusCode 400 — they weren't deployable
|
|
1361
|
-
result.failedSuggestions.forEach((item) => {
|
|
1362
|
-
failedSuggestions.push({ ...item, statusCode: 400 });
|
|
1363
|
-
this.log.info(`[edge-deploy] ${opportunity.getType()} suggestion ${item.suggestion.getId()} is ineligible: ${item.reason}`);
|
|
1364
|
-
});
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
this.log.error(`[edge-deploy] Error deploying suggestions: ${error.message}`, error);
|
|
1367
|
-
throw error;
|
|
1368
|
-
}
|
|
1605
|
+
// eslint-disable-next-line max-len
|
|
1606
|
+
const result = await this.#deployPerUrlSuggestions(site, opportunity, validSuggestions, updatedBy);
|
|
1607
|
+
const { succeededSuggestions: perUrlSucceeded, failedSuggestions: perUrlFailed } = result;
|
|
1608
|
+
succeededSuggestions = perUrlSucceeded;
|
|
1609
|
+
failedSuggestions.push(...perUrlFailed);
|
|
1369
1610
|
}
|
|
1370
1611
|
|
|
1371
|
-
//
|
|
1612
|
+
// Step 4: deploy pattern suggestions via metaconfig allowList.
|
|
1372
1613
|
const coveredSuggestions = [];
|
|
1373
|
-
|
|
1614
|
+
const deployedPatternSuggestions = [];
|
|
1615
|
+
if (patternSuggestions.length > 0) {
|
|
1616
|
+
const skippedInBatchIds = new Set(skippedInBatch.map((item) => item.suggestion.getId()));
|
|
1374
1617
|
const baseURL = site.getBaseURL();
|
|
1375
|
-
|
|
1618
|
+
let metaconfig = await this.fetchMetaconfig(baseURL);
|
|
1619
|
+
if (!metaconfig) {
|
|
1620
|
+
metaconfig = { siteId: site.getId() };
|
|
1621
|
+
}
|
|
1376
1622
|
|
|
1377
|
-
for (const { suggestion, allowedRegexPatterns } of
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
regexPatterns.push(new RegExp(pattern));
|
|
1384
|
-
} catch {
|
|
1385
|
-
this.log.warn(`[edge-deploy] Invalid regex pattern "${pattern}" for domain-wide suggestion ${suggestion.getId()}, skipping`);
|
|
1386
|
-
}
|
|
1623
|
+
for (const { suggestion, allowedRegexPatterns } of patternSuggestions) {
|
|
1624
|
+
if (!allowedRegexPatterns || allowedRegexPatterns.length === 0) {
|
|
1625
|
+
// eslint-disable-next-line max-len
|
|
1626
|
+
this.log.warn(`[edge-deploy] Pattern suggestion ${suggestion.getId()} has no allowedRegexPatterns, skipping`);
|
|
1627
|
+
// eslint-disable-next-line no-continue
|
|
1628
|
+
continue;
|
|
1387
1629
|
}
|
|
1388
1630
|
|
|
1389
1631
|
try {
|
|
1390
1632
|
// eslint-disable-next-line no-await-in-loop
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1633
|
+
await this.#deployPatternSuggestion(
|
|
1634
|
+
suggestion,
|
|
1635
|
+
allowedRegexPatterns,
|
|
1636
|
+
metaconfig,
|
|
1637
|
+
baseURL,
|
|
1638
|
+
skippedInBatchIds,
|
|
1639
|
+
allSuggestions,
|
|
1640
|
+
updatedBy,
|
|
1641
|
+
coveredSuggestions,
|
|
1642
|
+
);
|
|
1643
|
+
deployedPatternSuggestions.push(suggestion);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
// eslint-disable-next-line max-len
|
|
1646
|
+
this.log.error(`[edge-deploy] Error deploying pattern suggestion ${suggestion.getId()}: ${error.message}`, error);
|
|
1647
|
+
failedSuggestions.push({ suggestion, reason: 'Internal server error', statusCode: 500 });
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1398
1650
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
succeededSuggestions.push(suggestion);
|
|
1405
|
-
|
|
1406
|
-
if (regexPatterns.length > 0) {
|
|
1407
|
-
const covered = allSuggestions.filter((s) => {
|
|
1408
|
-
if (s.getId() === suggestion.getId()) {
|
|
1409
|
-
return false;
|
|
1410
|
-
}
|
|
1411
|
-
if (skippedInBatchIds.has(s.getId())) {
|
|
1412
|
-
return false;
|
|
1413
|
-
}
|
|
1414
|
-
if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
|
|
1415
|
-
return false;
|
|
1416
|
-
}
|
|
1417
|
-
if (s.getData()?.isDomainWide === true) {
|
|
1418
|
-
return false;
|
|
1419
|
-
}
|
|
1420
|
-
if (s.getData()?.edgeDeployed) {
|
|
1421
|
-
return false;
|
|
1422
|
-
}
|
|
1423
|
-
const url = s.getData()?.url;
|
|
1424
|
-
return url && regexPatterns.some((r) => r.test(url));
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
if (covered.length > 0) {
|
|
1428
|
-
try {
|
|
1429
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1430
|
-
await Promise.all(covered.map(async (cs) => {
|
|
1431
|
-
cs.setData({
|
|
1432
|
-
...cs.getData(),
|
|
1433
|
-
coveredByDomainWide: suggestion.getId(),
|
|
1434
|
-
});
|
|
1435
|
-
cs.setUpdatedBy(updatedBy);
|
|
1436
|
-
return cs.save();
|
|
1437
|
-
}));
|
|
1438
|
-
coveredSuggestions.push(...covered);
|
|
1439
|
-
} catch (coverError) {
|
|
1440
|
-
this.log.warn(`[edge-deploy] Failed to mark covered suggestions for domain-wide ${suggestion.getId()}: ${coverError.message}`);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1651
|
+
// Batch-save all pattern suggestions.
|
|
1652
|
+
if (deployedPatternSuggestions.length > 0) {
|
|
1653
|
+
try {
|
|
1654
|
+
await saveSuggestions(this.dataAccess, deployedPatternSuggestions);
|
|
1655
|
+
succeededSuggestions.push(...deployedPatternSuggestions);
|
|
1444
1656
|
} catch (error) {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1657
|
+
// eslint-disable-next-line max-len
|
|
1658
|
+
this.log.error(`[edge-deploy] Failed to save pattern suggestions: ${error.message}`);
|
|
1659
|
+
deployedPatternSuggestions.forEach((s) => {
|
|
1660
|
+
failedSuggestions.push({ suggestion: s, reason: 'Internal server error', statusCode: 500 });
|
|
1661
|
+
});
|
|
1448
1662
|
}
|
|
1449
1663
|
}
|
|
1450
1664
|
}
|
|
1451
1665
|
|
|
1452
|
-
//
|
|
1453
|
-
// surfaces as a per-item failure rather than swallowing the whole batch.
|
|
1666
|
+
// Step 5: mark same-batch skipped suggestions, then batch-save.
|
|
1454
1667
|
if (skippedInBatch.length > 0) {
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1668
|
+
const skippedSuggestions = skippedInBatch.map((item) => {
|
|
1669
|
+
const coverageField = item.isDomainWide ? 'coveredByDomainWide' : 'coveredByPattern';
|
|
1670
|
+
item.suggestion.setData({
|
|
1671
|
+
...item.suggestion.getData(),
|
|
1672
|
+
[coverageField]: 'same-batch-deployment',
|
|
1459
1673
|
skippedInDeployment: true,
|
|
1460
1674
|
});
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
return s;
|
|
1464
|
-
}));
|
|
1465
|
-
|
|
1466
|
-
results.forEach((result, i) => {
|
|
1467
|
-
const s = skippedInBatch[i];
|
|
1468
|
-
if (result.status === 'fulfilled') {
|
|
1469
|
-
succeededSuggestions.push(s);
|
|
1470
|
-
coveredSuggestions.push(s);
|
|
1471
|
-
} else {
|
|
1472
|
-
this.log.warn(`[edge-deploy] Failed to mark same-batch skipped suggestion ${s.getId()}`
|
|
1473
|
-
+ ` - ${result.reason?.message}`);
|
|
1474
|
-
failedSuggestions.push({ suggestion: s, reason: 'Failed to mark as covered by domain-wide', statusCode: 500 });
|
|
1475
|
-
}
|
|
1675
|
+
item.suggestion.setUpdatedBy(updatedBy);
|
|
1676
|
+
return item.suggestion;
|
|
1476
1677
|
});
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
await saveSuggestions(this.dataAccess, skippedSuggestions);
|
|
1681
|
+
succeededSuggestions.push(...skippedSuggestions);
|
|
1682
|
+
coveredSuggestions.push(...skippedSuggestions);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
// eslint-disable-next-line max-len
|
|
1685
|
+
this.log.warn(`[edge-deploy] Failed to save same-batch skipped suggestions: ${error.message}`);
|
|
1686
|
+
skippedSuggestions.forEach((s) => {
|
|
1687
|
+
// eslint-disable-next-line max-len
|
|
1688
|
+
failedSuggestions.push({ suggestion: s, reason: 'Failed to mark as covered', statusCode: 500 });
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1477
1691
|
}
|
|
1478
1692
|
|
|
1479
1693
|
return { succeededSuggestions, failedSuggestions, coveredSuggestions };
|