@adobe/spacecat-shared-tokowaka-client 1.19.1 → 1.21.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 +12 -0
- package/package.json +4 -1
- package/src/cdn/cloudfront/edge-code.js +152 -0
- package/src/cdn/cloudfront/index.js +1863 -0
- package/src/index.d.ts +262 -1
- package/src/index.js +48 -3
- package/src/utils/suggestion-utils.js +48 -5
- package/test/cdn/cloudfront/index.test.js +4185 -0
- package/test/index.test.js +160 -0
- package/test/utils/suggestion-utils.test.js +154 -1
package/src/index.d.ts
CHANGED
|
@@ -155,6 +155,268 @@ export class FastlyKVClient {
|
|
|
155
155
|
*/
|
|
156
156
|
export function calculateForwardedHost(url: string, logger?: { debug?: (msg: string) => void; error?: (msg: string) => void }): string;
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Temporary AWS credentials returned by {@link assumeConnectorRole}.
|
|
160
|
+
*/
|
|
161
|
+
export interface AWSCredentials {
|
|
162
|
+
accessKeyId: string;
|
|
163
|
+
secretAccessKey: string;
|
|
164
|
+
sessionToken: string;
|
|
165
|
+
expiration?: Date;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* One row of the Edge Optimize deploy/plan step contract.
|
|
170
|
+
*/
|
|
171
|
+
export interface DeployStep {
|
|
172
|
+
key: string;
|
|
173
|
+
label: string;
|
|
174
|
+
status?: string;
|
|
175
|
+
action?: string;
|
|
176
|
+
detail?: string;
|
|
177
|
+
probe?: Record<string, any>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── CloudFront "Optimize at Edge" control-plane (free functions) ──────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Assume the customer's cross-account connector role and return short-lived credentials.
|
|
184
|
+
*/
|
|
185
|
+
export function assumeConnectorRole(params: {
|
|
186
|
+
accountId: string;
|
|
187
|
+
externalId: string;
|
|
188
|
+
roleName?: string;
|
|
189
|
+
region?: string;
|
|
190
|
+
}): Promise<{ roleArn: string; accountId: string; credentials: AWSCredentials }>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* List the CloudFront distributions in the customer account using assumed-role credentials.
|
|
194
|
+
*/
|
|
195
|
+
export function listDistributions(
|
|
196
|
+
credentials: AWSCredentials,
|
|
197
|
+
region?: string,
|
|
198
|
+
): Promise<Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
domainName: string;
|
|
201
|
+
aliases: string[];
|
|
202
|
+
status: string;
|
|
203
|
+
enabled: boolean;
|
|
204
|
+
comment: string;
|
|
205
|
+
}>>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fetch a single CloudFront distribution's configuration using assumed-role credentials.
|
|
209
|
+
*/
|
|
210
|
+
export function getDistributionConfig(
|
|
211
|
+
credentials: AWSCredentials,
|
|
212
|
+
distributionId: string,
|
|
213
|
+
region?: string,
|
|
214
|
+
): Promise<{
|
|
215
|
+
origins: Array<{ id: string; domainName: string; originPath: string }>;
|
|
216
|
+
defaultCacheBehavior: { pathPattern: string; targetOriginId: string } | null;
|
|
217
|
+
cacheBehaviors: Array<{ pathPattern: string; targetOriginId: string }>;
|
|
218
|
+
}>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Add the Edge Optimize origin to a CloudFront distribution (idempotent + self-healing).
|
|
222
|
+
*/
|
|
223
|
+
export function createOrigin(
|
|
224
|
+
credentials: AWSCredentials,
|
|
225
|
+
distributionId: string,
|
|
226
|
+
originDomain?: string,
|
|
227
|
+
headers?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string },
|
|
228
|
+
region?: string,
|
|
229
|
+
): Promise<{ created: boolean; alreadyExisted: boolean; updated: boolean; originId: string }>;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create or update the routing CloudFront Function and publish it to LIVE (idempotent).
|
|
233
|
+
*/
|
|
234
|
+
export function createCloudFrontFunction(
|
|
235
|
+
credentials: AWSCredentials,
|
|
236
|
+
defaultOriginId: string,
|
|
237
|
+
distributionId: string,
|
|
238
|
+
targetedPaths?: string[] | null,
|
|
239
|
+
region?: string,
|
|
240
|
+
): Promise<{ name: string; created: boolean; stage: string }>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Add the Edge Optimize routing headers to the cache key for the target behavior.
|
|
244
|
+
*/
|
|
245
|
+
export function updateCacheSettings(
|
|
246
|
+
credentials: AWSCredentials,
|
|
247
|
+
distributionId: string,
|
|
248
|
+
pathPattern: string,
|
|
249
|
+
opts?: { setMinTTLZero?: boolean; region?: string },
|
|
250
|
+
): Promise<{
|
|
251
|
+
scenario: string;
|
|
252
|
+
policyId: string | null;
|
|
253
|
+
updated: boolean;
|
|
254
|
+
alreadyForwarded: boolean;
|
|
255
|
+
reused?: boolean;
|
|
256
|
+
}>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create (or update) the Edge Optimize Lambda@Edge function and publish a version (idempotent).
|
|
260
|
+
*/
|
|
261
|
+
export function createLambdaAtEdge(
|
|
262
|
+
credentials: AWSCredentials,
|
|
263
|
+
accountId: string,
|
|
264
|
+
opts?: {
|
|
265
|
+
region?: string;
|
|
266
|
+
distributionId?: string;
|
|
267
|
+
originDomain?: string;
|
|
268
|
+
retryDelayMs?: number;
|
|
269
|
+
},
|
|
270
|
+
): Promise<{
|
|
271
|
+
status: string;
|
|
272
|
+
functionArn?: string;
|
|
273
|
+
versionArn: string | null;
|
|
274
|
+
version?: string;
|
|
275
|
+
roleArn: string;
|
|
276
|
+
created: boolean;
|
|
277
|
+
alreadyExisted?: boolean;
|
|
278
|
+
}>;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Read-only status of the Edge Optimize Lambda@Edge function and its execution role.
|
|
282
|
+
*/
|
|
283
|
+
export function getLambdaAtEdgeStatus(
|
|
284
|
+
credentials: AWSCredentials,
|
|
285
|
+
distributionId: string,
|
|
286
|
+
region?: string,
|
|
287
|
+
): Promise<{
|
|
288
|
+
exists: boolean;
|
|
289
|
+
roleExists: boolean;
|
|
290
|
+
roleOk: boolean;
|
|
291
|
+
state?: string;
|
|
292
|
+
lastUpdateStatus?: string;
|
|
293
|
+
functionArn?: string;
|
|
294
|
+
versionArn: string | null;
|
|
295
|
+
version?: string;
|
|
296
|
+
ready: boolean;
|
|
297
|
+
}>;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Wire the routing CloudFront Function and the Lambda@Edge function onto a cache behavior.
|
|
301
|
+
*/
|
|
302
|
+
export function applyAssociations(
|
|
303
|
+
credentials: AWSCredentials,
|
|
304
|
+
distributionId: string,
|
|
305
|
+
pathPattern: string,
|
|
306
|
+
lambdaVersionArn: string,
|
|
307
|
+
region?: string,
|
|
308
|
+
): Promise<{ cloudFrontFunctionArn: string; lambdaArn: string }>;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Verify Edge Optimize routing end-to-end by probing as a bot and as a human.
|
|
312
|
+
*/
|
|
313
|
+
export function verifyRouting(url: string): Promise<{
|
|
314
|
+
passed: boolean;
|
|
315
|
+
requestId: string | null;
|
|
316
|
+
details: { bot: Record<string, any>; human: Record<string, any> };
|
|
317
|
+
}>;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Run one poll of the idempotent Edge Optimize "Deploy routing" orchestrator.
|
|
321
|
+
*/
|
|
322
|
+
export function runDeployStep(
|
|
323
|
+
credentials: AWSCredentials,
|
|
324
|
+
params: {
|
|
325
|
+
distributionId: string;
|
|
326
|
+
originId: string;
|
|
327
|
+
behavior: string;
|
|
328
|
+
originDomain?: string;
|
|
329
|
+
originHeaders?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string };
|
|
330
|
+
accountId: string;
|
|
331
|
+
},
|
|
332
|
+
region?: string,
|
|
333
|
+
): Promise<{ routingDeployed: boolean; verified: boolean; steps: DeployStep[] }>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Read-only "preview" of what {@link runDeployStep} would do, without mutating.
|
|
337
|
+
*/
|
|
338
|
+
export function planDeploy(
|
|
339
|
+
credentials: AWSCredentials,
|
|
340
|
+
params: {
|
|
341
|
+
distributionId: string;
|
|
342
|
+
originId?: string;
|
|
343
|
+
behavior: string;
|
|
344
|
+
originDomain?: string;
|
|
345
|
+
originHeaders?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string };
|
|
346
|
+
accountId?: string;
|
|
347
|
+
},
|
|
348
|
+
region?: string,
|
|
349
|
+
): Promise<{ canProceed: boolean; blocker: string | null; steps: DeployStep[] }>;
|
|
350
|
+
|
|
351
|
+
export interface CloudFrontEdgeClientOptions {
|
|
352
|
+
credentials: AWSCredentials;
|
|
353
|
+
region?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* CloudFront control-plane client initialized with short-lived AWS credentials.
|
|
358
|
+
*/
|
|
359
|
+
export class CloudFrontEdgeClient {
|
|
360
|
+
constructor(options: CloudFrontEdgeClientOptions);
|
|
361
|
+
|
|
362
|
+
listDistributions(): ReturnType<typeof listDistributions>;
|
|
363
|
+
|
|
364
|
+
getDistributionConfig(distributionId: string): ReturnType<typeof getDistributionConfig>;
|
|
365
|
+
|
|
366
|
+
createOrigin(
|
|
367
|
+
distributionId: string,
|
|
368
|
+
originDomain?: string,
|
|
369
|
+
headers?: { apiKey?: string; forwardedHost?: string; fetcherKey?: string },
|
|
370
|
+
): ReturnType<typeof createOrigin>;
|
|
371
|
+
|
|
372
|
+
createCloudFrontFunction(
|
|
373
|
+
defaultOriginId: string,
|
|
374
|
+
distributionId: string,
|
|
375
|
+
targetedPaths?: string[] | null,
|
|
376
|
+
): ReturnType<typeof createCloudFrontFunction>;
|
|
377
|
+
|
|
378
|
+
updateCacheSettings(
|
|
379
|
+
distributionId: string,
|
|
380
|
+
pathPattern: string,
|
|
381
|
+
opts?: { setMinTTLZero?: boolean; region?: string },
|
|
382
|
+
): ReturnType<typeof updateCacheSettings>;
|
|
383
|
+
|
|
384
|
+
createLambdaAtEdge(
|
|
385
|
+
accountId: string,
|
|
386
|
+
opts?: {
|
|
387
|
+
region?: string;
|
|
388
|
+
distributionId?: string;
|
|
389
|
+
originDomain?: string;
|
|
390
|
+
retryDelayMs?: number;
|
|
391
|
+
},
|
|
392
|
+
): ReturnType<typeof createLambdaAtEdge>;
|
|
393
|
+
|
|
394
|
+
getLambdaAtEdgeStatus(distributionId: string): ReturnType<typeof getLambdaAtEdgeStatus>;
|
|
395
|
+
|
|
396
|
+
applyAssociations(
|
|
397
|
+
distributionId: string,
|
|
398
|
+
pathPattern: string,
|
|
399
|
+
lambdaVersionArn: string,
|
|
400
|
+
): ReturnType<typeof applyAssociations>;
|
|
401
|
+
|
|
402
|
+
runDeployStep(params: Parameters<typeof runDeployStep>[1]): ReturnType<typeof runDeployStep>;
|
|
403
|
+
|
|
404
|
+
planDeploy(params: Parameters<typeof planDeploy>[1]): ReturnType<typeof planDeploy>;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Build the CloudFront Function (viewer-request) routing code.
|
|
409
|
+
*/
|
|
410
|
+
export function buildCloudfrontFunctionCode(
|
|
411
|
+
defaultOriginId: string,
|
|
412
|
+
targetedPaths?: string[] | null,
|
|
413
|
+
): string;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Build an in-memory zip containing a single file (used to package the Lambda@Edge code).
|
|
417
|
+
*/
|
|
418
|
+
export function buildLambdaZip(filename: string, content: string | Buffer): Buffer;
|
|
419
|
+
|
|
158
420
|
/**
|
|
159
421
|
* Base class for opportunity mappers
|
|
160
422
|
* Extend this class to create custom mappers for new opportunity types
|
|
@@ -481,4 +743,3 @@ export default class TokowakaClient {
|
|
|
481
743
|
*/
|
|
482
744
|
getSupportedOpportunityTypes(): string[];
|
|
483
745
|
}
|
|
484
|
-
|
package/src/index.js
CHANGED
|
@@ -50,6 +50,26 @@ import {
|
|
|
50
50
|
|
|
51
51
|
export { FastlyKVClient } from './fastly-kv-client.js';
|
|
52
52
|
export { calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
53
|
+
export { SUGGESTION_BULK_UPDATE_TYPE } from './utils/suggestion-utils.js';
|
|
54
|
+
|
|
55
|
+
// CloudFront control-plane (free functions + constants).
|
|
56
|
+
export {
|
|
57
|
+
assumeConnectorRole,
|
|
58
|
+
listDistributions,
|
|
59
|
+
getDistributionConfig,
|
|
60
|
+
createOrigin,
|
|
61
|
+
createCloudFrontFunction,
|
|
62
|
+
updateCacheSettings,
|
|
63
|
+
createLambdaAtEdge,
|
|
64
|
+
getLambdaAtEdgeStatus,
|
|
65
|
+
applyAssociations,
|
|
66
|
+
verifyRouting,
|
|
67
|
+
runDeployStep,
|
|
68
|
+
planDeploy,
|
|
69
|
+
CloudFrontEdgeClient,
|
|
70
|
+
buildCloudfrontFunctionCode,
|
|
71
|
+
buildLambdaZip,
|
|
72
|
+
} from './cdn/cloudfront/index.js';
|
|
53
73
|
|
|
54
74
|
const HTTP_BAD_REQUEST = 400;
|
|
55
75
|
const HTTP_INTERNAL_SERVER_ERROR = 500;
|
|
@@ -69,6 +89,7 @@ class TokowakaClient {
|
|
|
69
89
|
const {
|
|
70
90
|
TOKOWAKA_SITE_CONFIG_BUCKET: bucketName,
|
|
71
91
|
TOKOWAKA_PREVIEW_BUCKET: previewBucketName,
|
|
92
|
+
IMPORT_WORKER_QUEUE_URL: importWorkerQueueUrl,
|
|
72
93
|
} = env;
|
|
73
94
|
|
|
74
95
|
if (context.tokowakaClient) {
|
|
@@ -81,6 +102,8 @@ class TokowakaClient {
|
|
|
81
102
|
s3Client: s3?.s3Client ?? context.s3Client,
|
|
82
103
|
env,
|
|
83
104
|
dataAccess: context.dataAccess,
|
|
105
|
+
sqs: context.sqs,
|
|
106
|
+
importWorkerQueueUrl,
|
|
84
107
|
}, log);
|
|
85
108
|
context.tokowakaClient = client;
|
|
86
109
|
return client;
|
|
@@ -95,10 +118,13 @@ class TokowakaClient {
|
|
|
95
118
|
* @param {Object} config.env - Environment variables (for CDN credentials)
|
|
96
119
|
* @param {Object} [config.dataAccess] - Data access layer
|
|
97
120
|
* (provides Suggestion.saveMany for batch saves)
|
|
121
|
+
* @param {Object} [config.sqs] - SQS client (sendMessage(queueUrl, payload)). Used to offload
|
|
122
|
+
* very large covered-suggestion saves to the import worker instead of saving them directly.
|
|
123
|
+
* @param {string} [config.importWorkerQueueUrl] - Import worker SQS queue URL.
|
|
98
124
|
* @param {Object} log - Logger instance
|
|
99
125
|
*/
|
|
100
126
|
constructor({
|
|
101
|
-
bucketName, previewBucketName, s3Client, env = {}, dataAccess,
|
|
127
|
+
bucketName, previewBucketName, s3Client, env = {}, dataAccess, sqs, importWorkerQueueUrl,
|
|
102
128
|
}, log) {
|
|
103
129
|
this.log = log;
|
|
104
130
|
|
|
@@ -115,6 +141,8 @@ class TokowakaClient {
|
|
|
115
141
|
this.s3Client = s3Client;
|
|
116
142
|
this.env = env;
|
|
117
143
|
this.dataAccess = dataAccess;
|
|
144
|
+
this.sqs = sqs;
|
|
145
|
+
this.importWorkerQueueUrl = importWorkerQueueUrl;
|
|
118
146
|
|
|
119
147
|
this.mapperRegistry = new MapperRegistry(log);
|
|
120
148
|
this.cdnClientRegistry = new CdnClientRegistry(env, log);
|
|
@@ -1058,7 +1086,14 @@ class TokowakaClient {
|
|
|
1058
1086
|
this.log.info(`[edge-rollback] Cleaning ${covered.length} covered suggestion(s) for pattern ${suggestion.getId()} (isDomainWide=${isDomainWide}, fallback=${coveredFallback})`);
|
|
1059
1087
|
}
|
|
1060
1088
|
// eslint-disable-next-line no-await-in-loop, max-len
|
|
1061
|
-
await cleanupCoveredSuggestions(this.dataAccess, covered, coveredFallback, updatedBy, fieldsToStrip, this.log
|
|
1089
|
+
await cleanupCoveredSuggestions(this.dataAccess, covered, coveredFallback, updatedBy, fieldsToStrip, this.log, {
|
|
1090
|
+
sqs: this.sqs,
|
|
1091
|
+
queueUrl: this.importWorkerQueueUrl,
|
|
1092
|
+
siteId: site.getId(),
|
|
1093
|
+
unset: fieldsToStrip,
|
|
1094
|
+
updatedBy: updatedBy ?? coveredFallback,
|
|
1095
|
+
log: this.log,
|
|
1096
|
+
});
|
|
1062
1097
|
}
|
|
1063
1098
|
}
|
|
1064
1099
|
}
|
|
@@ -1485,6 +1520,7 @@ class TokowakaClient {
|
|
|
1485
1520
|
* @param {Array} allSuggestions - Full opportunity suggestion list
|
|
1486
1521
|
* @param {string} updatedBy
|
|
1487
1522
|
* @param {Array} coveredSuggestions - Accumulator (mutated)
|
|
1523
|
+
* @param {string} siteId
|
|
1488
1524
|
* @returns {Promise<void>} Throws on metaconfig upload failure
|
|
1489
1525
|
* @private
|
|
1490
1526
|
*/
|
|
@@ -1497,6 +1533,7 @@ class TokowakaClient {
|
|
|
1497
1533
|
allSuggestions,
|
|
1498
1534
|
updatedBy,
|
|
1499
1535
|
coveredSuggestions,
|
|
1536
|
+
siteId,
|
|
1500
1537
|
) {
|
|
1501
1538
|
const data = suggestion.getData();
|
|
1502
1539
|
const coverageField = data?.isDomainWide ? 'coveredByDomainWide' : 'coveredByPattern';
|
|
@@ -1567,7 +1604,14 @@ class TokowakaClient {
|
|
|
1567
1604
|
cs.setUpdatedBy(updatedBy);
|
|
1568
1605
|
});
|
|
1569
1606
|
try {
|
|
1570
|
-
await saveSuggestions(this.dataAccess, covered
|
|
1607
|
+
await saveSuggestions(this.dataAccess, covered, {
|
|
1608
|
+
sqs: this.sqs,
|
|
1609
|
+
queueUrl: this.importWorkerQueueUrl,
|
|
1610
|
+
siteId,
|
|
1611
|
+
set: { [coverageField]: suggestion.getId() },
|
|
1612
|
+
updatedBy,
|
|
1613
|
+
log: this.log,
|
|
1614
|
+
});
|
|
1571
1615
|
coveredSuggestions.push(...covered);
|
|
1572
1616
|
// eslint-disable-next-line max-len
|
|
1573
1617
|
this.log.info(`[edge-deploy] Marked ${covered.length} suggestions as ${coverageField}=${suggestion.getId()}`);
|
|
@@ -1660,6 +1704,7 @@ class TokowakaClient {
|
|
|
1660
1704
|
allSuggestions,
|
|
1661
1705
|
updatedBy,
|
|
1662
1706
|
coveredSuggestions,
|
|
1707
|
+
site.getId(),
|
|
1663
1708
|
);
|
|
1664
1709
|
deployedPatternSuggestions.push(suggestion);
|
|
1665
1710
|
} catch (error) {
|
|
@@ -92,26 +92,68 @@ export function filterEligibleSuggestions(suggestions, mapper) {
|
|
|
92
92
|
return { eligible, ineligible };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Import worker job type for a bulk suggestion field update (agnostic set/unset by id).
|
|
96
|
+
export const SUGGESTION_BULK_UPDATE_TYPE = 'suggestion-bulk-update';
|
|
97
|
+
|
|
95
98
|
/**
|
|
96
99
|
* Batch-saves suggestions using the optimal strategy based on count.
|
|
97
100
|
*
|
|
98
101
|
* - <= 1700: sequential chunked upsert via saveMany (chunkSize 25).
|
|
99
102
|
* ~4s for 1700 suggestions from the DB layer, but ~11s observed end-to-end
|
|
100
103
|
* from the API due to serialization, network, and Lambda overhead.
|
|
101
|
-
* - > 1700: parallel individual .save() via Promise.allSettled to
|
|
102
|
-
* sequential chunk bottleneck at scale.
|
|
104
|
+
* - > 1700 with no queueContext: parallel individual .save() via Promise.allSettled to
|
|
105
|
+
* avoid sequential chunk bottleneck at scale.
|
|
106
|
+
* - > 1700 with queueContext: enqueues a SUGGESTION_BULK_UPDATE_TYPE job to the import
|
|
107
|
+
* worker instead of saving here (the worker fetches by id and applies
|
|
108
|
+
* queueContext.set/unset itself). Falls back to the Promise.allSettled path above
|
|
109
|
+
* if sqs/queueUrl aren't configured or sendMessage fails, so a queue outage
|
|
110
|
+
* degrades rather than silently drops the save.
|
|
103
111
|
*
|
|
104
112
|
* @param {Object} dataAccess - Data access layer
|
|
105
113
|
* @param {Array} suggestions - Suggestion entities to save
|
|
114
|
+
* @param {Object} [queueContext] - sqs, queueUrl, siteId, set/unset,
|
|
115
|
+
* updatedBy (already resolved), log
|
|
106
116
|
* @returns {Promise<void>}
|
|
107
117
|
*/
|
|
108
118
|
const PARALLEL_SAVE_THRESHOLD = 1700;
|
|
109
119
|
|
|
110
|
-
export async function saveSuggestions(dataAccess, suggestions) {
|
|
120
|
+
export async function saveSuggestions(dataAccess, suggestions, queueContext) {
|
|
111
121
|
if (suggestions.length === 0) {
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
124
|
if (suggestions.length > PARALLEL_SAVE_THRESHOLD) {
|
|
125
|
+
if (queueContext) {
|
|
126
|
+
const {
|
|
127
|
+
sqs, queueUrl, siteId, set, unset, updatedBy, log,
|
|
128
|
+
} = queueContext;
|
|
129
|
+
|
|
130
|
+
if (queueUrl && sqs) {
|
|
131
|
+
try {
|
|
132
|
+
await sqs.sendMessage(queueUrl, {
|
|
133
|
+
type: SUGGESTION_BULK_UPDATE_TYPE,
|
|
134
|
+
siteId,
|
|
135
|
+
suggestionIds: suggestions.map((s) => s.getId()),
|
|
136
|
+
...(set && { set }),
|
|
137
|
+
...(unset && { unset }),
|
|
138
|
+
updatedBy,
|
|
139
|
+
});
|
|
140
|
+
log.info(
|
|
141
|
+
`[suggestion-bulk-update] Queued bulk update for ${suggestions.length} suggestion(s)`,
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
log.error(
|
|
146
|
+
`[suggestion-bulk-update] Failed to queue bulk update, falling back to direct save: ${error.message}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
log.warn(
|
|
151
|
+
'[suggestion-bulk-update] SQS/IMPORT_WORKER_QUEUE_URL not configured; '
|
|
152
|
+
+ `falling back to direct save for ${suggestions.length} suggestion(s)`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
115
157
|
const results = await Promise.allSettled(suggestions.map((s) => s.save()));
|
|
116
158
|
const failed = results.filter((r) => r.status === 'rejected');
|
|
117
159
|
if (failed.length > 0) {
|
|
@@ -150,10 +192,11 @@ export function stripSuggestion(suggestion, actorFallback, updatedBy) {
|
|
|
150
192
|
* @param {string|undefined} updatedBy - Explicit actor
|
|
151
193
|
* @param {string[]} fieldsToStrip - Specific fields to remove
|
|
152
194
|
* @param {Object} log - Logger instance
|
|
195
|
+
* @param {Object} [queueContext] - Passed through to saveSuggestions unchanged
|
|
153
196
|
* @returns {Promise<void>}
|
|
154
197
|
*/
|
|
155
198
|
// eslint-disable-next-line max-len
|
|
156
|
-
export async function cleanupCoveredSuggestions(dataAccess, covered, actorFallback, updatedBy, fieldsToStrip, log) {
|
|
199
|
+
export async function cleanupCoveredSuggestions(dataAccess, covered, actorFallback, updatedBy, fieldsToStrip, log, queueContext) {
|
|
157
200
|
if (covered.length === 0) {
|
|
158
201
|
return;
|
|
159
202
|
}
|
|
@@ -162,7 +205,7 @@ export async function cleanupCoveredSuggestions(dataAccess, covered, actorFallba
|
|
|
162
205
|
cs.setUpdatedBy(updatedBy ?? actorFallback);
|
|
163
206
|
});
|
|
164
207
|
try {
|
|
165
|
-
await saveSuggestions(dataAccess, covered);
|
|
208
|
+
await saveSuggestions(dataAccess, covered, queueContext);
|
|
166
209
|
} catch (error) {
|
|
167
210
|
// eslint-disable-next-line max-len
|
|
168
211
|
log.error(`[edge-rollback-failed] Failed to clean ${covered.length} covered suggestion(s): ${error.message}`);
|