@unito/integration-sdk 2.3.13 → 2.3.14

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.
@@ -1359,7 +1359,7 @@ class Provider {
1359
1359
  if (body.error) {
1360
1360
  reject(this.handleError(400, body.error.message, options));
1361
1361
  }
1362
- resolve({ status: 201, headers: response.headers, body: body });
1362
+ resolve({ status: 201, headers: response.headers, body });
1363
1363
  }
1364
1364
  catch (error) {
1365
1365
  reject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
@@ -1378,6 +1378,100 @@ class Provider {
1378
1378
  };
1379
1379
  return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
1380
1380
  }
1381
+ /**
1382
+ * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
1383
+ *
1384
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
1385
+ * @param stream The Readable stream containing the binary data to be sent.
1386
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
1387
+ * @returns The {@link Response} extracted from the provider.
1388
+ */
1389
+ async postStream(endpoint, stream, options) {
1390
+ const { url: providerUrl, headers: providerHeaders } = await this.prepareRequest(options);
1391
+ const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
1392
+ const headers = {
1393
+ 'Content-Type': 'application/octet-stream',
1394
+ Accept: 'application/json',
1395
+ ...providerHeaders,
1396
+ ...options.additionnalheaders,
1397
+ };
1398
+ const callToProvider = async () => {
1399
+ return new Promise((resolve, reject) => {
1400
+ let isSettled = false; // Prevent double rejection
1401
+ const cleanup = () => {
1402
+ if (!stream.destroyed) {
1403
+ stream.destroy();
1404
+ }
1405
+ };
1406
+ const safeReject = (error) => {
1407
+ if (!isSettled) {
1408
+ isSettled = true;
1409
+ cleanup();
1410
+ reject(error);
1411
+ }
1412
+ };
1413
+ const safeResolve = (response) => {
1414
+ if (!isSettled) {
1415
+ isSettled = true;
1416
+ resolve(response);
1417
+ }
1418
+ };
1419
+ try {
1420
+ const urlObj = new URL(absoluteUrl);
1421
+ const requestOptions = {
1422
+ hostname: urlObj.hostname,
1423
+ path: urlObj.pathname + urlObj.search,
1424
+ method: 'POST',
1425
+ headers,
1426
+ };
1427
+ const request = https.request(requestOptions, response => {
1428
+ response.setEncoding('utf8');
1429
+ let responseBody = '';
1430
+ response.on('data', chunk => {
1431
+ responseBody += chunk;
1432
+ });
1433
+ response.on('error', error => {
1434
+ safeReject(this.handleError(500, `Response stream error: "${error}"`, options));
1435
+ });
1436
+ response.on('end', () => {
1437
+ try {
1438
+ if (response.statusCode && response.statusCode >= 400) {
1439
+ safeReject(this.handleError(response.statusCode, responseBody, options));
1440
+ return;
1441
+ }
1442
+ const body = responseBody ? JSON.parse(responseBody) : undefined;
1443
+ safeResolve({
1444
+ status: response.statusCode || 200,
1445
+ headers: response.headers,
1446
+ body,
1447
+ });
1448
+ }
1449
+ catch (error) {
1450
+ safeReject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
1451
+ }
1452
+ });
1453
+ });
1454
+ request.on('timeout', () => {
1455
+ request.destroy();
1456
+ safeReject(this.handleError(408, 'Request timeout', options));
1457
+ });
1458
+ request.on('error', error => {
1459
+ safeReject(this.handleError(500, `Error while calling the provider: "${error}"`, options));
1460
+ });
1461
+ stream.on('error', error => {
1462
+ request.destroy();
1463
+ safeReject(this.handleError(500, `Stream error: "${error}"`, options));
1464
+ });
1465
+ // Stream the data directly without buffering
1466
+ stream.pipe(request);
1467
+ }
1468
+ catch (error) {
1469
+ safeReject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`, options));
1470
+ }
1471
+ });
1472
+ };
1473
+ return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
1474
+ }
1381
1475
  /**
1382
1476
  * Performs a PUT request to the provider.
1383
1477
  *
@@ -1,4 +1,6 @@
1
1
  import FormData from 'form-data';
2
+ import * as stream from 'stream';
3
+ import { IncomingHttpHeaders } from 'http';
2
4
  import * as HttpErrors from '../httpErrors.js';
3
5
  import { Credentials } from '../middlewares/credentials.js';
4
6
  import Logger from '../resources/logger.js';
@@ -54,7 +56,7 @@ export type RequestOptions = {
54
56
  export type Response<T> = {
55
57
  body: T;
56
58
  status: number;
57
- headers: Headers;
59
+ headers: Headers | IncomingHttpHeaders;
58
60
  };
59
61
  export type PreparedRequest = {
60
62
  url: string;
@@ -152,6 +154,15 @@ export declare class Provider {
152
154
  */
153
155
  post<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>>;
154
156
  postForm<T>(endpoint: string, form: FormData, options: RequestOptions): Promise<Response<T>>;
157
+ /**
158
+ * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
159
+ *
160
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
161
+ * @param stream The Readable stream containing the binary data to be sent.
162
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
163
+ * @returns The {@link Response} extracted from the provider.
164
+ */
165
+ postStream<T>(endpoint: string, stream: stream.Readable, options: RequestOptions): Promise<Response<T>>;
155
166
  /**
156
167
  * Performs a PUT request to the provider.
157
168
  *
@@ -140,7 +140,7 @@ export class Provider {
140
140
  if (body.error) {
141
141
  reject(this.handleError(400, body.error.message, options));
142
142
  }
143
- resolve({ status: 201, headers: response.headers, body: body });
143
+ resolve({ status: 201, headers: response.headers, body });
144
144
  }
145
145
  catch (error) {
146
146
  reject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
@@ -159,6 +159,100 @@ export class Provider {
159
159
  };
160
160
  return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
161
161
  }
162
+ /**
163
+ * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
164
+ *
165
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
166
+ * @param stream The Readable stream containing the binary data to be sent.
167
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
168
+ * @returns The {@link Response} extracted from the provider.
169
+ */
170
+ async postStream(endpoint, stream, options) {
171
+ const { url: providerUrl, headers: providerHeaders } = await this.prepareRequest(options);
172
+ const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
173
+ const headers = {
174
+ 'Content-Type': 'application/octet-stream',
175
+ Accept: 'application/json',
176
+ ...providerHeaders,
177
+ ...options.additionnalheaders,
178
+ };
179
+ const callToProvider = async () => {
180
+ return new Promise((resolve, reject) => {
181
+ let isSettled = false; // Prevent double rejection
182
+ const cleanup = () => {
183
+ if (!stream.destroyed) {
184
+ stream.destroy();
185
+ }
186
+ };
187
+ const safeReject = (error) => {
188
+ if (!isSettled) {
189
+ isSettled = true;
190
+ cleanup();
191
+ reject(error);
192
+ }
193
+ };
194
+ const safeResolve = (response) => {
195
+ if (!isSettled) {
196
+ isSettled = true;
197
+ resolve(response);
198
+ }
199
+ };
200
+ try {
201
+ const urlObj = new URL(absoluteUrl);
202
+ const requestOptions = {
203
+ hostname: urlObj.hostname,
204
+ path: urlObj.pathname + urlObj.search,
205
+ method: 'POST',
206
+ headers,
207
+ };
208
+ const request = https.request(requestOptions, response => {
209
+ response.setEncoding('utf8');
210
+ let responseBody = '';
211
+ response.on('data', chunk => {
212
+ responseBody += chunk;
213
+ });
214
+ response.on('error', error => {
215
+ safeReject(this.handleError(500, `Response stream error: "${error}"`, options));
216
+ });
217
+ response.on('end', () => {
218
+ try {
219
+ if (response.statusCode && response.statusCode >= 400) {
220
+ safeReject(this.handleError(response.statusCode, responseBody, options));
221
+ return;
222
+ }
223
+ const body = responseBody ? JSON.parse(responseBody) : undefined;
224
+ safeResolve({
225
+ status: response.statusCode || 200,
226
+ headers: response.headers,
227
+ body,
228
+ });
229
+ }
230
+ catch (error) {
231
+ safeReject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
232
+ }
233
+ });
234
+ });
235
+ request.on('timeout', () => {
236
+ request.destroy();
237
+ safeReject(this.handleError(408, 'Request timeout', options));
238
+ });
239
+ request.on('error', error => {
240
+ safeReject(this.handleError(500, `Error while calling the provider: "${error}"`, options));
241
+ });
242
+ stream.on('error', error => {
243
+ request.destroy();
244
+ safeReject(this.handleError(500, `Stream error: "${error}"`, options));
245
+ });
246
+ // Stream the data directly without buffering
247
+ stream.pipe(request);
248
+ }
249
+ catch (error) {
250
+ safeReject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`, options));
251
+ }
252
+ });
253
+ };
254
+ return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
255
+ }
162
256
  /**
163
257
  * Performs a PUT request to the provider.
164
258
  *
@@ -1,5 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
+ import { Readable } from 'stream';
4
+ import nock from 'nock';
3
5
  import { Provider } from '../../src/resources/provider.js';
4
6
  import * as HttpErrors from '../../src/httpErrors.js';
5
7
  import Logger from '../../src/resources/logger.js';
@@ -29,7 +31,7 @@ describe('Provider', () => {
29
31
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
30
32
  const actualResponse = await provider.get('/endpoint', {
31
33
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
32
- logger: logger,
34
+ logger,
33
35
  signal: new AbortController().signal,
34
36
  additionnalheaders: { 'X-Additional-Header': 'value1' },
35
37
  });
@@ -58,7 +60,7 @@ describe('Provider', () => {
58
60
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
59
61
  const actualResponse = await provider.get('/endpoint', {
60
62
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
61
- logger: logger,
63
+ logger,
62
64
  signal: new AbortController().signal,
63
65
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'text/html; charset=UTF-8' },
64
66
  });
@@ -87,7 +89,7 @@ describe('Provider', () => {
87
89
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
88
90
  const actualResponse = await provider.get('/endpoint', {
89
91
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
90
- logger: logger,
92
+ logger,
91
93
  signal: new AbortController().signal,
92
94
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/schema+json; charset=UTF-8' },
93
95
  });
@@ -116,7 +118,7 @@ describe('Provider', () => {
116
118
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
117
119
  const actualResponse = await provider.get('/endpoint', {
118
120
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
119
- logger: logger,
121
+ logger,
120
122
  signal: new AbortController().signal,
121
123
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/swagger+json; charset=UTF-8' },
122
124
  });
@@ -145,7 +147,7 @@ describe('Provider', () => {
145
147
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
146
148
  const actualResponse = await provider.get('/endpoint', {
147
149
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
148
- logger: logger,
150
+ logger,
149
151
  signal: new AbortController().signal,
150
152
  additionnalheaders: { 'X-Additional-Header': 'value1' },
151
153
  });
@@ -174,7 +176,7 @@ describe('Provider', () => {
174
176
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
175
177
  const providerResponse = await provider.streamingGet('/endpoint/123', {
176
178
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
177
- logger: logger,
179
+ logger,
178
180
  signal: new AbortController().signal,
179
181
  additionnalheaders: {
180
182
  Accept: 'application/json',
@@ -193,7 +195,7 @@ describe('Provider', () => {
193
195
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
194
196
  const actualResponse = await provider.get('https://my-cdn.my-domain.com/file.png', {
195
197
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
196
- logger: logger,
198
+ logger,
197
199
  signal: new AbortController().signal,
198
200
  });
199
201
  assert.equal(fetchMock.mock.calls.length, 1);
@@ -220,7 +222,7 @@ describe('Provider', () => {
220
222
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
221
223
  const actualResponse = await provider.get('', {
222
224
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
223
- logger: logger,
225
+ logger,
224
226
  signal: new AbortController().signal,
225
227
  });
226
228
  assert.equal(fetchMock.mock.calls.length, 1);
@@ -249,7 +251,7 @@ describe('Provider', () => {
249
251
  data: 'createdItemInfo',
250
252
  }, {
251
253
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
252
- logger: logger,
254
+ logger,
253
255
  signal: new AbortController().signal,
254
256
  additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' },
255
257
  });
@@ -282,7 +284,7 @@ describe('Provider', () => {
282
284
  { data: '3', data2: '4' },
283
285
  ], {
284
286
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
285
- logger: logger,
287
+ logger,
286
288
  signal: new AbortController().signal,
287
289
  additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
288
290
  });
@@ -315,7 +317,7 @@ describe('Provider', () => {
315
317
  data: 'updatedItemInfo',
316
318
  }, {
317
319
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
318
- logger: logger,
320
+ logger,
319
321
  signal: new AbortController().signal,
320
322
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
321
323
  });
@@ -347,7 +349,7 @@ describe('Provider', () => {
347
349
  // What matters is that the body of put is a buffer
348
350
  const actualResponse = await provider.putBuffer('endpoint/123', buffer, {
349
351
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
350
- logger: logger,
352
+ logger,
351
353
  signal: new AbortController().signal,
352
354
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/octet-stream' },
353
355
  });
@@ -379,7 +381,7 @@ describe('Provider', () => {
379
381
  data: 'updatedItemInfo',
380
382
  }, {
381
383
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
382
- logger: logger,
384
+ logger,
383
385
  signal: new AbortController().signal,
384
386
  queryParams: { param1: 'value1', param2: 'value2' },
385
387
  additionnalheaders: { 'X-Additional-Header': 'value1' },
@@ -410,7 +412,7 @@ describe('Provider', () => {
410
412
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
411
413
  const actualResponse = await provider.delete('/endpoint/123', {
412
414
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
413
- logger: logger,
415
+ logger,
414
416
  signal: new AbortController().signal,
415
417
  additionnalheaders: { 'X-Additional-Header': 'value1' },
416
418
  });
@@ -440,7 +442,7 @@ describe('Provider', () => {
440
442
  const requestBody = { webhookIds: [1, 2, 3] };
441
443
  const actualResponse = await provider.delete('/webhook', {
442
444
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
443
- logger: logger,
445
+ logger,
444
446
  signal: new AbortController().signal,
445
447
  additionnalheaders: { 'X-Additional-Header': 'value1' },
446
448
  }, requestBody);
@@ -483,7 +485,7 @@ describe('Provider', () => {
483
485
  const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
484
486
  const options = {
485
487
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
486
- logger: logger,
488
+ logger,
487
489
  signal: new AbortController().signal,
488
490
  additionnalheaders: { 'X-Additional-Header': 'value1' },
489
491
  };
@@ -529,7 +531,7 @@ describe('Provider', () => {
529
531
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
530
532
  const options = {
531
533
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
532
- logger: logger,
534
+ logger,
533
535
  signal: new AbortController().signal,
534
536
  additionnalheaders: { 'X-Additional-Header': 'value1' },
535
537
  };
@@ -570,7 +572,7 @@ describe('Provider', () => {
570
572
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
571
573
  const options = {
572
574
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
573
- logger: logger,
575
+ logger,
574
576
  signal: new AbortController().signal,
575
577
  additionnalheaders: { 'X-Additional-Header': 'value1' },
576
578
  };
@@ -606,7 +608,7 @@ describe('Provider', () => {
606
608
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
607
609
  const options = {
608
610
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
609
- logger: logger,
611
+ logger,
610
612
  signal: new AbortController().signal,
611
613
  additionnalheaders: { 'X-Additional-Header': 'value1' },
612
614
  };
@@ -628,7 +630,7 @@ describe('Provider', () => {
628
630
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
629
631
  const providerResponse = await provider.get('/endpoint/123', {
630
632
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
631
- logger: logger,
633
+ logger,
632
634
  signal: new AbortController().signal,
633
635
  });
634
636
  assert.ok(providerResponse);
@@ -642,7 +644,7 @@ describe('Provider', () => {
642
644
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
643
645
  const providerResponse = await provider.get('/endpoint/123', {
644
646
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
645
- logger: logger,
647
+ logger,
646
648
  signal: new AbortController().signal,
647
649
  });
648
650
  assert.ok(providerResponse);
@@ -656,7 +658,7 @@ describe('Provider', () => {
656
658
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
657
659
  const providerResponse = await provider.streamingGet('/endpoint/123', {
658
660
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
659
- logger: logger,
661
+ logger,
660
662
  signal: new AbortController().signal,
661
663
  });
662
664
  assert.ok(providerResponse);
@@ -670,7 +672,7 @@ describe('Provider', () => {
670
672
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
671
673
  const providerResponse = await provider.post('/endpoint/123', {}, {
672
674
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
673
- logger: logger,
675
+ logger,
674
676
  signal: new AbortController().signal,
675
677
  });
676
678
  assert.ok(providerResponse);
@@ -688,7 +690,7 @@ describe('Provider', () => {
688
690
  try {
689
691
  await provider.get('/endpoint/123', {
690
692
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
691
- logger: logger,
693
+ logger,
692
694
  signal: new AbortController().signal,
693
695
  });
694
696
  }
@@ -708,7 +710,7 @@ describe('Provider', () => {
708
710
  try {
709
711
  await provider.get('/endpoint/123', {
710
712
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
711
- logger: logger,
713
+ logger,
712
714
  signal: new AbortController().signal,
713
715
  });
714
716
  }
@@ -728,7 +730,7 @@ describe('Provider', () => {
728
730
  await provider.get('/endpoint/123', {
729
731
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
730
732
  signal: new AbortController().signal,
731
- logger: logger,
733
+ logger,
732
734
  });
733
735
  }
734
736
  catch (e) {
@@ -748,7 +750,7 @@ describe('Provider', () => {
748
750
  await provider.get('/endpoint/123', {
749
751
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
750
752
  signal: new AbortController().signal,
751
- logger: logger,
753
+ logger,
752
754
  });
753
755
  }
754
756
  catch (e) {
@@ -768,7 +770,7 @@ describe('Provider', () => {
768
770
  await provider.get('/endpoint/123', {
769
771
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
770
772
  signal: new AbortController().signal,
771
- logger: logger,
773
+ logger,
772
774
  });
773
775
  }
774
776
  catch (e) {
@@ -786,7 +788,7 @@ describe('Provider', () => {
786
788
  await provider.get('/endpoint/123', {
787
789
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
788
790
  signal: new AbortController().signal,
789
- logger: logger,
791
+ logger,
790
792
  });
791
793
  }
792
794
  catch (e) {
@@ -810,7 +812,7 @@ describe('Provider', () => {
810
812
  await provider.get('/endpoint/123', {
811
813
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
812
814
  signal: new AbortController().signal,
813
- logger: logger,
815
+ logger,
814
816
  });
815
817
  }
816
818
  catch (e) {
@@ -826,9 +828,117 @@ describe('Provider', () => {
826
828
  await provider.get('/endpoint/123', {
827
829
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
828
830
  signal: new AbortController().signal,
829
- logger: logger,
831
+ logger,
830
832
  });
831
833
  assert.equal(loggerStub.mock.callCount(), 1);
832
834
  assert.match(String(loggerStub.mock.calls[0]?.arguments[0]), /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/);
833
835
  });
836
+ // Stream upload will not load the data in memory and sending a binary in the body
837
+ it('postStream streams data without buffering', async () => {
838
+ const streamProvider = new Provider({
839
+ prepareRequest: requestOptions => ({
840
+ url: `https://www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
841
+ headers: {
842
+ 'X-Custom-Provider-Header': 'value',
843
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
844
+ },
845
+ }),
846
+ });
847
+ const testData = 'test binary data for streaming';
848
+ const stream = Readable.from([testData]);
849
+ const scope = nock('https://www.myApi.com')
850
+ .post('/upload', testData)
851
+ .matchHeader('content-type', 'application/octet-stream')
852
+ .matchHeader('accept', 'application/json')
853
+ .matchHeader('x-custom-provider-header', 'value')
854
+ .matchHeader('x-provider-credential-header', 'apikey#1111')
855
+ .reply(201, { success: true, id: '12345' });
856
+ const response = await streamProvider.postStream('/upload', stream, {
857
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
858
+ logger,
859
+ });
860
+ assert.ok(scope.isDone(), 'HTTPS request should have been made');
861
+ assert.equal(response.status, 201);
862
+ assert.deepEqual(response.body, { success: true, id: '12345' });
863
+ });
864
+ it('postStream with query params', async () => {
865
+ const streamProvider = new Provider({
866
+ prepareRequest: requestOptions => ({
867
+ url: `https://www.myApi.com`,
868
+ headers: {
869
+ 'X-Custom-Provider-Header': 'value',
870
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
871
+ },
872
+ }),
873
+ });
874
+ const testData = 'data with params';
875
+ const stream = Readable.from([testData]);
876
+ const scope = nock('https://www.myApi.com')
877
+ .post('/upload?key=value&format=json', testData)
878
+ .reply(200, { uploaded: true });
879
+ const response = await streamProvider.postStream('/upload', stream, {
880
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
881
+ logger,
882
+ queryParams: { key: 'value', format: 'json' },
883
+ });
884
+ assert.ok(scope.isDone());
885
+ assert.equal(response.status, 200);
886
+ assert.deepEqual(response.body, { uploaded: true });
887
+ });
888
+ it('postStream handles error responses', async () => {
889
+ const streamProvider = new Provider({
890
+ prepareRequest: requestOptions => ({
891
+ url: `https://www.myApi.com`,
892
+ headers: {
893
+ 'X-Custom-Provider-Header': 'value',
894
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
895
+ },
896
+ }),
897
+ });
898
+ const stream = Readable.from(['error test data']);
899
+ nock('https://www.myApi.com').post('/upload').reply(400, 'Bad request error');
900
+ let error;
901
+ try {
902
+ await streamProvider.postStream('/upload', stream, {
903
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
904
+ logger,
905
+ });
906
+ }
907
+ catch (e) {
908
+ error = e;
909
+ }
910
+ assert.ok(error instanceof HttpErrors.BadRequestError);
911
+ assert.equal(error.message, 'Bad request error');
912
+ });
913
+ it('postStream with rate limiter', async () => {
914
+ let rateLimiterCalled = false;
915
+ let rateLimiterCredentials;
916
+ const mockRateLimiter = async (options, request) => {
917
+ rateLimiterCalled = true;
918
+ rateLimiterCredentials = options.credentials;
919
+ return request();
920
+ };
921
+ const rateLimitedProvider = new Provider({
922
+ prepareRequest: requestOptions => ({
923
+ url: `https://www.myApi.com`,
924
+ headers: {
925
+ 'X-Custom-Provider-Header': 'value',
926
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
927
+ },
928
+ }),
929
+ rateLimiter: mockRateLimiter,
930
+ });
931
+ const testData = 'rate limited data';
932
+ const stream = Readable.from([testData]);
933
+ nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
934
+ await rateLimitedProvider.postStream('/upload', stream, {
935
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
936
+ logger,
937
+ });
938
+ assert.ok(rateLimiterCalled, 'Rate limiter should have been called');
939
+ assert.deepEqual(rateLimiterCredentials, {
940
+ apiKey: 'apikey#1111',
941
+ unitoCredentialId: '123',
942
+ });
943
+ });
834
944
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "2.3.13",
3
+ "version": "2.3.14",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -44,6 +44,7 @@
44
44
  "@typescript-eslint/eslint-plugin": "7.x",
45
45
  "@typescript-eslint/parser": "7.x",
46
46
  "eslint": "8.x",
47
+ "nock": "14.x",
47
48
  "nodemon": "3.x",
48
49
  "prettier": "3.x",
49
50
  "rollup": "4.x",
@@ -1,5 +1,7 @@
1
1
  import https from 'https';
2
2
  import FormData from 'form-data';
3
+ import * as stream from 'stream';
4
+ import { IncomingHttpHeaders } from 'http';
3
5
 
4
6
  import { buildHttpError } from '../errors.js';
5
7
  import * as HttpErrors from '../httpErrors.js';
@@ -56,7 +58,7 @@ export type RequestOptions = {
56
58
  export type Response<T> = {
57
59
  body: T;
58
60
  status: number;
59
- headers: Headers;
61
+ headers: Headers | IncomingHttpHeaders;
60
62
  };
61
63
 
62
64
  export type PreparedRequest = {
@@ -230,7 +232,7 @@ export class Provider {
230
232
  if (body.error) {
231
233
  reject(this.handleError(400, body.error.message, options));
232
234
  }
233
- resolve({ status: 201, headers: response.headers as unknown as Headers, body: body as T });
235
+ resolve({ status: 201, headers: response.headers, body });
234
236
  } catch (error) {
235
237
  reject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
236
238
  }
@@ -251,6 +253,114 @@ export class Provider {
251
253
  return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
252
254
  }
253
255
 
256
+ /**
257
+ * Performs a POST request to the provider streaming a Readable directly without loading it into memory.
258
+ *
259
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
260
+ * @param stream The Readable stream containing the binary data to be sent.
261
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
262
+ * @returns The {@link Response} extracted from the provider.
263
+ */
264
+ public async postStream<T>(endpoint: string, stream: stream.Readable, options: RequestOptions): Promise<Response<T>> {
265
+ const { url: providerUrl, headers: providerHeaders } = await this.prepareRequest(options);
266
+ const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
267
+ const headers = {
268
+ 'Content-Type': 'application/octet-stream',
269
+ Accept: 'application/json',
270
+ ...providerHeaders,
271
+ ...options.additionnalheaders,
272
+ };
273
+
274
+ const callToProvider = async (): Promise<Response<T>> => {
275
+ return new Promise((resolve, reject) => {
276
+ let isSettled = false; // Prevent double rejection
277
+
278
+ const cleanup = () => {
279
+ if (!stream.destroyed) {
280
+ stream.destroy();
281
+ }
282
+ };
283
+
284
+ const safeReject = (error: HttpErrors.HttpError) => {
285
+ if (!isSettled) {
286
+ isSettled = true;
287
+ cleanup();
288
+ reject(error);
289
+ }
290
+ };
291
+
292
+ const safeResolve = (response: Response<T>) => {
293
+ if (!isSettled) {
294
+ isSettled = true;
295
+ resolve(response);
296
+ }
297
+ };
298
+
299
+ try {
300
+ const urlObj = new URL(absoluteUrl);
301
+ const requestOptions: https.RequestOptions = {
302
+ hostname: urlObj.hostname,
303
+ path: urlObj.pathname + urlObj.search,
304
+ method: 'POST',
305
+ headers,
306
+ };
307
+
308
+ const request = https.request(requestOptions, response => {
309
+ response.setEncoding('utf8');
310
+ let responseBody = '';
311
+
312
+ response.on('data', chunk => {
313
+ responseBody += chunk;
314
+ });
315
+
316
+ response.on('error', error => {
317
+ safeReject(this.handleError(500, `Response stream error: "${error}"`, options));
318
+ });
319
+
320
+ response.on('end', () => {
321
+ try {
322
+ if (response.statusCode && response.statusCode >= 400) {
323
+ safeReject(this.handleError(response.statusCode, responseBody, options));
324
+ return;
325
+ }
326
+
327
+ const body = responseBody ? JSON.parse(responseBody) : undefined;
328
+ safeResolve({
329
+ status: response.statusCode || 200,
330
+ headers: response.headers,
331
+ body,
332
+ });
333
+ } catch (error) {
334
+ safeReject(this.handleError(500, `Failed to parse response body: "${error}"`, options));
335
+ }
336
+ });
337
+ });
338
+
339
+ request.on('timeout', () => {
340
+ request.destroy();
341
+ safeReject(this.handleError(408, 'Request timeout', options));
342
+ });
343
+
344
+ request.on('error', error => {
345
+ safeReject(this.handleError(500, `Error while calling the provider: "${error}"`, options));
346
+ });
347
+
348
+ stream.on('error', error => {
349
+ request.destroy();
350
+ safeReject(this.handleError(500, `Stream error: "${error}"`, options));
351
+ });
352
+
353
+ // Stream the data directly without buffering
354
+ stream.pipe(request);
355
+ } catch (error) {
356
+ safeReject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`, options));
357
+ }
358
+ });
359
+ };
360
+
361
+ return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
362
+ }
363
+
254
364
  /**
255
365
  * Performs a PUT request to the provider.
256
366
  *
@@ -409,7 +519,7 @@ export class Provider {
409
519
  response = await fetch(absoluteUrl, {
410
520
  method: options.method,
411
521
  headers,
412
- body: fetchBody,
522
+ body: fetchBody as BodyInit | null,
413
523
  ...(options.signal ? { signal: options.signal } : {}),
414
524
  });
415
525
  } catch (error) {
@@ -1,5 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
+ import { Readable } from 'stream';
4
+ import nock from 'nock';
3
5
 
4
6
  import { Provider } from '../../src/resources/provider.js';
5
7
  import * as HttpErrors from '../../src/httpErrors.js';
@@ -36,7 +38,7 @@ describe('Provider', () => {
36
38
 
37
39
  const actualResponse = await provider.get('/endpoint', {
38
40
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
39
- logger: logger,
41
+ logger,
40
42
  signal: new AbortController().signal,
41
43
  additionnalheaders: { 'X-Additional-Header': 'value1' },
42
44
  });
@@ -69,7 +71,7 @@ describe('Provider', () => {
69
71
 
70
72
  const actualResponse = await provider.get('/endpoint', {
71
73
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
72
- logger: logger,
74
+ logger,
73
75
  signal: new AbortController().signal,
74
76
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'text/html; charset=UTF-8' },
75
77
  });
@@ -104,7 +106,7 @@ describe('Provider', () => {
104
106
 
105
107
  const actualResponse = await provider.get('/endpoint', {
106
108
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
107
- logger: logger,
109
+ logger,
108
110
  signal: new AbortController().signal,
109
111
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/schema+json; charset=UTF-8' },
110
112
  });
@@ -139,7 +141,7 @@ describe('Provider', () => {
139
141
 
140
142
  const actualResponse = await provider.get('/endpoint', {
141
143
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
142
- logger: logger,
144
+ logger,
143
145
  signal: new AbortController().signal,
144
146
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/swagger+json; charset=UTF-8' },
145
147
  });
@@ -174,7 +176,7 @@ describe('Provider', () => {
174
176
 
175
177
  const actualResponse = await provider.get('/endpoint', {
176
178
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
177
- logger: logger,
179
+ logger,
178
180
  signal: new AbortController().signal,
179
181
  additionnalheaders: { 'X-Additional-Header': 'value1' },
180
182
  });
@@ -209,7 +211,7 @@ describe('Provider', () => {
209
211
 
210
212
  const providerResponse = await provider.streamingGet('/endpoint/123', {
211
213
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
212
- logger: logger,
214
+ logger,
213
215
  signal: new AbortController().signal,
214
216
  additionnalheaders: {
215
217
  Accept: 'application/json',
@@ -232,7 +234,7 @@ describe('Provider', () => {
232
234
 
233
235
  const actualResponse = await provider.get('https://my-cdn.my-domain.com/file.png', {
234
236
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
235
- logger: logger,
237
+ logger,
236
238
  signal: new AbortController().signal,
237
239
  });
238
240
 
@@ -263,7 +265,7 @@ describe('Provider', () => {
263
265
 
264
266
  const actualResponse = await provider.get('', {
265
267
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
266
- logger: logger,
268
+ logger,
267
269
  signal: new AbortController().signal,
268
270
  });
269
271
 
@@ -299,7 +301,7 @@ describe('Provider', () => {
299
301
  },
300
302
  {
301
303
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
302
- logger: logger,
304
+ logger,
303
305
  signal: new AbortController().signal,
304
306
  additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' },
305
307
  },
@@ -340,7 +342,7 @@ describe('Provider', () => {
340
342
  ],
341
343
  {
342
344
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
343
- logger: logger,
345
+ logger,
344
346
  signal: new AbortController().signal,
345
347
  additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
346
348
  },
@@ -381,7 +383,7 @@ describe('Provider', () => {
381
383
  },
382
384
  {
383
385
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
384
- logger: logger,
386
+ logger,
385
387
  signal: new AbortController().signal,
386
388
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
387
389
  },
@@ -419,7 +421,7 @@ describe('Provider', () => {
419
421
  // What matters is that the body of put is a buffer
420
422
  const actualResponse = await provider.putBuffer('endpoint/123', buffer, {
421
423
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
422
- logger: logger,
424
+ logger,
423
425
  signal: new AbortController().signal,
424
426
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/octet-stream' },
425
427
  });
@@ -458,7 +460,7 @@ describe('Provider', () => {
458
460
  },
459
461
  {
460
462
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
461
- logger: logger,
463
+ logger,
462
464
  signal: new AbortController().signal,
463
465
  queryParams: { param1: 'value1', param2: 'value2' },
464
466
  additionnalheaders: { 'X-Additional-Header': 'value1' },
@@ -494,7 +496,7 @@ describe('Provider', () => {
494
496
 
495
497
  const actualResponse = await provider.delete('/endpoint/123', {
496
498
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
497
- logger: logger,
499
+ logger,
498
500
  signal: new AbortController().signal,
499
501
  additionnalheaders: { 'X-Additional-Header': 'value1' },
500
502
  });
@@ -530,7 +532,7 @@ describe('Provider', () => {
530
532
  '/webhook',
531
533
  {
532
534
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
533
- logger: logger,
535
+ logger,
534
536
  signal: new AbortController().signal,
535
537
  additionnalheaders: { 'X-Additional-Header': 'value1' },
536
538
  },
@@ -581,7 +583,7 @@ describe('Provider', () => {
581
583
 
582
584
  const options = {
583
585
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
584
- logger: logger,
586
+ logger,
585
587
  signal: new AbortController().signal,
586
588
  additionnalheaders: { 'X-Additional-Header': 'value1' },
587
589
  };
@@ -634,7 +636,7 @@ describe('Provider', () => {
634
636
 
635
637
  const options = {
636
638
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
637
- logger: logger,
639
+ logger,
638
640
  signal: new AbortController().signal,
639
641
  additionnalheaders: { 'X-Additional-Header': 'value1' },
640
642
  };
@@ -681,7 +683,7 @@ describe('Provider', () => {
681
683
 
682
684
  const options = {
683
685
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
684
- logger: logger,
686
+ logger,
685
687
  signal: new AbortController().signal,
686
688
  additionnalheaders: { 'X-Additional-Header': 'value1' },
687
689
  };
@@ -722,7 +724,7 @@ describe('Provider', () => {
722
724
 
723
725
  const options = {
724
726
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
725
- logger: logger,
727
+ logger,
726
728
  signal: new AbortController().signal,
727
729
  additionnalheaders: { 'X-Additional-Header': 'value1' },
728
730
  };
@@ -748,7 +750,7 @@ describe('Provider', () => {
748
750
 
749
751
  const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
750
752
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
751
- logger: logger,
753
+ logger,
752
754
  signal: new AbortController().signal,
753
755
  });
754
756
 
@@ -766,7 +768,7 @@ describe('Provider', () => {
766
768
 
767
769
  const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
768
770
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
769
- logger: logger,
771
+ logger,
770
772
  signal: new AbortController().signal,
771
773
  });
772
774
 
@@ -784,7 +786,7 @@ describe('Provider', () => {
784
786
 
785
787
  const providerResponse = await provider.streamingGet('/endpoint/123', {
786
788
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
787
- logger: logger,
789
+ logger,
788
790
  signal: new AbortController().signal,
789
791
  });
790
792
 
@@ -805,7 +807,7 @@ describe('Provider', () => {
805
807
  {},
806
808
  {
807
809
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
808
- logger: logger,
810
+ logger,
809
811
  signal: new AbortController().signal,
810
812
  },
811
813
  );
@@ -829,7 +831,7 @@ describe('Provider', () => {
829
831
  try {
830
832
  await provider.get('/endpoint/123', {
831
833
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
832
- logger: logger,
834
+ logger,
833
835
  signal: new AbortController().signal,
834
836
  });
835
837
  } catch (e) {
@@ -853,7 +855,7 @@ describe('Provider', () => {
853
855
  try {
854
856
  await provider.get('/endpoint/123', {
855
857
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
856
- logger: logger,
858
+ logger,
857
859
  signal: new AbortController().signal,
858
860
  });
859
861
  } catch (e) {
@@ -877,7 +879,7 @@ describe('Provider', () => {
877
879
  await provider.get('/endpoint/123', {
878
880
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
879
881
  signal: new AbortController().signal,
880
- logger: logger,
882
+ logger,
881
883
  });
882
884
  } catch (e) {
883
885
  error = e;
@@ -900,7 +902,7 @@ describe('Provider', () => {
900
902
  await provider.get('/endpoint/123', {
901
903
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
902
904
  signal: new AbortController().signal,
903
- logger: logger,
905
+ logger,
904
906
  });
905
907
  } catch (e) {
906
908
  error = e;
@@ -923,7 +925,7 @@ describe('Provider', () => {
923
925
  await provider.get('/endpoint/123', {
924
926
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
925
927
  signal: new AbortController().signal,
926
- logger: logger,
928
+ logger,
927
929
  });
928
930
  } catch (e) {
929
931
  error = e;
@@ -944,7 +946,7 @@ describe('Provider', () => {
944
946
  await provider.get('/endpoint/123', {
945
947
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
946
948
  signal: new AbortController().signal,
947
- logger: logger,
949
+ logger,
948
950
  });
949
951
  } catch (e) {
950
952
  error = e;
@@ -972,7 +974,7 @@ describe('Provider', () => {
972
974
  await provider.get('/endpoint/123', {
973
975
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
974
976
  signal: new AbortController().signal,
975
- logger: logger,
977
+ logger,
976
978
  });
977
979
  } catch (e) {
978
980
  error = e;
@@ -992,7 +994,7 @@ describe('Provider', () => {
992
994
  await provider.get('/endpoint/123', {
993
995
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
994
996
  signal: new AbortController().signal,
995
- logger: logger,
997
+ logger,
996
998
  });
997
999
 
998
1000
  assert.equal(loggerStub.mock.callCount(), 1);
@@ -1001,4 +1003,133 @@ describe('Provider', () => {
1001
1003
  /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/,
1002
1004
  );
1003
1005
  });
1006
+
1007
+ // Stream upload will not load the data in memory and sending a binary in the body
1008
+ it('postStream streams data without buffering', async () => {
1009
+ const streamProvider = new Provider({
1010
+ prepareRequest: requestOptions => ({
1011
+ url: `https://www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
1012
+ headers: {
1013
+ 'X-Custom-Provider-Header': 'value',
1014
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1015
+ },
1016
+ }),
1017
+ });
1018
+
1019
+ const testData = 'test binary data for streaming';
1020
+ const stream = Readable.from([testData]);
1021
+
1022
+ const scope = nock('https://www.myApi.com')
1023
+ .post('/upload', testData)
1024
+ .matchHeader('content-type', 'application/octet-stream')
1025
+ .matchHeader('accept', 'application/json')
1026
+ .matchHeader('x-custom-provider-header', 'value')
1027
+ .matchHeader('x-provider-credential-header', 'apikey#1111')
1028
+ .reply(201, { success: true, id: '12345' });
1029
+
1030
+ const response = await streamProvider.postStream('/upload', stream, {
1031
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1032
+ logger,
1033
+ });
1034
+
1035
+ assert.ok(scope.isDone(), 'HTTPS request should have been made');
1036
+ assert.equal(response.status, 201);
1037
+ assert.deepEqual(response.body, { success: true, id: '12345' });
1038
+ });
1039
+
1040
+ it('postStream with query params', async () => {
1041
+ const streamProvider = new Provider({
1042
+ prepareRequest: requestOptions => ({
1043
+ url: `https://www.myApi.com`,
1044
+ headers: {
1045
+ 'X-Custom-Provider-Header': 'value',
1046
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1047
+ },
1048
+ }),
1049
+ });
1050
+
1051
+ const testData = 'data with params';
1052
+ const stream = Readable.from([testData]);
1053
+
1054
+ const scope = nock('https://www.myApi.com')
1055
+ .post('/upload?key=value&format=json', testData)
1056
+ .reply(200, { uploaded: true });
1057
+
1058
+ const response = await streamProvider.postStream('/upload', stream, {
1059
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1060
+ logger,
1061
+ queryParams: { key: 'value', format: 'json' },
1062
+ });
1063
+
1064
+ assert.ok(scope.isDone());
1065
+ assert.equal(response.status, 200);
1066
+ assert.deepEqual(response.body, { uploaded: true });
1067
+ });
1068
+
1069
+ it('postStream handles error responses', async () => {
1070
+ const streamProvider = new Provider({
1071
+ prepareRequest: requestOptions => ({
1072
+ url: `https://www.myApi.com`,
1073
+ headers: {
1074
+ 'X-Custom-Provider-Header': 'value',
1075
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1076
+ },
1077
+ }),
1078
+ });
1079
+
1080
+ const stream = Readable.from(['error test data']);
1081
+
1082
+ nock('https://www.myApi.com').post('/upload').reply(400, 'Bad request error');
1083
+
1084
+ let error;
1085
+ try {
1086
+ await streamProvider.postStream('/upload', stream, {
1087
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1088
+ logger,
1089
+ });
1090
+ } catch (e) {
1091
+ error = e;
1092
+ }
1093
+
1094
+ assert.ok(error instanceof HttpErrors.BadRequestError);
1095
+ assert.equal(error.message, 'Bad request error');
1096
+ });
1097
+
1098
+ it('postStream with rate limiter', async () => {
1099
+ let rateLimiterCalled = false;
1100
+ let rateLimiterCredentials;
1101
+
1102
+ const mockRateLimiter = async (options: any, request: () => Promise<any>) => {
1103
+ rateLimiterCalled = true;
1104
+ rateLimiterCredentials = options.credentials;
1105
+ return request();
1106
+ };
1107
+
1108
+ const rateLimitedProvider = new Provider({
1109
+ prepareRequest: requestOptions => ({
1110
+ url: `https://www.myApi.com`,
1111
+ headers: {
1112
+ 'X-Custom-Provider-Header': 'value',
1113
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1114
+ },
1115
+ }),
1116
+ rateLimiter: mockRateLimiter,
1117
+ });
1118
+
1119
+ const testData = 'rate limited data';
1120
+ const stream = Readable.from([testData]);
1121
+
1122
+ nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1123
+
1124
+ await rateLimitedProvider.postStream('/upload', stream, {
1125
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1126
+ logger,
1127
+ });
1128
+
1129
+ assert.ok(rateLimiterCalled, 'Rate limiter should have been called');
1130
+ assert.deepEqual(rateLimiterCredentials, {
1131
+ apiKey: 'apikey#1111',
1132
+ unitoCredentialId: '123',
1133
+ });
1134
+ });
1004
1135
  });