@memberjunction/storage 2.103.0 → 2.104.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/dist/config.d.ts +694 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +216 -0
- package/dist/config.js.map +1 -0
- package/dist/drivers/AWSFileStorage.d.ts.map +1 -1
- package/dist/drivers/AWSFileStorage.js +9 -5
- package/dist/drivers/AWSFileStorage.js.map +1 -1
- package/dist/drivers/AzureFileStorage.d.ts.map +1 -1
- package/dist/drivers/AzureFileStorage.js +7 -3
- package/dist/drivers/AzureFileStorage.js.map +1 -1
- package/dist/drivers/BoxFileStorage.d.ts +22 -45
- package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
- package/dist/drivers/BoxFileStorage.js +285 -326
- package/dist/drivers/BoxFileStorage.js.map +1 -1
- package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
- package/dist/drivers/DropboxFileStorage.js +8 -5
- package/dist/drivers/DropboxFileStorage.js.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.js +23 -9
- package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
- package/dist/drivers/GoogleFileStorage.d.ts.map +1 -1
- package/dist/drivers/GoogleFileStorage.js +26 -3
- package/dist/drivers/GoogleFileStorage.js.map +1 -1
- package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
- package/dist/drivers/SharePointFileStorage.js +10 -6
- package/dist/drivers/SharePointFileStorage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
|
@@ -37,7 +37,9 @@ const global_1 = require("@memberjunction/global");
|
|
|
37
37
|
const env = __importStar(require("env-var"));
|
|
38
38
|
const mime = __importStar(require("mime-types"));
|
|
39
39
|
const FileStorageBase_1 = require("../generic/FileStorageBase");
|
|
40
|
-
const
|
|
40
|
+
const box_node_sdk_1 = require("box-node-sdk");
|
|
41
|
+
const stream_1 = require("stream");
|
|
42
|
+
const config_1 = require("../config");
|
|
41
43
|
/**
|
|
42
44
|
* FileStorageBase implementation for Box.com cloud storage
|
|
43
45
|
*
|
|
@@ -126,6 +128,10 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
126
128
|
* Box enterprise ID for JWT auth
|
|
127
129
|
*/
|
|
128
130
|
_enterpriseId;
|
|
131
|
+
/**
|
|
132
|
+
* Box SDK client for making API calls
|
|
133
|
+
*/
|
|
134
|
+
_client;
|
|
129
135
|
/**
|
|
130
136
|
* Creates a new BoxFileStorage instance
|
|
131
137
|
*
|
|
@@ -136,24 +142,25 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
136
142
|
*/
|
|
137
143
|
constructor() {
|
|
138
144
|
super();
|
|
145
|
+
// Try to get config from centralized configuration
|
|
146
|
+
const config = (0, config_1.getProviderConfig)('box');
|
|
139
147
|
// Box auth can be via access token or refresh token
|
|
140
|
-
this._accessToken = env.get('STORAGE_BOX_ACCESS_TOKEN').asString();
|
|
141
|
-
this._refreshToken = env.get('STORAGE_BOX_REFRESH_TOKEN').asString();
|
|
142
|
-
this._clientId = env.get('STORAGE_BOX_CLIENT_ID').asString();
|
|
143
|
-
this._clientSecret = env.get('STORAGE_BOX_CLIENT_SECRET').asString();
|
|
144
|
-
this._enterpriseId = env.get('STORAGE_BOX_ENTERPRISE_ID').asString();
|
|
148
|
+
this._accessToken = config?.accessToken || env.get('STORAGE_BOX_ACCESS_TOKEN').asString();
|
|
149
|
+
this._refreshToken = config?.refreshToken || env.get('STORAGE_BOX_REFRESH_TOKEN').asString();
|
|
150
|
+
this._clientId = config?.clientID || env.get('STORAGE_BOX_CLIENT_ID').asString();
|
|
151
|
+
this._clientSecret = config?.clientSecret || env.get('STORAGE_BOX_CLIENT_SECRET').asString();
|
|
152
|
+
this._enterpriseId = config?.enterpriseID || env.get('STORAGE_BOX_ENTERPRISE_ID').asString();
|
|
145
153
|
if (this._refreshToken && (!this._clientId || !this._clientSecret)) {
|
|
146
154
|
throw new Error('Box storage with refresh token requires STORAGE_BOX_CLIENT_ID and STORAGE_BOX_CLIENT_SECRET');
|
|
147
155
|
}
|
|
148
156
|
// Root folder ID, optional (defaults to '0' which is root)
|
|
149
|
-
this._rootFolderId = env.get('STORAGE_BOX_ROOT_FOLDER_ID').default('0').asString();
|
|
157
|
+
this._rootFolderId = config?.rootFolderID || env.get('STORAGE_BOX_ROOT_FOLDER_ID').default('0').asString();
|
|
150
158
|
}
|
|
151
159
|
/**
|
|
152
160
|
* Initializes the Box storage driver
|
|
153
161
|
*
|
|
154
|
-
* This method must be called after creating a BoxFileStorage instance
|
|
155
|
-
*
|
|
156
|
-
* initial access token required for API calls.
|
|
162
|
+
* This method must be called after creating a BoxFileStorage instance.
|
|
163
|
+
* It initializes the Box SDK and creates the client for API calls.
|
|
157
164
|
*
|
|
158
165
|
* @returns A Promise that resolves when initialization is complete
|
|
159
166
|
*
|
|
@@ -165,9 +172,24 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
165
172
|
* ```
|
|
166
173
|
*/
|
|
167
174
|
async initialize() {
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
// Get access token if not provided
|
|
176
|
+
if (!this._accessToken) {
|
|
177
|
+
if (this._clientId && this._clientSecret && this._enterpriseId) {
|
|
178
|
+
// Use client credentials to get token
|
|
179
|
+
this._accessToken = await this._getAccessToken();
|
|
180
|
+
}
|
|
181
|
+
else if (this._refreshToken) {
|
|
182
|
+
// Use refresh token to get access token
|
|
183
|
+
const tokenData = await this._refreshAccessToken();
|
|
184
|
+
this._accessToken = tokenData.access_token;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new Error('Box storage requires either access token, refresh token, or client credentials');
|
|
188
|
+
}
|
|
170
189
|
}
|
|
190
|
+
// Initialize Box client with developer token auth
|
|
191
|
+
const auth = new box_node_sdk_1.BoxDeveloperTokenAuth({ token: this._accessToken });
|
|
192
|
+
this._client = new box_node_sdk_1.BoxClient({ auth });
|
|
171
193
|
}
|
|
172
194
|
/**
|
|
173
195
|
* Obtains an access token using client credentials flow
|
|
@@ -176,10 +198,10 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
176
198
|
* flow (JWT) with the enterprise as the subject.
|
|
177
199
|
*
|
|
178
200
|
* @private
|
|
179
|
-
* @returns A Promise that resolves
|
|
201
|
+
* @returns A Promise that resolves with the access token
|
|
180
202
|
* @throws Error if token acquisition fails
|
|
181
203
|
*/
|
|
182
|
-
async
|
|
204
|
+
async _getAccessToken() {
|
|
183
205
|
try {
|
|
184
206
|
const response = await fetch('https://api.box.com/oauth2/token', {
|
|
185
207
|
method: 'POST',
|
|
@@ -198,15 +220,49 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
198
220
|
throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
|
|
199
221
|
}
|
|
200
222
|
const tokenData = await response.json();
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
223
|
+
this._accessToken = tokenData.access_token;
|
|
224
|
+
this._tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000) - 60000;
|
|
225
|
+
return tokenData.access_token;
|
|
204
226
|
}
|
|
205
227
|
catch (error) {
|
|
206
228
|
console.error('Error getting Box access token', error);
|
|
207
229
|
throw new Error('Failed to authenticate with Box: ' + (error instanceof Error ? error.message : String(error)));
|
|
208
230
|
}
|
|
209
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Refreshes the access token using the refresh token
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
* @returns A Promise that resolves with the token data
|
|
237
|
+
*/
|
|
238
|
+
async _refreshAccessToken() {
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch('https://api.box.com/oauth2/token', {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: {
|
|
243
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
244
|
+
},
|
|
245
|
+
body: new URLSearchParams({
|
|
246
|
+
grant_type: 'refresh_token',
|
|
247
|
+
refresh_token: this._refreshToken,
|
|
248
|
+
client_id: this._clientId,
|
|
249
|
+
client_secret: this._clientSecret
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`);
|
|
254
|
+
}
|
|
255
|
+
const data = await response.json();
|
|
256
|
+
this._accessToken = data.access_token;
|
|
257
|
+
this._refreshToken = data.refresh_token || this._refreshToken;
|
|
258
|
+
this._tokenExpiresAt = Date.now() + (data.expires_in * 1000) - 60000;
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.error('Error refreshing Box access token', error);
|
|
263
|
+
throw new Error('Failed to refresh token: ' + (error instanceof Error ? error.message : String(error)));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
210
266
|
/**
|
|
211
267
|
* Returns the current Box API access token
|
|
212
268
|
*
|
|
@@ -223,126 +279,20 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
223
279
|
* ```
|
|
224
280
|
*/
|
|
225
281
|
async AccessToken() {
|
|
226
|
-
|
|
227
|
-
return this._accessToken;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Ensures a valid access token is available for API requests
|
|
231
|
-
*
|
|
232
|
-
* This method checks if the current token is valid, and if not, attempts
|
|
233
|
-
* to refresh or obtain a new token using the configured authentication method.
|
|
234
|
-
*
|
|
235
|
-
* @private
|
|
236
|
-
* @returns A Promise that resolves to a valid access token
|
|
237
|
-
* @throws Error if no valid token can be obtained
|
|
238
|
-
*/
|
|
239
|
-
async _ensureValidToken() {
|
|
240
|
-
// If we have a valid token, use it
|
|
282
|
+
// If we have a valid token, return it
|
|
241
283
|
if (this._accessToken && Date.now() < this._tokenExpiresAt) {
|
|
242
284
|
return this._accessToken;
|
|
243
285
|
}
|
|
244
|
-
//
|
|
286
|
+
// Otherwise refresh using SDK's built-in token management
|
|
245
287
|
if (this._refreshToken && this._clientId && this._clientSecret) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
headers: {
|
|
250
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
251
|
-
},
|
|
252
|
-
body: new URLSearchParams({
|
|
253
|
-
grant_type: 'refresh_token',
|
|
254
|
-
refresh_token: this._refreshToken,
|
|
255
|
-
client_id: this._clientId,
|
|
256
|
-
client_secret: this._clientSecret
|
|
257
|
-
})
|
|
258
|
-
});
|
|
259
|
-
if (!response.ok) {
|
|
260
|
-
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`);
|
|
261
|
-
}
|
|
262
|
-
const data = await response.json();
|
|
263
|
-
this._accessToken = data.access_token;
|
|
264
|
-
this._refreshToken = data.refresh_token || this._refreshToken;
|
|
265
|
-
this._tokenExpiresAt = Date.now() + (data.expires_in * 1000) - 60000; // Subtract 1 minute for safety
|
|
266
|
-
return this._accessToken;
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
console.error('Error refreshing Box access token', error);
|
|
270
|
-
// Fall through to client credentials if refresh fails
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// If we have client credentials, try to get a new access token
|
|
274
|
-
if (this._clientId && this._clientSecret && this._enterpriseId) {
|
|
275
|
-
try {
|
|
276
|
-
await this._setAccessToken();
|
|
277
|
-
return this._accessToken;
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
console.error('Error getting new access token via client credentials', error);
|
|
281
|
-
throw new Error('Failed to authenticate with Box: ' + (error instanceof Error ? error.message : String(error)));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// If we have an access token but it's expired and we can't refresh it
|
|
285
|
-
if (this._accessToken) {
|
|
286
|
-
console.warn('Using expired Box access token as no refresh mechanism is available');
|
|
287
|
-
return this._accessToken;
|
|
288
|
-
}
|
|
289
|
-
throw new Error('No valid Box access token available and no authentication method configured');
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Makes an authenticated API request to the Box API
|
|
293
|
-
*
|
|
294
|
-
* This helper method handles authentication, request formatting, and
|
|
295
|
-
* response parsing for all Box API calls.
|
|
296
|
-
*
|
|
297
|
-
* @private
|
|
298
|
-
* @param endpoint - The API endpoint to call (e.g., '/files/123')
|
|
299
|
-
* @param method - The HTTP method to use (default: 'GET')
|
|
300
|
-
* @param body - Optional request body (will be serialized as JSON unless it's FormData)
|
|
301
|
-
* @param headers - Optional additional headers
|
|
302
|
-
* @param baseUrl - Base URL to use (defaults to standard API URL)
|
|
303
|
-
* @returns A Promise that resolves to the API response data
|
|
304
|
-
* @throws Error if the API request fails
|
|
305
|
-
*/
|
|
306
|
-
async _apiRequest(endpoint, method = 'GET', body, headers = {}, baseUrl = this._baseApiUrl) {
|
|
307
|
-
const token = await this._ensureValidToken();
|
|
308
|
-
const requestHeaders = {
|
|
309
|
-
'Authorization': `Bearer ${token}`,
|
|
310
|
-
...headers
|
|
311
|
-
};
|
|
312
|
-
if (body && typeof body !== 'string' && !(body instanceof FormData) && !(body instanceof URLSearchParams)) {
|
|
313
|
-
requestHeaders['Content-Type'] = 'application/json';
|
|
314
|
-
body = JSON.stringify(body);
|
|
315
|
-
}
|
|
316
|
-
const url = `${baseUrl}${endpoint}`;
|
|
317
|
-
const response = await fetch(url, {
|
|
318
|
-
method,
|
|
319
|
-
headers: requestHeaders,
|
|
320
|
-
body
|
|
321
|
-
});
|
|
322
|
-
if (response.status === 204 || response.status === 202) {
|
|
323
|
-
return null; // No content response
|
|
288
|
+
const tokenData = await this._refreshAccessToken();
|
|
289
|
+
this._accessToken = tokenData.access_token;
|
|
290
|
+
this._tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
|
|
324
291
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return response;
|
|
292
|
+
else if (this._clientId && this._clientSecret && this._enterpriseId) {
|
|
293
|
+
this._accessToken = await this._getAccessToken();
|
|
328
294
|
}
|
|
329
|
-
|
|
330
|
-
const contentType = response.headers.get('content-type');
|
|
331
|
-
if (contentType && !contentType.includes('application/json')) {
|
|
332
|
-
if (response.ok) {
|
|
333
|
-
return response;
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
throw new Error(`Box API error: ${response.status} - ${response.statusText}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const data = await response.json();
|
|
340
|
-
if (!response.ok) {
|
|
341
|
-
console.error('Box API error', { status: response.status, data });
|
|
342
|
-
const errorData = data;
|
|
343
|
-
throw new Error(`Box API error: ${response.status} - ${errorData.message || errorData.error_description || errorData.error || JSON.stringify(data)}`);
|
|
344
|
-
}
|
|
345
|
-
return data;
|
|
295
|
+
return this._accessToken;
|
|
346
296
|
}
|
|
347
297
|
/**
|
|
348
298
|
* Parses a path string into Box API components
|
|
@@ -387,16 +337,43 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
387
337
|
* @throws Error if the item does not exist
|
|
388
338
|
*/
|
|
389
339
|
async _getIdFromPath(path) {
|
|
340
|
+
const itemInfo = await this._getItemInfoFromPath(path);
|
|
341
|
+
return itemInfo?.id || null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Gets both ID and type information for an item at the given path
|
|
345
|
+
*
|
|
346
|
+
* @param path - Path to the item
|
|
347
|
+
* @returns Object with id and type, or null if not found
|
|
348
|
+
*/
|
|
349
|
+
async _getItemInfoFromPath(path) {
|
|
390
350
|
try {
|
|
391
351
|
// Parse the path
|
|
392
352
|
const parsedPath = this._parsePath(path);
|
|
393
|
-
// If the id is already in the path,
|
|
353
|
+
// If the id is already in the path, we need to determine type separately
|
|
394
354
|
if (parsedPath.id) {
|
|
395
|
-
|
|
355
|
+
// Try as file first, then folder
|
|
356
|
+
try {
|
|
357
|
+
const fileInfo = await this._client.files.getFileById(parsedPath.id, {
|
|
358
|
+
queryParams: { fields: ['id', 'type'] }
|
|
359
|
+
});
|
|
360
|
+
return { id: fileInfo.id, type: fileInfo.type };
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
try {
|
|
364
|
+
const folderInfo = await this._client.folders.getFolderById(parsedPath.id, {
|
|
365
|
+
queryParams: { fields: ['id', 'type'] }
|
|
366
|
+
});
|
|
367
|
+
return { id: folderInfo.id, type: folderInfo.type };
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
396
373
|
}
|
|
397
374
|
// If it's root, return root folder id
|
|
398
375
|
if (!parsedPath.name) {
|
|
399
|
-
return this._rootFolderId;
|
|
376
|
+
return { id: this._rootFolderId, type: 'folder' };
|
|
400
377
|
}
|
|
401
378
|
// First, find the parent folder ID
|
|
402
379
|
let parentFolderId = this._rootFolderId;
|
|
@@ -408,27 +385,29 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
408
385
|
let hasMoreItems = true;
|
|
409
386
|
const LIMIT = 1000;
|
|
410
387
|
while (hasMoreItems) {
|
|
411
|
-
const folderItems = await this.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
388
|
+
const folderItems = await this._client.folders.getFolderItems(parentFolderId, {
|
|
389
|
+
queryParams: {
|
|
390
|
+
fields: ['name', 'type', 'id'],
|
|
391
|
+
limit: LIMIT,
|
|
392
|
+
offset: offset
|
|
393
|
+
}
|
|
415
394
|
});
|
|
416
395
|
// Look for the item by name
|
|
417
|
-
const item = folderItems.entries
|
|
418
|
-
if (item) {
|
|
419
|
-
return item.id;
|
|
396
|
+
const item = folderItems.entries?.find((i) => i.name === parsedPath.name);
|
|
397
|
+
if (item && item.id && item.type) {
|
|
398
|
+
return { id: item.id, type: item.type };
|
|
420
399
|
}
|
|
421
400
|
// Update pagination variables
|
|
422
|
-
offset += folderItems.entries
|
|
401
|
+
offset += folderItems.entries?.length || 0;
|
|
423
402
|
// Check if we've processed all items
|
|
424
|
-
hasMoreItems = folderItems.entries
|
|
403
|
+
hasMoreItems = (folderItems.entries?.length || 0) === LIMIT && offset < (folderItems.totalCount || 0);
|
|
425
404
|
}
|
|
426
405
|
// If we get here, the item was not found
|
|
427
406
|
console.log(`Item not found: ${parsedPath.name}`);
|
|
428
407
|
return null;
|
|
429
408
|
}
|
|
430
409
|
catch (error) {
|
|
431
|
-
console.error('Error in
|
|
410
|
+
console.error('Error in _getItemInfoFromPath', { path, error });
|
|
432
411
|
return null;
|
|
433
412
|
}
|
|
434
413
|
}
|
|
@@ -521,17 +500,11 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
521
500
|
}
|
|
522
501
|
}
|
|
523
502
|
}
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}, {}, this._uploadApiUrl);
|
|
530
|
-
// Return the upload URL with the session ID as the provider key
|
|
531
|
-
return {
|
|
532
|
-
UploadUrl: data.session_endpoints.upload_part,
|
|
533
|
-
ProviderKey: `session:${data.id}:${objectName}`
|
|
534
|
-
};
|
|
503
|
+
// Box SDK v10 doesn't have a simple createUploadSession on files manager
|
|
504
|
+
// We need to use chunkedUploads manager instead
|
|
505
|
+
// For now, return a simplified URL structure
|
|
506
|
+
// TODO: Implement proper chunked upload session support
|
|
507
|
+
throw new Error('Pre-authenticated upload URLs are not currently supported with Box SDK v10. Use PutObject instead.');
|
|
535
508
|
}
|
|
536
509
|
catch (error) {
|
|
537
510
|
console.error('Error creating pre-auth upload URL', { objectName, error });
|
|
@@ -576,12 +549,12 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
576
549
|
}
|
|
577
550
|
// Get the file ID
|
|
578
551
|
const fileId = await this._getIdFromPath(objectName);
|
|
579
|
-
//
|
|
580
|
-
const
|
|
581
|
-
if (!
|
|
552
|
+
// Get download URL using SDK
|
|
553
|
+
const downloadUrl = await this._client.downloads.getDownloadFileUrl(fileId);
|
|
554
|
+
if (!downloadUrl) {
|
|
582
555
|
throw new Error(`No download URL available for: ${objectName}`);
|
|
583
556
|
}
|
|
584
|
-
return
|
|
557
|
+
return downloadUrl;
|
|
585
558
|
}
|
|
586
559
|
catch (error) {
|
|
587
560
|
console.error('Error creating pre-auth download URL', { objectName, error });
|
|
@@ -621,8 +594,11 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
621
594
|
async MoveObject(oldObjectName, newObjectName) {
|
|
622
595
|
try {
|
|
623
596
|
// Get source info
|
|
624
|
-
const
|
|
625
|
-
|
|
597
|
+
const sourceInfo = await this._getItemInfoFromPath(oldObjectName);
|
|
598
|
+
if (!sourceInfo) {
|
|
599
|
+
console.log(`Item not found: ${oldObjectName}`);
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
626
602
|
// Get destination info
|
|
627
603
|
const destPath = this._parsePath(newObjectName);
|
|
628
604
|
let destParentId = this._rootFolderId;
|
|
@@ -636,12 +612,23 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
636
612
|
destParentId = await this._getIdFromPath(destPath.parent);
|
|
637
613
|
}
|
|
638
614
|
}
|
|
639
|
-
// Move the item
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
615
|
+
// Move the item using SDK
|
|
616
|
+
if (sourceInfo.type === 'folder') {
|
|
617
|
+
await this._client.folders.updateFolderById(sourceInfo.id, {
|
|
618
|
+
requestBody: {
|
|
619
|
+
parent: { id: destParentId },
|
|
620
|
+
name: destPath.name
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
await this._client.files.updateFileById(sourceInfo.id, {
|
|
626
|
+
requestBody: {
|
|
627
|
+
parent: { id: destParentId },
|
|
628
|
+
name: destPath.name
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
645
632
|
return true;
|
|
646
633
|
}
|
|
647
634
|
catch (error) {
|
|
@@ -683,15 +670,22 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
683
670
|
try {
|
|
684
671
|
// Handle session objects specially
|
|
685
672
|
if (objectName.startsWith('session:')) {
|
|
686
|
-
|
|
687
|
-
|
|
673
|
+
// Session support not implemented in SDK v10 migration
|
|
674
|
+
// Just return true for now
|
|
688
675
|
return true;
|
|
689
676
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
677
|
+
const itemInfo = await this._getItemInfoFromPath(objectName);
|
|
678
|
+
if (!itemInfo) {
|
|
679
|
+
console.log(`Item not found: ${objectName}`);
|
|
680
|
+
return true; // Already deleted/doesn't exist
|
|
681
|
+
}
|
|
682
|
+
// Delete the item using SDK
|
|
683
|
+
if (itemInfo.type === 'folder') {
|
|
684
|
+
await this._client.folders.deleteFolderById(itemInfo.id);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
await this._client.files.deleteFileById(itemInfo.id);
|
|
688
|
+
}
|
|
695
689
|
return true;
|
|
696
690
|
}
|
|
697
691
|
catch (error) {
|
|
@@ -747,9 +741,11 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
747
741
|
// If folder doesn't exist, return empty result
|
|
748
742
|
return { objects: [], prefixes: [] };
|
|
749
743
|
}
|
|
750
|
-
// Get folder contents
|
|
751
|
-
const result = await this.
|
|
752
|
-
|
|
744
|
+
// Get folder contents using SDK
|
|
745
|
+
const result = await this._client.folders.getFolderItems(folderId, {
|
|
746
|
+
queryParams: {
|
|
747
|
+
fields: ['id', 'name', 'type', 'size', 'content_type', 'modified_at', 'created_at', 'etag', 'sequence_id']
|
|
748
|
+
}
|
|
753
749
|
});
|
|
754
750
|
const objects = [];
|
|
755
751
|
const prefixes = [];
|
|
@@ -845,9 +841,9 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
845
841
|
return false;
|
|
846
842
|
}
|
|
847
843
|
}
|
|
848
|
-
// Create the folder
|
|
844
|
+
// Create the folder using SDK
|
|
849
845
|
try {
|
|
850
|
-
await this.
|
|
846
|
+
await this._client.folders.createFolder({
|
|
851
847
|
name: folderName,
|
|
852
848
|
parent: { id: parentFolderId }
|
|
853
849
|
});
|
|
@@ -856,8 +852,8 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
856
852
|
}
|
|
857
853
|
catch (error) {
|
|
858
854
|
// Handle conflicts - if the folder already exists, that's a success
|
|
859
|
-
if (error.
|
|
860
|
-
error.message.includes('item_name_in_use'))) {
|
|
855
|
+
if (error.statusCode === 409 ||
|
|
856
|
+
(error.message && error.message.includes('item_name_in_use'))) {
|
|
861
857
|
console.log(`Folder already exists (conflict): ${normalizedPath}`);
|
|
862
858
|
return true;
|
|
863
859
|
}
|
|
@@ -906,20 +902,17 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
906
902
|
*/
|
|
907
903
|
async GetFileRepresentations(fileId, repHints = 'png?dimensions=2048x2048') {
|
|
908
904
|
try {
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
905
|
+
// Get file with representations field - SDK handles auth automatically
|
|
906
|
+
const file = await this._client.files.getFileById(fileId, {
|
|
907
|
+
queryParams: {
|
|
908
|
+
fields: ['representations']
|
|
909
|
+
},
|
|
912
910
|
headers: {
|
|
913
|
-
|
|
914
|
-
'Content-Type': 'application/json',
|
|
915
|
-
'X-Rep-Hints': repHints
|
|
911
|
+
xRepHints: repHints
|
|
916
912
|
}
|
|
917
913
|
});
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
const data = await response.json();
|
|
922
|
-
return data;
|
|
914
|
+
// Convert to plain JSON object
|
|
915
|
+
return JSON.parse(JSON.stringify(file));
|
|
923
916
|
}
|
|
924
917
|
catch (error) {
|
|
925
918
|
console.error('Error getting file representations:', error);
|
|
@@ -927,43 +920,6 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
927
920
|
}
|
|
928
921
|
}
|
|
929
922
|
;
|
|
930
|
-
/**
|
|
931
|
-
* Helper function for making HTTP requests
|
|
932
|
-
*
|
|
933
|
-
* This method provides a Promise-based wrapper around Node.js https requests,
|
|
934
|
-
* simplifying the process of making API calls to the Box API.
|
|
935
|
-
*
|
|
936
|
-
* @private
|
|
937
|
-
* @param options - The HTTPS request options (URL, method, headers, etc.)
|
|
938
|
-
* @param data - Optional string data to send with the request
|
|
939
|
-
* @returns A Promise that resolves to the response data as a string
|
|
940
|
-
* @throws Error if the request fails or returns a non-2xx status code
|
|
941
|
-
*/
|
|
942
|
-
async _makeRequest(options, data) {
|
|
943
|
-
return new Promise((resolve, reject) => {
|
|
944
|
-
const req = https.request(options, (res) => {
|
|
945
|
-
let responseData = '';
|
|
946
|
-
res.on('data', (chunk) => {
|
|
947
|
-
responseData += chunk;
|
|
948
|
-
});
|
|
949
|
-
res.on('end', () => {
|
|
950
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
951
|
-
resolve(responseData);
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
reject(new Error(`Request failed with status code ${res.statusCode}: ${responseData}`));
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
req.on('error', (error) => {
|
|
959
|
-
reject(error);
|
|
960
|
-
});
|
|
961
|
-
if (data) {
|
|
962
|
-
req.write(data);
|
|
963
|
-
}
|
|
964
|
-
req.end();
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
923
|
/**
|
|
968
924
|
* Deletes a directory from Box storage
|
|
969
925
|
*
|
|
@@ -1012,17 +968,26 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1012
968
|
}
|
|
1013
969
|
// Check if folder is empty if not recursive
|
|
1014
970
|
if (!recursive) {
|
|
1015
|
-
const contents = await this.
|
|
1016
|
-
|
|
971
|
+
const contents = await this._client.folders.getFolderItems(folderId, {
|
|
972
|
+
queryParams: {
|
|
973
|
+
limit: 1
|
|
974
|
+
}
|
|
1017
975
|
});
|
|
1018
976
|
if (contents.entries.length > 0) {
|
|
1019
977
|
throw new Error('Directory is not empty');
|
|
1020
978
|
}
|
|
1021
979
|
}
|
|
1022
|
-
// Delete the folder
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
980
|
+
// Delete the folder using SDK
|
|
981
|
+
if (recursive) {
|
|
982
|
+
await this._client.folders.deleteFolderById(folderId, {
|
|
983
|
+
queryParams: {
|
|
984
|
+
recursive: true
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
await this._client.folders.deleteFolderById(folderId);
|
|
990
|
+
}
|
|
1026
991
|
return true;
|
|
1027
992
|
}
|
|
1028
993
|
catch (error) {
|
|
@@ -1066,14 +1031,19 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1066
1031
|
*/
|
|
1067
1032
|
async GetObjectMetadata(objectName) {
|
|
1068
1033
|
try {
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
// Get full metadata
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1034
|
+
const itemInfo = await this._getItemInfoFromPath(objectName);
|
|
1035
|
+
if (!itemInfo) {
|
|
1036
|
+
throw new Error(`Object not found: ${objectName}`);
|
|
1037
|
+
}
|
|
1038
|
+
// Get full metadata using the SDK based on type
|
|
1039
|
+
const options = {
|
|
1040
|
+
queryParams: {
|
|
1041
|
+
fields: ['id', 'name', 'type', 'size', 'content_type', 'modified_at', 'created_at', 'etag', 'sequence_id']
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
const metadata = itemInfo.type === 'folder'
|
|
1045
|
+
? await this._client.folders.getFolderById(itemInfo.id, options)
|
|
1046
|
+
: await this._client.files.getFileById(itemInfo.id, options);
|
|
1077
1047
|
// Parse path to get parent path
|
|
1078
1048
|
const parsedPath = this._parsePath(objectName);
|
|
1079
1049
|
return this._convertToMetadata(metadata, parsedPath.parent);
|
|
@@ -1116,64 +1086,30 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1116
1086
|
*/
|
|
1117
1087
|
async GetObject(objectName) {
|
|
1118
1088
|
try {
|
|
1119
|
-
//
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
// Find folder ID for the directory
|
|
1124
|
-
try {
|
|
1125
|
-
const folderId = await this._findFolderIdByPath(directoryPath);
|
|
1126
|
-
// Use pagination to handle large folders
|
|
1127
|
-
let file = null;
|
|
1128
|
-
let offset = 0;
|
|
1129
|
-
let hasMoreItems = true;
|
|
1130
|
-
const LIMIT = 1000;
|
|
1131
|
-
while (hasMoreItems && !file) {
|
|
1132
|
-
const folderItems = await this._apiRequest(`/folders/${folderId}/items`, 'GET', null, {
|
|
1133
|
-
'fields': 'name,type,id,size,created_at,modified_at',
|
|
1134
|
-
'limit': `${LIMIT}`,
|
|
1135
|
-
'offset': `${offset}`
|
|
1136
|
-
});
|
|
1137
|
-
// Look for the file
|
|
1138
|
-
file = folderItems.entries.find((item) => item.type === 'file' && item.name === fileName);
|
|
1139
|
-
// If file is found, break out of pagination loop
|
|
1140
|
-
if (file) {
|
|
1141
|
-
break;
|
|
1142
|
-
}
|
|
1143
|
-
// Update pagination variables
|
|
1144
|
-
offset += folderItems.entries.length;
|
|
1145
|
-
// Check if we've processed all items
|
|
1146
|
-
hasMoreItems = folderItems.entries.length === LIMIT && offset < folderItems.total_count;
|
|
1147
|
-
}
|
|
1148
|
-
if (file) {
|
|
1149
|
-
console.log(`✅ File found: ${file.name} (${file.id})`);
|
|
1150
|
-
// Use the file ID to get the content
|
|
1151
|
-
const fileResponse = await this._apiRequest(`/files/${file.id}/content`, 'GET');
|
|
1152
|
-
// If the API request returned a response object (for direct download)
|
|
1153
|
-
if (fileResponse instanceof Response) {
|
|
1154
|
-
const arrayBuffer = await fileResponse.arrayBuffer();
|
|
1155
|
-
return Buffer.from(arrayBuffer);
|
|
1156
|
-
}
|
|
1157
|
-
else {
|
|
1158
|
-
throw new Error('Unexpected response format when downloading file');
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
else {
|
|
1162
|
-
console.log(`❌ File not found in directory`);
|
|
1163
|
-
throw new Error(`File not found: ${fileName}`);
|
|
1164
|
-
}
|
|
1089
|
+
// Get file ID using path resolution
|
|
1090
|
+
const fileId = await this._getIdFromPath(objectName);
|
|
1091
|
+
if (!fileId) {
|
|
1092
|
+
throw new Error(`File not found: ${objectName}`);
|
|
1165
1093
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1094
|
+
console.log(`✅ File found with ID: ${fileId}`);
|
|
1095
|
+
// Use SDK to download file content as a stream
|
|
1096
|
+
const stream = await this._client.downloads.downloadFile(fileId);
|
|
1097
|
+
if (!stream) {
|
|
1098
|
+
throw new Error(`Failed to download file: ${objectName}`);
|
|
1169
1099
|
}
|
|
1100
|
+
// Convert stream to buffer
|
|
1101
|
+
return new Promise((resolve, reject) => {
|
|
1102
|
+
const chunks = [];
|
|
1103
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
1104
|
+
stream.on('error', reject);
|
|
1105
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
1106
|
+
});
|
|
1170
1107
|
}
|
|
1171
1108
|
catch (error) {
|
|
1172
1109
|
console.error('Error getting object', { objectName, error });
|
|
1173
1110
|
throw new Error(`Failed to get object: ${objectName}`);
|
|
1174
1111
|
}
|
|
1175
1112
|
}
|
|
1176
|
-
;
|
|
1177
1113
|
/**
|
|
1178
1114
|
* Uploads a file to Box storage
|
|
1179
1115
|
*
|
|
@@ -1240,30 +1176,44 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1240
1176
|
}
|
|
1241
1177
|
}
|
|
1242
1178
|
// Check if file already exists
|
|
1243
|
-
|
|
1244
|
-
const formData = new FormData();
|
|
1245
|
-
// Add file metadata
|
|
1246
|
-
const fileMetadata = {
|
|
1247
|
-
name: parsedPath.name,
|
|
1248
|
-
parent: { id: parentId }
|
|
1249
|
-
};
|
|
1250
|
-
formData.append('attributes', JSON.stringify(fileMetadata));
|
|
1251
|
-
// Create a file blob with the correct content type
|
|
1252
|
-
const fileBlob = new Blob([data], { type: contentType || 'application/octet-stream' });
|
|
1253
|
-
formData.append('file', fileBlob, parsedPath.name);
|
|
1254
|
-
// Upload the file
|
|
1255
|
-
const endpoint = fileId ? `/files/${fileId}/content` : '/files/content';
|
|
1256
|
-
console.log(`Uploading file using endpoint: ${endpoint}`);
|
|
1179
|
+
let fileId = null;
|
|
1257
1180
|
try {
|
|
1258
|
-
await this.
|
|
1259
|
-
|
|
1181
|
+
fileId = await this._getIdFromPath(objectName);
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
// File doesn't exist, we'll upload as new
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
// Convert Buffer to Readable stream for the SDK
|
|
1188
|
+
const fileStream = stream_1.Readable.from(data);
|
|
1189
|
+
if (fileId) {
|
|
1190
|
+
// Update existing file (upload new version)
|
|
1191
|
+
await this._client.uploads.uploadFileVersion(fileId, {
|
|
1192
|
+
attributes: {
|
|
1193
|
+
name: parsedPath.name
|
|
1194
|
+
},
|
|
1195
|
+
file: fileStream
|
|
1196
|
+
});
|
|
1197
|
+
console.log(`✅ File updated successfully: ${objectName}`);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
// Upload new file
|
|
1201
|
+
await this._client.uploads.uploadFile({
|
|
1202
|
+
attributes: {
|
|
1203
|
+
name: parsedPath.name,
|
|
1204
|
+
parent: { id: parentId }
|
|
1205
|
+
},
|
|
1206
|
+
file: fileStream
|
|
1207
|
+
});
|
|
1208
|
+
console.log(`✅ File uploaded successfully: ${objectName}`);
|
|
1209
|
+
}
|
|
1260
1210
|
return true;
|
|
1261
1211
|
}
|
|
1262
1212
|
catch (uploadError) {
|
|
1263
|
-
console.error(`Error uploading file
|
|
1264
|
-
if (uploadError.
|
|
1213
|
+
console.error(`Error uploading file:`, uploadError);
|
|
1214
|
+
if (uploadError.statusCode === 409) {
|
|
1265
1215
|
console.log(`File already exists (conflict): ${objectName}`);
|
|
1266
|
-
return
|
|
1216
|
+
return true;
|
|
1267
1217
|
}
|
|
1268
1218
|
return false;
|
|
1269
1219
|
}
|
|
@@ -1307,8 +1257,11 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1307
1257
|
async CopyObject(sourceObjectName, destinationObjectName) {
|
|
1308
1258
|
try {
|
|
1309
1259
|
// Get source info
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1260
|
+
const sourceInfo = await this._getItemInfoFromPath(sourceObjectName);
|
|
1261
|
+
if (!sourceInfo) {
|
|
1262
|
+
console.log(`Source item not found: ${sourceObjectName}`);
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1312
1265
|
if (sourceInfo.type !== 'file') {
|
|
1313
1266
|
throw new Error('Only files can be copied with CopyObject');
|
|
1314
1267
|
}
|
|
@@ -1325,8 +1278,8 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1325
1278
|
destParentId = await this._getIdFromPath(destPath.parent);
|
|
1326
1279
|
}
|
|
1327
1280
|
}
|
|
1328
|
-
// Copy the file
|
|
1329
|
-
await this.
|
|
1281
|
+
// Copy the file using SDK
|
|
1282
|
+
await this._client.files.copyFile(sourceInfo.id, {
|
|
1330
1283
|
parent: { id: destParentId },
|
|
1331
1284
|
name: destPath.name
|
|
1332
1285
|
});
|
|
@@ -1408,9 +1361,13 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1408
1361
|
try {
|
|
1409
1362
|
const folderId = await this._findFolderIdByPath(normalizedPath);
|
|
1410
1363
|
console.log(`✅ Directory ${normalizedPath} exists with ID: ${folderId}`);
|
|
1411
|
-
// Make a direct call to verify it's a folder
|
|
1364
|
+
// Make a direct call to verify it's a folder using SDK
|
|
1412
1365
|
try {
|
|
1413
|
-
const folderInfo = await this.
|
|
1366
|
+
const folderInfo = await this._client.folders.getFolderById(folderId, {
|
|
1367
|
+
queryParams: {
|
|
1368
|
+
fields: ['type']
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1414
1371
|
return folderInfo.type === 'folder';
|
|
1415
1372
|
}
|
|
1416
1373
|
catch (error) {
|
|
@@ -1461,13 +1418,15 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1461
1418
|
let hasMoreItems = true;
|
|
1462
1419
|
const LIMIT = 1000;
|
|
1463
1420
|
while (hasMoreItems && !folder) {
|
|
1464
|
-
const items = await this.
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1421
|
+
const items = await this._client.folders.getFolderItems(currentFolderId, {
|
|
1422
|
+
queryParams: {
|
|
1423
|
+
fields: ['name', 'type', 'id'],
|
|
1424
|
+
limit: LIMIT,
|
|
1425
|
+
offset: offset
|
|
1426
|
+
}
|
|
1468
1427
|
});
|
|
1469
1428
|
// Filter to only folders
|
|
1470
|
-
const folders = items.entries
|
|
1429
|
+
const folders = items.entries?.filter((item) => item.type === 'folder') || [];
|
|
1471
1430
|
// Look for the target folder
|
|
1472
1431
|
folder = folders.find((item) => item.name === segment);
|
|
1473
1432
|
// If folder is found, break out of pagination loop
|
|
@@ -1475,11 +1434,11 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
|
|
|
1475
1434
|
break;
|
|
1476
1435
|
}
|
|
1477
1436
|
// Update pagination variables
|
|
1478
|
-
offset += items.entries
|
|
1437
|
+
offset += items.entries?.length || 0;
|
|
1479
1438
|
// Check if we've processed all items
|
|
1480
|
-
hasMoreItems = items.entries
|
|
1439
|
+
hasMoreItems = (items.entries?.length || 0) === LIMIT && offset < (items.totalCount || 0);
|
|
1481
1440
|
}
|
|
1482
|
-
if (!folder) {
|
|
1441
|
+
if (!folder || !folder.id) {
|
|
1483
1442
|
throw new Error(`Folder not found: ${segment}`);
|
|
1484
1443
|
}
|
|
1485
1444
|
currentFolderId = folder.id;
|