@uploadista/data-store-s3 0.0.3

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 (65) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +588 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/observability.d.ts +45 -0
  9. package/dist/observability.d.ts.map +1 -0
  10. package/dist/observability.js +155 -0
  11. package/dist/s3-store-old.d.ts +51 -0
  12. package/dist/s3-store-old.d.ts.map +1 -0
  13. package/dist/s3-store-old.js +765 -0
  14. package/dist/s3-store.d.ts +9 -0
  15. package/dist/s3-store.d.ts.map +1 -0
  16. package/dist/s3-store.js +666 -0
  17. package/dist/services/__mocks__/s3-client-mock.service.d.ts +44 -0
  18. package/dist/services/__mocks__/s3-client-mock.service.d.ts.map +1 -0
  19. package/dist/services/__mocks__/s3-client-mock.service.js +379 -0
  20. package/dist/services/index.d.ts +2 -0
  21. package/dist/services/index.d.ts.map +1 -0
  22. package/dist/services/index.js +1 -0
  23. package/dist/services/s3-client.service.d.ts +68 -0
  24. package/dist/services/s3-client.service.d.ts.map +1 -0
  25. package/dist/services/s3-client.service.js +209 -0
  26. package/dist/test-observability.d.ts +6 -0
  27. package/dist/test-observability.d.ts.map +1 -0
  28. package/dist/test-observability.js +62 -0
  29. package/dist/types.d.ts +81 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +1 -0
  32. package/dist/utils/calculations.d.ts +7 -0
  33. package/dist/utils/calculations.d.ts.map +1 -0
  34. package/dist/utils/calculations.js +41 -0
  35. package/dist/utils/error-handling.d.ts +7 -0
  36. package/dist/utils/error-handling.d.ts.map +1 -0
  37. package/dist/utils/error-handling.js +29 -0
  38. package/dist/utils/index.d.ts +4 -0
  39. package/dist/utils/index.d.ts.map +1 -0
  40. package/dist/utils/index.js +3 -0
  41. package/dist/utils/stream-adapter.d.ts +14 -0
  42. package/dist/utils/stream-adapter.d.ts.map +1 -0
  43. package/dist/utils/stream-adapter.js +41 -0
  44. package/package.json +36 -0
  45. package/src/__tests__/integration/s3-store.integration.test.ts +548 -0
  46. package/src/__tests__/multipart-logic.test.ts +395 -0
  47. package/src/__tests__/s3-store.edge-cases.test.ts +681 -0
  48. package/src/__tests__/s3-store.performance.test.ts +622 -0
  49. package/src/__tests__/s3-store.test.ts +662 -0
  50. package/src/__tests__/utils/performance-helpers.ts +459 -0
  51. package/src/__tests__/utils/test-data-generator.ts +331 -0
  52. package/src/__tests__/utils/test-setup.ts +256 -0
  53. package/src/index.ts +1 -0
  54. package/src/s3-store.ts +1059 -0
  55. package/src/services/__mocks__/s3-client-mock.service.ts +604 -0
  56. package/src/services/index.ts +1 -0
  57. package/src/services/s3-client.service.ts +359 -0
  58. package/src/types.ts +96 -0
  59. package/src/utils/calculations.ts +61 -0
  60. package/src/utils/error-handling.ts +52 -0
  61. package/src/utils/index.ts +3 -0
  62. package/src/utils/stream-adapter.ts +50 -0
  63. package/tsconfig.json +19 -0
  64. package/tsconfig.tsbuildinfo +1 -0
  65. package/vitest.config.ts +15 -0
