@trayio/commons 5.18.0 → 5.20.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../src/http/Http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Http.d.ts","sourceRoot":"","sources":["../../src/http/Http.ts"],"names":[],"mappings":"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"}
package/dist/http/Http.js CHANGED
@@ -43,20 +43,69 @@ exports.MultipartHttpRequestBody = {
43
43
  if (value === null || typeof value !== 'object') {
44
44
  return TE.left(Error('Multipart body must be an object'));
45
45
  }
46
- const fields = {};
47
- const fileDescriptors = [];
48
- Object.entries(value).forEach(([key, val]) => {
49
- if (File_1.FileDescriptor.isFileDescriptor(val)) {
50
- fileDescriptors.push(val);
51
- }
52
- else if (!_.isObject(val)) {
53
- fields[key] = val;
46
+ /**
47
+ * Recursively traverses an object to find FileDescriptors at any nesting level.
48
+ * Returns an array of paths to FileDescriptors and the descriptors themselves.
49
+ */
50
+ const findFileDescriptors = (obj, currentPath = []) => {
51
+ const result = [];
52
+ for (const [key, val] of Object.entries(obj)) {
53
+ const path = [...currentPath, key];
54
+ if (File_1.FileDescriptor.isFileDescriptor(val)) {
55
+ result.push({ path, descriptor: val });
56
+ }
57
+ else if (_.isObject(val) && !Array.isArray(val)) {
58
+ result.push(...findFileDescriptors(val, path));
59
+ }
54
60
  }
55
- else {
56
- return TE.left(Error(`Multipart body field '${key}' must be a string, number, boolean or file reference`));
61
+ return result;
62
+ };
63
+ /**
64
+ * Converts nested object to flat fields structure, skipping objects that
65
+ * would contain FileDescriptors.
66
+ */
67
+ const extractFields = (obj, currentPath = []) => {
68
+ const fields = {};
69
+ for (const [key, val] of Object.entries(obj)) {
70
+ const path = [...currentPath, key];
71
+ if (File_1.FileDescriptor.isFileDescriptor(val)) {
72
+ // Skip FileDescriptors - do nothing
73
+ }
74
+ else if (Array.isArray(val)) {
75
+ // Serialize arrays as comma-separated strings
76
+ const fieldKey = path.join('.');
77
+ fields[fieldKey] = val.map(String).join(',');
78
+ }
79
+ else if (_.isObject(val)) {
80
+ // Recursively extract fields from nested objects
81
+ const nestedFields = extractFields(val, path);
82
+ Object.assign(fields, nestedFields);
83
+ }
84
+ else {
85
+ // Primitive values: use dot notation for nested paths
86
+ const fieldKey = path.join('.');
87
+ fields[fieldKey] = String(val);
88
+ }
57
89
  }
58
- });
59
- return (0, function_1.pipe)(TE.sequenceArray(fileDescriptors.map((fileReference) => (0, function_1.pipe)(fileStorage.read(fileReference.key), TE.map((file) => ({ key: file.reference.key, file }))))), TE.map((files) => {
90
+ return fields;
91
+ };
92
+ const fileDescriptorPaths = findFileDescriptors(value);
93
+ // Extract fields, using dot notation for nested structures
94
+ const fields = extractFields(value);
95
+ return (0, function_1.pipe)(TE.sequenceArray(fileDescriptorPaths.map(({ path, descriptor }) => (0, function_1.pipe)(fileStorage.read(descriptor.key), TE.map((file) => ({
96
+ key: file.reference.key,
97
+ file: {
98
+ ...file,
99
+ reference: {
100
+ ...file.reference,
101
+ // Preserve metadata from original descriptor (especially contentType)
102
+ metadata: {
103
+ ...file.reference.metadata,
104
+ ...descriptor.metadata,
105
+ },
106
+ },
107
+ },
108
+ }))))), TE.map((files) => {
60
109
  const filesRecord = {};
61
110
  files.forEach(({ key, file }) => {
62
111
  filesRecord[key] = file;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MultipartHttpRequestBody.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultipartHttpRequestBody.integration.test.d.ts","sourceRoot":"","sources":["../../src/http/MultipartHttpRequestBody.integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,516 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const stream_1 = require("stream");
30
+ const http_1 = require("http");
31
+ const formidable_1 = __importDefault(require("formidable"));
32
+ const fs = __importStar(require("fs/promises"));
33
+ const path = __importStar(require("path"));
34
+ const crypto = __importStar(require("crypto"));
35
+ const NodeFsFileStorage_1 = require("../file/NodeFsFileStorage");
36
+ const Http_1 = require("./Http");
37
+ /**
38
+ * Tests for MultipartHttpRequestBody using a server to receive the requests.
39
+ *
40
+ * These tests verify the complete roundtrip:
41
+ * 1. Client creates multipart body with nested FileDescriptors and fields (flattened to dot-notation)
42
+ * 2. Client sends HTTP POST with multipart/form-data
43
+ * 3. Server receives and parses the multipart body (fields remain flat)
44
+ * 4. File content is byte-for-byte identical (SHA-256 verified)
45
+ */
46
+ describe('MultipartHttpRequestBody - E2E', () => {
47
+ let clientStorage;
48
+ let clientTmpDir;
49
+ let serverTmpDir;
50
+ let serverPort;
51
+ let serverUrl;
52
+ let server;
53
+ jest.setTimeout(30000);
54
+ beforeAll(async () => {
55
+ // Setup client and server storage directories
56
+ clientTmpDir = path.join('/tmp', `e2e-client-${Date.now()}-${Math.random().toString(36).substring(7)}`);
57
+ serverTmpDir = path.join('/tmp', `e2e-server-${Date.now()}-${Math.random().toString(36).substring(7)}`);
58
+ await fs.mkdir(clientTmpDir, { recursive: true });
59
+ await fs.mkdir(serverTmpDir, { recursive: true });
60
+ clientStorage = new NodeFsFileStorage_1.NodeFsFileStorage(clientTmpDir);
61
+ // Start HTTP server that can receive multipart requests
62
+ serverPort = 3000 + Math.floor(Math.random() * 1000);
63
+ serverUrl = `http://localhost:${serverPort}`;
64
+ server = (0, http_1.createServer)(async (req, res) => {
65
+ if (req.method === 'POST' && req.url === '/upload') {
66
+ try {
67
+ const form = (0, formidable_1.default)({
68
+ uploadDir: serverTmpDir,
69
+ keepExtensions: true,
70
+ maxFileSize: 100 * 1024 * 1024, // 100MB
71
+ });
72
+ const [fields, files] = await form.parse(req);
73
+ // Flatten formidable fields (array values) to simple strings
74
+ const flattenedFields = {};
75
+ for (const [key, value] of Object.entries(fields)) {
76
+ flattenedFields[key] = Array.isArray(value)
77
+ ? value[0]
78
+ : value || '';
79
+ }
80
+ // Read file contents and compute hashes
81
+ const fileData = {};
82
+ for (const [key, fileArray] of Object.entries(files)) {
83
+ if (fileArray && fileArray[0]) {
84
+ const file = fileArray[0];
85
+ // eslint-disable-next-line no-await-in-loop
86
+ const content = await fs.readFile(file.filepath, 'utf-8');
87
+ const hash = crypto
88
+ .createHash('sha256')
89
+ .update(content)
90
+ .digest('hex');
91
+ fileData[key] = { content, hash };
92
+ }
93
+ }
94
+ res.writeHead(200, { 'Content-Type': 'application/json' });
95
+ res.end(JSON.stringify({
96
+ fields: flattenedFields,
97
+ files: fileData,
98
+ }));
99
+ }
100
+ catch (error) {
101
+ res.writeHead(500, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify({ error: error.message }));
103
+ }
104
+ }
105
+ else {
106
+ res.writeHead(404);
107
+ res.end('Not Found');
108
+ }
109
+ });
110
+ await new Promise((resolve) => {
111
+ server.listen(serverPort, () => {
112
+ // eslint-disable-next-line no-console
113
+ console.log(`E2E test server listening on port ${serverPort}`);
114
+ resolve();
115
+ });
116
+ });
117
+ });
118
+ afterAll(async () => {
119
+ // Cleanup
120
+ if (server) {
121
+ await new Promise((resolve) => {
122
+ server.close(() => resolve());
123
+ });
124
+ }
125
+ try {
126
+ await fs.rm(clientTmpDir, { recursive: true, force: true });
127
+ await fs.rm(serverTmpDir, { recursive: true, force: true });
128
+ }
129
+ catch (error) {
130
+ // Ignore cleanup errors
131
+ }
132
+ });
133
+ describe('File Transfer with Nested FileDescriptors', () => {
134
+ it('should send nested FileDescriptor and receive identical file content', async () => {
135
+ // 1. Create test file with known content
136
+ const originalContent = 'This is test file content for E2E validation! 🚀';
137
+ const originalHash = crypto
138
+ .createHash('sha256')
139
+ .update(originalContent)
140
+ .digest('hex');
141
+ const fileDescriptor = {
142
+ _tag: 'FileDescriptor',
143
+ key: 'test-image-key',
144
+ metadata: {
145
+ name: 'test-image.png',
146
+ size: originalContent.length,
147
+ contentType: 'image/png',
148
+ },
149
+ };
150
+ // Write file to client storage
151
+ const writeResult = await clientStorage.write({
152
+ reference: fileDescriptor,
153
+ content: stream_1.Readable.from(originalContent),
154
+ }, undefined)();
155
+ expect(writeResult._tag).toBe('Right');
156
+ // 2. Create multipart body with NESTED FileDescriptor
157
+ const bodyWithNestedFile = {
158
+ fileName: 'test-image.png',
159
+ overrideIfPresent: true,
160
+ imageContent: {
161
+ reference: fileDescriptor, // NESTED structure
162
+ },
163
+ };
164
+ const multipartResult = await Http_1.MultipartHttpRequestBody.create(bodyWithNestedFile, clientStorage)();
165
+ expect(multipartResult._tag).toBe('Right');
166
+ if (multipartResult._tag === 'Left') {
167
+ throw new Error(`Failed to create multipart body: ${multipartResult.left.message}`);
168
+ }
169
+ const multipartBody = multipartResult.right;
170
+ // Verify the file was detected in the nested structure
171
+ expect(Object.keys(multipartBody.files)).toContain('test-image-key');
172
+ expect(multipartBody.fields).toHaveProperty('fileName', 'test-image.png');
173
+ expect(multipartBody.fields).toHaveProperty('overrideIfPresent', 'true');
174
+ // 3. Send HTTP POST with multipart data
175
+ // eslint-disable-next-line global-require
176
+ const FormData = require('form-data');
177
+ const formData = new FormData();
178
+ // Add fields
179
+ for (const [key, value] of Object.entries(multipartBody.fields)) {
180
+ formData.append(key, value);
181
+ }
182
+ // Add files
183
+ for (const [key, file] of Object.entries(multipartBody.files)) {
184
+ const chunks = [];
185
+ // eslint-disable-next-line no-await-in-loop
186
+ for await (const chunk of file.content) {
187
+ chunks.push(Buffer.from(chunk));
188
+ }
189
+ const buffer = Buffer.concat(chunks);
190
+ formData.append(key, buffer, {
191
+ filename: file.reference.metadata.name,
192
+ contentType: file.reference.metadata.contentType,
193
+ });
194
+ }
195
+ const response = await new Promise((resolve, reject) => {
196
+ formData.submit(`${serverUrl}/upload`, (err, res) => {
197
+ if (err) {
198
+ reject(err);
199
+ return;
200
+ }
201
+ let body = '';
202
+ res.on('data', (chunk) => {
203
+ body += chunk.toString();
204
+ });
205
+ res.on('end', () => {
206
+ try {
207
+ const parsed = JSON.parse(body);
208
+ if (res.statusCode === 200) {
209
+ resolve(parsed);
210
+ }
211
+ else {
212
+ reject(new Error(`Server error: ${parsed.error}`));
213
+ }
214
+ }
215
+ catch (e) {
216
+ reject(new Error(`Failed to parse response: ${body}`));
217
+ }
218
+ });
219
+ res.on('error', reject);
220
+ });
221
+ });
222
+ // 4. Verify server received the file correctly
223
+ expect(response.files).toBeDefined();
224
+ expect(response.files['test-image-key']).toBeDefined();
225
+ const receivedFile = response.files['test-image-key'];
226
+ expect(receivedFile.content).toBe(originalContent);
227
+ expect(receivedFile.hash).toBe(originalHash);
228
+ // 5. Verify fields were sent correctly
229
+ expect(response.fields.fileName).toBe('test-image.png');
230
+ expect(response.fields.overrideIfPresent).toBe('true');
231
+ // eslint-disable-next-line no-console
232
+ console.log('✅ Nested FileDescriptor successfully sent and received with identical content!');
233
+ });
234
+ it('should handle multiple nested FileDescriptors at different levels', async () => {
235
+ // Create two test files
236
+ const content1 = 'First file content';
237
+ const content2 = 'Second file content';
238
+ const hash1 = crypto.createHash('sha256').update(content1).digest('hex');
239
+ const hash2 = crypto.createHash('sha256').update(content2).digest('hex');
240
+ const fileDescriptor1 = {
241
+ _tag: 'FileDescriptor',
242
+ key: 'file-1',
243
+ metadata: {
244
+ name: 'file1.txt',
245
+ size: content1.length,
246
+ contentType: 'text/plain',
247
+ },
248
+ };
249
+ const fileDescriptor2 = {
250
+ _tag: 'FileDescriptor',
251
+ key: 'file-2',
252
+ metadata: {
253
+ name: 'file2.txt',
254
+ size: content2.length,
255
+ contentType: 'text/plain',
256
+ },
257
+ };
258
+ // Write files to client storage
259
+ await clientStorage.write({ reference: fileDescriptor1, content: stream_1.Readable.from(content1) }, undefined)();
260
+ await clientStorage.write({ reference: fileDescriptor2, content: stream_1.Readable.from(content2) }, undefined)();
261
+ // Create body with multiple nested FileDescriptors
262
+ const bodyWithMultipleFiles = {
263
+ rootFile: fileDescriptor1,
264
+ nested: {
265
+ deepFile: {
266
+ reference: fileDescriptor2, // Nested level
267
+ },
268
+ },
269
+ description: 'Multiple files test',
270
+ };
271
+ const multipartResult = await Http_1.MultipartHttpRequestBody.create(bodyWithMultipleFiles, clientStorage)();
272
+ expect(multipartResult._tag).toBe('Right');
273
+ if (multipartResult._tag === 'Left') {
274
+ throw new Error(`Failed to create multipart body: ${multipartResult.left.message}`);
275
+ }
276
+ const multipartBody = multipartResult.right;
277
+ // Verify both files were detected
278
+ expect(Object.keys(multipartBody.files)).toHaveLength(2);
279
+ expect(multipartBody.files['file-1']).toBeDefined();
280
+ expect(multipartBody.files['file-2']).toBeDefined();
281
+ // Send to server
282
+ // eslint-disable-next-line global-require
283
+ const FormData = require('form-data');
284
+ const formData = new FormData();
285
+ for (const [key, value] of Object.entries(multipartBody.fields)) {
286
+ formData.append(key, value);
287
+ }
288
+ for (const [key, file] of Object.entries(multipartBody.files)) {
289
+ const chunks = [];
290
+ // eslint-disable-next-line no-await-in-loop
291
+ for await (const chunk of file.content) {
292
+ chunks.push(Buffer.from(chunk));
293
+ }
294
+ const buffer = Buffer.concat(chunks);
295
+ formData.append(key, buffer, {
296
+ filename: file.reference.metadata.name,
297
+ });
298
+ }
299
+ const response = await new Promise((resolve, reject) => {
300
+ formData.submit(`${serverUrl}/upload`, (err, res) => {
301
+ if (err) {
302
+ reject(err);
303
+ return;
304
+ }
305
+ let body = '';
306
+ res.on('data', (chunk) => {
307
+ body += chunk.toString();
308
+ });
309
+ res.on('end', () => {
310
+ try {
311
+ const parsed = JSON.parse(body);
312
+ if (res.statusCode === 200) {
313
+ resolve(parsed);
314
+ }
315
+ else {
316
+ reject(new Error(`Server error: ${parsed.error}`));
317
+ }
318
+ }
319
+ catch (e) {
320
+ reject(new Error(`Failed to parse response: ${body}`));
321
+ }
322
+ });
323
+ });
324
+ });
325
+ // Verify both files received correctly
326
+ expect(response.files['file-1'].content).toBe(content1);
327
+ expect(response.files['file-1'].hash).toBe(hash1);
328
+ expect(response.files['file-2'].content).toBe(content2);
329
+ expect(response.files['file-2'].hash).toBe(hash2);
330
+ // eslint-disable-next-line no-console
331
+ console.log('✅ Multiple nested files successfully transferred!');
332
+ });
333
+ });
334
+ describe('Nested Field Structures', () => {
335
+ it('should properly flatten and unflatten nested field structures', async () => {
336
+ // Create a test file
337
+ const fileDescriptor = {
338
+ _tag: 'FileDescriptor',
339
+ key: 'test-doc',
340
+ metadata: { name: 'doc.txt', size: 100, contentType: 'text/plain' },
341
+ };
342
+ await clientStorage.write({
343
+ reference: fileDescriptor,
344
+ content: stream_1.Readable.from('document content'),
345
+ }, undefined)();
346
+ // Create request body with nested structures AND a file
347
+ const bodyWithNestedStructure = {
348
+ title: 'Test Document',
349
+ enabled: true,
350
+ metadata: {
351
+ author: 'John Doe',
352
+ version: '1.0',
353
+ tags: {
354
+ category: 'documentation',
355
+ priority: 'high',
356
+ },
357
+ },
358
+ document: fileDescriptor,
359
+ };
360
+ const multipartResult = await Http_1.MultipartHttpRequestBody.create(bodyWithNestedStructure, clientStorage)();
361
+ expect(multipartResult._tag).toBe('Right');
362
+ if (multipartResult._tag === 'Left') {
363
+ throw new Error(`Failed: ${multipartResult.left.message}`);
364
+ }
365
+ const multipartBody = multipartResult.right;
366
+ // Verify client-side flattening happened
367
+ expect(multipartBody.fields['metadata.author']).toBe('John Doe');
368
+ expect(multipartBody.fields['metadata.version']).toBe('1.0');
369
+ expect(multipartBody.fields['metadata.tags.category']).toBe('documentation');
370
+ expect(multipartBody.fields['metadata.tags.priority']).toBe('high');
371
+ // Send to server
372
+ // eslint-disable-next-line global-require
373
+ const FormData = require('form-data');
374
+ const formData = new FormData();
375
+ for (const [key, value] of Object.entries(multipartBody.fields)) {
376
+ formData.append(key, value);
377
+ }
378
+ for (const [key, file] of Object.entries(multipartBody.files)) {
379
+ const chunks = [];
380
+ // eslint-disable-next-line no-await-in-loop
381
+ for await (const chunk of file.content) {
382
+ chunks.push(Buffer.from(chunk));
383
+ }
384
+ const buffer = Buffer.concat(chunks);
385
+ formData.append(key, buffer, {
386
+ filename: file.reference.metadata.name,
387
+ });
388
+ }
389
+ const response = await new Promise((resolve, reject) => {
390
+ formData.submit(`${serverUrl}/upload`, (err, res) => {
391
+ if (err) {
392
+ reject(err);
393
+ return;
394
+ }
395
+ let body = '';
396
+ res.on('data', (chunk) => {
397
+ body += chunk.toString();
398
+ });
399
+ res.on('end', () => {
400
+ try {
401
+ const parsed = JSON.parse(body);
402
+ if (res.statusCode === 200) {
403
+ resolve(parsed);
404
+ }
405
+ else {
406
+ reject(new Error(`Server error: ${parsed.error}`));
407
+ }
408
+ }
409
+ catch (e) {
410
+ reject(new Error(`Parse error: ${body}`));
411
+ }
412
+ });
413
+ });
414
+ });
415
+ // Verify fields were sent correctly (flat dot-notation)
416
+ expect(response.fields.title).toBe('Test Document');
417
+ expect(response.fields.enabled).toBe('true');
418
+ // Key assertion: nested fields are flattened with dot notation
419
+ expect(response.fields['metadata.author']).toBe('John Doe');
420
+ expect(response.fields['metadata.version']).toBe('1.0');
421
+ expect(response.fields['metadata.tags.category']).toBe('documentation');
422
+ expect(response.fields['metadata.tags.priority']).toBe('high');
423
+ // eslint-disable-next-line no-console
424
+ console.log('✅ Nested field structure properly flattened with dot notation!');
425
+ });
426
+ it('should handle Library Service uploadAssetImage scenario', async () => {
427
+ // Simulates Library Service uploadAssetImage with additional metadata
428
+ const fileDescriptor = {
429
+ _tag: 'FileDescriptor',
430
+ key: 'asset-123',
431
+ metadata: { name: 'logo.png', size: 2048, contentType: 'image/png' },
432
+ };
433
+ await clientStorage.write({
434
+ reference: fileDescriptor,
435
+ content: stream_1.Readable.from('image data'),
436
+ }, undefined)();
437
+ const bodyLikeLibraryService = {
438
+ fileName: 'logo.png',
439
+ overrideIfPresent: true,
440
+ uploadMetadata: {
441
+ source: 'admin-panel',
442
+ uploadedBy: {
443
+ userId: '123',
444
+ userName: 'admin',
445
+ },
446
+ timestamp: '2026-04-27T00:00:00Z',
447
+ },
448
+ imageContent: fileDescriptor,
449
+ };
450
+ const multipartResult = await Http_1.MultipartHttpRequestBody.create(bodyLikeLibraryService, clientStorage)();
451
+ expect(multipartResult._tag).toBe('Right');
452
+ if (multipartResult._tag === 'Left') {
453
+ throw new Error(`Failed: ${multipartResult.left.message}`);
454
+ }
455
+ const multipartBody = multipartResult.right;
456
+ // Verify flattening
457
+ expect(multipartBody.fields['uploadMetadata.source']).toBe('admin-panel');
458
+ expect(multipartBody.fields['uploadMetadata.uploadedBy.userId']).toBe('123');
459
+ expect(multipartBody.fields['uploadMetadata.uploadedBy.userName']).toBe('admin');
460
+ // Send to server
461
+ // eslint-disable-next-line global-require
462
+ const FormData = require('form-data');
463
+ const formData = new FormData();
464
+ for (const [key, value] of Object.entries(multipartBody.fields)) {
465
+ formData.append(key, value);
466
+ }
467
+ for (const [key, file] of Object.entries(multipartBody.files)) {
468
+ const chunks = [];
469
+ // eslint-disable-next-line no-await-in-loop
470
+ for await (const chunk of file.content) {
471
+ chunks.push(Buffer.from(chunk));
472
+ }
473
+ const buffer = Buffer.concat(chunks);
474
+ formData.append(key, buffer, {
475
+ filename: file.reference.metadata.name,
476
+ });
477
+ }
478
+ const response = await new Promise((resolve, reject) => {
479
+ formData.submit(`${serverUrl}/upload`, (err, res) => {
480
+ if (err) {
481
+ reject(err);
482
+ return;
483
+ }
484
+ let body = '';
485
+ res.on('data', (chunk) => {
486
+ body += chunk.toString();
487
+ });
488
+ res.on('end', () => {
489
+ try {
490
+ const parsed = JSON.parse(body);
491
+ if (res.statusCode === 200) {
492
+ resolve(parsed);
493
+ }
494
+ else {
495
+ reject(new Error(`Server error: ${parsed.error}`));
496
+ }
497
+ }
498
+ catch (e) {
499
+ reject(new Error(`Parse error: ${body}`));
500
+ }
501
+ });
502
+ });
503
+ });
504
+ // Verify fields were sent correctly (flat dot-notation)
505
+ expect(response.fields.fileName).toBe('logo.png');
506
+ expect(response.fields.overrideIfPresent).toBe('true');
507
+ // Nested metadata flattened with dot notation
508
+ expect(response.fields['uploadMetadata.source']).toBe('admin-panel');
509
+ expect(response.fields['uploadMetadata.uploadedBy.userId']).toBe('123');
510
+ expect(response.fields['uploadMetadata.uploadedBy.userName']).toBe('admin');
511
+ expect(response.fields['uploadMetadata.timestamp']).toBe('2026-04-27T00:00:00Z');
512
+ // eslint-disable-next-line no-console
513
+ console.log('✅ Library Service-like nested structure flattened correctly!');
514
+ });
515
+ });
516
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MultipartHttpRequestBody.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultipartHttpRequestBody.unit.test.d.ts","sourceRoot":"","sources":["../../src/http/MultipartHttpRequestBody.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const stream_1 = require("stream");
27
+ const NodeFsFileStorage_1 = require("../file/NodeFsFileStorage");
28
+ const Http_1 = require("./Http");
29
+ /**
30
+ * Unit tests for MultipartHttpRequestBody.create() function.
31
+ * These tests verify the client-side flattening logic without HTTP roundtrips.
32
+ */
33
+ describe('MultipartHttpRequestBody.create', () => {
34
+ let clientStorage;
35
+ let tmpDir;
36
+ beforeAll(async () => {
37
+ tmpDir = `/tmp/multipart-unit-test-${Date.now()}`;
38
+ const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
39
+ await fs.mkdir(tmpDir, { recursive: true });
40
+ clientStorage = new NodeFsFileStorage_1.NodeFsFileStorage(tmpDir);
41
+ });
42
+ afterAll(async () => {
43
+ const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
44
+ try {
45
+ await fs.rm(tmpDir, { recursive: true, force: true });
46
+ }
47
+ catch (error) {
48
+ // Ignore cleanup errors
49
+ }
50
+ });
51
+ describe('Nested FileDescriptor Detection', () => {
52
+ it('should detect FileDescriptor at root level', async () => {
53
+ const fileDescriptor = {
54
+ _tag: 'FileDescriptor',
55
+ key: 'root-file',
56
+ metadata: { name: 'file.txt', size: 100, contentType: 'text/plain' },
57
+ };
58
+ await clientStorage.write({ reference: fileDescriptor, content: stream_1.Readable.from('content') }, undefined)();
59
+ const result = await Http_1.MultipartHttpRequestBody.create({ file: fileDescriptor }, clientStorage)();
60
+ expect(result._tag).toBe('Right');
61
+ if (result._tag === 'Right') {
62
+ expect(Object.keys(result.right.files)).toContain('root-file');
63
+ }
64
+ });
65
+ it('should detect FileDescriptor nested one level deep', async () => {
66
+ const fileDescriptor = {
67
+ _tag: 'FileDescriptor',
68
+ key: 'nested-file',
69
+ metadata: { name: 'file.txt', size: 100, contentType: 'text/plain' },
70
+ };
71
+ await clientStorage.write({ reference: fileDescriptor, content: stream_1.Readable.from('content') }, undefined)();
72
+ const result = await Http_1.MultipartHttpRequestBody.create({
73
+ data: {
74
+ file: fileDescriptor,
75
+ },
76
+ }, clientStorage)();
77
+ expect(result._tag).toBe('Right');
78
+ if (result._tag === 'Right') {
79
+ expect(Object.keys(result.right.files)).toContain('nested-file');
80
+ }
81
+ });
82
+ it('should detect FileDescriptor nested multiple levels deep', async () => {
83
+ const fileDescriptor = {
84
+ _tag: 'FileDescriptor',
85
+ key: 'deep-file',
86
+ metadata: { name: 'file.txt', size: 100, contentType: 'text/plain' },
87
+ };
88
+ await clientStorage.write({ reference: fileDescriptor, content: stream_1.Readable.from('content') }, undefined)();
89
+ const result = await Http_1.MultipartHttpRequestBody.create({
90
+ level1: {
91
+ level2: {
92
+ level3: {
93
+ file: fileDescriptor,
94
+ },
95
+ },
96
+ },
97
+ }, clientStorage)();
98
+ expect(result._tag).toBe('Right');
99
+ if (result._tag === 'Right') {
100
+ expect(Object.keys(result.right.files)).toContain('deep-file');
101
+ }
102
+ });
103
+ it('should detect multiple FileDescriptors at different levels', async () => {
104
+ const file1 = {
105
+ _tag: 'FileDescriptor',
106
+ key: 'file-1',
107
+ metadata: { name: 'file1.txt', size: 100, contentType: 'text/plain' },
108
+ };
109
+ const file2 = {
110
+ _tag: 'FileDescriptor',
111
+ key: 'file-2',
112
+ metadata: { name: 'file2.txt', size: 100, contentType: 'text/plain' },
113
+ };
114
+ await clientStorage.write({ reference: file1, content: stream_1.Readable.from('content1') }, undefined)();
115
+ await clientStorage.write({ reference: file2, content: stream_1.Readable.from('content2') }, undefined)();
116
+ const result = await Http_1.MultipartHttpRequestBody.create({
117
+ rootFile: file1,
118
+ nested: {
119
+ deepFile: file2,
120
+ },
121
+ }, clientStorage)();
122
+ expect(result._tag).toBe('Right');
123
+ if (result._tag === 'Right') {
124
+ expect(Object.keys(result.right.files)).toHaveLength(2);
125
+ expect(result.right.files['file-1']).toBeDefined();
126
+ expect(result.right.files['file-2']).toBeDefined();
127
+ }
128
+ });
129
+ });
130
+ describe('Field Flattening', () => {
131
+ it('should flatten nested fields with dot notation', async () => {
132
+ const result = await Http_1.MultipartHttpRequestBody.create({
133
+ title: 'Test',
134
+ metadata: {
135
+ author: 'John',
136
+ version: '1.0',
137
+ },
138
+ }, clientStorage)();
139
+ expect(result._tag).toBe('Right');
140
+ if (result._tag === 'Right') {
141
+ expect(result.right.fields.title).toBe('Test');
142
+ expect(result.right.fields['metadata.author']).toBe('John');
143
+ expect(result.right.fields['metadata.version']).toBe('1.0');
144
+ }
145
+ });
146
+ it('should flatten deeply nested fields', async () => {
147
+ const result = await Http_1.MultipartHttpRequestBody.create({
148
+ level1: {
149
+ level2: {
150
+ level3: {
151
+ value: 'deep',
152
+ },
153
+ },
154
+ },
155
+ }, clientStorage)();
156
+ expect(result._tag).toBe('Right');
157
+ if (result._tag === 'Right') {
158
+ expect(result.right.fields['level1.level2.level3.value']).toBe('deep');
159
+ }
160
+ });
161
+ it('should handle primitive values at root level', async () => {
162
+ const result = await Http_1.MultipartHttpRequestBody.create({
163
+ string: 'text',
164
+ number: 123,
165
+ boolean: true,
166
+ null: null,
167
+ }, clientStorage)();
168
+ expect(result._tag).toBe('Right');
169
+ if (result._tag === 'Right') {
170
+ expect(result.right.fields.string).toBe('text');
171
+ expect(result.right.fields.number).toBe('123');
172
+ expect(result.right.fields.boolean).toBe('true');
173
+ expect(result.right.fields.null).toBe('null');
174
+ }
175
+ });
176
+ });
177
+ describe('Array Support', () => {
178
+ it('should serialize arrays as comma-separated strings', async () => {
179
+ const result = await Http_1.MultipartHttpRequestBody.create({
180
+ title: 'Test',
181
+ tags: ['tag1', 'tag2', 'tag3'],
182
+ nested: {
183
+ items: ['a', 'b', 'c'],
184
+ },
185
+ }, clientStorage)();
186
+ expect(result._tag).toBe('Right');
187
+ if (result._tag === 'Right') {
188
+ expect(result.right.fields.title).toBe('Test');
189
+ expect(result.right.fields.tags).toBe('tag1,tag2,tag3');
190
+ expect(result.right.fields['nested.items']).toBe('a,b,c');
191
+ }
192
+ });
193
+ it('should handle empty arrays', async () => {
194
+ const result = await Http_1.MultipartHttpRequestBody.create({
195
+ title: 'Test',
196
+ tags: [],
197
+ }, clientStorage)();
198
+ expect(result._tag).toBe('Right');
199
+ if (result._tag === 'Right') {
200
+ expect(result.right.fields.title).toBe('Test');
201
+ expect(result.right.fields.tags).toBe(''); // Empty array becomes empty string
202
+ }
203
+ });
204
+ it('should handle arrays with mixed types', async () => {
205
+ const result = await Http_1.MultipartHttpRequestBody.create({
206
+ values: [1, 'text', true, null],
207
+ }, clientStorage)();
208
+ expect(result._tag).toBe('Right');
209
+ if (result._tag === 'Right') {
210
+ // All values converted to strings and joined
211
+ expect(result.right.fields.values).toBe('1,text,true,null');
212
+ }
213
+ });
214
+ });
215
+ describe('Metadata Preservation', () => {
216
+ it('should preserve contentType from original descriptor', async () => {
217
+ const fileDescriptor = {
218
+ _tag: 'FileDescriptor',
219
+ key: 'test-file',
220
+ metadata: {
221
+ name: 'image.png',
222
+ size: 1024,
223
+ contentType: 'image/png',
224
+ },
225
+ };
226
+ await clientStorage.write({ reference: fileDescriptor, content: stream_1.Readable.from('data') }, undefined)();
227
+ const result = await Http_1.MultipartHttpRequestBody.create({ file: fileDescriptor }, clientStorage)();
228
+ expect(result._tag).toBe('Right');
229
+ if (result._tag === 'Right') {
230
+ const file = result.right.files['test-file'];
231
+ expect(file.reference.metadata.contentType).toBe('image/png');
232
+ expect(file.reference.metadata.name).toBe('image.png');
233
+ }
234
+ });
235
+ });
236
+ describe('Mixed Structures', () => {
237
+ it('should handle body with both fields and FileDescriptors', async () => {
238
+ const fileDescriptor = {
239
+ _tag: 'FileDescriptor',
240
+ key: 'doc',
241
+ metadata: {
242
+ name: 'doc.pdf',
243
+ size: 2048,
244
+ contentType: 'application/pdf',
245
+ },
246
+ };
247
+ await clientStorage.write({ reference: fileDescriptor, content: stream_1.Readable.from('pdf data') }, undefined)();
248
+ const result = await Http_1.MultipartHttpRequestBody.create({
249
+ title: 'Document',
250
+ category: 'report',
251
+ metadata: {
252
+ author: 'Alice',
253
+ tags: ['important', 'confidential'],
254
+ },
255
+ document: fileDescriptor,
256
+ }, clientStorage)();
257
+ expect(result._tag).toBe('Right');
258
+ if (result._tag === 'Right') {
259
+ // Check fields
260
+ expect(result.right.fields.title).toBe('Document');
261
+ expect(result.right.fields.category).toBe('report');
262
+ expect(result.right.fields['metadata.author']).toBe('Alice');
263
+ expect(result.right.fields['metadata.tags']).toBe('important,confidential');
264
+ // Check file
265
+ expect(result.right.files.doc).toBeDefined();
266
+ expect(result.right.files.doc.reference.metadata.contentType).toBe('application/pdf');
267
+ }
268
+ });
269
+ });
270
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayio/commons",
3
- "version": "5.18.0",
3
+ "version": "5.20.0",
4
4
  "description": "Extensions to the standard/core libraries and basic features",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js"