@objectstack/service-storage 4.0.3 → 4.0.4

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/service-storage@4.0.3 build /home/runner/work/framework/framework/packages/services/service-storage
2
+ > @objectstack/service-storage@4.0.4 build /home/runner/work/framework/framework/packages/services/service-storage
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.cjs 5.36 KB
14
- CJS dist/index.cjs.map 11.56 KB
15
- CJS ⚡️ Build success in 112ms
16
13
  ESM dist/index.js 4.04 KB
17
14
  ESM dist/index.js.map 10.95 KB
18
- ESM ⚡️ Build success in 119ms
15
+ ESM ⚡️ Build success in 67ms
16
+ CJS dist/index.cjs 5.36 KB
17
+ CJS dist/index.cjs.map 11.56 KB
18
+ CJS ⚡️ Build success in 69ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 11080ms
20
+ DTS ⚡️ Build success in 14628ms
21
21
  DTS dist/index.d.ts 4.20 KB
22
22
  DTS dist/index.d.cts 4.20 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @objectstack/service-storage
2
2
 
3
+ ## 4.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [326b66b]
8
+ - @objectstack/spec@4.0.4
9
+ - @objectstack/core@4.0.4
10
+
3
11
  ## 4.0.3
4
12
 
5
13
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,466 @@
1
+ # @objectstack/service-storage
2
+
3
+ Storage Service for ObjectStack — implements `IStorageService` with local filesystem and S3 adapter skeleton.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Adapters**: Local filesystem (development) and S3-compatible storage (production)
8
+ - **File Upload**: Upload files with automatic path management
9
+ - **File Download**: Retrieve files with streaming support
10
+ - **URL Generation**: Generate signed URLs for secure access
11
+ - **Metadata**: Store and retrieve file metadata
12
+ - **Directory Operations**: Create, list, and delete directories
13
+ - **Multipart Upload**: Support for large file uploads
14
+ - **Type-Safe**: Full TypeScript support
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @objectstack/service-storage
20
+ ```
21
+
22
+ For S3 adapter:
23
+ ```bash
24
+ pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
25
+ ```
26
+
27
+ ## Basic Usage
28
+
29
+ ```typescript
30
+ import { defineStack } from '@objectstack/spec';
31
+ import { ServiceStorage } from '@objectstack/service-storage';
32
+
33
+ const stack = defineStack({
34
+ services: [
35
+ ServiceStorage.configure({
36
+ adapter: 'local', // or 's3'
37
+ basePath: './uploads',
38
+ }),
39
+ ],
40
+ });
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ### Local Filesystem Adapter (Development)
46
+
47
+ ```typescript
48
+ ServiceStorage.configure({
49
+ adapter: 'local',
50
+ basePath: './uploads',
51
+ baseUrl: 'http://localhost:3000/uploads',
52
+ });
53
+ ```
54
+
55
+ ### S3 Adapter (Production)
56
+
57
+ ```typescript
58
+ ServiceStorage.configure({
59
+ adapter: 's3',
60
+ s3: {
61
+ bucket: 'my-bucket',
62
+ region: 'us-east-1',
63
+ credentials: {
64
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
65
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
66
+ },
67
+ },
68
+ baseUrl: 'https://my-bucket.s3.amazonaws.com',
69
+ });
70
+ ```
71
+
72
+ ### S3-Compatible Services (Cloudflare R2, DigitalOcean Spaces, MinIO)
73
+
74
+ ```typescript
75
+ ServiceStorage.configure({
76
+ adapter: 's3',
77
+ s3: {
78
+ bucket: 'my-bucket',
79
+ region: 'auto',
80
+ endpoint: 'https://r2.cloudflarestorage.com/account-id',
81
+ credentials: {
82
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
83
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
84
+ },
85
+ },
86
+ });
87
+ ```
88
+
89
+ ## Service API
90
+
91
+ ```typescript
92
+ // Get storage service
93
+ const storage = kernel.getService<IStorageService>('storage');
94
+ ```
95
+
96
+ ### File Upload
97
+
98
+ ```typescript
99
+ // Upload a file from buffer
100
+ await storage.upload({
101
+ path: 'documents/contract.pdf',
102
+ data: fileBuffer,
103
+ contentType: 'application/pdf',
104
+ metadata: {
105
+ userId: 'user:123',
106
+ category: 'contracts',
107
+ },
108
+ });
109
+
110
+ // Upload from stream
111
+ await storage.uploadStream({
112
+ path: 'videos/demo.mp4',
113
+ stream: fileStream,
114
+ contentType: 'video/mp4',
115
+ });
116
+
117
+ // Upload with automatic path generation
118
+ const path = await storage.uploadAuto({
119
+ data: fileBuffer,
120
+ fileName: 'profile.jpg',
121
+ folder: 'avatars',
122
+ contentType: 'image/jpeg',
123
+ });
124
+ // Returns: 'avatars/2024/01/15/abc123-profile.jpg'
125
+ ```
126
+
127
+ ### File Download
128
+
129
+ ```typescript
130
+ // Download file as buffer
131
+ const file = await storage.download('documents/contract.pdf');
132
+ console.log(file.data); // Buffer
133
+ console.log(file.contentType); // 'application/pdf'
134
+ console.log(file.size); // File size in bytes
135
+
136
+ // Download as stream
137
+ const stream = await storage.downloadStream('videos/demo.mp4');
138
+ stream.pipe(res); // Pipe to HTTP response
139
+ ```
140
+
141
+ ### File Management
142
+
143
+ ```typescript
144
+ // Check if file exists
145
+ const exists = await storage.exists('documents/contract.pdf');
146
+
147
+ // Get file metadata
148
+ const metadata = await storage.getMetadata('documents/contract.pdf');
149
+ // {
150
+ // size: 1024000,
151
+ // contentType: 'application/pdf',
152
+ // lastModified: Date,
153
+ // metadata: { userId: 'user:123', category: 'contracts' }
154
+ // }
155
+
156
+ // Delete file
157
+ await storage.delete('documents/contract.pdf');
158
+
159
+ // Copy file
160
+ await storage.copy({
161
+ from: 'documents/contract.pdf',
162
+ to: 'archive/2024/contract.pdf',
163
+ });
164
+
165
+ // Move file
166
+ await storage.move({
167
+ from: 'temp/upload.pdf',
168
+ to: 'documents/contract.pdf',
169
+ });
170
+ ```
171
+
172
+ ### Directory Operations
173
+
174
+ ```typescript
175
+ // List files in directory
176
+ const files = await storage.list('documents', {
177
+ recursive: false,
178
+ limit: 100,
179
+ });
180
+ // Returns: ['contract.pdf', 'invoice.pdf', 'report.docx']
181
+
182
+ // List with metadata
183
+ const files = await storage.listDetailed('documents');
184
+ // Returns: [
185
+ // { path: 'contract.pdf', size: 1024000, lastModified: Date },
186
+ // { path: 'invoice.pdf', size: 512000, lastModified: Date },
187
+ // ]
188
+
189
+ // Delete directory and all contents
190
+ await storage.deleteDirectory('temp');
191
+ ```
192
+
193
+ ### URL Generation
194
+
195
+ ```typescript
196
+ // Generate public URL (for public files)
197
+ const url = storage.getUrl('public/logo.png');
198
+ // 'https://my-bucket.s3.amazonaws.com/public/logo.png'
199
+
200
+ // Generate signed URL (for private files, expires in 1 hour)
201
+ const signedUrl = await storage.getSignedUrl('documents/contract.pdf', {
202
+ expiresIn: 3600,
203
+ operation: 'read', // or 'write'
204
+ });
205
+ // 'https://my-bucket.s3.amazonaws.com/documents/contract.pdf?X-Amz-Signature=...'
206
+
207
+ // Generate upload URL (for direct client uploads)
208
+ const uploadUrl = await storage.getUploadUrl('uploads/temp.pdf', {
209
+ expiresIn: 900, // 15 minutes
210
+ contentType: 'application/pdf',
211
+ maxSize: 10485760, // 10MB
212
+ });
213
+ ```
214
+
215
+ ## Advanced Features
216
+
217
+ ### Multipart Upload (Large Files)
218
+
219
+ ```typescript
220
+ // Initialize multipart upload
221
+ const uploadId = await storage.initMultipartUpload({
222
+ path: 'large-files/video.mp4',
223
+ contentType: 'video/mp4',
224
+ });
225
+
226
+ // Upload parts (can be done in parallel)
227
+ const parts = [];
228
+ for (let i = 0; i < chunks.length; i++) {
229
+ const part = await storage.uploadPart({
230
+ uploadId,
231
+ partNumber: i + 1,
232
+ data: chunks[i],
233
+ });
234
+ parts.push(part);
235
+ }
236
+
237
+ // Complete multipart upload
238
+ await storage.completeMultipartUpload({
239
+ uploadId,
240
+ parts,
241
+ });
242
+
243
+ // Or abort if failed
244
+ await storage.abortMultipartUpload(uploadId);
245
+ ```
246
+
247
+ ### Direct Browser Upload
248
+
249
+ ```typescript
250
+ // Server: Generate presigned POST URL
251
+ const presignedPost = await storage.getPresignedPost({
252
+ path: 'uploads/${filename}',
253
+ conditions: [
254
+ ['content-length-range', 0, 10485760], // Max 10MB
255
+ ['starts-with', '$Content-Type', 'image/'], // Only images
256
+ ],
257
+ expiresIn: 900, // 15 minutes
258
+ });
259
+
260
+ // Client: Upload directly to S3 from browser
261
+ const formData = new FormData();
262
+ Object.entries(presignedPost.fields).forEach(([key, value]) => {
263
+ formData.append(key, value);
264
+ });
265
+ formData.append('file', file);
266
+
267
+ await fetch(presignedPost.url, {
268
+ method: 'POST',
269
+ body: formData,
270
+ });
271
+ ```
272
+
273
+ ### Image Processing Integration
274
+
275
+ ```typescript
276
+ // Upload original image
277
+ await storage.upload({
278
+ path: 'images/original/photo.jpg',
279
+ data: imageBuffer,
280
+ contentType: 'image/jpeg',
281
+ });
282
+
283
+ // Generate and upload thumbnails
284
+ const thumbnail = await resizeImage(imageBuffer, { width: 200, height: 200 });
285
+ await storage.upload({
286
+ path: 'images/thumbnails/photo.jpg',
287
+ data: thumbnail,
288
+ contentType: 'image/jpeg',
289
+ });
290
+
291
+ const medium = await resizeImage(imageBuffer, { width: 800, height: 800 });
292
+ await storage.upload({
293
+ path: 'images/medium/photo.jpg',
294
+ data: medium,
295
+ contentType: 'image/jpeg',
296
+ });
297
+ ```
298
+
299
+ ### File Attachments for Records
300
+
301
+ ```typescript
302
+ // Attach file to a record
303
+ await storage.upload({
304
+ path: `attachments/opportunity/${opportunityId}/proposal.pdf`,
305
+ data: fileBuffer,
306
+ contentType: 'application/pdf',
307
+ metadata: {
308
+ objectType: 'opportunity',
309
+ recordId: opportunityId,
310
+ uploadedBy: 'user:123',
311
+ },
312
+ });
313
+
314
+ // List attachments for a record
315
+ const attachments = await storage.list(`attachments/opportunity/${opportunityId}`);
316
+
317
+ // Delete all attachments when record is deleted
318
+ await storage.deleteDirectory(`attachments/opportunity/${opportunityId}`);
319
+ ```
320
+
321
+ ## REST API Endpoints
322
+
323
+ ```
324
+ POST /api/v1/storage/upload # Upload file
325
+ GET /api/v1/storage/download/:path # Download file
326
+ DELETE /api/v1/storage/:path # Delete file
327
+ GET /api/v1/storage/list # List files
328
+ POST /api/v1/storage/signed-url # Generate signed URL
329
+ POST /api/v1/storage/upload-url # Generate upload URL
330
+ GET /api/v1/storage/metadata/:path # Get file metadata
331
+ ```
332
+
333
+ ## Client Integration
334
+
335
+ ### React Component Example
336
+
337
+ ```typescript
338
+ import { useStorage } from '@objectstack/client-react';
339
+
340
+ function FileUploader() {
341
+ const { upload, uploading, progress } = useStorage();
342
+
343
+ const handleUpload = async (file: File) => {
344
+ const path = await upload({
345
+ file,
346
+ folder: 'documents',
347
+ onProgress: (percent) => console.log(`Upload: ${percent}%`),
348
+ });
349
+
350
+ console.log('Uploaded to:', path);
351
+ };
352
+
353
+ return (
354
+ <div>
355
+ <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
356
+ {uploading && <progress value={progress} max="100" />}
357
+ </div>
358
+ );
359
+ }
360
+ ```
361
+
362
+ ## Common Patterns
363
+
364
+ ### User Avatar Upload
365
+
366
+ ```typescript
367
+ async function uploadAvatar(userId: string, imageFile: Buffer) {
368
+ // Upload original
369
+ const path = `avatars/${userId}/original.jpg`;
370
+ await storage.upload({
371
+ path,
372
+ data: imageFile,
373
+ contentType: 'image/jpeg',
374
+ });
375
+
376
+ // Generate thumbnail
377
+ const thumbnail = await resizeImage(imageFile, { width: 128, height: 128 });
378
+ await storage.upload({
379
+ path: `avatars/${userId}/thumbnail.jpg`,
380
+ data: thumbnail,
381
+ contentType: 'image/jpeg',
382
+ });
383
+
384
+ return {
385
+ original: storage.getUrl(path),
386
+ thumbnail: storage.getUrl(`avatars/${userId}/thumbnail.jpg`),
387
+ };
388
+ }
389
+ ```
390
+
391
+ ### Document Management
392
+
393
+ ```typescript
394
+ async function uploadDocument(doc: {
395
+ recordId: string;
396
+ file: Buffer;
397
+ fileName: string;
398
+ uploadedBy: string;
399
+ }) {
400
+ const path = `documents/${doc.recordId}/${Date.now()}-${doc.fileName}`;
401
+
402
+ await storage.upload({
403
+ path,
404
+ data: doc.file,
405
+ contentType: getMimeType(doc.fileName),
406
+ metadata: {
407
+ recordId: doc.recordId,
408
+ uploadedBy: doc.uploadedBy,
409
+ fileName: doc.fileName,
410
+ },
411
+ });
412
+
413
+ // Create signed URL for secure download
414
+ const downloadUrl = await storage.getSignedUrl(path, { expiresIn: 86400 }); // 24 hours
415
+
416
+ return { path, downloadUrl };
417
+ }
418
+ ```
419
+
420
+ ## Best Practices
421
+
422
+ 1. **Path Organization**: Use hierarchical paths (e.g., `object/recordId/filename`)
423
+ 2. **Content Types**: Always specify correct `contentType`
424
+ 3. **Security**: Use signed URLs for private files
425
+ 4. **Cleanup**: Delete files when records are deleted
426
+ 5. **Validation**: Validate file types and sizes before upload
427
+ 6. **Metadata**: Store useful metadata with files
428
+ 7. **Backups**: Implement backup strategy for S3 buckets
429
+
430
+ ## Performance Considerations
431
+
432
+ - **Streaming**: Use streams for large files to reduce memory usage
433
+ - **CDN**: Put CloudFront or similar CDN in front of S3
434
+ - **Compression**: Compress files before upload when appropriate
435
+ - **Caching**: Cache file URLs and metadata
436
+ - **Multipart**: Use multipart upload for files > 5MB
437
+
438
+ ## Contract Implementation
439
+
440
+ Implements `IStorageService` from `@objectstack/spec/contracts`:
441
+
442
+ ```typescript
443
+ interface IStorageService {
444
+ upload(options: UploadOptions): Promise<void>;
445
+ uploadStream(options: UploadStreamOptions): Promise<void>;
446
+ download(path: string): Promise<FileData>;
447
+ downloadStream(path: string): Promise<ReadableStream>;
448
+ delete(path: string): Promise<void>;
449
+ exists(path: string): Promise<boolean>;
450
+ getMetadata(path: string): Promise<FileMetadata>;
451
+ list(path: string, options?: ListOptions): Promise<string[]>;
452
+ getUrl(path: string): string;
453
+ getSignedUrl(path: string, options?: SignedUrlOptions): Promise<string>;
454
+ }
455
+ ```
456
+
457
+ ## License
458
+
459
+ Apache-2.0
460
+
461
+ ## See Also
462
+
463
+ - [AWS S3 Documentation](https://docs.aws.amazon.com/s3/)
464
+ - [Cloudflare R2 Documentation](https://developers.cloudflare.com/r2/)
465
+ - [@objectstack/spec/contracts](../../spec/src/contracts/)
466
+ - [File Upload Guide](/content/docs/guides/storage/)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-storage",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton",
6
6
  "type": "module",
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "4.0.3",
18
- "@objectstack/spec": "4.0.3"
17
+ "@objectstack/core": "4.0.4",
18
+ "@objectstack/spec": "4.0.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^25.6.0",