@memberjunction/storage 3.1.0 → 3.2.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.
Files changed (47) hide show
  1. package/dist/__tests__/FileStorageBase.test.d.ts +6 -0
  2. package/dist/__tests__/FileStorageBase.test.d.ts.map +1 -0
  3. package/dist/__tests__/FileStorageBase.test.js +213 -0
  4. package/dist/__tests__/FileStorageBase.test.js.map +1 -0
  5. package/dist/__tests__/util.test.d.ts +7 -0
  6. package/dist/__tests__/util.test.d.ts.map +1 -0
  7. package/dist/__tests__/util.test.js +326 -0
  8. package/dist/__tests__/util.test.js.map +1 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +28 -14
  11. package/dist/config.js.map +1 -1
  12. package/dist/drivers/AWSFileStorage.d.ts +20 -0
  13. package/dist/drivers/AWSFileStorage.d.ts.map +1 -1
  14. package/dist/drivers/AWSFileStorage.js +43 -18
  15. package/dist/drivers/AWSFileStorage.js.map +1 -1
  16. package/dist/drivers/AzureFileStorage.d.ts +1 -1
  17. package/dist/drivers/AzureFileStorage.d.ts.map +1 -1
  18. package/dist/drivers/AzureFileStorage.js +17 -17
  19. package/dist/drivers/AzureFileStorage.js.map +1 -1
  20. package/dist/drivers/BoxFileStorage.d.ts +47 -1
  21. package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
  22. package/dist/drivers/BoxFileStorage.js +219 -95
  23. package/dist/drivers/BoxFileStorage.js.map +1 -1
  24. package/dist/drivers/DropboxFileStorage.d.ts +59 -0
  25. package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
  26. package/dist/drivers/DropboxFileStorage.js +314 -62
  27. package/dist/drivers/DropboxFileStorage.js.map +1 -1
  28. package/dist/drivers/GoogleDriveFileStorage.d.ts +29 -0
  29. package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
  30. package/dist/drivers/GoogleDriveFileStorage.js +220 -72
  31. package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
  32. package/dist/drivers/GoogleFileStorage.d.ts.map +1 -1
  33. package/dist/drivers/GoogleFileStorage.js +12 -12
  34. package/dist/drivers/GoogleFileStorage.js.map +1 -1
  35. package/dist/drivers/SharePointFileStorage.d.ts +64 -5
  36. package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
  37. package/dist/drivers/SharePointFileStorage.js +265 -94
  38. package/dist/drivers/SharePointFileStorage.js.map +1 -1
  39. package/dist/generic/FileStorageBase.d.ts +79 -13
  40. package/dist/generic/FileStorageBase.d.ts.map +1 -1
  41. package/dist/generic/FileStorageBase.js +57 -12
  42. package/dist/generic/FileStorageBase.js.map +1 -1
  43. package/dist/util.d.ts +429 -11
  44. package/dist/util.d.ts.map +1 -1
  45. package/dist/util.js +677 -16
  46. package/dist/util.js.map +1 -1
  47. package/package.json +11 -5
package/dist/util.js CHANGED
@@ -3,10 +3,193 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deleteObject = exports.moveObject = exports.createDownloadUrl = exports.createUploadUrl = void 0;
6
+ exports.searchAcrossAccounts = exports.searchAcrossProviders = exports.copyObjectBetweenProviders = exports.listObjects = exports.deleteObject = exports.copyObject = exports.moveObject = exports.createDownloadUrl = exports.createUploadUrl = exports.initializeDriverWithAccountCredentials = exports.initializeDriverWithUserCredentials = void 0;
7
7
  const global_1 = require("@memberjunction/global");
8
+ const core_1 = require("@memberjunction/core");
9
+ const credentials_1 = require("@memberjunction/credentials");
8
10
  const mime_types_1 = __importDefault(require("mime-types"));
9
11
  const FileStorageBase_1 = require("./generic/FileStorageBase");
