@adobe/spacecat-shared-utils 1.87.1 → 1.89.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/package.json +1 -1
- package/src/constants.js +2 -0
- package/src/index.d.ts +32 -0
- package/src/index.js +4 -3
- package/src/llmo-strategy.d.ts +93 -0
- package/src/llmo-strategy.js +95 -0
- package/src/metrics-store.js +60 -0
- package/src/s3.js +47 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-utils-v1.89.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.88.0...@adobe/spacecat-shared-utils-v1.89.0) (2026-01-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* moved `calculateCpcValue` function to spacecat-shared-utils ([#1048](https://github.com/adobe/spacecat-shared/issues/1048)) ([5006e5b](https://github.com/adobe/spacecat-shared/commit/5006e5be51c8959f2e6d72664251e1e0264d44a1))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-utils-v1.88.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.87.1...@adobe/spacecat-shared-utils-v1.88.0) (2026-01-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* new json obj in S3 for storing opportunity workspace data ([#1278](https://github.com/adobe/spacecat-shared/issues/1278)) ([68b0db0](https://github.com/adobe/spacecat-shared/commit/68b0db054d9eb43d485cdec9dd55059f8e07318e))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-utils-v1.87.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.87.0...@adobe/spacecat-shared-utils-v1.87.1) (2026-01-19)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { AUTHORING_TYPES, DELIVERY_TYPES } from './aem.js';
|
|
|
17
17
|
|
|
18
18
|
export { OPPORTUNITY_TYPES } from './constants.js';
|
|
19
19
|
|
|
20
|
+
export const DEFAULT_CPC_VALUE: number;
|
|
21
|
+
|
|
20
22
|
/** UTILITY FUNCTIONS */
|
|
21
23
|
export function arrayEquals<T>(a: T[], b: T[]): boolean;
|
|
22
24
|
|
|
@@ -287,6 +289,35 @@ export function getStoredMetrics(config: object, context: object):
|
|
|
287
289
|
*/
|
|
288
290
|
export function storeMetrics(content: object, config: object, context: object): Promise<string>;
|
|
289
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Retrieves an object from S3 by its key and returns its JSON parsed content.
|
|
294
|
+
* If the object is not JSON, returns the raw body.
|
|
295
|
+
* If the object is not found, returns null.
|
|
296
|
+
* @param s3Client - The S3 client
|
|
297
|
+
* @param bucketName - The name of the S3 bucket
|
|
298
|
+
* @param key - The key of the S3 object
|
|
299
|
+
* @param log - A logger instance
|
|
300
|
+
* @returns The content of the S3 object or null if not found
|
|
301
|
+
*/
|
|
302
|
+
export function getObjectFromKey(
|
|
303
|
+
s3Client: any,
|
|
304
|
+
bucketName: string,
|
|
305
|
+
key: string,
|
|
306
|
+
log: any
|
|
307
|
+
): Promise<any | null>;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Fetches the organic traffic data for a site from S3 and calculates the CPC value
|
|
311
|
+
* @param context - Context object
|
|
312
|
+
* @param context.env - Environment variables
|
|
313
|
+
* @param context.env.S3_IMPORTER_BUCKET_NAME - S3 importer bucket name
|
|
314
|
+
* @param context.s3Client - S3 client
|
|
315
|
+
* @param context.log - Logger
|
|
316
|
+
* @param siteId - The site ID
|
|
317
|
+
* @returns CPC value in dollars
|
|
318
|
+
*/
|
|
319
|
+
export function calculateCPCValue(context: object, siteId: string): Promise<number>;
|
|
320
|
+
|
|
290
321
|
export function s3Wrapper(fn: (request: object, context: object) => Promise<Response>):
|
|
291
322
|
(request: object, context: object) => Promise<Response>;
|
|
292
323
|
|
|
@@ -327,6 +358,7 @@ export function extractUrlsFromOpportunity(opts: {
|
|
|
327
358
|
}): string[];
|
|
328
359
|
|
|
329
360
|
export * as llmoConfig from './llmo-config.js';
|
|
361
|
+
export * as llmoStrategy from './llmo-strategy.js';
|
|
330
362
|
export * as schemas from './schemas.js';
|
|
331
363
|
|
|
332
364
|
export { type detectLocale } from './locale-detect/index.js';
|
package/src/index.js
CHANGED
|
@@ -76,11 +76,11 @@ export {
|
|
|
76
76
|
extractUrlsFromSuggestion,
|
|
77
77
|
} from './url-extractors.js';
|
|
78
78
|
|
|
79
|
-
export { getStoredMetrics, storeMetrics } from './metrics-store.js';
|
|
79
|
+
export { getStoredMetrics, storeMetrics, calculateCPCValue } from './metrics-store.js';
|
|
80
80
|
|
|
81
|
-
export { s3Wrapper } from './s3.js';
|
|
81
|
+
export { s3Wrapper, getObjectFromKey } from './s3.js';
|
|
82
82
|
|
|
83
|
-
export { OPPORTUNITY_TYPES } from './constants.js';
|
|
83
|
+
export { OPPORTUNITY_TYPES, DEFAULT_CPC_VALUE } from './constants.js';
|
|
84
84
|
|
|
85
85
|
export { fetch } from './adobe-fetch.js';
|
|
86
86
|
export { tracingFetch, SPACECAT_USER_AGENT } from './tracing-fetch.js';
|
|
@@ -107,6 +107,7 @@ export { detectAEMVersion, DELIVERY_TYPES, AUTHORING_TYPES } from './aem.js';
|
|
|
107
107
|
export { determineAEMCSPageId, getPageEditUrl } from './aem-content-api-utils.js';
|
|
108
108
|
|
|
109
109
|
export * as llmoConfig from './llmo-config.js';
|
|
110
|
+
export * as llmoStrategy from './llmo-strategy.js';
|
|
110
111
|
export * as schemas from './schemas.js';
|
|
111
112
|
|
|
112
113
|
export { detectLocale } from './locale-detect/locale-detect.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the S3 path for the strategy file.
|
|
17
|
+
* @param siteId - The ID of the site.
|
|
18
|
+
* @returns The strategy file path.
|
|
19
|
+
*/
|
|
20
|
+
export function strategyPath(siteId: string): string;
|
|
21
|
+
|
|
22
|
+
export interface ReadStrategyOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Optional version ID of the strategy to read.
|
|
25
|
+
* Defaults to the latest version.
|
|
26
|
+
*/
|
|
27
|
+
version?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional S3 bucket name.
|
|
30
|
+
*/
|
|
31
|
+
s3Bucket?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ReadStrategyResult {
|
|
35
|
+
/**
|
|
36
|
+
* The strategy data, or null if it doesn't exist.
|
|
37
|
+
*/
|
|
38
|
+
data: object | null;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the strategy exists.
|
|
41
|
+
*/
|
|
42
|
+
exists: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The version ID of the strategy, if it exists.
|
|
45
|
+
*/
|
|
46
|
+
version?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads the strategy JSON for a given site.
|
|
51
|
+
* Returns null if the strategy does not exist.
|
|
52
|
+
*
|
|
53
|
+
* @param siteId - The ID of the site.
|
|
54
|
+
* @param s3Client - The S3 client to use for reading the strategy.
|
|
55
|
+
* @param options - Optional configuration.
|
|
56
|
+
* @returns The strategy data, existence flag, and version ID.
|
|
57
|
+
* @throws Error if reading the strategy fails for reasons other than it not existing.
|
|
58
|
+
*/
|
|
59
|
+
export function readStrategy(
|
|
60
|
+
siteId: string,
|
|
61
|
+
s3Client: S3Client,
|
|
62
|
+
options?: ReadStrategyOptions
|
|
63
|
+
): Promise<ReadStrategyResult>;
|
|
64
|
+
|
|
65
|
+
export interface WriteStrategyOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Optional S3 bucket name.
|
|
68
|
+
*/
|
|
69
|
+
s3Bucket?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WriteStrategyResult {
|
|
73
|
+
/**
|
|
74
|
+
* The version ID of the written strategy.
|
|
75
|
+
*/
|
|
76
|
+
version: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Writes the strategy JSON for a given site.
|
|
81
|
+
*
|
|
82
|
+
* @param siteId - The ID of the site.
|
|
83
|
+
* @param data - The data object to write (any valid JSON).
|
|
84
|
+
* @param s3Client - The S3 client to use for writing the strategy.
|
|
85
|
+
* @param options - Optional configuration.
|
|
86
|
+
* @returns The version of the strategy written.
|
|
87
|
+
*/
|
|
88
|
+
export function writeStrategy(
|
|
89
|
+
siteId: string,
|
|
90
|
+
data: object,
|
|
91
|
+
s3Client: S3Client,
|
|
92
|
+
options?: WriteStrategyOptions
|
|
93
|
+
): Promise<WriteStrategyResult>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @import { S3Client } from "@aws-sdk/client-s3"
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} siteId The ID of the site to get the strategy file path for.
|
|
21
|
+
* @returns {string} The strategy file path for the given site ID.
|
|
22
|
+
*/
|
|
23
|
+
export function strategyPath(siteId) {
|
|
24
|
+
return `workspace/llmo/${siteId}/strategy.json`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads the strategy JSON for a given site.
|
|
29
|
+
* Returns null if the strategy does not exist.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} siteId The ID of the site.
|
|
32
|
+
* @param {S3Client} s3Client The S3 client to use for reading the strategy.
|
|
33
|
+
* @param {object} [options]
|
|
34
|
+
* @param {string} [options.version] Optional version ID of the strategy to read.
|
|
35
|
+
* Defaults to the latest version.
|
|
36
|
+
* @param {string} [options.s3Bucket] Optional S3 bucket name.
|
|
37
|
+
* @returns {Promise<{data: object | null, exists: boolean, version?: string}>} The strategy data,
|
|
38
|
+
* a flag indicating if it existed, and the version ID if it exists.
|
|
39
|
+
* @throws {Error} If reading the strategy fails for reasons other than it not existing.
|
|
40
|
+
*/
|
|
41
|
+
export async function readStrategy(siteId, s3Client, options) {
|
|
42
|
+
const version = options?.version;
|
|
43
|
+
const s3Bucket = options?.s3Bucket || process.env.S3_BUCKET_NAME;
|
|
44
|
+
|
|
45
|
+
const getObjectCommand = new GetObjectCommand({
|
|
46
|
+
Bucket: s3Bucket,
|
|
47
|
+
Key: strategyPath(siteId),
|
|
48
|
+
VersionId: version,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await s3Client.send(getObjectCommand);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (e.name === 'NoSuchKey' || e.name === 'NotFound') {
|
|
56
|
+
// Strategy does not exist yet. Return null.
|
|
57
|
+
return { data: null, exists: false, version: undefined };
|
|
58
|
+
}
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const body = res.Body;
|
|
63
|
+
if (!body) {
|
|
64
|
+
throw new Error('Strategy body is empty');
|
|
65
|
+
}
|
|
66
|
+
const text = await body.transformToString();
|
|
67
|
+
const data = JSON.parse(text);
|
|
68
|
+
return { data, exists: true, version: res.VersionId || undefined };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Writes the strategy JSON for a given site.
|
|
73
|
+
* @param {string} siteId The ID of the site.
|
|
74
|
+
* @param {object} data The data object to write (any valid JSON).
|
|
75
|
+
* @param {S3Client} s3Client The S3 client to use for writing the strategy.
|
|
76
|
+
* @param {object} [options]
|
|
77
|
+
* @param {string} [options.s3Bucket] Optional S3 bucket name.
|
|
78
|
+
* @returns {Promise<{ version: string }>} The version of the strategy written.
|
|
79
|
+
*/
|
|
80
|
+
export async function writeStrategy(siteId, data, s3Client, options) {
|
|
81
|
+
const s3Bucket = options?.s3Bucket || process.env.S3_BUCKET_NAME;
|
|
82
|
+
|
|
83
|
+
const putObjectCommand = new PutObjectCommand({
|
|
84
|
+
Bucket: s3Bucket,
|
|
85
|
+
Key: strategyPath(siteId),
|
|
86
|
+
Body: JSON.stringify(data, null, 2),
|
|
87
|
+
ContentType: 'application/json',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const res = await s3Client.send(putObjectCommand);
|
|
91
|
+
if (!res.VersionId) {
|
|
92
|
+
throw new Error('Failed to get version ID after writing strategy');
|
|
93
|
+
}
|
|
94
|
+
return { version: res.VersionId };
|
|
95
|
+
}
|
package/src/metrics-store.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
13
|
+
import { getObjectFromKey } from './s3.js';
|
|
14
|
+
import { DEFAULT_CPC_VALUE } from './constants.js';
|
|
13
15
|
|
|
14
16
|
function createFilePath({ siteId, source, metric }) {
|
|
15
17
|
if (!siteId) {
|
|
@@ -80,3 +82,61 @@ export async function storeMetrics(content, config, context) {
|
|
|
80
82
|
throw new Error(`Failed to upload metrics to ${filePath}, error: ${e.message}`);
|
|
81
83
|
}
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fetches the organic traffic data for a site from S3 and calculate the CPC value as per
|
|
88
|
+
* https://wiki.corp.adobe.com/pages/viewpage.action?spaceKey=AEMSites&title=Success+Studio+Projected+Business+Impact+Metrics#SuccessStudioProjectedBusinessImpactMetrics-IdentifyingCPCvalueforadomain
|
|
89
|
+
* @param context
|
|
90
|
+
* @param siteId
|
|
91
|
+
* @returns {object} Object containing either { success: true, value: number } on success
|
|
92
|
+
* or { success: false, reason: string, value: number } on failure
|
|
93
|
+
*/
|
|
94
|
+
export async function calculateCPCValue(context, siteId) {
|
|
95
|
+
if (!context?.env?.S3_IMPORTER_BUCKET_NAME) {
|
|
96
|
+
throw new Error('S3 importer bucket name is required');
|
|
97
|
+
}
|
|
98
|
+
if (!context.s3Client) {
|
|
99
|
+
throw new Error('S3 client is required');
|
|
100
|
+
}
|
|
101
|
+
if (!context.log) {
|
|
102
|
+
throw new Error('Logger is required');
|
|
103
|
+
}
|
|
104
|
+
if (!siteId) {
|
|
105
|
+
throw new Error('SiteId is required');
|
|
106
|
+
}
|
|
107
|
+
const { s3Client, log } = context;
|
|
108
|
+
const bucketName = context.env.S3_IMPORTER_BUCKET_NAME;
|
|
109
|
+
const key = `metrics/${siteId}/ahrefs/organic-traffic.json`;
|
|
110
|
+
try {
|
|
111
|
+
const organicTrafficData = await getObjectFromKey(s3Client, bucketName, key, log);
|
|
112
|
+
if (!Array.isArray(organicTrafficData) || organicTrafficData.length === 0) {
|
|
113
|
+
log.warn(`Organic traffic data not available for ${siteId}. Using Default CPC value.`);
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
reason: 'Organic traffic data not available',
|
|
117
|
+
value: DEFAULT_CPC_VALUE,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const lastTraffic = organicTrafficData.at(-1);
|
|
121
|
+
if (!lastTraffic.cost || !lastTraffic.value) {
|
|
122
|
+
log.warn(`Invalid organic traffic data present for ${siteId} - cost:${lastTraffic.cost} value:${lastTraffic.value}, Using Default CPC value.`);
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
reason: 'Invalid organic traffic data',
|
|
126
|
+
value: DEFAULT_CPC_VALUE,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// dividing by 100 for cents to dollar conversion
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
value: lastTraffic.cost / lastTraffic.value / 100,
|
|
133
|
+
};
|
|
134
|
+
} catch (err) {
|
|
135
|
+
log.error(`Error fetching organic traffic data for site ${siteId}. Using Default CPC value.`, err);
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
reason: 'Error fetching organic traffic data',
|
|
139
|
+
value: DEFAULT_CPC_VALUE,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/s3.js
CHANGED
|
@@ -10,9 +10,55 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { S3Client } from '@aws-sdk/client-s3';
|
|
13
|
+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
14
14
|
import { instrumentAWSClient } from './xray.js';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves an object from S3 by its key and returns its JSON parsed content.
|
|
18
|
+
* If the object is not JSON, returns the raw body.
|
|
19
|
+
* If the object is not found, returns null.
|
|
20
|
+
* @param {import('@aws-sdk/client-s3').S3Client} s3Client - an S3 client
|
|
21
|
+
* @param {string} bucketName - the name of the S3 bucket
|
|
22
|
+
* @param {string} key - the key of the S3 object
|
|
23
|
+
* @param {import('@azure/logger').Logger} log - a logger instance
|
|
24
|
+
* @returns {Promise<import('@aws-sdk/client-s3').GetObjectOutput['Body'] | null>}
|
|
25
|
+
* - the content of the S3 object
|
|
26
|
+
*/
|
|
27
|
+
export async function getObjectFromKey(s3Client, bucketName, key, log) {
|
|
28
|
+
if (!s3Client || !bucketName || !key) {
|
|
29
|
+
log.error(
|
|
30
|
+
'Invalid input parameters in getObjectFromKey: ensure s3Client, bucketName, and key are provided.',
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const command = new GetObjectCommand({
|
|
35
|
+
Bucket: bucketName,
|
|
36
|
+
Key: key,
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const response = await s3Client.send(command);
|
|
40
|
+
const contentType = response.ContentType;
|
|
41
|
+
const body = await response.Body.transformToString();
|
|
42
|
+
|
|
43
|
+
if (contentType && contentType.includes('application/json')) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(body);
|
|
46
|
+
} catch (parseError) {
|
|
47
|
+
log.error(`Unable to parse content for key ${key}`, parseError);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Always return body for non-JSON content types
|
|
52
|
+
return body;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.error(
|
|
55
|
+
`Error while fetching S3 object from bucket ${bucketName} using key ${key}`,
|
|
56
|
+
err,
|
|
57
|
+
);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
/**
|
|
17
63
|
* Adds an S3Client instance and bucket to the context.
|
|
18
64
|
*
|