@unito/integration-sdk 1.0.10 → 1.0.12
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/handler.js +5 -5
- package/dist/src/index.cjs +43 -15
- package/dist/src/resources/provider.d.ts +30 -4
- package/dist/src/resources/provider.js +38 -10
- package/dist/test/resources/provider.test.js +72 -0
- package/package.json +1 -1
- package/src/handler.ts +5 -5
- package/src/resources/provider.ts +48 -11
- package/test/resources/provider.test.ts +83 -0
package/dist/src/handler.js
CHANGED
|
@@ -42,10 +42,10 @@ function assertWebhookParseRequestPayload(body) {
|
|
|
42
42
|
if (typeof body !== 'object' || body === null) {
|
|
43
43
|
throw new BadRequestError('Invalid WebhookParseRequestPayload');
|
|
44
44
|
}
|
|
45
|
-
if (!('payload' in body) || body.payload !== 'string') {
|
|
45
|
+
if (!('payload' in body) || typeof body.payload !== 'string') {
|
|
46
46
|
throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
|
|
47
47
|
}
|
|
48
|
-
if (!('url' in body) || body.url !== 'string') {
|
|
48
|
+
if (!('url' in body) || typeof body.url !== 'string') {
|
|
49
49
|
throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -53,13 +53,13 @@ function assertWebhookSubscriptionRequestPayload(body) {
|
|
|
53
53
|
if (typeof body !== 'object' || body === null) {
|
|
54
54
|
throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
|
|
55
55
|
}
|
|
56
|
-
if (!('itemPath' in body) || body.itemPath !== 'string') {
|
|
56
|
+
if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
|
|
57
57
|
throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
|
|
58
58
|
}
|
|
59
|
-
if (!('targetUrl' in body) || body.targetUrl !== 'string') {
|
|
59
|
+
if (!('targetUrl' in body) || typeof body.targetUrl !== 'string') {
|
|
60
60
|
throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
|
|
61
61
|
}
|
|
62
|
-
if (!('action' in body) || body.action !== 'string') {
|
|
62
|
+
if (!('action' in body) || typeof body.action !== 'string') {
|
|
63
63
|
throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
|
|
64
64
|
}
|
|
65
65
|
if (!['start', 'stop'].includes(body.action)) {
|
package/dist/src/index.cjs
CHANGED
|
@@ -565,10 +565,10 @@ function assertWebhookParseRequestPayload(body) {
|
|
|
565
565
|
if (typeof body !== 'object' || body === null) {
|
|
566
566
|
throw new BadRequestError('Invalid WebhookParseRequestPayload');
|
|
567
567
|
}
|
|
568
|
-
if (!('payload' in body) || body.payload !== 'string') {
|
|
568
|
+
if (!('payload' in body) || typeof body.payload !== 'string') {
|
|
569
569
|
throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
|
|
570
570
|
}
|
|
571
|
-
if (!('url' in body) || body.url !== 'string') {
|
|
571
|
+
if (!('url' in body) || typeof body.url !== 'string') {
|
|
572
572
|
throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
|
|
573
573
|
}
|
|
574
574
|
}
|
|
@@ -576,13 +576,13 @@ function assertWebhookSubscriptionRequestPayload(body) {
|
|
|
576
576
|
if (typeof body !== 'object' || body === null) {
|
|
577
577
|
throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
|
|
578
578
|
}
|
|
579
|
-
if (!('itemPath' in body) || body.itemPath !== 'string') {
|
|
579
|
+
if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
|
|
580
580
|
throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
|
|
581
581
|
}
|
|
582
|
-
if (!('targetUrl' in body) || body.targetUrl !== 'string') {
|
|
582
|
+
if (!('targetUrl' in body) || typeof body.targetUrl !== 'string') {
|
|
583
583
|
throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
|
|
584
584
|
}
|
|
585
|
-
if (!('action' in body) || body.action !== 'string') {
|
|
585
|
+
if (!('action' in body) || typeof body.action !== 'string') {
|
|
586
586
|
throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
|
|
587
587
|
}
|
|
588
588
|
if (!['start', 'stop'].includes(body.action)) {
|
|
@@ -953,22 +953,46 @@ class Integration {
|
|
|
953
953
|
* Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
|
|
954
954
|
*
|
|
955
955
|
* Needs to be initialized with a prepareRequest function to define the Provider's base URL and any specific headers to
|
|
956
|
-
* add to the requests,
|
|
956
|
+
* add to the requests, can also be configured to use a provided rate limiting function, and custom error handler.
|
|
957
|
+
*
|
|
958
|
+
* Multiple `Provider` instances can be created, with different configurations to call different providers APIs with
|
|
959
|
+
* different rateLimiting functions, as needed.
|
|
957
960
|
* @see {@link RateLimiter}
|
|
958
961
|
* @see {@link prepareRequest}
|
|
962
|
+
* @see {@link customErrorHandler}
|
|
959
963
|
*/
|
|
960
964
|
class Provider {
|
|
965
|
+
/**
|
|
966
|
+
* The Rate Limiter function to use to limit the rate of calls made to the provider based on the caller's credentials.
|
|
967
|
+
*/
|
|
961
968
|
rateLimiter = undefined;
|
|
969
|
+
/**
|
|
970
|
+
* Function called before each request to define the Provider's base URL and any specific headers to add to the requests.
|
|
971
|
+
*
|
|
972
|
+
* This is applied at large to all requests made to the provider. If you need to add specific headers to a single request,
|
|
973
|
+
* pass it through the RequestOptions object when calling the Provider's methods.
|
|
974
|
+
*/
|
|
962
975
|
prepareRequest;
|
|
976
|
+
/**
|
|
977
|
+
* (Optional) Custom error handler to handle specific errors returned by the provider.
|
|
978
|
+
*
|
|
979
|
+
* If provided, this method should only care about custom errors returned by the provider and return the corresponding
|
|
980
|
+
* HttpError from the SDK. If the error encountered is a standard error, it should return undefined and let the SDK handle it.
|
|
981
|
+
*
|
|
982
|
+
* @see buildHttpError for the list of standard errors the SDK can handle.
|
|
983
|
+
*/
|
|
984
|
+
customErrorHandler;
|
|
963
985
|
/**
|
|
964
986
|
* Initializes a Provider with the given options.
|
|
965
987
|
*
|
|
966
|
-
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
967
|
-
* @property
|
|
988
|
+
* @property {@link prepareRequest} - function to define the Provider's base URL and specific headers to add to the request.
|
|
989
|
+
* @property {@link RateLimiter} - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
990
|
+
* @property {@link customErrorHandler} - function to handle specific errors returned by the provider.
|
|
968
991
|
*/
|
|
969
992
|
constructor(options) {
|
|
970
993
|
this.prepareRequest = options.prepareRequest;
|
|
971
994
|
this.rateLimiter = options.rateLimiter;
|
|
995
|
+
this.customErrorHandler = options.customErrorHandler;
|
|
972
996
|
}
|
|
973
997
|
/**
|
|
974
998
|
* Performs a GET request to the provider.
|
|
@@ -1132,16 +1156,16 @@ class Provider {
|
|
|
1132
1156
|
if (error instanceof Error) {
|
|
1133
1157
|
switch (error.name) {
|
|
1134
1158
|
case 'AbortError':
|
|
1135
|
-
throw
|
|
1159
|
+
throw this.handleError(408, 'Request aborted');
|
|
1136
1160
|
case 'TimeoutError':
|
|
1137
|
-
throw
|
|
1161
|
+
throw this.handleError(408, 'Request timeout');
|
|
1138
1162
|
}
|
|
1139
1163
|
}
|
|
1140
|
-
throw
|
|
1164
|
+
throw this.handleError(500, `Unexpected error while calling the provider: "${error}"`);
|
|
1141
1165
|
}
|
|
1142
1166
|
if (response.status >= 400) {
|
|
1143
1167
|
const textResult = await response.text();
|
|
1144
|
-
throw
|
|
1168
|
+
throw this.handleError(response.status, textResult);
|
|
1145
1169
|
}
|
|
1146
1170
|
const responseContentType = response.headers.get('content-type');
|
|
1147
1171
|
let body;
|
|
@@ -1150,13 +1174,13 @@ class Provider {
|
|
|
1150
1174
|
// (Provider's response Content-Type might be more specific, e.g. application/json;charset=utf-8)
|
|
1151
1175
|
// Default to application/json if no Content-Type header is provided
|
|
1152
1176
|
if (responseContentType && !responseContentType.includes('application/json')) {
|
|
1153
|
-
throw
|
|
1177
|
+
throw this.handleError(500, `Unsupported content-type. Expected 'application/json', got '${responseContentType}'`);
|
|
1154
1178
|
}
|
|
1155
1179
|
try {
|
|
1156
1180
|
body = response.body ? await response.json() : undefined;
|
|
1157
1181
|
}
|
|
1158
1182
|
catch (err) {
|
|
1159
|
-
throw
|
|
1183
|
+
throw this.handleError(500, `Invalid JSON response`);
|
|
1160
1184
|
}
|
|
1161
1185
|
}
|
|
1162
1186
|
else if (headers.Accept == 'application/octet-stream') {
|
|
@@ -1164,12 +1188,16 @@ class Provider {
|
|
|
1164
1188
|
body = response.body;
|
|
1165
1189
|
}
|
|
1166
1190
|
else {
|
|
1167
|
-
throw
|
|
1191
|
+
throw this.handleError(500, 'Unsupported accept header');
|
|
1168
1192
|
}
|
|
1169
1193
|
return { status: response.status, headers: response.headers, body };
|
|
1170
1194
|
};
|
|
1171
1195
|
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
1172
1196
|
}
|
|
1197
|
+
handleError(responseStatus, message) {
|
|
1198
|
+
const customError = this.customErrorHandler?.(responseStatus, message);
|
|
1199
|
+
return customError ?? buildHttpError(responseStatus, message);
|
|
1200
|
+
}
|
|
1173
1201
|
}
|
|
1174
1202
|
|
|
1175
1203
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as HttpErrors from '../httpErrors.js';
|
|
1
2
|
import { Credentials } from '../middlewares/credentials.js';
|
|
2
3
|
import Logger from '../resources/logger.js';
|
|
3
4
|
/**
|
|
@@ -58,12 +59,25 @@ export type Response<T> = {
|
|
|
58
59
|
* Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
|
|
59
60
|
*
|
|
60
61
|
* Needs to be initialized with a prepareRequest function to define the Provider's base URL and any specific headers to
|
|
61
|
-
* add to the requests,
|
|
62
|
+
* add to the requests, can also be configured to use a provided rate limiting function, and custom error handler.
|
|
63
|
+
*
|
|
64
|
+
* Multiple `Provider` instances can be created, with different configurations to call different providers APIs with
|
|
65
|
+
* different rateLimiting functions, as needed.
|
|
62
66
|
* @see {@link RateLimiter}
|
|
63
67
|
* @see {@link prepareRequest}
|
|
68
|
+
* @see {@link customErrorHandler}
|
|
64
69
|
*/
|
|
65
70
|
export declare class Provider {
|
|
71
|
+
/**
|
|
72
|
+
* The Rate Limiter function to use to limit the rate of calls made to the provider based on the caller's credentials.
|
|
73
|
+
*/
|
|
66
74
|
protected rateLimiter: RateLimiter | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Function called before each request to define the Provider's base URL and any specific headers to add to the requests.
|
|
77
|
+
*
|
|
78
|
+
* This is applied at large to all requests made to the provider. If you need to add specific headers to a single request,
|
|
79
|
+
* pass it through the RequestOptions object when calling the Provider's methods.
|
|
80
|
+
*/
|
|
67
81
|
protected prepareRequest: (options: {
|
|
68
82
|
credentials: Credentials;
|
|
69
83
|
logger: Logger;
|
|
@@ -71,15 +85,26 @@ export declare class Provider {
|
|
|
71
85
|
url: string;
|
|
72
86
|
headers: Record<string, string>;
|
|
73
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* (Optional) Custom error handler to handle specific errors returned by the provider.
|
|
90
|
+
*
|
|
91
|
+
* If provided, this method should only care about custom errors returned by the provider and return the corresponding
|
|
92
|
+
* HttpError from the SDK. If the error encountered is a standard error, it should return undefined and let the SDK handle it.
|
|
93
|
+
*
|
|
94
|
+
* @see buildHttpError for the list of standard errors the SDK can handle.
|
|
95
|
+
*/
|
|
96
|
+
protected customErrorHandler: ((responseStatus: number, message: string) => HttpErrors.HttpError | undefined) | undefined;
|
|
74
97
|
/**
|
|
75
98
|
* Initializes a Provider with the given options.
|
|
76
99
|
*
|
|
77
|
-
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
78
|
-
* @property
|
|
100
|
+
* @property {@link prepareRequest} - function to define the Provider's base URL and specific headers to add to the request.
|
|
101
|
+
* @property {@link RateLimiter} - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
102
|
+
* @property {@link customErrorHandler} - function to handle specific errors returned by the provider.
|
|
79
103
|
*/
|
|
80
104
|
constructor(options: {
|
|
81
105
|
prepareRequest: typeof Provider.prototype.prepareRequest;
|
|
82
|
-
rateLimiter?: RateLimiter;
|
|
106
|
+
rateLimiter?: RateLimiter | undefined;
|
|
107
|
+
customErrorHandler?: typeof Provider.prototype.customErrorHandler;
|
|
83
108
|
});
|
|
84
109
|
/**
|
|
85
110
|
* Performs a GET request to the provider.
|
|
@@ -157,4 +182,5 @@ export declare class Provider {
|
|
|
157
182
|
*/
|
|
158
183
|
delete<T = undefined>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
|
|
159
184
|
private fetchWrapper;
|
|
185
|
+
private handleError;
|
|
160
186
|
}
|
|
@@ -5,22 +5,46 @@ import { buildHttpError } from '../errors.js';
|
|
|
5
5
|
* Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
|
|
6
6
|
*
|
|
7
7
|
* Needs to be initialized with a prepareRequest function to define the Provider's base URL and any specific headers to
|
|
8
|
-
* add to the requests,
|
|
8
|
+
* add to the requests, can also be configured to use a provided rate limiting function, and custom error handler.
|
|
9
|
+
*
|
|
10
|
+
* Multiple `Provider` instances can be created, with different configurations to call different providers APIs with
|
|
11
|
+
* different rateLimiting functions, as needed.
|
|
9
12
|
* @see {@link RateLimiter}
|
|
10
13
|
* @see {@link prepareRequest}
|
|
14
|
+
* @see {@link customErrorHandler}
|
|
11
15
|
*/
|
|
12
16
|
export class Provider {
|
|
17
|
+
/**
|
|
18
|
+
* The Rate Limiter function to use to limit the rate of calls made to the provider based on the caller's credentials.
|
|
19
|
+
*/
|
|
13
20
|
rateLimiter = undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Function called before each request to define the Provider's base URL and any specific headers to add to the requests.
|
|
23
|
+
*
|
|
24
|
+
* This is applied at large to all requests made to the provider. If you need to add specific headers to a single request,
|
|
25
|
+
* pass it through the RequestOptions object when calling the Provider's methods.
|
|
26
|
+
*/
|
|
14
27
|
prepareRequest;
|
|
28
|
+
/**
|
|
29
|
+
* (Optional) Custom error handler to handle specific errors returned by the provider.
|
|
30
|
+
*
|
|
31
|
+
* If provided, this method should only care about custom errors returned by the provider and return the corresponding
|
|
32
|
+
* HttpError from the SDK. If the error encountered is a standard error, it should return undefined and let the SDK handle it.
|
|
33
|
+
*
|
|
34
|
+
* @see buildHttpError for the list of standard errors the SDK can handle.
|
|
35
|
+
*/
|
|
36
|
+
customErrorHandler;
|
|
15
37
|
/**
|
|
16
38
|
* Initializes a Provider with the given options.
|
|
17
39
|
*
|
|
18
|
-
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
19
|
-
* @property
|
|
40
|
+
* @property {@link prepareRequest} - function to define the Provider's base URL and specific headers to add to the request.
|
|
41
|
+
* @property {@link RateLimiter} - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
42
|
+
* @property {@link customErrorHandler} - function to handle specific errors returned by the provider.
|
|
20
43
|
*/
|
|
21
44
|
constructor(options) {
|
|
22
45
|
this.prepareRequest = options.prepareRequest;
|
|
23
46
|
this.rateLimiter = options.rateLimiter;
|
|
47
|
+
this.customErrorHandler = options.customErrorHandler;
|
|
24
48
|
}
|
|
25
49
|
/**
|
|
26
50
|
* Performs a GET request to the provider.
|
|
@@ -184,16 +208,16 @@ export class Provider {
|
|
|
184
208
|
if (error instanceof Error) {
|
|
185
209
|
switch (error.name) {
|
|
186
210
|
case 'AbortError':
|
|
187
|
-
throw
|
|
211
|
+
throw this.handleError(408, 'Request aborted');
|
|
188
212
|
case 'TimeoutError':
|
|
189
|
-
throw
|
|
213
|
+
throw this.handleError(408, 'Request timeout');
|
|
190
214
|
}
|
|
191
215
|
}
|
|
192
|
-
throw
|
|
216
|
+
throw this.handleError(500, `Unexpected error while calling the provider: "${error}"`);
|
|
193
217
|
}
|
|
194
218
|
if (response.status >= 400) {
|
|
195
219
|
const textResult = await response.text();
|
|
196
|
-
throw
|
|
220
|
+
throw this.handleError(response.status, textResult);
|
|
197
221
|
}
|
|
198
222
|
const responseContentType = response.headers.get('content-type');
|
|
199
223
|
let body;
|
|
@@ -202,13 +226,13 @@ export class Provider {
|
|
|
202
226
|
// (Provider's response Content-Type might be more specific, e.g. application/json;charset=utf-8)
|
|
203
227
|
// Default to application/json if no Content-Type header is provided
|
|
204
228
|
if (responseContentType && !responseContentType.includes('application/json')) {
|
|
205
|
-
throw
|
|
229
|
+
throw this.handleError(500, `Unsupported content-type. Expected 'application/json', got '${responseContentType}'`);
|
|
206
230
|
}
|
|
207
231
|
try {
|
|
208
232
|
body = response.body ? await response.json() : undefined;
|
|
209
233
|
}
|
|
210
234
|
catch (err) {
|
|
211
|
-
throw
|
|
235
|
+
throw this.handleError(500, `Invalid JSON response`);
|
|
212
236
|
}
|
|
213
237
|
}
|
|
214
238
|
else if (headers.Accept == 'application/octet-stream') {
|
|
@@ -216,10 +240,14 @@ export class Provider {
|
|
|
216
240
|
body = response.body;
|
|
217
241
|
}
|
|
218
242
|
else {
|
|
219
|
-
throw
|
|
243
|
+
throw this.handleError(500, 'Unsupported accept header');
|
|
220
244
|
}
|
|
221
245
|
return { status: response.status, headers: response.headers, body };
|
|
222
246
|
};
|
|
223
247
|
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
224
248
|
}
|
|
249
|
+
handleError(responseStatus, message) {
|
|
250
|
+
const customError = this.customErrorHandler?.(responseStatus, message);
|
|
251
|
+
return customError ?? buildHttpError(responseStatus, message);
|
|
252
|
+
}
|
|
225
253
|
}
|
|
@@ -249,6 +249,78 @@ describe('Provider', () => {
|
|
|
249
249
|
]);
|
|
250
250
|
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
251
251
|
});
|
|
252
|
+
it('uses custom error handler if provided', async (context) => {
|
|
253
|
+
const rateLimitedProvider = new Provider({
|
|
254
|
+
prepareRequest: requestOptions => {
|
|
255
|
+
return {
|
|
256
|
+
url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
|
|
257
|
+
headers: {
|
|
258
|
+
'X-Custom-Provider-Header': 'value',
|
|
259
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
rateLimiter: undefined,
|
|
264
|
+
// Change from normal behavior 400 -> 429
|
|
265
|
+
customErrorHandler: (responseStatus) => responseStatus === 400 ? new HttpErrors.RateLimitExceededError('Weird provider behavior') : undefined,
|
|
266
|
+
});
|
|
267
|
+
const response = new Response(undefined, {
|
|
268
|
+
status: 400,
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
});
|
|
271
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
272
|
+
const options = {
|
|
273
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
274
|
+
logger: logger,
|
|
275
|
+
signal: new AbortController().signal,
|
|
276
|
+
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
277
|
+
};
|
|
278
|
+
let error;
|
|
279
|
+
try {
|
|
280
|
+
await rateLimitedProvider.delete('/endpoint/123', options);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
error = e;
|
|
284
|
+
}
|
|
285
|
+
assert.ok(error instanceof HttpErrors.HttpError);
|
|
286
|
+
assert.equal(error.message, 'Weird provider behavior');
|
|
287
|
+
});
|
|
288
|
+
it('uses default behavior if custom error handler returns undefined', async (context) => {
|
|
289
|
+
const rateLimitedProvider = new Provider({
|
|
290
|
+
prepareRequest: requestOptions => {
|
|
291
|
+
return {
|
|
292
|
+
url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
|
|
293
|
+
headers: {
|
|
294
|
+
'X-Custom-Provider-Header': 'value',
|
|
295
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
rateLimiter: undefined,
|
|
300
|
+
// Custom Error Handler returning undefined (default behavior should apply)
|
|
301
|
+
customErrorHandler: () => undefined,
|
|
302
|
+
});
|
|
303
|
+
const response = new Response(undefined, {
|
|
304
|
+
status: 404,
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
});
|
|
307
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
308
|
+
const options = {
|
|
309
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
310
|
+
logger: logger,
|
|
311
|
+
signal: new AbortController().signal,
|
|
312
|
+
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
313
|
+
};
|
|
314
|
+
let error;
|
|
315
|
+
try {
|
|
316
|
+
await rateLimitedProvider.delete('/endpoint/123', options);
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
error = e;
|
|
320
|
+
}
|
|
321
|
+
assert.ok(error instanceof HttpErrors.HttpError);
|
|
322
|
+
assert.equal(error.message, 'Not found');
|
|
323
|
+
});
|
|
252
324
|
it('returns valid json response', async (context) => {
|
|
253
325
|
const response = new Response(`{ "validJson": true }`, {
|
|
254
326
|
status: 200,
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -217,11 +217,11 @@ function assertWebhookParseRequestPayload(body: unknown): asserts body is API.We
|
|
|
217
217
|
throw new BadRequestError('Invalid WebhookParseRequestPayload');
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
if (!('payload' in body) || body.payload !== 'string') {
|
|
220
|
+
if (!('payload' in body) || typeof body.payload !== 'string') {
|
|
221
221
|
throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
if (!('url' in body) || body.url !== 'string') {
|
|
224
|
+
if (!('url' in body) || typeof body.url !== 'string') {
|
|
225
225
|
throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
|
|
226
226
|
}
|
|
227
227
|
}
|
|
@@ -231,15 +231,15 @@ function assertWebhookSubscriptionRequestPayload(body: unknown): asserts body is
|
|
|
231
231
|
throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
if (!('itemPath' in body) || body.itemPath !== 'string') {
|
|
234
|
+
if (!('itemPath' in body) || typeof body.itemPath !== 'string') {
|
|
235
235
|
throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
if (!('targetUrl' in body) || body.targetUrl !== 'string') {
|
|
238
|
+
if (!('targetUrl' in body) || typeof body.targetUrl !== 'string') {
|
|
239
239
|
throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
if (!('action' in body) || body.action !== 'string') {
|
|
242
|
+
if (!('action' in body) || typeof body.action !== 'string') {
|
|
243
243
|
throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildHttpError } from '../errors.js';
|
|
2
|
+
import * as HttpErrors from '../httpErrors.js';
|
|
2
3
|
import { Credentials } from '../middlewares/credentials.js';
|
|
3
4
|
import Logger from '../resources/logger.js';
|
|
4
5
|
|
|
@@ -59,26 +60,56 @@ export type Response<T> = {
|
|
|
59
60
|
* Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
|
|
60
61
|
*
|
|
61
62
|
* Needs to be initialized with a prepareRequest function to define the Provider's base URL and any specific headers to
|
|
62
|
-
* add to the requests,
|
|
63
|
+
* add to the requests, can also be configured to use a provided rate limiting function, and custom error handler.
|
|
64
|
+
*
|
|
65
|
+
* Multiple `Provider` instances can be created, with different configurations to call different providers APIs with
|
|
66
|
+
* different rateLimiting functions, as needed.
|
|
63
67
|
* @see {@link RateLimiter}
|
|
64
68
|
* @see {@link prepareRequest}
|
|
69
|
+
* @see {@link customErrorHandler}
|
|
65
70
|
*/
|
|
66
71
|
export class Provider {
|
|
72
|
+
/**
|
|
73
|
+
* The Rate Limiter function to use to limit the rate of calls made to the provider based on the caller's credentials.
|
|
74
|
+
*/
|
|
67
75
|
protected rateLimiter: RateLimiter | undefined = undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Function called before each request to define the Provider's base URL and any specific headers to add to the requests.
|
|
78
|
+
*
|
|
79
|
+
* This is applied at large to all requests made to the provider. If you need to add specific headers to a single request,
|
|
80
|
+
* pass it through the RequestOptions object when calling the Provider's methods.
|
|
81
|
+
*/
|
|
68
82
|
protected prepareRequest: (options: { credentials: Credentials; logger: Logger }) => {
|
|
69
83
|
url: string;
|
|
70
84
|
headers: Record<string, string>;
|
|
71
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* (Optional) Custom error handler to handle specific errors returned by the provider.
|
|
88
|
+
*
|
|
89
|
+
* If provided, this method should only care about custom errors returned by the provider and return the corresponding
|
|
90
|
+
* HttpError from the SDK. If the error encountered is a standard error, it should return undefined and let the SDK handle it.
|
|
91
|
+
*
|
|
92
|
+
* @see buildHttpError for the list of standard errors the SDK can handle.
|
|
93
|
+
*/
|
|
94
|
+
protected customErrorHandler:
|
|
95
|
+
| ((responseStatus: number, message: string) => HttpErrors.HttpError | undefined)
|
|
96
|
+
| undefined;
|
|
72
97
|
|
|
73
98
|
/**
|
|
74
99
|
* Initializes a Provider with the given options.
|
|
75
100
|
*
|
|
76
|
-
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
77
|
-
* @property
|
|
101
|
+
* @property {@link prepareRequest} - function to define the Provider's base URL and specific headers to add to the request.
|
|
102
|
+
* @property {@link RateLimiter} - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
103
|
+
* @property {@link customErrorHandler} - function to handle specific errors returned by the provider.
|
|
78
104
|
*/
|
|
79
|
-
constructor(options: {
|
|
105
|
+
constructor(options: {
|
|
106
|
+
prepareRequest: typeof Provider.prototype.prepareRequest;
|
|
107
|
+
rateLimiter?: RateLimiter | undefined;
|
|
108
|
+
customErrorHandler?: typeof Provider.prototype.customErrorHandler;
|
|
109
|
+
}) {
|
|
80
110
|
this.prepareRequest = options.prepareRequest;
|
|
81
111
|
this.rateLimiter = options.rateLimiter;
|
|
112
|
+
this.customErrorHandler = options.customErrorHandler;
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
/**
|
|
@@ -262,18 +293,18 @@ export class Provider {
|
|
|
262
293
|
if (error instanceof Error) {
|
|
263
294
|
switch (error.name) {
|
|
264
295
|
case 'AbortError':
|
|
265
|
-
throw
|
|
296
|
+
throw this.handleError(408, 'Request aborted');
|
|
266
297
|
case 'TimeoutError':
|
|
267
|
-
throw
|
|
298
|
+
throw this.handleError(408, 'Request timeout');
|
|
268
299
|
}
|
|
269
300
|
}
|
|
270
301
|
|
|
271
|
-
throw
|
|
302
|
+
throw this.handleError(500, `Unexpected error while calling the provider: "${error}"`);
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
if (response.status >= 400) {
|
|
275
306
|
const textResult = await response.text();
|
|
276
|
-
throw
|
|
307
|
+
throw this.handleError(response.status, textResult);
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
const responseContentType = response.headers.get('content-type');
|
|
@@ -284,7 +315,7 @@ export class Provider {
|
|
|
284
315
|
// (Provider's response Content-Type might be more specific, e.g. application/json;charset=utf-8)
|
|
285
316
|
// Default to application/json if no Content-Type header is provided
|
|
286
317
|
if (responseContentType && !responseContentType.includes('application/json')) {
|
|
287
|
-
throw
|
|
318
|
+
throw this.handleError(
|
|
288
319
|
500,
|
|
289
320
|
`Unsupported content-type. Expected 'application/json', got '${responseContentType}'`,
|
|
290
321
|
);
|
|
@@ -293,13 +324,13 @@ export class Provider {
|
|
|
293
324
|
try {
|
|
294
325
|
body = response.body ? await response.json() : undefined;
|
|
295
326
|
} catch (err) {
|
|
296
|
-
throw
|
|
327
|
+
throw this.handleError(500, `Invalid JSON response`);
|
|
297
328
|
}
|
|
298
329
|
} else if (headers.Accept == 'application/octet-stream') {
|
|
299
330
|
// When we expect octet-stream, we accept any Content-Type the provider sends us, we just want to stream it.
|
|
300
331
|
body = response.body as T;
|
|
301
332
|
} else {
|
|
302
|
-
throw
|
|
333
|
+
throw this.handleError(500, 'Unsupported accept header');
|
|
303
334
|
}
|
|
304
335
|
|
|
305
336
|
return { status: response.status, headers: response.headers, body };
|
|
@@ -307,4 +338,10 @@ export class Provider {
|
|
|
307
338
|
|
|
308
339
|
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
309
340
|
}
|
|
341
|
+
|
|
342
|
+
private handleError(responseStatus: number, message: string): HttpErrors.HttpError {
|
|
343
|
+
const customError = this.customErrorHandler?.(responseStatus, message);
|
|
344
|
+
|
|
345
|
+
return customError ?? buildHttpError(responseStatus, message);
|
|
346
|
+
}
|
|
310
347
|
}
|
|
@@ -297,6 +297,89 @@ describe('Provider', () => {
|
|
|
297
297
|
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
it('uses custom error handler if provided', async context => {
|
|
301
|
+
const rateLimitedProvider = new Provider({
|
|
302
|
+
prepareRequest: requestOptions => {
|
|
303
|
+
return {
|
|
304
|
+
url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
|
|
305
|
+
headers: {
|
|
306
|
+
'X-Custom-Provider-Header': 'value',
|
|
307
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
rateLimiter: undefined,
|
|
312
|
+
// Change from normal behavior 400 -> 429
|
|
313
|
+
customErrorHandler: (responseStatus: number) =>
|
|
314
|
+
responseStatus === 400 ? new HttpErrors.RateLimitExceededError('Weird provider behavior') : undefined,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const response = new Response(undefined, {
|
|
318
|
+
status: 400,
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
323
|
+
|
|
324
|
+
const options = {
|
|
325
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
326
|
+
logger: logger,
|
|
327
|
+
signal: new AbortController().signal,
|
|
328
|
+
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let error;
|
|
332
|
+
try {
|
|
333
|
+
await rateLimitedProvider.delete('/endpoint/123', options);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
error = e;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
assert.ok(error instanceof HttpErrors.HttpError);
|
|
339
|
+
assert.equal(error.message, 'Weird provider behavior');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('uses default behavior if custom error handler returns undefined', async context => {
|
|
343
|
+
const rateLimitedProvider = new Provider({
|
|
344
|
+
prepareRequest: requestOptions => {
|
|
345
|
+
return {
|
|
346
|
+
url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
|
|
347
|
+
headers: {
|
|
348
|
+
'X-Custom-Provider-Header': 'value',
|
|
349
|
+
'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
rateLimiter: undefined,
|
|
354
|
+
// Custom Error Handler returning undefined (default behavior should apply)
|
|
355
|
+
customErrorHandler: () => undefined,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const response = new Response(undefined, {
|
|
359
|
+
status: 404,
|
|
360
|
+
headers: { 'Content-Type': 'application/json' },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
364
|
+
|
|
365
|
+
const options = {
|
|
366
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
367
|
+
logger: logger,
|
|
368
|
+
signal: new AbortController().signal,
|
|
369
|
+
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
let error;
|
|
373
|
+
try {
|
|
374
|
+
await rateLimitedProvider.delete('/endpoint/123', options);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
error = e;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
assert.ok(error instanceof HttpErrors.HttpError);
|
|
380
|
+
assert.equal(error.message, 'Not found');
|
|
381
|
+
});
|
|
382
|
+
|
|
300
383
|
it('returns valid json response', async context => {
|
|
301
384
|
const response = new Response(`{ "validJson": true }`, {
|
|
302
385
|
status: 200,
|