12
+ /**
13
+ * @deprecated This function is being replaced by the enterprise file storage model.
14
+ * Use FileStorageAccount with Credential Engine instead.
15
+ *
16
+ * Initializes a storage driver with user-specific OAuth credentials.
17
+ * NOTE: This function currently only supports non-OAuth providers.
18
+ * OAuth provider support will be added via the Credential Engine integration.
19
+ *
20
+ * @param options - Configuration options including provider, user ID, and context
21
+ * @returns A promise resolving to an initialized FileStorageBase driver
22
+ * @throws Error if the provider requires OAuth (not yet supported in enterprise model)
23
+ */
24
+ async function initializeDriverWithUserCredentials(options) {
25
+ const { providerEntity } = options;
26
+ // Create the driver instance
27
+ const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
28
+ // Check if this provider requires OAuth authentication
29
+ if (providerEntity.RequiresOAuth) {
30
+ // TODO: Implement Credential Engine integration for OAuth providers
31
+ // This will load credentials from FileStorageAccount -> Credential
32
+ throw new Error(`Provider "${providerEntity.Name}" requires OAuth authentication. ` + `Enterprise OAuth support via Credential Engine is not yet implemented.`);
33
+ }
34
+ else {
35
+ // Provider doesn't require OAuth - use admin/global configuration
36
+ const configJson = providerEntity.Configuration;
37
+ if (configJson) {
38
+ const config = JSON.parse(configJson);
39
+ await driver.initialize(config);
40
+ console.log(`[initializeDriverWithUserCredentials] Initialized ${providerEntity.Name} with admin configuration`);
41
+ }
42
+ }
43
+ return driver;
44
+ }
45
+ exports.initializeDriverWithUserCredentials = initializeDriverWithUserCredentials;
46
+ /**
47
+ * Initializes a storage driver using account-based credentials from the Credential Engine.
48
+ * This is the enterprise model where credentials are stored in the Credential entity
49
+ * and decrypted at runtime.
50
+ *
51
+ * For providers that issue new refresh tokens on each token refresh (like Box.com),
52
+ * this function automatically configures a callback to persist the new tokens back
53
+ * to the Credential entity in the database.
54
+ *
55
+ * @param options - Configuration options including account, provider, and context user
56
+ * @returns A promise resolving to an initialized FileStorageBase driver
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const driver = await initializeDriverWithAccountCredentials({
61
+ * accountEntity,
62
+ * providerEntity,
63
+ * contextUser
64
+ * });
65
+ *
66
+ * // Driver is now ready to use
67
+ * const objects = await driver.ListObjects('/');
68
+ * ```
69
+ */
70
+ async function initializeDriverWithAccountCredentials(options) {
71
+ const { accountEntity, providerEntity, contextUser } = options;
72
+ // Create the driver instance using the provider's driver key
73
+ const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
74
+ if (!driver) {
75
+ throw new Error(`Failed to create storage driver for provider "${providerEntity.Name}" ` + `with driver key "${providerEntity.ServerDriverKey}"`);
76
+ }
77
+ // Build the base config with account information (required in enterprise model)
78
+ const baseConfig = {
79
+ accountId: accountEntity.ID,
80
+ accountName: accountEntity.Name,
81
+ };
82
+ // Check if the account has a credential configured
83
+ if (accountEntity.CredentialID) {
84
+ // Initialize the Credential Engine if not already done
85
+ await credentials_1.CredentialEngine.Instance.Config(false, contextUser);
86
+ // Get the credential by ID and decrypt it
87
+ const credentialEntity = credentials_1.CredentialEngine.Instance.getCredentialById(accountEntity.CredentialID);
88
+ if (!credentialEntity) {
89
+ throw new Error(`Credential with ID ${accountEntity.CredentialID} not found for account "${accountEntity.Name}"`);
90
+ }
91
+ // Resolve the credential to get decrypted values
92
+ const resolved = await credentials_1.CredentialEngine.Instance.getCredential(credentialEntity.Name, {
93
+ credentialId: accountEntity.CredentialID,
94
+ contextUser,
95
+ subsystem: 'FileStorage',
96
+ });
97
+ (0, core_1.LogStatus)(`[initializeDriverWithAccountCredentials] Decrypted credential "${credentialEntity.Name}" for account "${accountEntity.Name}"`);
98
+ // Create a token refresh callback to persist new tokens back to the database
99
+ // This is critical for providers like Box.com that issue new refresh tokens on each refresh
100
+ const onTokenRefresh = async (newRefreshToken, newAccessToken) => {
101
+ try {
102
+ (0, core_1.LogStatus)(`[initializeDriverWithAccountCredentials] Token refresh callback invoked for account "${accountEntity.Name}"`);
103
+ // Get the current credential values and update with new tokens
104
+ const updatedValues = { ...resolved.values };
105
+ updatedValues.refreshToken = newRefreshToken;
106
+ if (newAccessToken) {
107
+ updatedValues.accessToken = newAccessToken;
108
+ }
109
+ // Update the credential in the database using the Credential Engine
110
+ await credentials_1.CredentialEngine.Instance.updateCredential(accountEntity.CredentialID, updatedValues, contextUser);
111
+ (0, core_1.LogStatus)(`[initializeDriverWithAccountCredentials] Successfully persisted new tokens for account "${accountEntity.Name}"`);
112
+ }
113
+ catch (error) {
114
+ // Log the error but don't throw - the driver still has the tokens in memory
115
+ // and can continue operating. However, the next server restart will fail.
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ console.error(`[initializeDriverWithAccountCredentials] Failed to persist new tokens for account "${accountEntity.Name}": ${errorMessage}`);
118
+ console.error(`[initializeDriverWithAccountCredentials] WARNING: Authentication may fail after server restart!`);
119
+ }
120
+ };
121
+ // Add the token refresh callback to the config
122
+ baseConfig.onTokenRefresh = onTokenRefresh;
123
+ // Initialize the driver with account info + decrypted credential values + token refresh callback
124
+ await driver.initialize({
125
+ ...baseConfig,
126
+ ...resolved.values,
127
+ });
128
+ }
129
+ else {
130
+ // No credential configured - fall back to provider's static configuration
131
+ const configJson = providerEntity.Configuration;
132
+ if (configJson) {
133
+ const config = JSON.parse(configJson);
134
+ // Merge account info with provider configuration
135
+ await driver.initialize({
136
+ ...baseConfig,
137
+ ...config,
138
+ });
139
+ (0, core_1.LogStatus)(`[initializeDriverWithAccountCredentials] Initialized "${accountEntity.Name}" with provider configuration (no credential)`);
140
+ }
141
+ else {
142
+ // Initialize with just account info (driver may use env vars or other config)
143
+ await driver.initialize(baseConfig);
144
+ (0, core_1.LogStatus)(`[initializeDriverWithAccountCredentials] Warning: No credential or configuration for account "${accountEntity.Name}"`);
145
+ }
146
+ }
147
+ return driver;
148
+ }
149
+ exports.initializeDriverWithAccountCredentials = initializeDriverWithAccountCredentials;
150
+ /**
151
+ * Internal helper to initialize a storage driver with appropriate credentials.
152
+ *
153
+ * Supports two modes:
154
+ * 1. Enterprise model (preferred): When accountEntity is provided in userContext,
155
+ * uses the Credential Engine to decrypt credentials from the account's CredentialID.
156
+ * 2. Legacy model: Uses provider configuration or throws for OAuth providers.
157
+ *
158
+ * @param providerEntity - The file storage provider entity
159
+ * @param userContext - Optional user context, may include accountEntity for enterprise model
160
+ * @returns An initialized FileStorageBase driver
161
+ */
162
+ async function initializeDriver(providerEntity, userContext) {
163
+ // Enterprise model: Use account-based credentials if accountEntity is provided
164
+ if (userContext?.accountEntity) {
165
+ return initializeDriverWithAccountCredentials({
166
+ accountEntity: userContext.accountEntity,
167
+ providerEntity,
168
+ contextUser: userContext.contextUser,
169
+ });
170
+ }
171
+ // Legacy model: Use the deprecated user credentials approach
172
+ if (userContext) {
173
+ return initializeDriverWithUserCredentials({
174
+ providerEntity,
175
+ userID: userContext.userID,
176
+ contextUser: userContext.contextUser,
177
+ });
178
+ }
179
+ // No user context - use admin/legacy initialization
180
+ const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
181
+ // Check if this provider requires OAuth but no user context was provided
182
+ if (providerEntity.RequiresOAuth) {
183
+ throw new Error(`Provider "${providerEntity.Name}" requires OAuth authentication. ` + `Please provide userContext parameter with userID and contextUser.`);
184
+ }
185
+ // Initialize with admin configuration
186
+ const configJson = providerEntity.Get('Configuration');
187
+ if (configJson) {
188
+ const config = JSON.parse(configJson);
189
+ await driver.initialize(config);
190
+ }
191
+ return driver;
192
+ }
10
193
  /**
11
194
  * Creates a pre-authenticated upload URL for a file in the specified file storage provider.
12
195
  *
@@ -27,6 +210,7 @@ const FileStorageBase_1 = require("./generic/FileStorageBase");
27
210
  * - ProviderID: The ID of the storage provider to use
28
211
  * - ContentType: (Optional) The MIME type of the file
29
212
  * - ProviderKey: (Optional) Provider-specific key for the file
213
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
30
214
  * @returns A promise that resolves to an object containing:
31
215
  * - updatedInput: The input object with additional metadata (Status, ContentType, and possibly ProviderKey)
32
216
  * - UploadUrl: The pre-authenticated URL for uploading the file
@@ -41,7 +225,8 @@ const FileStorageBase_1 = require("./generic/FileStorageBase");
41
225
  * ID: '123',
42
226
  * Name: 'report.pdf',
43
227
  * ProviderID: fileStorageProvider.ID
44
- * }
228
+ * },
229
+ * { userID: currentUser.ID, contextUser } // Required for OAuth providers
45
230
  * );
46
231
  *
47
232
  * // The content type is automatically determined from the file extension
@@ -54,12 +239,13 @@ const FileStorageBase_1 = require("./generic/FileStorageBase");
54
239
  * console.log(result.UploadUrl);
55
240
  * ```
56
241
  */
