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 +81 -64
- package/nestjs-r2-storage.gif +0 -0
- package/package.json +2 -2
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
|
+

|
|
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
|
|
29
|
-
|
|
30
|
-
| `private`
|
|
31
|
-
| `public-read` | Allowed
|
|
32
|
-
| `hybrid`
|
|
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:
|
|
42
|
-
publicUrlBase:
|
|
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:
|
|
62
|
-
publicUrlBase:
|
|
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:
|
|
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
|
|
84
|
-
import { R2StorageModule } from
|
|
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:
|
|
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
|
|
106
|
-
import {
|
|
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:
|
|
118
|
-
{
|
|
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(
|
|
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:
|
|
136
|
-
{ field:
|
|
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:
|
|
160
|
+
{ field: "image", urlField: "image_url", sizeField: "image_size" },
|
|
147
161
|
];
|
|
148
162
|
|
|
149
|
-
const result = await this.photoManager.updateObjectWithPhotos(
|
|
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:
|
|
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(
|
|
198
|
+
const uploadUrl = await cloudflare.getUploadUrl("avatar.png", 1024000);
|
|
181
199
|
|
|
182
200
|
// Generate download URL
|
|
183
|
-
const downloadUrl = await cloudflare.getDownloadUrl(
|
|
201
|
+
const downloadUrl = await cloudflare.getDownloadUrl("uploads/avatar_123.png");
|
|
184
202
|
|
|
185
203
|
// Delete file
|
|
186
|
-
await cloudflare.deleteFile(
|
|
204
|
+
await cloudflare.deleteFile("uploads/avatar.png");
|
|
187
205
|
|
|
188
206
|
// Check if file exists
|
|
189
|
-
const exists = await cloudflare.fileExists(
|
|
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(
|
|
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:
|
|
223
|
-
{ field:
|
|
224
|
-
{ field:
|
|
225
|
-
{ field:
|
|
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:
|
|
273
|
-
{ field:
|
|
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
|
|
339
|
-
|
|
340
|
-
| `shop.logo`
|
|
341
|
-
| `user.profile.image`
|
|
342
|
-
| `gallery[].photo`
|
|
343
|
-
| `products[].images[]`
|
|
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
|
|
349
|
-
|
|
350
|
-
| `endpoint`
|
|
351
|
-
| `accessKeyId`
|
|
352
|
-
| `secretAccessKey` | string | Yes
|
|
353
|
-
| `bucketName`
|
|
354
|
-
| `region`
|
|
355
|
-
| `publicUrlBase`
|
|
356
|
-
| `signedUrlExpiry` | number | No
|
|
357
|
-
| `accessMode`
|
|
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
|
|
383
|
+
import { AccessModeError } from "nestjs-r2-storage";
|
|
367
384
|
|
|
368
385
|
try {
|
|
369
|
-
const result = await cloudflare.getUploadUrl(
|
|
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.
|
|
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 <
|
|
21
|
+
"author": "Nurul Islam Rimon <nurulislamrimon@gmail.com>",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|