@memberjunction/storage 3.4.0 → 4.1.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.js +10 -9
  2. package/dist/__tests__/FileStorageBase.test.js.map +1 -1
  3. package/dist/__tests__/util.test.js +29 -49
  4. package/dist/__tests__/util.test.js.map +1 -1
  5. package/dist/config.js +56 -63
  6. package/dist/config.js.map +1 -1
  7. package/dist/drivers/AWSFileStorage.d.ts +2 -3
  8. package/dist/drivers/AWSFileStorage.d.ts.map +1 -1
  9. package/dist/drivers/AWSFileStorage.js +31 -69
  10. package/dist/drivers/AWSFileStorage.js.map +1 -1
  11. package/dist/drivers/AzureFileStorage.d.ts +1 -2
  12. package/dist/drivers/AzureFileStorage.d.ts.map +1 -1
  13. package/dist/drivers/AzureFileStorage.js +29 -65
  14. package/dist/drivers/AzureFileStorage.js.map +1 -1
  15. package/dist/drivers/BoxFileStorage.d.ts +2 -3
  16. package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
  17. package/dist/drivers/BoxFileStorage.js +33 -92
  18. package/dist/drivers/BoxFileStorage.js.map +1 -1
  19. package/dist/drivers/DropboxFileStorage.d.ts +2 -3
  20. package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
  21. package/dist/drivers/DropboxFileStorage.js +19 -57
  22. package/dist/drivers/DropboxFileStorage.js.map +1 -1
  23. package/dist/drivers/GoogleDriveFileStorage.d.ts +2 -3
  24. package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
  25. package/dist/drivers/GoogleDriveFileStorage.js +27 -57
  26. package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
  27. package/dist/drivers/GoogleFileStorage.d.ts +1 -2
  28. package/dist/drivers/GoogleFileStorage.d.ts.map +1 -1
  29. package/dist/drivers/GoogleFileStorage.js +17 -47
  30. package/dist/drivers/GoogleFileStorage.js.map +1 -1
  31. package/dist/drivers/SharePointFileStorage.d.ts +2 -3
  32. package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
  33. package/dist/drivers/SharePointFileStorage.js +33 -131
  34. package/dist/drivers/SharePointFileStorage.js.map +1 -1
  35. package/dist/generic/FileStorageBase.d.ts +0 -1
  36. package/dist/generic/FileStorageBase.d.ts.map +1 -1
  37. package/dist/generic/FileStorageBase.js +2 -16
  38. package/dist/generic/FileStorageBase.js.map +1 -1
  39. package/dist/index.d.ts +10 -10
  40. package/dist/index.js +10 -26
  41. package/dist/index.js.map +1 -1
  42. package/dist/util.d.ts +2 -2
  43. package/dist/util.d.ts.map +1 -1
  44. package/dist/util.js +30 -47
  45. package/dist/util.js.map +1 -1
  46. package/package.json +24 -23
  47. package/readme.md +688 -371
package/readme.md CHANGED
@@ -2,11 +2,67 @@
2
2
 
3
3
  The `@memberjunction/storage` library provides a unified interface for interacting with various cloud storage providers. It abstracts the complexities of different storage services behind a consistent API, making it easy to work with files stored across different cloud platforms.
4
4
 