57
- const createUploadUrl = async (providerEntity, input) => {
242
+ const createUploadUrl = async (providerEntity, input, userContext) => {
58
243
  const { Name, ProviderID } = input;
59
244
  const ContentType = input.ContentType ?? mime_types_1.default.lookup(input.Name) ?? 'application/octet-stream';
60
245
  const Status = 'Uploading';
61
246
  await providerEntity.Load(ProviderID);
62
- const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
247
+ // Initialize driver with user credentials if available, otherwise use admin config
248
+ const driver = await initializeDriver(providerEntity, userContext);
63
249
  const { UploadUrl, ...maybeProviderKey } = await driver.CreatePreAuthUploadUrl(Name);
64
250
  const updatedInput = { ...input, ...maybeProviderKey, ContentType, Status };
65
251
  return { updatedInput, UploadUrl };
@@ -76,6 +262,7 @@ exports.createUploadUrl = createUploadUrl;
76
262
  * @param providerEntity - The file storage provider entity containing connection details
77
263
  * @param providerKeyOrName - The provider key or name of the file to download
78
264
  * (use the ProviderKey if it was returned during upload, otherwise use the file Name)
265
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
79
266
  * @returns A promise that resolves to the pre-authenticated download URL as a string
80
267
  *
81
268
  * @example
@@ -84,17 +271,17 @@ exports.createUploadUrl = createUploadUrl;
84
271
  * const fileStorageProvider = await entityMgr.FindById('FileStorageProvider', 'azure-main');
85
272
  *
86
273
  * // Using the file name
87
- * const downloadUrl = await createDownloadUrl(fileStorageProvider, 'reports/annual-report.pdf');
274
+ * const downloadUrl = await createDownloadUrl(fileStorageProvider, 'reports/annual-report.pdf', userContext);
88
275
  *
89
276
  * // Or using the provider key if returned during upload
90
- * const downloadUrl = await createDownloadUrl(fileStorageProvider, file.ProviderKey);
277
+ * const downloadUrl = await createDownloadUrl(fileStorageProvider, file.ProviderKey, userContext);
91
278
  *
92
279
  * // The download URL can be provided to clients for direct download
93
280
  * console.log(downloadUrl);
94
281
  * ```
95
282
  */
96
- const createDownloadUrl = async (providerEntity, providerKeyOrName) => {
97
- const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
283
+ const createDownloadUrl = async (providerEntity, providerKeyOrName, userContext) => {
284
+ const driver = await initializeDriver(providerEntity, userContext);
98
285
  return driver.CreatePreAuthDownloadUrl(providerKeyOrName);
99
286
  };
100
287
  exports.createDownloadUrl = createDownloadUrl;
@@ -109,6 +296,7 @@ exports.createDownloadUrl = createDownloadUrl;
109
296
  * @param oldProviderKeyOrName - The key or name of the object's current location
110
297
  * (use the ProviderKey if it was returned during upload, otherwise use the file Name)
111
298
  * @param newProviderKeyOrName - The key or name for the object's new location
299
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
112
300
  * @returns A promise that resolves to a boolean indicating whether the move operation was successful
113
301
  *
114
302
  * @example
@@ -120,7 +308,8 @@ exports.createDownloadUrl = createDownloadUrl;
120
308
  * const success = await moveObject(
121
309
  * fileStorageProvider,
122
310
  * 'drafts/report.docx',
123
- * 'published/final-report.docx'
311
+ * 'published/final-report.docx',
312
+ * userContext
124
313
  * );
125
314
  *
126
315
  * if (success) {
@@ -130,11 +319,46 @@ exports.createDownloadUrl = createDownloadUrl;
130
319
  * }
131
320
  * ```
132
321
  */
133
- const moveObject = async (providerEntity, oldProviderKeyOrName, newProviderKeyOrName) => {
134
- const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
322
+ const moveObject = async (providerEntity, oldProviderKeyOrName, newProviderKeyOrName, userContext) => {
323
+ const driver = await initializeDriver(providerEntity, userContext);
135
324
  return driver.MoveObject(oldProviderKeyOrName, newProviderKeyOrName);
136
325
  };
137
326
  exports.moveObject = moveObject;
327
+ /**
328
+ * Copies an object from one location to another within the specified file storage provider.
329
+ *
330
+ * This utility function handles copying files by instantiating the appropriate storage
331
+ * provider driver and delegating to its CopyObject method. It can be used to duplicate files
332
+ * within the same storage provider, either in the same folder with a different name or to
333
+ * a different folder.
334
+ *
335
+ * @param providerEntity - The file storage provider entity containing connection details
336
+ * @param sourceProviderKeyOrName - The key or name of the source file to copy
337
+ * @param destinationProviderKeyOrName - The key or name for the destination copy
338
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
339
+ * @returns A promise that resolves to a boolean indicating whether the copy was successful
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * const success = await copyObject(
344
+ * providerEntity,
345
+ * 'documents/report.pdf',
346
+ * 'documents/archive/report-2024.pdf',
347
+ * userContext
348
+ * );
349
+ *
350
+ * if (success) {
351
+ * console.log('File successfully copied');
352
+ * } else {
353
+ * console.log('Failed to copy file');
354
+ * }
355
+ * ```
356
+ */
357
+ const copyObject = async (providerEntity, sourceProviderKeyOrName, destinationProviderKeyOrName, userContext) => {
358
+ const driver = await initializeDriver(providerEntity, userContext);
359
+ return driver.CopyObject(sourceProviderKeyOrName, destinationProviderKeyOrName);
360
+ };
361
+ exports.copyObject = copyObject;
138
362
  /**
139
363
  * Deletes a file from the specified file storage provider.
140
364
  *
@@ -145,6 +369,7 @@ exports.moveObject = moveObject;
145
369
  * @param providerEntity - The file storage provider entity containing connection details
146
370
  * @param providerKeyOrName - The key or name of the file to delete
147
371
  * (use the ProviderKey if it was returned during upload, otherwise use the file Name)
372
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
148
373
  * @returns A promise that resolves to a boolean indicating whether the deletion was successful
149
374
  *
150
375
  * @example
@@ -153,10 +378,10 @@ exports.moveObject = moveObject;
153
378
  * const fileStorageProvider = await entityMgr.FindById('FileStorageProvider', 'azure-main');
154
379
  *
155
380
  * // Delete using the file name
156
- * const deleted = await deleteObject(fileStorageProvider, 'temp/obsolete-document.pdf');
381
+ * const deleted = await deleteObject(fileStorageProvider, 'temp/obsolete-document.pdf', userContext);
157
382
  *
158
383
  * // Or using the provider key if returned during upload
159
- * const deleted = await deleteObject(fileStorageProvider, file.ProviderKey);
384
+ * const deleted = await deleteObject(fileStorageProvider, file.ProviderKey, userContext);
160
385
  *
161
386
  * if (deleted) {
162
387
  * console.log('File successfully deleted');
@@ -165,9 +390,445 @@ exports.moveObject = moveObject;
165
390
  * }
166
391
  * ```
167
392
  */
168
- const deleteObject = (providerEntity, providerKeyOrName) => {
169
- const driver = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(FileStorageBase_1.FileStorageBase, providerEntity.ServerDriverKey);
170
- return driver.DeleteObject(providerKeyOrName);
393
+ const deleteObject = async (providerEntity, providerKeyOrName, userContext) => {
394
+ console.log('[deleteObject] Called with:', {
395
+ providerName: providerEntity.Name,
396
+ providerID: providerEntity.ID,
397
+ serverDriverKey: providerEntity.ServerDriverKey,
398
+ providerKeyOrName,
399
+ });
400
+ const driver = await initializeDriver(providerEntity, userContext);
401
+ console.log('[deleteObject] Driver initialized:', {
402
+ driverType: driver.constructor.name,
403
+ hasDeleteMethod: typeof driver.DeleteObject === 'function',
404
+ });
405
+ console.log('[deleteObject] Calling driver.DeleteObject...');
406
+ const result = await driver.DeleteObject(providerKeyOrName);
407
+ console.log('[deleteObject] Result:', result);
408
+ return result;
171
409
  };
172
410
  exports.deleteObject = deleteObject;
411
+ /**
412
+ * Lists objects (files) and prefixes (directories) in a storage provider at the specified path.
413
+ *
414
+ * This utility function provides access to the storage provider's file and folder listing
415
+ * functionality. It returns both files and directories found at the specified path prefix,
416
+ * allowing for hierarchical navigation through the storage provider's contents.
417
+ *
418
+ * @param providerEntity - The file storage provider entity containing connection details
419
+ * @param prefix - The path prefix to list objects from (e.g., "/" for root, "documents/" for a specific folder)
420
+ * @param delimiter - The character used to group keys into a hierarchy (defaults to "/")
421
+ * @param userContext - Optional user context for OAuth providers (required if provider.RequiresOAuth is true)
422
+ * @returns A promise that resolves to a StorageListResult containing:
423
+ * - objects: Array of file metadata (name, size, contentType, lastModified, etc.)
424
+ * - prefixes: Array of directory/folder path strings
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // List contents of the root directory
429
+ * const fileStorageProvider = await entityMgr.FindById('FileStorageProvider', 'aws-s3-main');
430
+ * const result = await listObjects(fileStorageProvider, '/', '/', userContext);
431
+ *
432
+ * // Display files
433
+ * for (const file of result.objects) {
434
+ * console.log(`File: ${file.name} (${file.size} bytes)`);
435
+ * }
436
+ *
437
+ * // Display folders
438
+ * for (const folder of result.prefixes) {
439
+ * console.log(`Folder: ${folder}`);
440
+ * }
441
+ *
442
+ * // List contents of a specific folder
443
+ * const docsResult = await listObjects(fileStorageProvider, 'documents/', '/', userContext);
444
+ * ```
445
+ */
446
+ const listObjects = async (providerEntity, prefix, delimiter = '/', userContext) => {
447
+ console.log('[listObjects] Starting with:', {
448
+ providerName: providerEntity.Name,
449
+ serverDriverKey: providerEntity.ServerDriverKey,
450
+ prefix,
451
+ delimiter,
452
+ hasUserContext: !!userContext,
453
+ });
454
+ const driver = await initializeDriver(providerEntity, userContext);
455
+ console.log('[listObjects] Driver initialized:', {
456
+ driverType: driver.constructor.name,
457
+ isConfigured: driver.IsConfigured,
458
+ });
459
+ const result = await driver.ListObjects(prefix, delimiter);
460
+ console.log('[listObjects] Result:', {
461
+ objectsCount: result.objects.length,
462
+ prefixesCount: result.prefixes.length,
463
+ });
464
+ return result;
465
+ };
466
+ exports.listObjects = listObjects;
467
+ /**
468
+ * Copies a file from one storage provider to another.
469
+ *
470
+ * This utility function enables transferring files between different storage providers
471
+ * (e.g., from Dropbox to Google Drive, or from S3 to Azure). The transfer happens
472
+ * server-side, so the file data flows: Source Provider → Server → Destination Provider.
473
+ *
474
+ * @param sourceProviderEntity - The source file storage provider entity
475
+ * @param destinationProviderEntity - The destination file storage provider entity
476
+ * @param sourcePath - The path to the file in the source provider
477
+ * @param destinationPath - The path where the file should be saved in the destination provider
478
+ * @param options - Optional user context for OAuth providers
479
+ * @returns A promise that resolves to a CopyBetweenProvidersResult
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * // Copy a file from Dropbox to Google Drive
484
+ * const sourceProvider = await entityMgr.FindById('FileStorageProvider', 'dropbox-id');
485
+ * const destProvider = await entityMgr.FindById('FileStorageProvider', 'gdrive-id');
486
+ *
487
+ * const result = await copyObjectBetweenProviders(
488
+ * sourceProvider,
489
+ * destProvider,
490
+ * 'documents/report.pdf',
491
+ * 'imported/report.pdf',
492
+ * {
493
+ * sourceUserContext: { userID: currentUser.ID, contextUser },
494
+ * destinationUserContext: { userID: currentUser.ID, contextUser }
495
+ * }
496
+ * );
497
+ *
498
+ * if (result.success) {
499
+ * console.log(`Transferred ${result.bytesTransferred} bytes`);
500
+ * }
501
+ * ```
502
+ */
503
+ const copyObjectBetweenProviders = async (sourceProviderEntity, destinationProviderEntity, sourcePath, destinationPath, options) => {
504
+ console.log('[copyObjectBetweenProviders] Starting transfer:', {
505
+ sourceProvider: sourceProviderEntity.Name,
506
+ destinationProvider: destinationProviderEntity.Name,
507
+ sourcePath,
508
+ destinationPath,
509
+ });
510
+ const result = {
511
+ success: false,
512
+ message: '',
513
+ sourceProvider: sourceProviderEntity.Name,
514
+ destinationProvider: destinationProviderEntity.Name,
515
+ sourcePath,
516
+ destinationPath,
517
+ };
518
+ try {
519
+ // Initialize source driver with user credentials if available
520
+ const sourceDriver = await initializeDriver(sourceProviderEntity, options?.sourceUserContext);
521
+ // Initialize destination driver with user credentials if available
522
+ const destDriver = await initializeDriver(destinationProviderEntity, options?.destinationUserContext);
523
+ console.log('[copyObjectBetweenProviders] Drivers initialized, fetching file from source...');
524
+ // Normalize source path: remove leading/trailing slashes and collapse multiple slashes
525
+ // This ensures consistent path handling across different storage providers
526
+ const normalizedSourcePath = sourcePath.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
527
+ console.log('[copyObjectBetweenProviders] Path normalization:', {
528
+ original: sourcePath,
529
+ normalized: normalizedSourcePath,
530
+ });
531
+ // Get the file from source provider
532
+ const fileData = await sourceDriver.GetObject({ fullPath: normalizedSourcePath });
533
+ if (!fileData || fileData.length === 0) {
534
+ result.message = `Failed to retrieve file from source: ${normalizedSourcePath}`;
535
+ console.error('[copyObjectBetweenProviders]', result.message);
536
+ return result;
537
+ }
538
+ console.log('[copyObjectBetweenProviders] File retrieved, size:', fileData.length, 'bytes');
539
+ // Get metadata for content type
540
+ let contentType = 'application/octet-stream';
541
+ try {
542
+ const metadata = await sourceDriver.GetObjectMetadata({ fullPath: normalizedSourcePath });
543
+ if (metadata.contentType) {
544
+ contentType = metadata.contentType;
545
+ }
546
+ }
547
+ catch {
548
+ // Use mime-types to guess content type from filename
549
+ const mimeType = mime_types_1.default.lookup(sourcePath);
550
+ if (mimeType) {
551
+ contentType = mimeType;
552
+ }
553
+ }
554
+ console.log('[copyObjectBetweenProviders] Uploading to destination with contentType:', contentType);
555
+ // Upload to destination provider
556
+ const uploadSuccess = await destDriver.PutObject(destinationPath, fileData, contentType);
557
+ if (uploadSuccess) {
558
+ result.success = true;
559
+ result.bytesTransferred = fileData.length;
560
+ result.message = `Successfully copied ${fileData.length} bytes from ${sourceProviderEntity.Name} to ${destinationProviderEntity.Name}`;
561
+ console.log('[copyObjectBetweenProviders]', result.message);
562
+ }
563
+ else {
564
+ result.message = `Failed to upload file to destination: ${destinationPath}`;
565
+ console.error('[copyObjectBetweenProviders]', result.message);
566
+ }
567
+ return result;
568
+ }
569
+ catch (error) {
570
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
571
+ result.message = `Transfer failed: ${errorMessage}`;
572
+ console.error('[copyObjectBetweenProviders] Error:', error);
573
+ return result;
574
+ }
575
+ };
576
+ exports.copyObjectBetweenProviders = copyObjectBetweenProviders;
577
+ /**
578
+ * Searches for files across multiple storage providers in parallel.
579
+ *
580
+ * This utility function enables searching for files across multiple storage providers
581
+ * simultaneously. Each provider is queried in parallel using Promise.allSettled,
582
+ * ensuring that failures in one provider don't affect results from others.
583
+ *
584
+ * Providers that don't support search (SupportsSearch = false) will return a result
585
+ * with success=false and an appropriate error message rather than being silently skipped.
586
+ *
587
+ * @param providerEntities - Array of provider entities to search
588
+ * @param query - Search query string
589
+ * @param options - Optional search configuration including user contexts for OAuth providers
590
+ * @returns A promise that resolves to aggregated results grouped by provider
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * // Search across Google Drive and Dropbox
595
+ * const providers = [googleDriveProvider, dropboxProvider];
596
+ * const userContexts = new Map([
597
+ * [googleDriveProvider.ID, { userID: currentUser.ID, contextUser }],
598
+ * [dropboxProvider.ID, { userID: currentUser.ID, contextUser }]
599
+ * ]);
600
+ *
601
+ * const result = await searchAcrossProviders(providers, 'quarterly report', {
602
+ * maxResultsPerProvider: 25,
603
+ * fileTypes: ['pdf', 'docx'],
604
+ * providerUserContexts: userContexts
605
+ * });
606
+ *
607
+ * // Process results by provider
608
+ * for (const providerResult of result.providerResults) {
609
+ * if (providerResult.success) {
610
+ * console.log(`${providerResult.providerName}: ${providerResult.results.length} results`);
611
+ * } else {
612
+ * console.log(`${providerResult.providerName}: ${providerResult.errorMessage}`);
613
+ * }
614
+ * }
615
+ * ```
616
+ */
617
+ const searchAcrossProviders = async (providerEntities, query, options) => {
618
+ console.log('[searchAcrossProviders] Starting search:', {
619
+ providerCount: providerEntities.length,
620
+ providers: providerEntities.map((p) => p.Name),
621
+ query,
622
+ options: { ...options, providerUserContexts: options?.providerUserContexts ? '[Map]' : undefined },
623
+ });
624
+ const maxResults = options?.maxResultsPerProvider ?? 50;
625
+ const searchOptions = {
626
+ maxResults,
627
+ fileTypes: options?.fileTypes,
628
+ searchContent: options?.searchContent ?? false,
629
+ };
630
+ // Create search promises for each provider
631
+ const searchPromises = providerEntities.map(async (providerEntity) => {
632
+ const providerResult = {
633
+ providerID: providerEntity.ID,
634
+ providerName: providerEntity.Name,
635
+ success: false,
636
+ results: [],
637
+ hasMore: false,
638
+ };
639
+ try {
640
+ // Check if provider supports search
641
+ if (!providerEntity.SupportsSearch) {
642
+ providerResult.errorMessage = 'This provider does not support search';
643
+ console.log(`[searchAcrossProviders] ${providerEntity.Name}: Does not support search`);
644
+ return providerResult;
645
+ }
646
+ // Get user context for this provider if available
647
+ const userContext = options?.providerUserContexts?.get(providerEntity.ID);
648
+ // Initialize driver with user credentials if available
649
+ const driver = await initializeDriver(providerEntity, userContext);
650
+ // Execute search
651
+ console.log(`[searchAcrossProviders] ${providerEntity.Name}: Executing search...`);
652
+ const searchResult = await driver.SearchFiles(query, searchOptions);
653
+ providerResult.success = true;
654
+ providerResult.results = searchResult.results;
655
+ providerResult.totalMatches = searchResult.totalMatches;
656
+ providerResult.hasMore = searchResult.hasMore;
657
+ providerResult.nextPageToken = searchResult.nextPageToken;
658
+ console.log(`[searchAcrossProviders] ${providerEntity.Name}: Found ${searchResult.results.length} results`);
659
+ }
660
+ catch (error) {
661
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
662
+ providerResult.errorMessage = errorMessage;
663
+ console.error(`[searchAcrossProviders] ${providerEntity.Name}: Error -`, errorMessage);
664
+ }
665
+ return providerResult;
666
+ });
667
+ // Execute all searches in parallel
668
+ const settledResults = await Promise.allSettled(searchPromises);
669
+ // Aggregate results
670
+ const providerResults = [];
671
+ let totalResultsReturned = 0;
672
+ let successfulProviders = 0;
673
+ let failedProviders = 0;
674
+ for (const settled of settledResults) {
675
+ if (settled.status === 'fulfilled') {
676
+ const result = settled.value;
677
+ providerResults.push(result);
678
+ if (result.success) {
679
+ successfulProviders++;
680
+ totalResultsReturned += result.results.length;
681
+ }
682
+ else {
683
+ failedProviders++;
684
+ }
685
+ }
686
+ else {
687
+ // Promise rejected (shouldn't happen with our try/catch, but handle it)
688
+ failedProviders++;
689
+ console.error('[searchAcrossProviders] Promise rejected:', settled.reason);
690
+ }
691
+ }
692
+ const aggregatedResult = {
693
+ providerResults,
694
+ totalResultsReturned,
695
+ successfulProviders,
696
+ failedProviders,
697
+ };
698
+ console.log('[searchAcrossProviders] Search complete:', {
699
+ totalResultsReturned,
700
+ successfulProviders,
701
+ failedProviders,
702
+ });
703
+ return aggregatedResult;
704
+ };
705
+ exports.searchAcrossProviders = searchAcrossProviders;
706
+ /**
707
+ * Searches for files across multiple storage accounts in parallel.
708
+ *
709
+ * This is the enterprise version of search that uses the account-based credential model.
710
+ * Each account is searched independently, allowing multiple accounts from the same
711
+ * provider type to be searched (e.g., two different Dropbox accounts).
712
+ *
713
+ * @param accounts - Array of account/provider pairs to search
714
+ * @param query - Search query string
715
+ * @param options - Search configuration including context user for credentials
716
+ * @returns A promise that resolves to aggregated results grouped by account
717
+ *
718
+ * @example
719
+ * ```typescript
720
+ * const accounts = [
721
+ * { accountEntity: researchDropbox, providerEntity: dropboxProvider },
722
+ * { accountEntity: marketingDropbox, providerEntity: dropboxProvider },
723
+ * { accountEntity: engineeringGDrive, providerEntity: gdriveProvider }
724
+ * ];
725
+ *
726
+ * const result = await searchAcrossAccounts(accounts, 'quarterly report', {
727
+ * maxResultsPerAccount: 25,
728
+ * fileTypes: ['pdf', 'docx'],
729
+ * contextUser: currentUser
730
+ * });
731
+ *
732
+ * for (const accountResult of result.accountResults) {
733
+ * if (accountResult.success) {
734
+ * console.log(`${accountResult.accountName}: ${accountResult.results.length} results`);
735
+ * }
736
+ * }
737
+ * ```
738
+ */
739
+ const searchAcrossAccounts = async (accounts, query, options) => {
740
+ console.log('[searchAcrossAccounts] Starting search:', {
741
+ accountCount: accounts.length,
742
+ accounts: accounts.map((a) => ({ account: a.accountEntity.Name, provider: a.providerEntity.Name })),
743
+ query,
744
+ maxResultsPerAccount: options.maxResultsPerAccount,
745
+ fileTypes: options.fileTypes,
746
+ searchContent: options.searchContent,
747
+ });
748
+ const maxResults = options.maxResultsPerAccount ?? 50;
749
+ const searchOptions = {
750
+ maxResults,
751
+ fileTypes: options.fileTypes,
752
+ searchContent: options.searchContent ?? false,
753
+ };
754
+ // Create search promises for each account
755
+ const searchPromises = accounts.map(async ({ accountEntity, providerEntity }) => {
756
+ const accountResult = {
757
+ accountID: accountEntity.ID,
758
+ accountName: accountEntity.Name,
759
+ providerID: providerEntity.ID,
760
+ providerName: providerEntity.Name,
761
+ success: false,
762
+ results: [],
763
+ hasMore: false,
764
+ };
765
+ try {
766
+ // Check if provider supports search
767
+ if (!providerEntity.SupportsSearch) {
768
+ accountResult.errorMessage = 'This provider does not support search';
769
+ console.log(`[searchAcrossAccounts] ${accountEntity.Name}: Provider does not support search`);
770
+ return accountResult;
771
+ }
772
+ // Initialize driver with account-based credentials
773
+ const driver = await initializeDriverWithAccountCredentials({
774
+ accountEntity,
775
+ providerEntity,
776
+ contextUser: options.contextUser,
777
+ });
778
+ // Execute search
779
+ console.log(`[searchAcrossAccounts] ${accountEntity.Name}: Executing search...`);
780
+ const searchResult = await driver.SearchFiles(query, searchOptions);
781
+ accountResult.success = true;
782
+ accountResult.results = searchResult.results;
783
+ accountResult.totalMatches = searchResult.totalMatches;
784
+ accountResult.hasMore = searchResult.hasMore;
785
+ accountResult.nextPageToken = searchResult.nextPageToken;
786
+ console.log(`[searchAcrossAccounts] ${accountEntity.Name}: Found ${searchResult.results.length} results`);
787
+ }
788
+ catch (error) {
789
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
790
+ accountResult.errorMessage = errorMessage;
791
+ console.error(`[searchAcrossAccounts] ${accountEntity.Name}: Error -`, errorMessage);
792
+ }
793
+ return accountResult;
794
+ });
795
+ // Execute all searches in parallel
796
+ const settledResults = await Promise.allSettled(searchPromises);
797
+ // Aggregate results
798
+ const accountResults = [];
799
+ let totalResultsReturned = 0;
800
+ let successfulAccounts = 0;
801
+ let failedAccounts = 0;
802
+ for (const settled of settledResults) {
803
+ if (settled.status === 'fulfilled') {
804
+ const result = settled.value;
805
+ accountResults.push(result);
806
+ if (result.success) {
807
+ successfulAccounts++;
808
+ totalResultsReturned += result.results.length;
809
+ }
810
+ else {
811
+ failedAccounts++;
812
+ }
813
+ }
814
+ else {
815
+ // Promise rejected (shouldn't happen with our try/catch, but handle it)
816
+ failedAccounts++;
817
+ console.error('[searchAcrossAccounts] Promise rejected:', settled.reason);
818
+ }
819
+ }
820
+ const aggregatedResult = {
821
+ accountResults,
822
+ totalResultsReturned,
823
+ successfulAccounts,
824
+ failedAccounts,
825
+ };
826
+ console.log('[searchAcrossAccounts] Search complete:', {
827
+ totalResultsReturned,
828
+ successfulAccounts,
829
+ failedAccounts,
830
+ });
831
+ return aggregatedResult;
832
+ };
833
+ exports.searchAcrossAccounts = searchAcrossAccounts;
173
834
  //# sourceMappingURL=util.js.map