nestjs-r2-storage 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  **Author:** Nurul Islam Rimon
7
7
  **GitHub:** [https://github.com/nurulislamrimon/nestjs-r2-storage](https://github.com/nurulislamrimon/nestjs-r2-storage)
8
8
 
9
+ ![Nestjs R2 Storage](nestjs-r2-storage.gif)
10
+
9
11
  Production-ready NestJS module for Cloudflare R2 object storage management.
10
12
 
11
13
  ## Features
@@ -25,11 +27,11 @@ Cloudflare R2 does NOT enforce ACLs like AWS S3 - the R2 API ignores ACL headers
25
27
 
26
28
  ### Modes
27
29
 
28
- | Mode | Public URLs | Signed URLs | Use Case |
29
- |------|-------------|-------------|----------|
30
- | `private` | Not allowed | Required | Maximum security - only signed access |
31
- | `public-read` | Allowed | Optional | Public files (e.g., static assets) |
32
- | `hybrid` | Allowed | Allowed | Mixed content (default) |
30
+ | Mode | Public URLs | Signed URLs | Use Case |
31
+ | ------------- | ----------- | ----------- | ------------------------------------- |
32
+ | `private` | Not allowed | Required | Maximum security - only signed access |
33
+ | `public-read` | Allowed | Optional | Public files (e.g., static assets) |
34
+ | `hybrid` | Allowed | Allowed | Mixed content (default) |
33
35
 
34
36
  ### Private Mode
35
37
 
@@ -38,12 +40,13 @@ Only presigned URLs are allowed. Public URL generation throws `AccessModeError`.
38
40
  ```typescript
39
41
  R2StorageModule.forRoot({
40
42
  // ... other options
41
- accessMode: 'private',
42
- publicUrlBase: 'https://cdn.example.com', // still configured but not used
43
+ accessMode: "private",
44
+ publicUrlBase: "https://cdn.example.com", // still configured but not used
43
45
  });
44
46
  ```
45
47
 
46
48
  Response in private mode:
49
+
47
50
  ```json
48
51
  {
49
52
  "uploadUrl": "https://signed-url...",
@@ -58,8 +61,8 @@ Public URLs are generated. Signed URLs are optional.
58
61
  ```typescript
59
62
  R2StorageModule.forRoot({
60
63
  // ... other options
61
- accessMode: 'public-read',
62
- publicUrlBase: 'https://cdn.example.com',
64
+ accessMode: "public-read",
65
+ publicUrlBase: "https://cdn.example.com",
63
66
  });
64
67
  ```
65
68
 
@@ -70,7 +73,7 @@ Both public and signed access are allowed for backward compatibility.
70
73
  ```typescript
71
74
  R2StorageModule.forRoot({
72
75
  // ... other options
73
- accessMode: 'hybrid', // default
76
+ accessMode: "hybrid", // default
74
77
  });
75
78
  ```
76
79
 
@@ -80,8 +83,8 @@ R2StorageModule.forRoot({
80
83
 
81
84
  ```typescript
82
85
  // app.module.ts
83
- import { Module } from '@nestjs/common';
84
- import { R2StorageModule } from 'nestjs-r2-storage';
86
+ import { Module } from "@nestjs/common";
87
+ import { R2StorageModule } from "nestjs-r2-storage";
85
88
 
86
89
  @Module({
87
90
  imports: [
@@ -90,7 +93,7 @@ import { R2StorageModule } from 'nestjs-r2-storage';
90
93
  accessKeyId: process.env.R2_ACCESS_KEY,
91
94
  secretAccessKey: process.env.R2_SECRET_KEY,
92
95
  bucketName: process.env.R2_BUCKET,
93
- region: 'auto',
96
+ region: "auto",
94
97
  publicUrlBase: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
95
98
  signedUrlExpiry: 3600,
96
99
  }),
@@ -102,8 +105,12 @@ export class AppModule {}
102
105
  ### 2. Use in Your Service
103
106
 
104
107
  ```typescript
105
- import { Injectable } from '@nestjs/common';
106
- import { PhotoManagerService, PhotoField, CloudflareService } from 'nestjs-r2-storage';
108
+ import { Injectable } from "@nestjs/common";
109
+ import {
110
+ PhotoManagerService,
111
+ PhotoField,
112
+ CloudflareService,
113
+ } from "nestjs-r2-storage";
107
114
 
108
115
  @Injectable()
109
116
  export class ProductService {
@@ -114,12 +121,19 @@ export class ProductService {
114
121
 
115
122
  async createProduct(payload: any) {
116
123
  const photoFields: PhotoField[] = [
117
- { field: 'image', urlField: 'image_url', sizeField: 'image_size' },
118
- { field: 'gallery[].photo', urlField: 'photo_url', sizeField: 'photo_size' },
124
+ { field: "image", urlField: "image_url", sizeField: "image_size" },
125
+ {
126
+ field: "gallery[].photo",
127
+ urlField: "photo_url",
128
+ sizeField: "photo_size",
129
+ },
119
130
  ];
120
131
 
121
- const result = await this.photoManager.createObjectWithPhotos(payload, photoFields);
122
-
132
+ const result = await this.photoManager.createObjectWithPhotos(
133
+ payload,
134
+ photoFields,
135
+ );
136
+
123
137
  // Return upload URLs to client for direct upload
124
138
  return {
125
139
  product: result.updatedPayload,
@@ -130,10 +144,10 @@ export class ProductService {
130
144
 
131
145
  async getProduct(id: string) {
132
146
  const product = await this.findProduct(id);
133
-
147
+
134
148
  const photoFields: PhotoField[] = [
135
- { field: 'image', urlField: 'image_url' },
136
- { field: 'gallery[].photo', urlField: 'photo_url' },
149
+ { field: "image", urlField: "image_url" },
150
+ { field: "gallery[].photo", urlField: "photo_url" },
137
151
  ];
138
152
 
139
153
  return this.photoManager.appendPhotoUrls(product, photoFields);
@@ -141,13 +155,17 @@ export class ProductService {
141
155
 
142
156
  async updateProduct(id: string, payload: any) {
143
157
  const existing = await this.findProduct(id);
144
-
158
+
145
159
  const photoFields: PhotoField[] = [
146
- { field: 'image', urlField: 'image_url', sizeField: 'image_size' },
160
+ { field: "image", urlField: "image_url", sizeField: "image_size" },
147
161
  ];
148
162
 
149
- const result = await this.photoManager.updateObjectWithPhotos(payload, existing, photoFields);
150
-
163
+ const result = await this.photoManager.updateObjectWithPhotos(
164
+ payload,
165
+ existing,
166
+ photoFields,
167
+ );
168
+
151
169
  return {
152
170
  product: result.updatedPayload,
153
171
  uploadUrls: result.uploadUrls,
@@ -158,9 +176,9 @@ export class ProductService {
158
176
 
159
177
  async deleteProduct(id: string) {
160
178
  const product = await this.findProduct(id);
161
-
179
+
162
180
  const photoFields: PhotoField[] = [
163
- { field: 'image', urlField: 'image_url' },
181
+ { field: "image", urlField: "image_url" },
164
182
  ];
165
183
 
166
184
  await this.photoManager.deletePhotosFromObject(product, photoFields);
@@ -177,16 +195,16 @@ Direct R2 operations.
177
195
 
178
196
  ```typescript
179
197
  // Generate upload URL
180
- const uploadUrl = await cloudflare.getUploadUrl('avatar.png', 1024000);
198
+ const uploadUrl = await cloudflare.getUploadUrl("avatar.png", 1024000);
181
199
 
182
200
  // Generate download URL
183
- const downloadUrl = await cloudflare.getDownloadUrl('uploads/avatar_123.png');
201
+ const downloadUrl = await cloudflare.getDownloadUrl("uploads/avatar_123.png");
184
202
 
185
203
  // Delete file
186
- await cloudflare.deleteFile('uploads/avatar.png');
204
+ await cloudflare.deleteFile("uploads/avatar.png");
187
205
 
188
206
  // Check if file exists
189
- const exists = await cloudflare.fileExists('uploads/avatar.png');
207
+ const exists = await cloudflare.fileExists("uploads/avatar.png");
190
208
  ```
191
209
 
192
210
  ### Presigned URL Security
@@ -198,7 +216,7 @@ The module uses secure presigned URL generation:
198
216
  - **Minimal signing** - Only signs `host` and `content-type` headers
199
217
 
200
218
  ```typescript
201
- const result = await cloudflare.getUploadUrl('avatar.png', 1024000);
219
+ const result = await cloudflare.getUploadUrl("avatar.png", 1024000);
202
220
 
203
221
  // result = {
204
222
  // uploadUrl: "https://signed-url...",
@@ -219,28 +237,27 @@ Adds signed URLs to response objects.
219
237
 
220
238
  ```typescript
221
239
  const photoFields: PhotoField[] = [
222
- { field: 'avatar', urlField: 'avatar_url' },
223
- { field: 'shop.logo', urlField: 'logo_url' },
224
- { field: 'products[].image', urlField: 'image_url' },
225
- { field: 'gallery[].photo', urlField: 'photo_url' },
240
+ { field: "avatar", urlField: "avatar_url" },
241
+ { field: "shop.logo", urlField: "logo_url" },
242
+ { field: "products[].image", urlField: "image_url" },
243
+ { field: "gallery[].photo", urlField: "photo_url" },
226
244
  ];
227
245
 
228
246
  const result = await photoManager.appendPhotoUrls(product, photoFields);
229
247
  ```
230
248
 
231
249
  Input:
250
+
232
251
  ```json
233
252
  {
234
253
  "name": "Laptop",
235
254
  "image": "laptop.png",
236
- "gallery": [
237
- { "photo": "photo1.jpg" },
238
- { "photo": "photo2.jpg" }
239
- ]
255
+ "gallery": [{ "photo": "photo1.jpg" }, { "photo": "photo2.jpg" }]
240
256
  }
241
257
  ```
242
258
 
243
259
  Output:
260
+
244
261
  ```json
245
262
  {
246
263
  "name": "Laptop",
@@ -264,13 +281,13 @@ const payload = {
264
281
  image_size: 42000,
265
282
  gallery: [
266
283
  { photo: "photo1.jpg", photo_size: 10000 },
267
- { photo: "photo2.jpg", photo_size: 15000 }
268
- ]
284
+ { photo: "photo2.jpg", photo_size: 15000 },
285
+ ],
269
286
  };
270
287
 
271
288
  const photoFields: PhotoField[] = [
272
- { field: 'image', sizeField: 'image_size' },
273
- { field: 'gallery[].photo', sizeField: 'gallery[].photo_size' },
289
+ { field: "image", sizeField: "image_size" },
290
+ { field: "gallery[].photo", sizeField: "gallery[].photo_size" },
274
291
  ];
275
292
 
276
293
  const result = await photoManager.createObjectWithPhotos(payload, photoFields);
@@ -335,26 +352,26 @@ variants[].images[] -> variants[0].images[0], variants[0].images[1], ...
335
352
 
336
353
  ### Supported Patterns
337
354
 
338
- | Path | Description |
339
- |------|-------------|
340
- | `shop.logo` | Simple nested field |
341
- | `user.profile.image` | Deeply nested with dots |
342
- | `gallery[].photo` | Array of objects |
343
- | `products[].images[]` | Array containing array |
355
+ | Path | Description |
356
+ | -------------------------- | ------------------------------- |
357
+ | `shop.logo` | Simple nested field |
358
+ | `user.profile.image` | Deeply nested with dots |
359
+ | `gallery[].photo` | Array of objects |
360
+ | `products[].images[]` | Array containing array |
344
361
  | `variants[0].images[].url` | Indexed array with nested array |
345
362
 
346
363
  ## Configuration Options
347
364
 
348
- | Option | Type | Required | Description |
349
- |--------|------|----------|-------------|
350
- | `endpoint` | string | Yes | R2 endpoint URL |
351
- | `accessKeyId` | string | Yes | R2 access key ID |
352
- | `secretAccessKey` | string | Yes | R2 secret access key |
353
- | `bucketName` | string | Yes | R2 bucket name |
354
- | `region` | string | No | AWS region (default: 'auto') |
355
- | `publicUrlBase` | string | No | Base URL for public access |
356
- | `signedUrlExpiry` | number | No | Signed URL expiry in seconds (default: 3600) |
357
- | `accessMode` | string | No | Access mode: `private`, `public-read`, `hybrid` (default: `hybrid`) |
365
+ | Option | Type | Required | Description |
366
+ | ----------------- | ------ | -------- | ------------------------------------------------------------------- |
367
+ | `endpoint` | string | Yes | R2 endpoint URL |
368
+ | `accessKeyId` | string | Yes | R2 access key ID |
369
+ | `secretAccessKey` | string | Yes | R2 secret access key |
370
+ | `bucketName` | string | Yes | R2 bucket name |
371
+ | `region` | string | No | AWS region (default: 'auto') |
372
+ | `publicUrlBase` | string | No | Base URL for public access |
373
+ | `signedUrlExpiry` | number | No | Signed URL expiry in seconds (default: 3600) |
374
+ | `accessMode` | string | No | Access mode: `private`, `public-read`, `hybrid` (default: `hybrid`) |
358
375
 
359
376
  ## Error Handling
360
377
 
@@ -363,10 +380,10 @@ variants[].images[] -> variants[0].images[0], variants[0].images[1], ...
363
380
  Thrown when attempting to generate public URLs in `private` access mode.
364
381
 
365
382
  ```typescript
366
- import { AccessModeError } from 'nestjs-r2-storage';
383
+ import { AccessModeError } from "nestjs-r2-storage";
367
384
 
368
385
  try {
369
- const result = await cloudflare.getUploadUrl('file.png', 1024);
386
+ const result = await cloudflare.getUploadUrl("file.png", 1024);
370
387
  } catch (error) {
371
388
  if (error instanceof AccessModeError) {
372
389
  console.log(error.message); // "Public URL generation is not allowed in 'private' access mode..."
@@ -384,7 +401,7 @@ R2StorageModule.forRootAsync({
384
401
  secretAccessKey: process.env.R2_SECRET_KEY,
385
402
  bucketName: process.env.R2_BUCKET,
386
403
  }),
387
- })
404
+ });
388
405
  ```
389
406
 
390
407
  ## Changelog
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestjs-r2-storage",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "Production-ready NestJS module for Cloudflare R2 object storage management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "file-upload",
19
19
  "file-management"
20
20
  ],
21
- "author": "Nurul Islam Rimon <nurul.islam.rakon@gmail.com>",
21
+ "author": "Nurul Islam Rimon <nurulislamrimon@gmail.com>",
22
22
  "license": "MIT",
23
23
  "repository": {
24
24
  "type": "git",