@@ -0,0 +1,395 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ calcOffsetFromParts,
4
+ calcOptimalPartSize,
5
+ getExpirationDate,
6
+ partKey,
7
+ shouldUseExpirationTags,
8
+ } from "../utils/calculations";
9
+
10
+ describe("Multipart Upload Logic", () => {
11
+ describe("calcOffsetFromParts", () => {
12
+ it("should calculate offset from empty parts list", () => {
13
+ expect(calcOffsetFromParts([])).toBe(0);
14
+ expect(calcOffsetFromParts(undefined)).toBe(0);
15
+ });
16
+
17
+ it("should calculate offset from single part", () => {
18
+ const parts = [{ Size: 1024 }];
19
+ expect(calcOffsetFromParts(parts)).toBe(1024);
20
+ });
21
+
22
+ it("should calculate offset from multiple parts", () => {
23
+ const parts = [{ Size: 1024 }, { Size: 2048 }, { Size: 512 }];
24
+ expect(calcOffsetFromParts(parts)).toBe(3584);
25
+ });
26
+
27
+ it("should handle parts with undefined sizes", () => {
28
+ const parts = [{ Size: 1024 }, { Size: undefined }, { Size: 512 }];
29
+ expect(calcOffsetFromParts(parts)).toBe(1536);
30
+ });
31
+
32
+ it("should handle parts with mixed sizes", () => {
33
+ const parts = [
34
+ { Size: 5 * 1024 * 1024 }, // 5MB
35
+ { Size: 8 * 1024 * 1024 }, // 8MB
36
+ { Size: 3 * 1024 * 1024 }, // 3MB
37
+ ];
38
+ expect(calcOffsetFromParts(parts)).toBe(16 * 1024 * 1024); // 16MB
39
+ });
40
+ });
41
+
42
+ describe("calcOptimalPartSize", () => {
43
+ const minPartSize = 5 * 1024 * 1024; // 5MB
44
+ const preferredPartSize = 8 * 1024 * 1024; // 8MB
45
+ const maxMultipartParts = 10000;
46
+
47
+ it("should handle undefined file size", () => {
48
+ const partSize = calcOptimalPartSize(
49
+ undefined,
50
+ preferredPartSize,
51
+ minPartSize,
52
+ maxMultipartParts,
53
+ );
54
+
55
+ // Should use a size that works with the maximum upload size
56
+ expect(partSize).toBeGreaterThanOrEqual(minPartSize);
57
+ });
58
+
59
+ it("should use file size for small files", () => {
60
+ const fileSize = 1 * 1024 * 1024; // 1MB
61
+ const partSize = calcOptimalPartSize(
62
+ fileSize,
63
+ preferredPartSize,
64
+ minPartSize,
65
+ maxMultipartParts,
66
+ );
67
+
68
+ // For small files, should align to reasonable boundary but stay small
69
+ expect(partSize).toBeLessThanOrEqual(preferredPartSize);
70
+ expect(partSize % 1024).toBe(0); // Should be aligned to 1KB boundaries
71
+ });
72
+
73
+ it("should use preferred part size for medium files", () => {
74
+ const fileSize = 50 * 1024 * 1024; // 50MB
75
+ const partSize = calcOptimalPartSize(
76
+ fileSize,
77
+ preferredPartSize,
78
+ minPartSize,
79
+ maxMultipartParts,
80
+ );
81
+
82
+ expect(partSize).toBe(preferredPartSize);
83
+ });
84
+
85
+ it("should adjust part size for very large files", () => {
86
+ const fileSize = 100 * 1024 * 1024 * 1024; // 100GB
87
+ const partSize = calcOptimalPartSize(
88
+ fileSize,
89
+ preferredPartSize,
90
+ minPartSize,
91
+ maxMultipartParts,
92
+ );
93
+
94
+ // Should be larger than preferred to stay within part limits
95
+ expect(partSize).toBeGreaterThan(preferredPartSize);
96
+
97
+ // Should not exceed part count limit
98
+ const estimatedParts = Math.ceil(fileSize / partSize);
99
+ expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
100
+ });
101
+
102
+ it("should respect minimum part size for multipart uploads", () => {
103
+ const fileSize = 20 * 1024 * 1024; // 20MB
104
+ const smallPreferredSize = 1 * 1024 * 1024; // 1MB (below S3 minimum)
105
+
106
+ const partSize = calcOptimalPartSize(
107
+ fileSize,
108
+ smallPreferredSize,
109
+ minPartSize,
110
+ maxMultipartParts,
111
+ );
112
+
113
+ expect(partSize).toBeGreaterThanOrEqual(minPartSize);
114
+ });
115
+
116
+ it("should handle edge case at part limit boundary", () => {
117
+ // File size that would require exactly max parts with preferred size
118
+ const fileSize = preferredPartSize * maxMultipartParts;
119
+
120
+ const partSize = calcOptimalPartSize(
121
+ fileSize,
122
+ preferredPartSize,
123
+ minPartSize,
124
+ maxMultipartParts,
125
+ );
126
+
127
+ expect(partSize).toBe(preferredPartSize);
128
+
129
+ const estimatedParts = Math.ceil(fileSize / partSize);
130
+ expect(estimatedParts).toBe(maxMultipartParts);
131
+ });
132
+
133
+ it("should handle file size just over part limit boundary", () => {
134
+ // File size that would require one more than max parts with preferred size
135
+ const fileSize = preferredPartSize * maxMultipartParts + 1;
136
+
137
+ const partSize = calcOptimalPartSize(
138
+ fileSize,
139
+ preferredPartSize,
140
+ minPartSize,
141
+ maxMultipartParts,
142
+ );
143
+
144
+ expect(partSize).toBeGreaterThan(preferredPartSize);
145
+
146
+ const estimatedParts = Math.ceil(fileSize / partSize);
147
+ expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
148
+ });
149
+
150
+ it("should align part sizes to 1KB boundaries", () => {
151
+ const fileSizes = [
152
+ 13 * 1024 * 1024 + 500, // 13.5MB + some bytes
153
+ 25 * 1024 * 1024 + 777, // 25MB + some bytes
154
+ 100 * 1024 * 1024 + 123, // 100MB + some bytes
155
+ ];
156
+
157
+ fileSizes.forEach((fileSize) => {
158
+ const partSize = calcOptimalPartSize(
159
+ fileSize,
160
+ preferredPartSize,
161
+ minPartSize,
162
+ maxMultipartParts,
163
+ );
164
+
165
+ expect(partSize % 1024).toBe(0); // Should be aligned to 1KB
166
+ });
167
+ });
168
+
169
+ it("should handle maximum upload size", () => {
170
+ const maxUploadSize = 5 * 1024 * 1024 * 1024 * 1024; // 5TB
171
+
172
+ const partSize = calcOptimalPartSize(
173
+ maxUploadSize,
174
+ preferredPartSize,
175
+ minPartSize,
176
+ maxMultipartParts,
177
+ );
178
+
179
+ expect(partSize).toBeGreaterThanOrEqual(minPartSize);
180
+
181
+ const estimatedParts = Math.ceil(maxUploadSize / partSize);
182
+ expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
183
+ });
184
+ });
185
+
186
+ describe("partKey", () => {
187
+ it("should generate consistent part keys", () => {
188
+ expect(partKey("test-upload-id")).toBe("test-upload-id.part");
189
+ expect(partKey("upload-123")).toBe("upload-123.part");
190
+ expect(partKey("")).toBe(".part");
191
+ });
192
+
193
+ it("should handle special characters in upload IDs", () => {
194
+ expect(partKey("upload-with-dashes")).toBe("upload-with-dashes.part");
195
+ expect(partKey("upload_with_underscores")).toBe(
196
+ "upload_with_underscores.part",
197
+ );
198
+ expect(partKey("upload.with.dots")).toBe("upload.with.dots.part");
199
+ });
200
+ });
201
+
202
+ describe("shouldUseExpirationTags", () => {
203
+ it("should return false when expiration is disabled", () => {
204
+ expect(shouldUseExpirationTags(0, true)).toBe(false);
205
+ expect(shouldUseExpirationTags(0, false)).toBe(false);
206
+ });
207
+
208
+ it("should return false when tags are disabled", () => {
209
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
210
+ expect(shouldUseExpirationTags(oneWeek, false)).toBe(false);
211
+ });
212
+
213
+ it("should return true when both expiration and tags are enabled", () => {
214
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
215
+ expect(shouldUseExpirationTags(oneWeek, true)).toBe(true);
216
+
217
+ const oneDay = 24 * 60 * 60 * 1000;
218
+ expect(shouldUseExpirationTags(oneDay, true)).toBe(true);
219
+ });
220
+ });
221
+
222
+ describe("getExpirationDate", () => {
223
+ it("should calculate expiration date correctly", () => {
224
+ const createdAt = "2023-01-01T00:00:00.000Z";
225
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
226
+
227
+ const expirationDate = getExpirationDate(createdAt, oneWeek);
228
+
229
+ expect(expirationDate.getTime()).toBe(
230
+ new Date(createdAt).getTime() + oneWeek,
231
+ );
232
+ });
233
+
234
+ it("should handle different date formats", () => {
235
+ const formats = [
236
+ "2023-01-01T00:00:00.000Z",
237
+ "2023-01-01T12:30:45.123Z",
238
+ "2023-12-31T23:59:59.999Z",
239
+ ];
240
+
241
+ const oneDay = 24 * 60 * 60 * 1000;
242
+
243
+ formats.forEach((dateStr) => {
244
+ const originalDate = new Date(dateStr);
245
+ const expirationDate = getExpirationDate(dateStr, oneDay);
246
+
247
+ expect(expirationDate.getTime()).toBe(originalDate.getTime() + oneDay);
248
+ });
249
+ });
250
+
251
+ it("should handle different expiration periods", () => {
252
+ const createdAt = "2023-06-15T10:30:00.000Z";
253
+ const baseTime = new Date(createdAt).getTime();
254
+
255
+ const periods = {
256
+ oneHour: 60 * 60 * 1000,
257
+ oneDay: 24 * 60 * 60 * 1000,
258
+ oneWeek: 7 * 24 * 60 * 60 * 1000,
259
+ oneMonth: 30 * 24 * 60 * 60 * 1000,
260
+ };
261
+
262
+ Object.entries(periods).forEach(([_name, period]) => {
263
+ const expirationDate = getExpirationDate(createdAt, period);
264
+ expect(expirationDate.getTime()).toBe(baseTime + period);
265
+ });
266
+ });
267
+
268
+ it("should handle zero expiration period", () => {
269
+ const createdAt = "2023-01-01T00:00:00.000Z";
270
+ const expirationDate = getExpirationDate(createdAt, 0);
271
+
272
+ expect(expirationDate.getTime()).toBe(new Date(createdAt).getTime());
273
+ });
274
+
275
+ it("should handle very large expiration periods", () => {
276
+ const createdAt = "2023-01-01T00:00:00.000Z";
277
+ const oneYear = 365 * 24 * 60 * 60 * 1000;
278
+
279
+ const expirationDate = getExpirationDate(createdAt, oneYear);
280
+
281
+ expect(expirationDate.getTime()).toBe(
282
+ new Date(createdAt).getTime() + oneYear,
283
+ );
284
+ expect(expirationDate.getFullYear()).toBe(2024);
285
+ });
286
+ });
287
+
288
+ describe("Real-world scenarios", () => {
289
+ const minPartSize = 5 * 1024 * 1024; // 5MB
290
+ const preferredPartSize = 8 * 1024 * 1024; // 8MB
291
+ const maxMultipartParts = 10000;
292
+
293
+ it("should handle typical video file upload (1GB)", () => {
294
+ const fileSize = 1024 * 1024 * 1024; // 1GB
295
+ const partSize = calcOptimalPartSize(
296
+ fileSize,
297
+ preferredPartSize,
298
+ minPartSize,
299
+ maxMultipartParts,
300
+ );
301
+
302
+ expect(partSize).toBe(preferredPartSize); // Should use preferred 8MB
303
+
304
+ const parts = Math.ceil(fileSize / partSize);
305
+ expect(parts).toBeLessThanOrEqual(maxMultipartParts);
306
+ expect(parts).toBe(128); // 1GB / 8MB = 128 parts
307
+ });
308
+
309
+ it("should handle large backup file (500GB)", () => {
310
+ const fileSize = 500 * 1024 * 1024 * 1024; // 500GB
311
+ const partSize = calcOptimalPartSize(
312
+ fileSize,
313
+ preferredPartSize,
314
+ minPartSize,
315
+ maxMultipartParts,
316
+ );
317
+
318
+ // Should be larger than preferred to stay within part limits
319
+ expect(partSize).toBeGreaterThan(preferredPartSize);
320
+
321
+ const parts = Math.ceil(fileSize / partSize);
322
+ expect(parts).toBeLessThanOrEqual(maxMultipartParts);
323
+
324
+ // Calculate expected part size
325
+ const expectedMinPartSize = Math.ceil(fileSize / maxMultipartParts);
326
+ expect(partSize).toBeGreaterThanOrEqual(expectedMinPartSize);
327
+ });
328
+
329
+ it("should handle edge case: exactly 5GB file with 5MB parts", () => {
330
+ const fileSize = 5 * 1024 * 1024 * 1024; // 5GB
331
+ const smallPreferredSize = 5 * 1024 * 1024; // 5MB
332
+
333
+ const partSize = calcOptimalPartSize(
334
+ fileSize,
335
+ smallPreferredSize,
336
+ minPartSize,
337
+ maxMultipartParts,
338
+ );
339
+
340
+ expect(partSize).toBe(smallPreferredSize); // Should use 5MB
341
+
342
+ const parts = Math.ceil(fileSize / partSize);
343
+ expect(parts).toBe(1024); // 5GB / 5MB = 1024 parts
344
+ expect(parts).toBeLessThanOrEqual(maxMultipartParts);
345
+ });
346
+
347
+ it("should optimize for files that exceed part count limits", () => {
348
+ // File that would require 20,000 parts with 8MB parts
349
+ const fileSize = 20000 * preferredPartSize;
350
+
351
+ const partSize = calcOptimalPartSize(
352
+ fileSize,
353
+ preferredPartSize,
354
+ minPartSize,
355
+ maxMultipartParts,
356
+ );
357
+
358
+ // Should be larger than preferred to fit within limit
359
+ expect(partSize).toBeGreaterThan(preferredPartSize);
360
+
361
+ const parts = Math.ceil(fileSize / partSize);
362
+ expect(parts).toBeLessThanOrEqual(maxMultipartParts);
363
+
364
+ // Should be close to the limit but not exceed it
365
+ expect(parts).toBeGreaterThan(maxMultipartParts * 0.9); // At least 90% of limit used
366
+ });
367
+
368
+ it("should handle small files efficiently", () => {
369
+ const smallSizes = [
370
+ 1024, // 1KB
371
+ 100 * 1024, // 100KB
372
+ 1024 * 1024, // 1MB
373
+ 4.9 * 1024 * 1024, // 4.9MB (just under S3 minimum)
374
+ ];
375
+
376
+ smallSizes.forEach((fileSize) => {
377
+ const partSize = calcOptimalPartSize(
378
+ fileSize,
379
+ preferredPartSize,
380
+ minPartSize,
381
+ maxMultipartParts,
382
+ );
383
+
384
+ if (fileSize < minPartSize) {
385
+ // For small files, part size should be small but aligned
386
+ expect(partSize).toBeLessThan(minPartSize);
387
+ expect(partSize % 1024).toBe(0); // 1KB aligned
388
+ } else {
389
+ // For files at or above minimum, should respect minimum
390
+ expect(partSize).toBeGreaterThanOrEqual(minPartSize);
391
+ }
392
+ });
393
+ });
394
+ });
395
+ });