@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.
package/dist/src/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|