5
- [![MemberJunction Logo](https://memberjunction.com/images/MJ_Dark_Logo_Transparent_tm.png)](https://memberjunction.com)
5
+ Part of the [MemberJunction](https://github.com/MemberJunction/MJ) framework.
6
6
 
7
7
  ## Overview
8
8
 
9
- This library is a key component of the MemberJunction platform, providing seamless file storage operations across multiple cloud providers. It offers a provider-agnostic approach to file management, allowing applications to switch between storage providers without code changes.
9
+ This library is a key component of the MemberJunction platform, providing seamless file storage operations across multiple cloud providers. It offers a provider-agnostic approach to file management, allowing applications to switch between storage providers without code changes. The package supports both simple single-tenant deployments using environment variables and enterprise multi-tenant deployments using the MemberJunction Credential Engine for secure credential management.
10
+
11
+ ## Architecture
12
+
13
+ The library is organized around an abstract base class (`FileStorageBase`) with concrete driver implementations for each supported cloud storage provider. A set of high-level utility functions in `util.ts` bridge the gap between MemberJunction entities and the underlying drivers. Configuration is handled through Zod-validated schemas loaded from `mj.config.cjs` or environment variables.
14
+
15
+ ```mermaid
16
+ graph TB
17
+ subgraph Application["Application Layer"]
18
+ style Application fill:#2d6a9f,stroke:#1a4971,color:#fff
19
+ UtilFunctions["Utility Functions<br/>createUploadUrl, createDownloadUrl,<br/>moveObject, deleteObject, listObjects,<br/>copyObjectBetweenProviders,<br/>searchAcrossProviders"]
20
+ end
21
+
22
+ subgraph Core["Core Abstractions"]
23
+ style Core fill:#7c5295,stroke:#563a6b,color:#fff
24
+ FSBase["FileStorageBase<br/>(Abstract Base Class)"]
25
+ Config["StorageConfig<br/>(Zod Schema)"]
26
+ end
27
+
28
+ subgraph Drivers["Storage Provider Drivers"]
29
+ style Drivers fill:#2d8659,stroke:#1a5c3a,color:#fff
30
+ AWS["AWSFileStorage<br/>'AWS S3'"]
31
+ Azure["AzureFileStorage<br/>'Azure Blob Storage'"]
32
+ GCS["GoogleFileStorage<br/>'Google Cloud Storage'"]
33
+ GDrive["GoogleDriveFileStorage<br/>'Google Drive Storage'"]
34
+ SP["SharePointFileStorage<br/>'SharePoint'"]
35
+ Dropbox["DropboxFileStorage<br/>'Dropbox'"]
36
+ Box["BoxFileStorage<br/>'Box'"]
37
+ end
38
+
39
+ subgraph MJ["MemberJunction Integration"]
40
+ style MJ fill:#b8762f,stroke:#8a5722,color:#fff
41
+ ClassFactory["MJGlobal ClassFactory<br/>@RegisterClass"]
42
+ Entities["FileStorageProviderEntity<br/>FileStorageAccountEntity"]
43
+ CredEngine["CredentialEngine<br/>Secure credential decryption"]
44
+ end
45
+
46
+ UtilFunctions --> FSBase
47
+ UtilFunctions --> ClassFactory
48
+ UtilFunctions --> Entities
49
+ UtilFunctions --> CredEngine
50
+ FSBase --> Config
51
+ AWS --> FSBase
52
+ Azure --> FSBase
53
+ GCS --> FSBase
54
+ GDrive --> FSBase
55
+ SP --> FSBase
56
+ Dropbox --> FSBase
57
+ Box --> FSBase
58
+ ClassFactory --> AWS
59
+ ClassFactory --> Azure
60
+ ClassFactory --> GCS
61
+ ClassFactory --> GDrive
62
+ ClassFactory --> SP
63
+ ClassFactory --> Dropbox
64
+ ClassFactory --> Box
65
+ ```
10
66
 
11
67
  ## Features
12
68
 
@@ -16,27 +72,37 @@ This library is a key component of the MemberJunction platform, providing seamle
16
72
  - **Pre-authenticated URLs**: Secure upload and download operations using time-limited URLs
17
73
  - **Metadata Support**: Store and retrieve custom metadata with your files
18
74
  - **Error Handling**: Provider-specific errors are normalized with clear error messages
19
- - **Commonly Supported Storage Providers**:
20
- - [AWS S3](https://aws.amazon.com/s3/)
21
- - [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs)
22
- - [Google Cloud Storage](https://cloud.google.com/storage)
23
- - [Google Drive](https://developers.google.com/drive/api/guides/about-sdk)
24
- - [Microsoft SharePoint](https://learn.microsoft.com/en-us/sharepoint/dev/)
25
- - [Dropbox](https://www.dropbox.com/developers/documentation)
26
- - [Box](https://developer.box.com/guides/)
27
- - **Common File Operations**:
28
- - Upload files (via pre-authenticated URLs)
29
- - Download files (via pre-authenticated URLs)
30
- - Copy and move files
31
- - Delete files and directories
32
- - List files and directories with metadata
33
- - Create and manage directories
34
- - Get detailed file metadata
35
- - Check file/directory existence
36
- - Direct upload/download via Buffer
37
- - **Search files** using native provider search APIs
75
+ - **Zod-Validated Configuration**: All configuration schemas are validated at load time via Zod
76
+ - **Enterprise Credential Management**: Integrates with `@memberjunction/credentials` for secure, multi-tenant credential storage and decryption
77
+ - **Cross-Provider Copy**: Copy files between different storage providers server-side
78
+ - **Multi-Provider Search**: Search for files across multiple providers or accounts in parallel
79
+ - **Token Refresh Persistence**: Automatic callback system to persist new OAuth tokens (critical for providers like Box that rotate refresh tokens)
38
80
  - **Extensible**: Easy to add new storage providers by extending `FileStorageBase`
39
81
 
82
+ ### Supported Storage Providers
83
+
84
+ | Provider | Driver Key | Search | Pre-auth URLs | Native Directories |
85
+ |---|---|---|---|---|
86
+ | [AWS S3](https://aws.amazon.com/s3/) | `AWS S3` | No | Yes | Simulated |
87
+ | [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs) | `Azure Blob Storage` | No | Yes | Simulated |
88
+ | [Google Cloud Storage](https://cloud.google.com/storage) | `Google Cloud Storage` | No | Yes | Simulated |
89
+ | [Google Drive](https://developers.google.com/drive/api/guides/about-sdk) | `Google Drive Storage` | Yes (content) | Yes | Native |
90
+ | [Microsoft SharePoint](https://learn.microsoft.com/en-us/sharepoint/dev/) | `SharePoint` | Yes (content) | Yes | Native |
91
+ | [Dropbox](https://www.dropbox.com/developers/documentation) | `Dropbox` | Yes (content) | Yes | Native |
92
+ | [Box](https://developer.box.com/guides/) | `Box` | Yes (metadata) | Yes | Native |
93
+
94
+ ### File Operations
95
+
96
+ - Upload files (via pre-authenticated URLs or direct Buffer upload)
97
+ - Download files (via pre-authenticated URLs or direct Buffer download)
98
+ - Copy and move files (within and across providers)
99
+ - Delete files and directories (with optional recursive deletion)
100
+ - List files and directories with full metadata
101
+ - Create and manage directories
102
+ - Get detailed file metadata without downloading content
103
+ - Check file/directory existence
104
+ - Search files using native provider search APIs
105
+
40
106
  ## Installation
41
107
 
42
108
  ```bash
@@ -46,24 +112,56 @@ npm install @memberjunction/storage
46
112
  ## Dependencies
47
113
 
48
114
  This package depends on:
49
- - `@memberjunction/core` - Core MemberJunction functionality
50
- - `@memberjunction/core-entities` - Entity definitions including `FileStorageProviderEntity`
51
- - `@memberjunction/global` - Global utilities and class registration
52
- - Provider-specific SDKs (installed as dependencies)
115
+
116
+ | Package | Purpose |
117
+ |---|---|
118
+ | `@memberjunction/core` | Core MemberJunction functionality (logging, UserInfo) |
119
+ | `@memberjunction/core-entities` | Entity definitions (FileStorageProviderEntity, FileStorageAccountEntity) |
120
+ | `@memberjunction/global` | Global class factory and `@RegisterClass` decorator |
121
+ | `@memberjunction/credentials` | Credential Engine for secure credential decryption |
122
+ | `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` | AWS S3 SDK |
123
+ | `@azure/storage-blob`, `@azure/identity` | Azure Blob Storage SDK |
124
+ | `@google-cloud/storage` | Google Cloud Storage SDK |
125
+ | `googleapis` | Google Drive API |
126
+ | `@microsoft/microsoft-graph-client` | SharePoint via Microsoft Graph |
127
+ | `dropbox` | Dropbox SDK |
128
+ | `box-node-sdk` | Box SDK |
129
+ | `cosmiconfig` | Configuration file loading |
130
+ | `zod` | Configuration schema validation |
131
+ | `env-var` | Environment variable parsing |
132
+ | `mime-types` | MIME type detection |
53
133
 
54
134
  ## Usage
55
135
 
56
- ### Standard Usage Pattern
136
+ ### Initialization Flow
137
+
138
+ Every storage driver follows a two-step initialization pattern. The `initialize()` method is smart enough to handle both simple deployments (environment variables) and multi-tenant deployments (database credentials).
139
+
140
+ ```mermaid
141
+ flowchart TD
142
+ Start(["Create Driver Instance"]) --> Constructor["Constructor<br/>Loads env vars / config defaults"]
143
+ Constructor --> Init{"Call initialize()"}
57
144
 
58
- **CRITICAL**: Always follow these steps when using storage providers:
145
+ Init -->|"No config"| EnvPath["Use environment variables<br/>already loaded by constructor"]
146
+ Init -->|"With config"| ConfigPath["Override credentials<br/>with provided config"]
59
147
 
60
- 1. Create provider instance
61
- 2. **Call `initialize()`** (with or without config)
62
- 3. Use provider
148
+ EnvPath --> SetAccount["Set accountId / accountName<br/>if provided"]
149
+ ConfigPath --> SetAccount
63
150
 
64
- The `initialize()` method is smart enough to handle both simple deployments (environment variables) and multi-tenant deployments (database credentials).
151
+ SetAccount --> Reinit["Reinitialize SDK client<br/>with final credentials"]
152
+ Reinit --> Ready(["Driver Ready"])
153
+
154
+ style Start fill:#2d6a9f,stroke:#1a4971,color:#fff
155
+ style Ready fill:#2d8659,stroke:#1a5c3a,color:#fff
156
+ style Init fill:#b8762f,stroke:#8a5722,color:#fff
157
+ style Constructor fill:#7c5295,stroke:#563a6b,color:#fff
158
+ style EnvPath fill:#7c5295,stroke:#563a6b,color:#fff
159
+ style ConfigPath fill:#7c5295,stroke:#563a6b,color:#fff
160
+ style SetAccount fill:#7c5295,stroke:#563a6b,color:#fff
161
+ style Reinit fill:#7c5295,stroke:#563a6b,color:#fff
162
+ ```
65
163
 
66
- ### Basic Setup - Simple Deployment (Environment Variables)
164
+ ### Simple Deployment (Environment Variables)
67
165
 
68
166
  For single-tenant applications, development, testing, or simple production deployments:
69
167
 
@@ -89,11 +187,11 @@ await storage.ListObjects('/');
89
187
 
90
188
  ### Multi-Tenant Enterprise (Database Credentials)
91
189
 
92
- For enterprise applications managing multiple storage accounts:
190
+ For enterprise applications managing multiple storage accounts via the MemberJunction entity system and Credential Engine:
93
191
 
94
192
  ```typescript
95
193
  import { FileStorageEngine } from '@memberjunction/core-entities';
96
- import { initializeDriverWithAccountCredentials } from '@memberjunction/storage/util';
194
+ import { initializeDriverWithAccountCredentials } from '@memberjunction/storage';
97
195
 
98
196
  // Load account from database
99
197
  const engine = FileStorageEngine.Instance;
@@ -112,34 +210,74 @@ const storage = await initializeDriverWithAccountCredentials({
112
210
  await storage.ListObjects('/');
113
211
  ```
114
212
 
115
- **Key Advantage**: The `initializeDriverWithAccountCredentials()` utility:
116
- - Automatically retrieves the credential by ID
117
- - Decrypts it using CredentialEngine
118
- - Calls `initialize()` with the decrypted values
119
- - Returns a fully configured provider instance
213
+ The `initializeDriverWithAccountCredentials()` utility:
214
+ - Creates the driver instance via the MJGlobal ClassFactory
215
+ - Retrieves the credential by ID from the Credential Engine
216
+ - Decrypts the credential values
217
+ - Configures a token refresh callback to persist rotated tokens back to the database
218
+ - Calls `initialize()` with decrypted values and account information
219
+
220
+ ### Enterprise Credential Flow
221
+
222
+ ```mermaid
223
+ sequenceDiagram
224
+ participant App as Application
225
+ participant Util as initializeDriverWithAccountCredentials
226
+ participant CF as ClassFactory
227
+ participant CE as CredentialEngine
228
+ participant Driver as Storage Driver
229
+
230
+ App->>Util: { accountEntity, providerEntity, contextUser }
231
+ Util->>CF: CreateInstance(FileStorageBase, driverKey)
232
+ CF-->>Util: driver instance
233
+
234
+ alt Account has CredentialID
235
+ Util->>CE: Config(false, contextUser)
236
+ Util->>CE: getCredentialById(credentialID)
237
+ CE-->>Util: credentialEntity
238
+ Util->>CE: getCredential(name, options)
239
+ CE-->>Util: { values: decrypted credentials }
240
+ Util->>Util: Create onTokenRefresh callback
241
+ Util->>Driver: initialize({ accountId, ...decryptedValues, onTokenRefresh })
242
+ else No CredentialID
243
+ alt Provider has Configuration JSON
244
+ Util->>Driver: initialize({ accountId, ...providerConfig })
245
+ else No configuration
246
+ Util->>Driver: initialize({ accountId })
247
+ end
248
+ end
249
+
250
+ Driver-->>App: initialized driver
251
+ ```
120
252
 
121
- ### Using Utility Functions (Recommended)
253
+ ### Using Utility Functions
122
254
 
123
255
  The library provides high-level utility functions that work with MemberJunction's entity system:
124
256
 
125
257
  ```typescript
126
- import { createUploadUrl, createDownloadUrl, deleteObject, moveObject } from '@memberjunction/storage';
258
+ import {
259
+ createUploadUrl,
260
+ createDownloadUrl,
261
+ moveObject,
262
+ deleteObject,
263
+ listObjects,
264
+ copyObject
265
+ } from '@memberjunction/storage';
127
266
  import { FileStorageProviderEntity } from '@memberjunction/core-entities';
128
267
  import { Metadata } from '@memberjunction/core';
129
268
 
130
- // Load a FileStorageProviderEntity from the database
131
269
  async function fileOperationsExample() {
132
270
  const md = new Metadata();
133
271
  const provider = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers');
134
272
  await provider.Load('your-provider-id');
135
-
273
+
136
274
  // Create pre-authenticated upload URL
137
275
  const { updatedInput, UploadUrl } = await createUploadUrl(
138
- provider,
139
- {
140
- ID: '123',
141
- Name: 'documents/report.pdf',
142
- ProviderID: provider.ID
276
+ provider,
277
+ {
278
+ ID: '123',
279
+ Name: 'documents/report.pdf',
280
+ ProviderID: provider.ID
143
281
  }
144
282
  );
145
283
 
@@ -147,25 +285,27 @@ async function fileOperationsExample() {
147
285
  console.log(`Upload URL: ${UploadUrl}`);
148
286
  console.log(`File status: ${updatedInput.Status}`); // 'Uploading'
149
287
  console.log(`Content type: ${updatedInput.ContentType}`); // 'application/pdf'
150
-
288
+
151
289
  // If a ProviderKey was returned, use it for future operations
152
290
  const fileIdentifier = updatedInput.ProviderKey || updatedInput.Name;
153
-
154
- // Later, create pre-authenticated download URL
291
+
292
+ // Create pre-authenticated download URL
155
293
  const downloadUrl = await createDownloadUrl(provider, fileIdentifier);
156
- console.log(`Download URL: ${downloadUrl}`);
157
-
158
- // Move the file to a new location
159
- const moved = await moveObject(
160
- provider,
161
- fileIdentifier,
162
- 'documents/archived/report_2024.pdf'
163
- );
164
- console.log(`File moved: ${moved}`);
165
-
166
- // Delete the file when no longer needed
167
- const deleted = await deleteObject(provider, 'documents/archived/report_2024.pdf');
168
- console.log(`File deleted: ${deleted}`);
294
+
295
+ // List directory contents
296
+ const contents = await listObjects(provider, 'documents/');
297
+ for (const file of contents.objects) {
298
+ console.log(`${file.name} (${file.size} bytes)`);
299
+ }
300
+
301
+ // Copy a file
302
+ await copyObject(provider, fileIdentifier, 'documents/report-backup.pdf');
303
+
304
+ // Move a file
305
+ await moveObject(provider, fileIdentifier, 'archive/report.pdf');
306
+
307
+ // Delete a file
308
+ await deleteObject(provider, 'archive/report.pdf');
169
309
  }
170
310
  ```
171
311
 
@@ -174,184 +314,344 @@ async function fileOperationsExample() {
174
314
  You can work directly with a storage provider by instantiating it:
175
315
 
176
316
  ```typescript
177
- import { FileStorageBase } from '@memberjunction/storage';
317
+ import { AzureFileStorage, FileStorageBase } from '@memberjunction/storage';
178
318
  import { MJGlobal } from '@memberjunction/global';
179
319
 
180
320
  async function directProviderExample() {
181
321
  // Method 1: Direct instantiation (simple deployment with env vars)
182
- const storage = new AzureFileStorage(); // Constructor loads env vars
183
- await storage.initialize(); // ALWAYS call initialize()
322
+ const storage = new AzureFileStorage();
323
+ await storage.initialize();
184
324
 
185
325
  // Method 2: Using class factory (dynamic provider selection)
186
326
  const storage2 = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
187
327
  FileStorageBase,
188
328
  'Azure Blob Storage'
189
329
  );
190
- await storage2.initialize(); // ALWAYS call initialize()
191
-
192
- // Method 3: Multi-tenant with manual initialization
193
- const storage3 = new AzureFileStorage();
194
- await storage3.initialize({
195
- accountId: '12345',
196
- accountName: 'Azure Account',
197
- accountName: 'myaccount',
198
- accountKey: '...',
199
- defaultContainer: 'my-container'
200
- });
201
-
202
- // Now you can use any of the storage methods:
330
+ await storage2.initialize();
203
331
 
204
332
  // List all files in a directory
205
333
  const result = await storage.ListObjects('documents/');
206
334
  console.log('Files:', result.objects);
207
335
  console.log('Directories:', result.prefixes);
208
336
 
209
- // Display detailed metadata for each file
210
- for (const file of result.objects) {
211
- console.log(`\nFile: ${file.name}`);
212
- console.log(` Path: ${file.path}`);
213
- console.log(` Full Path: ${file.fullPath}`);
214
- console.log(` Size: ${file.size} bytes`);
215
- console.log(` Type: ${file.contentType}`);
216
- console.log(` Modified: ${file.lastModified}`);
217
- console.log(` Is Directory: ${file.isDirectory}`);
218
- }
219
-
220
- // Create a directory
221
- const dirCreated = await storage.CreateDirectory('documents/reports/');
222
- console.log(`Directory created: ${dirCreated}`);
223
-
224
337
  // Upload a file directly with metadata
225
338
  const content = Buffer.from('Hello, World!');
226
- const uploaded = await storage.PutObject(
339
+ await storage.PutObject(
227
340
  'documents/reports/hello.txt',
228
341
  content,
229
342
  'text/plain',
230
- {
231
- author: 'John Doe',
232
- department: 'Engineering',
233
- version: '1.0'
234
- }
343
+ { author: 'John Doe', department: 'Engineering' }
235
344
  );
236
- console.log(`File uploaded: ${uploaded}`);
237
345
 
238
346
  // Get file metadata without downloading content
239
- const metadata = await storage.GetObjectMetadata('documents/reports/hello.txt');
347
+ const metadata = await storage.GetObjectMetadata({
348
+ fullPath: 'documents/reports/hello.txt'
349
+ });
240
350
  console.log('File metadata:', metadata);
241
351
 
242
352
  // Download file content
243
- const fileContent = await storage.GetObject('documents/reports/hello.txt');
353
+ const fileContent = await storage.GetObject({
354
+ fullPath: 'documents/reports/hello.txt'
355
+ });
244
356
  console.log('File content:', fileContent.toString('utf8'));
245
357
 
246
358
  // Copy a file
247
- const copied = await storage.CopyObject(
359
+ await storage.CopyObject(
248
360
  'documents/reports/hello.txt',
249
361
  'documents/archive/hello-backup.txt'
250
362
  );
251
- console.log(`File copied: ${copied}`);
252
363
 
253
364
  // Check if a file exists
254
365
  const exists = await storage.ObjectExists('documents/reports/hello.txt');
255
- console.log(`File exists: ${exists}`);
256
366
 
257
367
  // Check if a directory exists
258
368
  const dirExists = await storage.DirectoryExists('documents/reports/');
259
- console.log(`Directory exists: ${dirExists}`);
260
369
 
261
370
  // Delete a directory and all its contents
262
- const dirDeleted = await storage.DeleteDirectory('documents/reports/', true);
263
- console.log(`Directory deleted: ${dirDeleted}`);
371
+ await storage.DeleteDirectory('documents/reports/', true);
372
+ }
373
+ ```
374
+
375
+ ### Cross-Provider File Copy
376
+
377
+ Transfer files between different storage providers server-side:
378
+
379
+ ```typescript
380
+ import { copyObjectBetweenProviders } from '@memberjunction/storage';
381
+
382
+ const result = await copyObjectBetweenProviders(
383
+ sourceProviderEntity,
384
+ destProviderEntity,
385
+ 'documents/report.pdf',
386
+ 'imported/report.pdf',
387
+ {
388
+ sourceUserContext: { userID: currentUser.ID, contextUser },
389
+ destinationUserContext: { userID: currentUser.ID, contextUser }
390
+ }
391
+ );
392
+
393
+ if (result.success) {
394
+ console.log(`Transferred ${result.bytesTransferred} bytes`);
264
395
  }
265
396
  ```
266
397
 
267
- ### Key Principle
398
+ ```mermaid
399
+ flowchart LR
400
+ Source["Source Provider<br/>(e.g. Dropbox)"] -->|"GetObject()"| Server["MJ Server<br/>(in-memory Buffer)"]
401
+ Server -->|"PutObject()"| Dest["Destination Provider<br/>(e.g. Google Drive)"]
268
402
 
269
- **Always call `initialize()`** after creating a provider instance. It's smart enough to:
270
- - Use environment variables when called with no config
271
- - Override with database credentials when called with config
272
- - Handle both simple and multi-tenant deployments seamlessly
403
+ style Source fill:#2d6a9f,stroke:#1a4971,color:#fff
404
+ style Server fill:#b8762f,stroke:#8a5722,color:#fff
405
+ style Dest fill:#2d8659,stroke:#1a5c3a,color:#fff
406
+ ```
273
407
 
274
408
  ### Searching Files
275
409
 
276
- Providers with native search capabilities support the `SearchFiles` method for finding files by name, content, and metadata:
410
+ Providers with native search capabilities support the `SearchFiles` method:
277
411
 
278
412
  ```typescript
279
- import { FileStorageBase, FileSearchOptions, UnsupportedOperationError } from '@memberjunction/storage';
280
- import { MJGlobal } from '@memberjunction/global';
413
+ import { FileStorageBase, UnsupportedOperationError } from '@memberjunction/storage';
281
414
 
282
- async function searchExample() {
283
- const storage = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
284
- FileStorageBase,
285
- 'Google Drive Storage'
286
- );
287
-
288
- try {
289
- // Simple search for files matching a query
290
- const results = await storage.SearchFiles('quarterly report');
415
+ try {
416
+ // Simple search
417
+ const results = await storage.SearchFiles('quarterly report');
291
418
 
292
- console.log(`Found ${results.results.length} files`);
293
- for (const file of results.results) {
294
- console.log(` ${file.path} (${file.size} bytes)`);
295
- if (file.excerpt) {
296
- console.log(` Excerpt: ${file.excerpt}`);
297
- }
419
+ for (const file of results.results) {
420
+ console.log(` ${file.path} (${file.size} bytes)`);
421
+ if (file.excerpt) {
422
+ console.log(` Excerpt: ${file.excerpt}`);
298
423
  }
424
+ }
299
425
 
300
- // Advanced search with filters
301
- const pdfResults = await storage.SearchFiles('budget 2024', {
302
- fileTypes: ['pdf', 'docx'],
303
- modifiedAfter: new Date('2024-01-01'),
304
- pathPrefix: 'documents/finance/',
305
- maxResults: 50
306
- });
307
-
308
- // Content search (searches inside files)
309
- const contentResults = await storage.SearchFiles('machine learning', {
310
- searchContent: true,
311
- fileTypes: ['pdf', 'docx', 'txt']
312
- });
313
-
314
- // Check for more results
315
- if (contentResults.hasMore) {
316
- console.log(`Total matches: ${contentResults.totalMatches}`);
317
- console.log(`Next page token: ${contentResults.nextPageToken}`);
318
- }
426
+ // Advanced search with filters
427
+ const filtered = await storage.SearchFiles('budget 2024', {
428
+ fileTypes: ['pdf', 'docx'],
429
+ modifiedAfter: new Date('2024-01-01'),
430
+ pathPrefix: 'documents/finance/',
431
+ maxResults: 50,
432
+ searchContent: true
433
+ });
319
434
 
320
- } catch (error) {
321
- if (error instanceof UnsupportedOperationError) {
322
- console.log('This provider does not support file search');
323
- // Fall back to ListObjects or other approaches
324
- } else {
325
- throw error;
326
- }
435
+ if (filtered.hasMore) {
436
+ console.log(`Total matches: ${filtered.totalMatches}`);
437
+ }
438
+ } catch (error) {
439
+ if (error instanceof UnsupportedOperationError) {
440
+ console.log('This provider does not support file search');
441
+ }
442
+ }
443
+ ```
444
+
445
+ ### Multi-Provider and Multi-Account Search
446
+
447
+ Search across multiple providers or accounts in parallel:
448
+
449
+ ```typescript
450
+ import { searchAcrossProviders, searchAcrossAccounts } from '@memberjunction/storage';
451
+
452
+ // Search across multiple providers
453
+ const providerResults = await searchAcrossProviders(
454
+ [googleDriveProvider, dropboxProvider, boxProvider],
455
+ 'quarterly report',
456
+ {
457
+ maxResultsPerProvider: 25,
458
+ fileTypes: ['pdf', 'docx'],
459
+ providerUserContexts: new Map([
460
+ [googleDriveProvider.ID, { userID: currentUser.ID, contextUser }],
461
+ [dropboxProvider.ID, { userID: currentUser.ID, contextUser }]
462
+ ])
463
+ }
464
+ );
465
+
466
+ for (const pr of providerResults.providerResults) {
467
+ if (pr.success) {
468
+ console.log(`${pr.providerName}: ${pr.results.length} results`);
469
+ } else {
470
+ console.log(`${pr.providerName}: ${pr.errorMessage}`);
327
471
  }
328
472
  }
473
+
474
+ // Enterprise: Search across multiple accounts (including multiple accounts of same type)
475
+ const accountResults = await searchAcrossAccounts(
476
+ [
477
+ { accountEntity: researchDropbox, providerEntity: dropboxProvider },
478
+ { accountEntity: marketingDropbox, providerEntity: dropboxProvider },
479
+ { accountEntity: engineeringGDrive, providerEntity: gdriveProvider }
480
+ ],
481
+ 'quarterly report',
482
+ {
483
+ maxResultsPerAccount: 25,
484
+ fileTypes: ['pdf', 'docx'],
485
+ contextUser: currentUser
486
+ }
487
+ );
488
+
489
+ console.log(`Total results: ${accountResults.totalResultsReturned}`);
490
+ console.log(`Successful: ${accountResults.successfulAccounts}`);
491
+ console.log(`Failed: ${accountResults.failedAccounts}`);
329
492
  ```
330
493
 
331
- **Provider Search Support:**
332
- - ✅ **Google Drive**: Full support with content search
333
- - **SharePoint**: Full support via Microsoft Graph Search
334
- - ✅ **Dropbox**: Full support with content search
335
- - **Box**: Full support with metadata search
336
- - ❌ **AWS S3**: Not supported (throws UnsupportedOperationError)
337
- - ❌ **Azure Blob**: Not supported (throws UnsupportedOperationError)
338
- - **Google Cloud Storage**: Not supported (throws UnsupportedOperationError)
494
+ ## Configuration
495
+
496
+ ### Configuration File (`mj.config.cjs`)
497
+
498
+ Storage providers can be configured via the `mj.config.cjs` file at the repository root. The configuration is validated using Zod schemas at load time.
499
+
500
+ ```javascript
501
+ module.exports = {
502
+ storageProviders: {
503
+ aws: {
504
+ accessKeyID: 'your-key',
505
+ secretAccessKey: 'your-secret',
506
+ region: 'us-east-1',
507
+ defaultBucket: 'my-bucket',
508
+ keyPrefix: '/'
509
+ },
510
+ azure: {
511
+ accountName: 'your-account',
512
+ accountKey: 'your-key',
513
+ connectionString: 'optional-conn-string',
514
+ defaultContainer: 'my-container'
515
+ },
516
+ googleCloud: {
517
+ projectID: 'your-project',
518
+ keyFilename: '/path/to/keyfile.json',
519
+ keyJSON: '{"type":"service_account",...}',
520
+ defaultBucket: 'my-bucket'
521
+ },
522
+ googleDrive: {
523
+ clientID: 'your-client-id',
524
+ clientSecret: 'your-client-secret',
525
+ refreshToken: 'your-refresh-token',
526
+ rootFolderID: 'optional-root-folder'
527
+ },
528
+ dropbox: {
529
+ accessToken: 'your-access-token',
530
+ refreshToken: 'your-refresh-token',
531
+ clientID: 'your-app-key',
532
+ clientSecret: 'your-app-secret',
533
+ rootPath: '/optional/root'
534
+ },
535
+ box: {
536
+ clientID: 'your-client-id',
537
+ clientSecret: 'your-client-secret',
538
+ accessToken: 'your-access-token',
539
+ refreshToken: 'your-refresh-token',
540
+ enterpriseID: 'your-enterprise-id',
541
+ rootFolderID: '0'
542
+ },
543
+ sharePoint: {
544
+ clientID: 'your-client-id',
545
+ clientSecret: 'your-client-secret',
546
+ tenantID: 'your-tenant-id',
547
+ siteID: 'your-site-id',
548
+ driveID: 'your-drive-id',
549
+ rootFolderID: 'optional-root-folder'
550
+ }
551
+ }
552
+ };
553
+ ```
339
554
 
340
- For providers without native search, consider using `ListObjects` with client-side filtering or implementing an external search index.
555
+ ### Environment Variables
556
+
557
+ Each provider also supports environment variable configuration. Environment variables take lower priority than config file values (config file wins if both are present).
558
+
559
+ #### AWS S3
560
+
561
+ | Variable | Description |
562
+ |---|---|
563
+ | `STORAGE_AWS_ACCESS_KEY_ID` | AWS access key ID |
564
+ | `STORAGE_AWS_SECRET_ACCESS_KEY` | AWS secret access key |
565
+ | `STORAGE_AWS_REGION` | AWS region (e.g., `us-east-1`) |
566
+ | `STORAGE_AWS_BUCKET_NAME` | S3 bucket name |
567
+ | `STORAGE_AWS_KEY_PREFIX` | Key prefix (defaults to `/`) |
568
+
569
+ #### Azure Blob Storage
570
+
571
+ | Variable | Description |
572
+ |---|---|
573
+ | `STORAGE_AZURE_ACCOUNT_NAME` | Storage account name |
574
+ | `STORAGE_AZURE_ACCOUNT_KEY` | Storage account key |
575
+ | `STORAGE_AZURE_CONNECTION_STRING` | Connection string (alternative to name/key) |
576
+ | `STORAGE_AZURE_CONTAINER` or `STORAGE_AZURE_DEFAULT_CONTAINER` | Container name |
577
+
578
+ #### Google Cloud Storage
579
+
580
+ | Variable | Description |
581
+ |---|---|
582
+ | `STORAGE_GOOGLE_KEY_JSON` | JSON string of service account credentials |
583
+ | `STORAGE_GOOGLE_CLOUD_KEY_FILENAME` | Path to service account key file |
584
+ | `STORAGE_GOOGLE_BUCKET_NAME` or `STORAGE_GOOGLE_CLOUD_DEFAULT_BUCKET` | GCS bucket name |
585
+ | `STORAGE_GOOGLE_CLOUD_PROJECT_ID` | Google Cloud project ID |
586
+
587
+ #### Google Drive
588
+
589
+ | Variable | Description |
590
+ |---|---|
591
+ | `STORAGE_GOOGLE_DRIVE_CLIENT_ID` | OAuth client ID |
592
+ | `STORAGE_GOOGLE_DRIVE_CLIENT_SECRET` | OAuth client secret |
593
+ | `STORAGE_GOOGLE_DRIVE_REFRESH_TOKEN` | OAuth refresh token |
594
+ | `STORAGE_GOOGLE_DRIVE_REDIRECT_URI` | OAuth redirect URI |
595
+ | `STORAGE_GDRIVE_ROOT_FOLDER_ID` | Root folder ID (optional) |
596
+ | `STORAGE_GDRIVE_KEY_FILE` | Service account key file (legacy) |
597
+ | `STORAGE_GDRIVE_CREDENTIALS_JSON` | Service account credentials JSON (legacy) |
598
+
599
+ #### SharePoint
600
+
601
+ | Variable | Description |
602
+ |---|---|
603
+ | `STORAGE_SHAREPOINT_CLIENT_ID` | Azure AD client ID |
604
+ | `STORAGE_SHAREPOINT_CLIENT_SECRET` | Azure AD client secret |
605
+ | `STORAGE_SHAREPOINT_TENANT_ID` | Azure AD tenant ID |
606
+ | `STORAGE_SHAREPOINT_SITE_ID` | SharePoint site ID |
607
+ | `STORAGE_SHAREPOINT_DRIVE_ID` | Document library drive ID |
608
+ | `STORAGE_SHAREPOINT_ROOT_FOLDER_ID` | Root folder ID (optional) |
609
+
610
+ #### Dropbox
611
+
612
+ | Variable | Description |
613
+ |---|---|
614
+ | `STORAGE_DROPBOX_ACCESS_TOKEN` | Dropbox access token |
615
+ | `STORAGE_DROPBOX_REFRESH_TOKEN` | Dropbox refresh token |
616
+ | `STORAGE_DROPBOX_CLIENT_ID` or `STORAGE_DROPBOX_APP_KEY` | App key |
617
+ | `STORAGE_DROPBOX_CLIENT_SECRET` or `STORAGE_DROPBOX_APP_SECRET` | App secret |
618
+ | `STORAGE_DROPBOX_ROOT_PATH` | Root path (optional) |
619
+
620
+ #### Box
621
+
622
+ | Variable | Description |
623
+ |---|---|
624
+ | `STORAGE_BOX_CLIENT_ID` | Box client ID |
625
+ | `STORAGE_BOX_CLIENT_SECRET` | Box client secret |
626
+ | `STORAGE_BOX_ACCESS_TOKEN` | Box access token |
627
+ | `STORAGE_BOX_REFRESH_TOKEN` | Box refresh token |
628
+ | `STORAGE_BOX_ENTERPRISE_ID` | Box enterprise ID (for JWT auth) |
629
+ | `STORAGE_BOX_ROOT_FOLDER_ID` | Root folder ID (optional) |
341
630
 
342
631
  ## API Reference
343
632
 
344
633
  ### Core Types
345
634
 
346
635
  #### `CreatePreAuthUploadUrlPayload`
636
+
347
637
  ```typescript
348
638
  type CreatePreAuthUploadUrlPayload = {
349
- UploadUrl: string; // Pre-authenticated URL for upload
350
- ProviderKey?: string; // Optional provider-specific key
639
+ UploadUrl: string; // Pre-authenticated URL for upload
640
+ ProviderKey?: string; // Optional provider-specific key for future reference
641
+ };
642
+ ```
643
+
644
+ #### `GetObjectParams` / `GetObjectMetadataParams`
645
+
646
+ ```typescript
647
+ type GetObjectParams = {
648
+ objectId?: string; // Provider-specific ID (preferred for performance)
649
+ fullPath?: string; // Full path to the object (fallback)
351
650
  };
352
651
  ```
353
652
 
354
653
  #### `StorageObjectMetadata`
654
+
355
655
  ```typescript
356
656
  type StorageObjectMetadata = {
357
657
  name: string; // Object name (filename)
@@ -363,11 +663,12 @@ type StorageObjectMetadata = {
363
663
  isDirectory: boolean; // Whether this is a directory
364
664
  etag?: string; // Entity tag for caching
365
665
  cacheControl?: string; // Cache control directives
366
- customMetadata?: Record<string, string>; // Custom metadata
666
+ customMetadata?: Record<string, string>;
367
667
  };
368
668
  ```
369
669
 
370
670
  #### `StorageListResult`
671
+
371
672
  ```typescript
372
673
  type StorageListResult = {
373
674
  objects: StorageObjectMetadata[]; // Files found
@@ -376,155 +677,113 @@ type StorageListResult = {
376
677
  ```
377
678
 
378
679
  #### `FileSearchOptions`
680
+
379
681
  ```typescript
380
682
  type FileSearchOptions = {
381
- maxResults?: number; // Maximum results (default: 100)
382
- fileTypes?: string[]; // Filter by MIME types or extensions
383
- modifiedAfter?: Date; // Only files modified after this date
384
- modifiedBefore?: Date; // Only files modified before this date
385
- pathPrefix?: string; // Search within specific directory
386
- searchContent?: boolean; // Search file contents (default: false)
387
- providerSpecific?: Record<string, any>; // Provider-specific options
683
+ maxResults?: number; // Maximum results (default: 100)
684
+ fileTypes?: string[]; // Filter by MIME types or extensions
685
+ modifiedAfter?: Date; // Only files modified after this date
686
+ modifiedBefore?: Date; // Only files modified before this date
687
+ pathPrefix?: string; // Search within specific directory
688
+ searchContent?: boolean; // Search file contents (default: false)
689
+ providerSpecific?: Record<string, unknown>;// Provider-specific options
388
690
  };
389
691
  ```
390
692
 
391
693
  #### `FileSearchResult`
694
+
392
695
  ```typescript
393
696
  type FileSearchResult = {
394
- path: string; // Full path to file
395
- name: string; // Filename only
396
- size: number; // Size in bytes
397
- contentType: string; // MIME type
398
- lastModified: Date; // Last modification date
399
- relevance?: number; // Relevance score (0.0-1.0)
400
- excerpt?: string; // Text excerpt with match context
401
- matchInFilename?: boolean; // Whether match is in filename
402
- customMetadata?: Record<string, string>; // Custom metadata
403
- providerData?: Record<string, any>; // Provider-specific data
697
+ path: string; // Full path to file
698
+ name: string; // Filename only
699
+ size: number; // Size in bytes
700
+ contentType: string; // MIME type
701
+ lastModified: Date; // Last modification date
702
+ relevance?: number; // Relevance score (0.0-1.0)
703
+ excerpt?: string; // Text excerpt with match context
704
+ matchInFilename?: boolean; // Whether match is in filename
705
+ objectId?: string; // Provider-specific ID for direct access
706
+ customMetadata?: Record<string, string>;
707
+ providerData?: Record<string, unknown>;
404
708
  };
405
709
  ```
406
710
 
407
711
  #### `FileSearchResultSet`
712
+
408
713
  ```typescript
409
714
  type FileSearchResultSet = {
410
- results: FileSearchResult[]; // Array of matching files
411
- totalMatches?: number; // Total matches (if available)
412
- hasMore: boolean; // More results available?
413
- nextPageToken?: string; // Token for next page
715
+ results: FileSearchResult[]; // Array of matching files
716
+ totalMatches?: number; // Total matches (if available)
717
+ hasMore: boolean; // More results available?
718
+ nextPageToken?: string; // Token for next page
414
719
  };
415
720
  ```
416
721
 
417
- ### FileStorageBase Methods
418
-
419
- All storage providers implement these methods:
722
+ #### `StorageProviderConfig`
420
723
 
421
- - **`initialize(config?: StorageProviderConfig): Promise<void>`** - **REQUIRED**: Always call after creating instance. Omit config for env vars, provide config for multi-tenant.
422
- - `CreatePreAuthUploadUrl(objectName: string): Promise<CreatePreAuthUploadUrlPayload>`
423
- - `CreatePreAuthDownloadUrl(objectName: string): Promise<string>`
424
- - `MoveObject(oldObjectName: string, newObjectName: string): Promise<boolean>`
425
- - `DeleteObject(objectName: string): Promise<boolean>`
426
- - `ListObjects(prefix: string, delimiter?: string): Promise<StorageListResult>`
427
- - `CreateDirectory(directoryPath: string): Promise<boolean>`
428
- - `DeleteDirectory(directoryPath: string, recursive?: boolean): Promise<boolean>`
429
- - `GetObjectMetadata(objectName: string): Promise<StorageObjectMetadata>`
430
- - `GetObject(objectName: string): Promise<Buffer>`
431
- - `PutObject(objectName: string, data: Buffer, contentType?: string, metadata?: Record<string, string>): Promise<boolean>`
432
- - `CopyObject(sourceObjectName: string, destinationObjectName: string): Promise<boolean>`
433
- - `ObjectExists(objectName: string): Promise<boolean>`
434
- - `DirectoryExists(directoryPath: string): Promise<boolean>`
435
- - `SearchFiles(query: string, options?: FileSearchOptions): Promise<FileSearchResultSet>` (throws `UnsupportedOperationError` for providers without native search)
436
-
437
- ### Utility Functions
438
-
439
- - `createUploadUrl<T>(provider: FileStorageProviderEntity, input: T): Promise<{ updatedInput: T & { Status: string; ContentType: string }, UploadUrl: string }>`
440
- - `createDownloadUrl(provider: FileStorageProviderEntity, providerKeyOrName: string): Promise<string>`
441
- - `moveObject(provider: FileStorageProviderEntity, oldProviderKeyOrName: string, newProviderKeyOrName: string): Promise<boolean>`
442
- - `deleteObject(provider: FileStorageProviderEntity, providerKeyOrName: string): Promise<boolean>`
443
-
444
- ## Architecture
445
-
446
- The library uses a class hierarchy with `FileStorageBase` as the abstract base class that defines the common interface. Each storage provider implements this interface:
447
-
448
- ```
449
- FileStorageBase (Abstract Base Class)
450
- ├── AWSFileStorage (@RegisterClass: 'AWS S3')
451
- ├── AzureFileStorage (@RegisterClass: 'Azure Blob Storage')
452
- ├── GoogleFileStorage (@RegisterClass: 'Google Cloud Storage')
453
- ├── GoogleDriveFileStorage (@RegisterClass: 'Google Drive')
454
- ├── SharePointFileStorage (@RegisterClass: 'SharePoint')
455
- ├── DropboxFileStorage (@RegisterClass: 'Dropbox')
456
- └── BoxFileStorage (@RegisterClass: 'Box')
724
+ ```typescript
725
+ interface StorageProviderConfig {
726
+ accountId?: string; // FileStorageAccount ID (multi-tenant tracking)
727
+ accountName?: string; // Account display name (logging)
728
+ [key: string]: unknown; // Provider-specific configuration values
729
+ }
457
730
  ```
458
731
 
459
- Classes are registered with the MemberJunction global class factory using the `@RegisterClass` decorator, enabling dynamic instantiation based on provider keys.
460
-
461
- ### Integration with MemberJunction
732
+ #### `UnsupportedOperationError`
462
733
 
463
- This library integrates seamlessly with the MemberJunction platform:
734
+ Custom error class thrown when a provider does not support a specific operation (e.g., `SearchFiles` on AWS S3).
464
735
 
465
- 1. **Entity System**: Works with `FileStorageProviderEntity` from `@memberjunction/core-entities`
466
- 2. **Class Factory**: Uses `@memberjunction/global` for dynamic provider instantiation
467
- 3. **Configuration**: Provider settings are stored in the MemberJunction database
468
- 4. **Type Safety**: Fully typed interfaces ensure compile-time safety
469
-
470
- ## Storage Provider Configuration
471
-
472
- Each storage provider requires specific environment variables. Please refer to the official documentation for each provider for detailed information on authentication and additional configuration options.
473
-
474
- ### AWS S3
475
- - `STORAGE_AWS_BUCKET`: S3 bucket name
476
- - `STORAGE_AWS_REGION`: AWS region (e.g., 'us-east-1')
477
- - `STORAGE_AWS_ACCESS_KEY_ID`: AWS access key ID
478
- - `STORAGE_AWS_SECRET_ACCESS_KEY`: AWS secret access key
479
-
480
- For more information, see [AWS S3 Documentation](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html).
481
-
482
- ### Azure Blob Storage
483
- - `STORAGE_AZURE_CONTAINER`: Container name
484
- - `STORAGE_AZURE_ACCOUNT_NAME`: Account name
485
- - `STORAGE_AZURE_ACCOUNT_KEY`: Account key
486
-
487
- For more information, see [Azure Blob Storage Documentation](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-nodejs).
488
-
489
- ### Google Cloud Storage
490
- - `STORAGE_GOOGLE_BUCKET`: GCS bucket name
491
- - `STORAGE_GOOGLE_KEY_FILE_PATH`: Path to service account key file (JSON)
492
-
493
- For more information, see [Google Cloud Storage Documentation](https://cloud.google.com/storage/docs/reference/libraries#client-libraries-install-nodejs).
494
-
495
- ### Google Drive
496
- - `STORAGE_GOOGLE_DRIVE_CLIENT_ID`: OAuth client ID
497
- - `STORAGE_GOOGLE_DRIVE_CLIENT_SECRET`: OAuth client secret
498
- - `STORAGE_GOOGLE_DRIVE_REDIRECT_URI`: OAuth redirect URI
499
- - `STORAGE_GOOGLE_DRIVE_REFRESH_TOKEN`: OAuth refresh token
500
-
501
- For more information, see [Google Drive API Documentation](https://developers.google.com/drive/api/guides/about-sdk).
502
-
503
- ### SharePoint
504
- - `STORAGE_SHAREPOINT_SITE_URL`: SharePoint site URL
505
- - `STORAGE_SHAREPOINT_CLIENT_ID`: Azure AD client ID
506
- - `STORAGE_SHAREPOINT_CLIENT_SECRET`: Azure AD client secret
507
- - `STORAGE_SHAREPOINT_TENANT_ID`: Azure AD tenant ID
508
-
509
- For more information, see [Microsoft Graph API Documentation](https://learn.microsoft.com/en-us/graph/api/resources/sharepoint).
736
+ ### FileStorageBase Methods
510
737
 
511
- ### Dropbox
512
- - `STORAGE_DROPBOX_ACCESS_TOKEN`: Dropbox access token
513
- - `STORAGE_DROPBOX_REFRESH_TOKEN`: Dropbox refresh token (optional)
514
- - `STORAGE_DROPBOX_APP_KEY`: Dropbox app key
515
- - `STORAGE_DROPBOX_APP_SECRET`: Dropbox app secret
738
+ All storage providers implement these methods:
516
739
 
517
- For more information, see [Dropbox API Documentation](https://www.dropbox.com/developers/documentation/javascript).
740
+ | Method | Returns | Description |
741
+ |---|---|---|
742
+ | `initialize(config?)` | `Promise<void>` | Initialize the driver. Always call after construction. |
743
+ | `CreatePreAuthUploadUrl(objectName)` | `Promise<CreatePreAuthUploadUrlPayload>` | Generate pre-authenticated upload URL |
744
+ | `CreatePreAuthDownloadUrl(objectName)` | `Promise<string>` | Generate pre-authenticated download URL |
745
+ | `MoveObject(oldName, newName)` | `Promise<boolean>` | Move/rename a file |
746
+ | `DeleteObject(objectName)` | `Promise<boolean>` | Delete a file |
747
+ | `ListObjects(prefix, delimiter?)` | `Promise<StorageListResult>` | List files and directories |
748
+ | `CreateDirectory(directoryPath)` | `Promise<boolean>` | Create a directory |
749
+ | `DeleteDirectory(path, recursive?)` | `Promise<boolean>` | Delete a directory |
750
+ | `GetObjectMetadata(params)` | `Promise<StorageObjectMetadata>` | Get file metadata without downloading |
751
+ | `GetObject(params)` | `Promise<Buffer>` | Download file content |
752
+ | `PutObject(name, data, contentType?, metadata?)` | `Promise<boolean>` | Upload file content directly |
753
+ | `CopyObject(source, destination)` | `Promise<boolean>` | Copy a file |
754
+ | `ObjectExists(objectName)` | `Promise<boolean>` | Check if a file exists |
755
+ | `DirectoryExists(directoryPath)` | `Promise<boolean>` | Check if a directory exists |
756
+ | `SearchFiles(query, options?)` | `Promise<FileSearchResultSet>` | Search files (throws `UnsupportedOperationError` if not supported) |
757
+ | `get IsConfigured` | `boolean` | Check if driver is properly configured |
758
+ | `get AccountId` | `string \| undefined` | Get the associated account ID |
759
+ | `get AccountName` | `string \| undefined` | Get the associated account name |
518
760
 
519
- ### Box
520
- - `STORAGE_BOX_CLIENT_ID`: Box client ID
521
- - `STORAGE_BOX_CLIENT_SECRET`: Box client secret
522
- - `STORAGE_BOX_ENTERPRISE_ID`: Box enterprise ID
523
- - `STORAGE_BOX_JWT_KEY_ID`: Box JWT key ID
524
- - `STORAGE_BOX_PRIVATE_KEY`: Box private key (base64 encoded)
525
- - `STORAGE_BOX_PRIVATE_KEY_PASSPHRASE`: Box private key passphrase (optional)
761
+ ### Utility Functions
526
762
 
527
- For more information, see [Box Platform Documentation](https://developer.box.com/guides/authentication/).
763
+ High-level functions that integrate with MemberJunction's entity system:
764
+
765
+ | Function | Description |
766
+ |---|---|
767
+ | `createUploadUrl(provider, input, userContext?)` | Create pre-authenticated upload URL with automatic MIME type detection |
768
+ | `createDownloadUrl(provider, keyOrName, userContext?)` | Create pre-authenticated download URL |
769
+ | `moveObject(provider, oldKey, newKey, userContext?)` | Move a file within a provider |
770
+ | `copyObject(provider, source, destination, userContext?)` | Copy a file within a provider |
771
+ | `deleteObject(provider, keyOrName, userContext?)` | Delete a file |
772
+ | `listObjects(provider, prefix, delimiter?, userContext?)` | List files and directories |
773
+ | `copyObjectBetweenProviders(source, dest, sourcePath, destPath, options?)` | Transfer file between providers |
774
+ | `searchAcrossProviders(providers, query, options?)` | Search multiple providers in parallel |
775
+ | `searchAcrossAccounts(accounts, query, options)` | Search multiple accounts in parallel (enterprise) |
776
+ | `initializeDriverWithAccountCredentials(options)` | Initialize driver using enterprise credential model |
777
+ | `initializeDriverWithUserCredentials(options)` | Initialize driver with user context (deprecated) |
778
+
779
+ ### Configuration Functions
780
+
781
+ | Function | Description |
782
+ |---|---|
783
+ | `getStorageConfig()` | Get full storage configuration (loads from `mj.config.cjs` on first call) |
784
+ | `getStorageProvidersConfig()` | Get just the storage providers configuration |
785
+ | `getProviderConfig(provider)` | Get configuration for a specific provider by key |
786
+ | `clearStorageConfig()` | Clear cached configuration (useful for testing) |
528
787
 
529
788
  ## Implementing Additional Providers
530
789
 
@@ -533,63 +792,55 @@ The library is designed to be extensible. To add a new storage provider:
533
792
  ### 1. Create a New Provider Class
534
793
 
535
794
  ```typescript
536
- import { FileStorageBase, StorageObjectMetadata, StorageListResult } from '@memberjunction/storage';
795
+ import {
796
+ FileStorageBase,
797
+ StorageProviderConfig,
798
+ StorageObjectMetadata,
799
+ StorageListResult,
800
+ CreatePreAuthUploadUrlPayload,
801
+ GetObjectParams,
802
+ GetObjectMetadataParams,
803
+ FileSearchOptions,
804
+ FileSearchResultSet,
805
+ } from '@memberjunction/storage';
537
806
  import { RegisterClass } from '@memberjunction/global';
538
807
 
539
808
  @RegisterClass(FileStorageBase, 'My Custom Storage')
540
809
  export class MyCustomStorage extends FileStorageBase {
541
810
  protected readonly providerName = 'My Custom Storage';
542
-
811
+
812
+ private _apiKey: string | undefined;
813
+ private _isConfigured = false;
814
+
543
815
  constructor() {
544
816
  super();
545
- // Initialize your storage client here
546
- }
547
-
548
- public async initialize(): Promise<void> {
549
- // Optional: Perform async initialization
550
- // e.g., authenticate, verify permissions
551
- }
552
-
553
- public async CreatePreAuthUploadUrl(objectName: string): Promise<CreatePreAuthUploadUrlPayload> {
554
- // Implement upload URL generation
555
- // Return { UploadUrl: string, ProviderKey?: string }
817
+ // Load from environment variables
818
+ this._apiKey = process.env.STORAGE_MYCUSTOM_API_KEY;
556
819
  }
557
-
558
- public async CreatePreAuthDownloadUrl(objectName: string): Promise<string> {
559
- // Implement download URL generation
560
- }
561
-
562
- // Implement all other abstract methods...
563
- }
564
- ```
565
-
566
- ### 2. Handle Unsupported Operations
567
-
568
- If your provider doesn't support certain operations:
569
820
 
570
- ```typescript
571
- public async CreateDirectory(directoryPath: string): Promise<boolean> {
572
- // If directories aren't supported
573
- this.throwUnsupportedOperationError('CreateDirectory');
574
- }
575
- ```
576
-
577
- ### 3. Register Environment Variables
821
+ public async initialize(config?: StorageProviderConfig): Promise<void> {
822
+ await super.initialize(config); // Sets accountId and accountName
823
+ if (config) {
824
+ // Override env var defaults with config values if present
825
+ if (config.apiKey) this._apiKey = config.apiKey as string;
826
+ }
827
+ this._isConfigured = !!this._apiKey;
828
+ }
578
829
 
579
- Document required environment variables:
830
+ public get IsConfigured(): boolean {
831
+ return this._isConfigured;
832
+ }
580
833
 
581
- ```typescript
582
- import * as env from 'env-var';
834
+ // Implement all abstract methods...
583
835
 
584
- constructor() {
585
- super();
586
- const apiKey = env.get('STORAGE_MYCUSTOM_API_KEY').required().asString();
587
- const endpoint = env.get('STORAGE_MYCUSTOM_ENDPOINT').required().asString();
588
- // Use these to initialize your client
836
+ public async SearchFiles(query: string, options?: FileSearchOptions): Promise<FileSearchResultSet> {
837
+ // If not supported:
838
+ this.throwUnsupportedOperationError('SearchFiles');
839
+ }
589
840
  }
590
841
  ```
591
842
 
592
- ### 4. Export from Index
843
+ ### 2. Export from Index
593
844
 
594
845
  Add to `src/index.ts`:
595
846
 
@@ -597,24 +848,80 @@ Add to `src/index.ts`:
597
848
  export * from './drivers/MyCustomStorage';
598
849
  ```
599
850
 
600
- ### 5. Add to Documentation
851
+ ## Class Hierarchy
852
+
853
+ ```mermaid
854
+ classDiagram
855
+ class FileStorageBase {
856
+ <<abstract>>
857
+ #providerName: string
858
+ #_accountId: string
859
+ #_accountName: string
860
+ +initialize(config?) Promise~void~
861
+ +get IsConfigured() boolean
862
+ +get AccountId() string
863
+ +get AccountName() string
864
+ +CreatePreAuthUploadUrl(name) Promise
865
+ +CreatePreAuthDownloadUrl(name) Promise
866
+ +MoveObject(old, new) Promise
867
+ +DeleteObject(name) Promise
868
+ +ListObjects(prefix, delimiter?) Promise
869
+ +CreateDirectory(path) Promise
870
+ +DeleteDirectory(path, recursive?) Promise
871
+ +GetObjectMetadata(params) Promise
872
+ +GetObject(params) Promise
873
+ +PutObject(name, data, type?, meta?) Promise
874
+ +CopyObject(source, dest) Promise
875
+ +ObjectExists(name) Promise
876
+ +DirectoryExists(path) Promise
877
+ +SearchFiles(query, options?) Promise
878
+ #throwUnsupportedOperationError(method) never
879
+ }
880
+
881
+ class AWSFileStorage {
882
+ @RegisterClass 'AWS S3'
883
+ }
884
+ class AzureFileStorage {
885
+ @RegisterClass 'Azure Blob Storage'
886
+ }
887
+ class GoogleFileStorage {
888
+ @RegisterClass 'Google Cloud Storage'
889
+ }
890
+ class GoogleDriveFileStorage {
891
+ @RegisterClass 'Google Drive Storage'
892
+ }
893
+ class SharePointFileStorage {
894
+ @RegisterClass 'SharePoint'
895
+ }
896
+ class DropboxFileStorage {
897
+ @RegisterClass 'Dropbox'
898
+ }
899
+ class BoxFileStorage {
900
+ @RegisterClass 'Box'
901
+ }
601
902
 
602
- Update this README with configuration requirements and any provider-specific notes.
903
+ FileStorageBase <|-- AWSFileStorage
904
+ FileStorageBase <|-- AzureFileStorage
905
+ FileStorageBase <|-- GoogleFileStorage
906
+ FileStorageBase <|-- GoogleDriveFileStorage
907
+ FileStorageBase <|-- SharePointFileStorage
908
+ FileStorageBase <|-- DropboxFileStorage
909
+ FileStorageBase <|-- BoxFileStorage
910
+ ```
603
911
 
604
912
  ## Error Handling
605
913
 
606
- The library provides consistent error handling across all providers:
607
-
608
914
  ### UnsupportedOperationError
609
915
 
610
- Thrown when a provider doesn't support a specific operation:
916
+ Thrown when a provider does not support a specific operation:
611
917
 
612
918
  ```typescript
613
919
  try {
614
- await storage.CreateDirectory('/some/path/');
920
+ await storage.SearchFiles('quarterly report');
615
921
  } catch (error) {
616
922
  if (error instanceof UnsupportedOperationError) {
617
- console.log(`Provider doesn't support directories: ${error.message}`);
923
+ console.log(`Provider doesn't support search: ${error.message}`);
924
+ // Fall back to ListObjects with client-side filtering
618
925
  }
619
926
  }
620
927
  ```
@@ -625,10 +932,10 @@ Each provider may throw errors specific to its underlying SDK. These are not wra
625
932
 
626
933
  ```typescript
627
934
  try {
628
- await storage.GetObject('non-existent-file.txt');
935
+ await storage.GetObject({ fullPath: 'non-existent-file.txt' });
629
936
  } catch (error) {
630
937
  // Handle provider-specific errors
631
- if (error.code === 'NoSuchKey') { // AWS S3
938
+ if (error.code === 'NoSuchKey') { // AWS S3
632
939
  console.log('File not found');
633
940
  } else if (error.code === 'BlobNotFound') { // Azure
634
941
  console.log('Blob not found');
@@ -636,39 +943,49 @@ try {
636
943
  }
637
944
  ```
638
945
 
639
- ## Best Practices
946
+ ## Testing
640
947
 
641
- 1. **Use ProviderKey**: Always check for and use `ProviderKey` if returned by `CreatePreAuthUploadUrl`
642
- 2. **Error Handling**: Implement proper error handling for both generic and provider-specific errors
643
- 3. **Environment Variables**: Store sensitive credentials securely and never commit them to version control
644
- 4. **Content Types**: Always specify content types for better browser handling and security
645
- 5. **Metadata**: Use custom metadata to store additional information without modifying file content
646
- 6. **Directory Paths**: Always use trailing slashes for directory paths (e.g., `documents/` not `documents`)
647
- 7. **Initialize Providers**: Call `initialize()` on providers that require async setup
948
+ The package includes Jest-based unit tests for the base class behavior and the enterprise credential initialization flow.
648
949
 
649
- ## Performance Considerations
950
+ ```bash
951
+ # Run tests
952
+ npm test
650
953
 
651
- - **Pre-authenticated URLs**: Use these for client uploads/downloads to reduce server load
652
- - **Buffering**: The `GetObject` and `PutObject` methods load entire files into memory; for large files, consider streaming approaches
653
- - **List Operations**: Use appropriate prefixes and delimiters to limit results
654
- - **Caching**: Utilize ETags and cache control headers when available
954
+ # Run tests in watch mode
955
+ npm run test:watch
655
956
 
656
- ## Contributing
957
+ # Run tests with coverage
958
+ npm run test:coverage
959
+ ```
657
960
 
658
- Contributions are welcome! To add a new storage provider:
961
+ Test files are located at:
962
+ - `src/__tests__/FileStorageBase.test.ts` -- Tests for the `FileStorageBase` abstract class, `initialize()` method, and account information handling
963
+ - `src/__tests__/util.test.ts` -- Tests for `initializeDriverWithAccountCredentials` and the enterprise credential model
659
964
 
660
- 1. Fork the repository
661
- 2. Create a feature branch (`git checkout -b feature/new-provider`)
662
- 3. Create your provider class in `src/drivers/`
663
- 4. Implement all required methods from `FileStorageBase`
664
- 5. Add comprehensive tests
665
- 6. Update documentation
666
- 7. Submit a pull request
965
+ ## Build
667
966
 
668
- ## License
967
+ ```bash
968
+ # Build the package
969
+ npm run build
669
970
 
670
- ISC
971
+ # Watch mode for development
972
+ npm run watch
973
+ ```
671
974
 
672
- ---
975
+ The package uses TypeScript with `tsc` and `tsc-alias` for path alias resolution. Output is emitted to the `dist/` directory.
976
+
977
+ ## Best Practices
673
978
 
674
- Part of the [MemberJunction](https://memberjunction.com) platform.
979
+ 1. **Always call `initialize()`**: After creating a provider instance, always call `initialize()` before using the driver, even if no configuration is needed.
980
+ 2. **Use ProviderKey**: Always check for and use `ProviderKey` if returned by `CreatePreAuthUploadUrl` for subsequent operations.
981
+ 3. **Use `objectId` when available**: The `GetObject` and `GetObjectMetadata` methods accept either `objectId` or `fullPath`. Using `objectId` bypasses path resolution and is significantly faster.
982
+ 4. **Use enterprise credential model**: Prefer `initializeDriverWithAccountCredentials` over direct instantiation for multi-tenant applications.
983
+ 5. **Handle unsupported operations**: Wrap `SearchFiles` calls in try/catch for `UnsupportedOperationError` since not all providers support search.
984
+ 6. **Directory paths**: Always use trailing slashes for directory paths (e.g., `documents/` not `documents`).
985
+ 7. **Content types**: Always specify content types for better browser handling and security.
986
+ 8. **Buffer operations**: `GetObject` and `PutObject` load entire files into memory; consider pre-authenticated URLs for large files.
987
+ 9. **Batch searches**: Use `searchAcrossProviders` or `searchAcrossAccounts` for parallel multi-source search instead of sequential calls.
988
+
989
+ ## License
990
+
991
+ ISC