1
1
/*
2
- * Copyright 2024 Adobe. All rights reserved.
2
+ * Copyright 2025 Adobe. All rights reserved.
3
3
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
* you may not use this file except in compliance with the License. You may obtain a copy
5
5
* of the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -14,17 +14,31 @@ import { callPreviewPublish } from '../../utils/admin.js';
14
14
import { BatchProcessor } from '../../utils/batch.js' ;
15
15
import { errorWithResponse } from '../../utils/http.js' ;
16
16
17
- /* eslint-disable no-await-in-loop, max-len */
18
-
19
17
export default class StorageClient {
20
18
/**
21
- * Constructs a new StorageClient instance.
22
- * @param {Context } ctx - The context object
23
- * @param {Config } config - The configuration object.
19
+ * @param {Context } ctx
20
+ * @returns {StorageClient }
21
+ */
22
+ static fromContext ( ctx ) {
23
+ if ( ! ctx . attributes . storageClient ) {
24
+ ctx . attributes . storageClient = new StorageClient ( ctx ) ;
25
+ }
26
+ return ctx . attributes . storageClient ;
27
+ }
28
+
29
+ /** @type {Context } */
30
+ ctx = undefined ;
31
+
32
+ /**
33
+ * @param {Context } ctx
24
34
*/
25
- constructor ( ctx , config ) {
35
+ constructor ( ctx ) {
26
36
this . ctx = ctx ;
27
- this . config = config ;
37
+ }
38
+
39
+ /** @type {Config } */
40
+ get config ( ) {
41
+ return this . ctx . config ;
28
42
}
29
43
30
44
/**
@@ -33,11 +47,21 @@ export default class StorageClient {
33
47
* @returns {Promise<Product> } - A promise that resolves to the product.
34
48
*/
35
49
async fetchProduct ( sku ) {
36
- const { log } = this . ctx ;
37
- const key = `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /products/${ sku } .json` ;
50
+ const {
51
+ log,
52
+ env,
53
+ config : {
54
+ org,
55
+ site,
56
+ storeCode,
57
+ storeViewCode,
58
+ } ,
59
+ } = this . ctx ;
60
+
61
+ const key = `${ org } /${ site } /${ storeCode } /${ storeViewCode } /products/${ sku } .json` ;
38
62
log . debug ( 'Fetching product from R2:' , key ) ;
39
63
40
- const object = await this . ctx . env . CATALOG_BUCKET . get ( key ) ;
64
+ const object = await env . CATALOG_BUCKET . get ( key ) ;
41
65
if ( ! object ) {
42
66
// Product not found in R2
43
67
throw errorWithResponse ( 404 , 'Product not found' ) ;
@@ -74,29 +98,38 @@ export default class StorageClient {
74
98
* @returns {Promise<Partial<BatchResult>[]> } - Resolves with an array of save results.
75
99
*/
76
100
async storeProductsBatch ( batch ) {
101
+ const {
102
+ env,
103
+ log,
104
+ config : {
105
+ org,
106
+ site,
107
+ storeCode,
108
+ storeViewCode,
109
+ } ,
110
+ } = this . ctx ;
111
+
77
112
const storePromises = batch . map ( async ( product ) => {
78
- const { sku, name } = product ;
79
- const key = `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /products/${ sku } .json` ;
113
+ const { sku, name, urlKey } = product ;
114
+ const key = `${ org } /${ site } /${ storeCode } /${ storeViewCode } /products/${ sku } .json` ;
80
115
const body = JSON . stringify ( product ) ;
81
116
82
117
try {
83
118
const customMetadata = { sku, name } ;
84
-
85
- const { urlKey } = product ;
86
119
if ( urlKey ) {
87
120
customMetadata . urlKey = urlKey ;
88
121
}
89
122
90
123
// Attempt to save the product
91
- const putResponse = await this . ctx . env . CATALOG_BUCKET . put ( key , body , {
124
+ const putResponse = await env . CATALOG_BUCKET . put ( key , body , {
92
125
httpMetadata : { contentType : 'application/json' } ,
93
126
customMetadata,
94
127
} ) ;
95
128
96
129
// If urlKey exists, save the urlKey metadata
97
130
if ( urlKey ) {
98
- const metadataKey = `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /urlkeys/${ urlKey } ` ;
99
- await this . ctx . env . CATALOG_BUCKET . put ( metadataKey , '' , {
131
+ const metadataKey = `${ org } /${ site } /${ storeCode } /${ storeViewCode } /urlkeys/${ urlKey } ` ;
132
+ await env . CATALOG_BUCKET . put ( metadataKey , '' , {
100
133
httpMetadata : { contentType : 'application/octet-stream' } ,
101
134
customMetadata,
102
135
} ) ;
@@ -116,7 +149,7 @@ export default class StorageClient {
116
149
117
150
return result ;
118
151
} catch ( error ) {
119
- this . ctx . log . error ( `Error storing product SKU: ${ sku } :` , error ) ;
152
+ log . error ( `Error storing product SKU: ${ sku } :` , error ) ;
120
153
return {
121
154
sku,
122
155
status : error . code || 500 ,
@@ -152,10 +185,16 @@ export default class StorageClient {
152
185
* @returns {Promise<Partial<BatchResult>[]> } - Resolves with an array of deletion results.
153
186
*/
154
187
async deleteProductsBatch ( batch ) {
155
- const { log, env } = this . ctx ;
156
188
const {
157
- org, site, storeCode, storeViewCode,
158
- } = this . config ;
189
+ log,
190
+ env,
191
+ config : {
192
+ org,
193
+ site,
194
+ storeCode,
195
+ storeViewCode,
196
+ } ,
197
+ } = this . ctx ;
159
198
160
199
const deletionPromises = batch . map ( async ( sku ) => {
161
200
try {
@@ -178,7 +217,7 @@ export default class StorageClient {
178
217
await env . CATALOG_BUCKET . delete ( urlKeyPath ) ;
179
218
}
180
219
181
- const adminResponse = await callPreviewPublish ( this . config , 'DELETE' , sku , urlKey ) ;
220
+ const adminResponse = await callPreviewPublish ( this . ctx . config , 'DELETE' , sku , urlKey ) ;
182
221
/**
183
222
* @type {Partial<BatchResult> }
184
223
*/
@@ -209,9 +248,19 @@ export default class StorageClient {
209
248
* @returns {Promise<string> } - A promise that resolves to the SKU.
210
249
*/
211
250
async lookupSku ( urlKey ) {
251
+ const {
252
+ env,
253
+ config : {
254
+ org,
255
+ site,
256
+ storeCode,
257
+ storeViewCode,
258
+ } ,
259
+ } = this . ctx ;
260
+
212
261
// Make a HEAD request to retrieve the SKU from metadata based on the URL key
213
- const urlKeyPath = `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /urlkeys/${ urlKey } ` ;
214
- const headResponse = await this . ctx . env . CATALOG_BUCKET . head ( urlKeyPath ) ;
262
+ const urlKeyPath = `${ org } /${ site } /${ storeCode } /${ storeViewCode } /urlkeys/${ urlKey } ` ;
263
+ const headResponse = await env . CATALOG_BUCKET . head ( urlKeyPath ) ;
215
264
216
265
if ( ! headResponse || ! headResponse . customMetadata ?. sku ) {
217
266
// SKU not found for the provided URL key
@@ -227,10 +276,20 @@ export default class StorageClient {
227
276
* @returns {Promise<string | undefined> } - A promise that resolves to the URL key or undefined.
228
277
*/
229
278
async lookupUrlKey ( sku ) {
279
+ const {
280
+ env,
281
+ config : {
282
+ org,
283
+ site,
284
+ storeCode,
285
+ storeViewCode,
286
+ } ,
287
+ } = this . ctx ;
288
+
230
289
// Construct the path to the product JSON file
231
- const productKey = `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /products/${ sku } .json` ;
290
+ const productKey = `${ org } /${ site } /${ storeCode } /${ storeViewCode } /products/${ sku } .json` ;
232
291
233
- const headResponse = await this . ctx . env . CATALOG_BUCKET . head ( productKey ) ;
292
+ const headResponse = await env . CATALOG_BUCKET . head ( productKey ) ;
234
293
if ( ! headResponse || ! headResponse . customMetadata ) {
235
294
return undefined ;
236
295
}
@@ -249,9 +308,18 @@ export default class StorageClient {
249
308
* @returns {Promise<Product[]> } - A promise that resolves to the products.
250
309
*/
251
310
async listAllProducts ( ) {
252
- const bucket = this . ctx . env . CATALOG_BUCKET ;
253
- const listResponse = await bucket . list ( {
254
- prefix : `${ this . config . org } /${ this . config . site } /${ this . config . storeCode } /${ this . config . storeViewCode } /products/` ,
311
+ const {
312
+ env,
313
+ config : {
314
+ org,
315
+ site,
316
+ storeCode,
317
+ storeViewCode,
318
+ } ,
319
+ } = this . ctx ;
320
+
321
+ const listResponse = await env . CATALOG_BUCKET . list ( {
322
+ prefix : `${ org } /${ site } /${ storeCode } /${ storeViewCode } /products/` ,
255
323
} ) ;
256
324
const files = listResponse . objects ;
257
325
@@ -271,18 +339,19 @@ export default class StorageClient {
271
339
272
340
// Process each chunk sequentially
273
341
for ( const chunk of fileChunks ) {
342
+ // eslint-disable-next-line no-await-in-loop
274
343
const chunkResults = await Promise . all (
275
344
chunk . map ( async ( file ) => {
276
345
const objectKey = file . key ;
277
346
278
- const headResponse = await bucket . head ( objectKey ) ;
347
+ const headResponse = await env . CATALOG_BUCKET . head ( objectKey ) ;
279
348
if ( headResponse ) {
280
349
const { customMetadata } = headResponse ;
281
350
const { sku } = customMetadata ;
282
351
return {
283
352
...customMetadata ,
284
353
links : {
285
- product : `${ this . ctx . url . origin } /${ this . config . org } /${ this . config . site } /catalog/${ this . config . storeCode } /${ this . config . storeViewCode } /product/${ sku } ` ,
354
+ product : `${ this . ctx . url . origin } /${ org } /${ site } /catalog/${ storeCode } /${ storeViewCode } /product/${ sku } ` ,
286
355
} ,
287
356
} ;
288
357
} else {
0 commit comments