@unito/integration-sdk 0.1.11 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/src/handler.d.ts +39 -0
  2. package/dist/src/handler.js +9 -0
  3. package/dist/src/httpErrors.d.ts +29 -0
  4. package/dist/src/httpErrors.js +30 -0
  5. package/dist/src/index.cjs +274 -16
  6. package/dist/src/index.d.ts +1 -0
  7. package/dist/src/integration.d.ts +49 -0
  8. package/dist/src/integration.js +51 -0
  9. package/dist/src/middlewares/filters.d.ts +11 -2
  10. package/dist/src/middlewares/secrets.d.ts +5 -0
  11. package/dist/src/middlewares/signal.d.ts +15 -0
  12. package/dist/src/middlewares/signal.js +22 -0
  13. package/dist/src/resources/cache.d.ts +51 -1
  14. package/dist/src/resources/cache.js +51 -1
  15. package/dist/src/resources/context.d.ts +42 -13
  16. package/dist/src/resources/logger.d.ts +17 -0
  17. package/dist/src/resources/logger.js +17 -0
  18. package/dist/src/resources/provider.d.ts +90 -5
  19. package/dist/src/resources/provider.js +92 -11
  20. package/dist/test/middlewares/signal.test.d.ts +1 -0
  21. package/dist/test/middlewares/signal.test.js +20 -0
  22. package/dist/test/resources/provider.test.js +116 -21
  23. package/package.json +4 -4
  24. package/src/handler.ts +48 -0
  25. package/src/httpErrors.ts +30 -0
  26. package/src/index.ts +1 -0
  27. package/src/integration.ts +51 -0
  28. package/src/middlewares/filters.ts +11 -2
  29. package/src/middlewares/secrets.ts +5 -0
  30. package/src/middlewares/signal.ts +41 -0
  31. package/src/resources/cache.ts +51 -1
  32. package/src/resources/context.ts +50 -33
  33. package/src/resources/logger.ts +17 -0
  34. package/src/resources/provider.ts +115 -16
  35. package/test/middlewares/signal.test.ts +28 -0
  36. package/test/resources/provider.test.ts +122 -21
@@ -23,19 +23,46 @@ export type RateLimiter = <T>(
23
23
  targetFunction: () => Promise<Response<T>>,
24
24
  ) => Promise<Response<T>>;
25
25
 
26
- export interface RequestOptions {
26
+ /**
27
+ * RequestOptions are the options passed to the Provider's call.
28
+ *
29
+ * @property credentials - The credentials to use for the call.
30
+ * @property logger - The logger to use during the call.
31
+ * @property queryParams - The query parameters to add when calling the provider.
32
+ * @property additionnalheaders - The headers to add when calling the provider.
33
+ */
34
+ export type RequestOptions = {
27
35
  credentials: Credentials;
28
36
  logger: Logger;
37
+ signal: AbortSignal;
29
38
  queryParams?: { [key: string]: string };
30
39
  additionnalheaders?: { [key: string]: string };
31
- }
40
+ };
32
41
 
33
- export interface Response<T> {
42
+ /**
43
+ * Response object returned by the Provider's method.
44
+ *
45
+ * Contains;
46
+ * - the body typed as specified when calling the method
47
+ * - the status code of the response
48
+ * - the headers of the response.
49
+ */
50
+ export type Response<T> = {
34
51
  body: T;
35
52
  status: number;
36
53
  headers: Headers;
37
- }
54
+ };
38
55
 
56
+ /**
57
+ * The Provider class is a wrapper around the fetch function to call a provider's HTTP API.
58
+ *
59
+ * Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
60
+ *
61
+ * 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, and can also be configured to use a provided rate limiting function.
63
+ * @see {@link RateLimiter}
64
+ * @see {@link prepareRequest}
65
+ */
39
66
  export class Provider {
40
67
  protected rateLimiter: RateLimiter | undefined = undefined;
41
68
  protected prepareRequest: (options: { credentials: Credentials; logger: Logger }) => {
@@ -44,7 +71,7 @@ export class Provider {
44
71
  };
45
72
 
46
73
  /**
47
- * Initialize a Provider with the given options.
74
+ * Initializes a Provider with the given options.
48
75
  *
49
76
  * @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
50
77
  * @property rateLimiter - function to limit the rate of calls to the provider based on the caller's credentials.
@@ -54,39 +81,85 @@ export class Provider {
54
81
  this.rateLimiter = options.rateLimiter;
55
82
  }
56
83
 
84
+ /**
85
+ * Performs a GET request to the provider.
86
+ *
87
+ * Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
88
+ * adds the following headers:
89
+ * - Accept: application/json
90
+ *
91
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
92
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
93
+ * @returns The {@link Response} extracted from the provider.
94
+ */
57
95
  public async get<T>(endpoint: string, options: RequestOptions): Promise<Response<T>> {
58
96
  return this.fetchWrapper<T>(endpoint, null, {
59
97
  ...options,
60
98
  method: 'GET',
61
99
  defaultHeaders: {
62
- 'Content-Type': 'application/json',
63
100
  Accept: 'application/json',
64
101
  },
65
102
  });
66
103
  }
67
104
 
105
+ /**
106
+ * Performs a POST request to the provider.
107
+ *
108
+ * Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
109
+ * adds the following headers:
110
+ * - Content-Type: application/json',
111
+ * - Accept: application/json
112
+ *
113
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
114
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
115
+ * @returns The {@link Response} extracted from the provider.
116
+ */
68
117
  public async post<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>> {
69
118
  return this.fetchWrapper<T>(endpoint, body, {
70
119
  ...options,
71
120
  method: 'POST',
72
121
  defaultHeaders: {
73
- 'Content-Type': 'application/x-www-form-urlencoded',
122
+ 'Content-Type': 'application/json',
74
123
  Accept: 'application/json',
75
124
  },
76
125
  });
77
126
  }
78
127
 
128
+ /**
129
+ * Performs a PUT request to the provider.
130
+ *
131
+ * Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
132
+ * adds the following headers:
133
+ * - Content-Type: application/json',
134
+ * - Accept: application/json
135
+ *
136
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
137
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
138
+ * @returns The {@link Response} extracted from the provider.
139
+ */
79
140
  public async put<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>> {
80
141
  return this.fetchWrapper<T>(endpoint, body, {
81
142
  ...options,
82
143
  method: 'PUT',
83
144
  defaultHeaders: {
84
- 'Content-Type': 'application/x-www-form-urlencoded',
145
+ 'Content-Type': 'application/json',
85
146
  Accept: 'application/json',
86
147
  },
87
148
  });
88
149
  }
89
150
 
151
+ /**
152
+ * Performs a PATCH request to the provider.
153
+ *
154
+ * Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
155
+ * adds the following headers:
156
+ * - Content-Type: application/json',
157
+ * - Accept: application/json
158
+ *
159
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
160
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
161
+ * @returns The {@link Response} extracted from the provider.
162
+ */
90
163
  public async patch<T>(
91
164
  endpoint: string,
92
165
  body: Record<string, unknown>,
@@ -96,18 +169,28 @@ export class Provider {
96
169
  ...options,
97
170
  method: 'PATCH',
98
171
  defaultHeaders: {
99
- 'Content-Type': 'application/x-www-form-urlencoded',
172
+ 'Content-Type': 'application/json',
100
173
  Accept: 'application/json',
101
174
  },
102
175
  });
103
176
  }
104
177
 
178
+ /**
179
+ * Performs a DELETE request to the provider.
180
+ *
181
+ * Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
182
+ * adds the following headers:
183
+ * - Accept: application/json
184
+ *
185
+ * @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
186
+ * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
187
+ * @returns The {@link Response} extracted from the provider.
188
+ */
105
189
  public async delete<T = undefined>(endpoint: string, options: RequestOptions): Promise<Response<T>> {
106
190
  return this.fetchWrapper<T>(endpoint, null, {
107
191
  ...options,
108
192
  method: 'DELETE',
109
193
  defaultHeaders: {
110
- 'Content-Type': 'application/x-www-form-urlencoded',
111
194
  Accept: 'application/json',
112
195
  },
113
196
  });
@@ -116,7 +199,7 @@ export class Provider {
116
199
  private async fetchWrapper<T>(
117
200
  endpoint: string,
118
201
  body: Record<string, unknown> | null,
119
- options: RequestOptions & { defaultHeaders: { 'Content-Type': string; Accept: string }; method: string },
202
+ options: RequestOptions & { defaultHeaders: { 'Content-Type'?: string; Accept?: string }; method: string },
120
203
  ): Promise<Response<T>> {
121
204
  const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
122
205
 
@@ -139,11 +222,27 @@ export class Provider {
139
222
  }
140
223
 
141
224
  const callToProvider = async (): Promise<Response<T>> => {
142
- const response = await fetch(absoluteUrl, {
143
- method: options.method,
144
- headers,
145
- body: stringifiedBody,
146
- });
225
+ let response: globalThis.Response;
226
+
227
+ try {
228
+ response = await fetch(absoluteUrl, {
229
+ method: options.method,
230
+ signal: options.signal,
231
+ headers,
232
+ body: stringifiedBody,
233
+ });
234
+ } catch (error) {
235
+ if (error instanceof Error) {
236
+ switch (error.name) {
237
+ case 'AbortError':
238
+ throw buildHttpError(408, 'Request aborted');
239
+ case 'TimeoutError':
240
+ throw buildHttpError(408, 'Request timeout');
241
+ }
242
+ }
243
+
244
+ throw buildHttpError(500, `Unexpected error while calling the provider: "${error}"`);
245
+ }
147
246
 
148
247
  if (response.status >= 400) {
149
248
  const textResult = await response.text();
@@ -0,0 +1,28 @@
1
+ import express from 'express';
2
+ import assert from 'node:assert/strict';
3
+ import { describe, it } from 'node:test';
4
+ import middleware from '../../src/middlewares/signal.js';
5
+
6
+ describe('signal middleware', () => {
7
+ it('uses header', () => {
8
+ const deadline = Math.floor((Date.now() + 5000) / 1000);
9
+
10
+ const request = { header: (_key: string) => deadline } as unknown as express.Request;
11
+ const response = { locals: {} } as express.Response;
12
+
13
+ middleware(request, response, () => {});
14
+
15
+ assert.ok(response.locals.signal instanceof AbortSignal);
16
+ assert.equal(response.locals.signal.aborted, false);
17
+ });
18
+
19
+ it('defaults', () => {
20
+ const request = { header: (_key: string) => undefined } as unknown as express.Request;
21
+ const response = { locals: {} } as express.Response;
22
+
23
+ middleware(request, response, () => {});
24
+
25
+ assert.ok(response.locals.signal instanceof AbortSignal);
26
+ assert.equal(response.locals.signal.aborted, false);
27
+ });
28
+ });
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
 
4
4
  import { Provider } from '../../src/resources/provider.js';
5
+ import * as HttpErrors from '../../src/httpErrors.js';
5
6
  import Logger from '../../src/resources/logger.js';
6
7
 
7
8
  describe('Provider', () => {
@@ -30,6 +31,7 @@ describe('Provider', () => {
30
31
  const actualResponse = await provider.get('/endpoint', {
31
32
  credentials: { apiKey: 'apikey#1111' },
32
33
  logger: logger,
34
+ signal: new AbortController().signal,
33
35
  additionnalheaders: { 'X-Additional-Header': 'value1' },
34
36
  });
35
37
 
@@ -39,8 +41,8 @@ describe('Provider', () => {
39
41
  {
40
42
  method: 'GET',
41
43
  body: null,
44
+ signal: new AbortController().signal,
42
45
  headers: {
43
- 'Content-Type': 'application/json',
44
46
  Accept: 'application/json',
45
47
  'X-Custom-Provider-Header': 'value',
46
48
  'X-Provider-Credential-Header': 'apikey#1111',
@@ -67,7 +69,8 @@ describe('Provider', () => {
67
69
  {
68
70
  credentials: { apiKey: 'apikey#1111' },
69
71
  logger: logger,
70
- additionnalheaders: { 'X-Additional-Header': 'value1' },
72
+ signal: new AbortController().signal,
73
+ additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' },
71
74
  },
72
75
  );
73
76
 
@@ -77,6 +80,7 @@ describe('Provider', () => {
77
80
  {
78
81
  method: 'POST',
79
82
  body: 'data=createdItemInfo',
83
+ signal: new AbortController().signal,
80
84
  headers: {
81
85
  'Content-Type': 'application/x-www-form-urlencoded',
82
86
  Accept: 'application/json',
@@ -106,6 +110,7 @@ describe('Provider', () => {
106
110
  {
107
111
  credentials: { apiKey: 'apikey#1111' },
108
112
  logger: logger,
113
+ signal: new AbortController().signal,
109
114
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
110
115
  },
111
116
  );
@@ -116,6 +121,7 @@ describe('Provider', () => {
116
121
  {
117
122
  method: 'PUT',
118
123
  body: JSON.stringify({ data: 'updatedItemInfo' }),
124
+ signal: new AbortController().signal,
119
125
  headers: {
120
126
  'Content-Type': 'application/json',
121
127
  Accept: 'application/json',
@@ -144,8 +150,9 @@ describe('Provider', () => {
144
150
  {
145
151
  credentials: { apiKey: 'apikey#1111' },
146
152
  logger: logger,
153
+ signal: new AbortController().signal,
147
154
  queryParams: { param1: 'value1', param2: 'value2' },
148
- additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
155
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
149
156
  },
150
157
  );
151
158
 
@@ -155,6 +162,7 @@ describe('Provider', () => {
155
162
  {
156
163
  method: 'PATCH',
157
164
  body: JSON.stringify({ data: 'updatedItemInfo' }),
165
+ signal: new AbortController().signal,
158
166
  headers: {
159
167
  'Content-Type': 'application/json',
160
168
  Accept: 'application/json',
@@ -178,7 +186,8 @@ describe('Provider', () => {
178
186
  const actualResponse = await provider.delete('/endpoint/123', {
179
187
  credentials: { apiKey: 'apikey#1111' },
180
188
  logger: logger,
181
- additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
189
+ signal: new AbortController().signal,
190
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
182
191
  });
183
192
 
184
193
  assert.equal(fetchMock.mock.calls.length, 1);
@@ -187,8 +196,8 @@ describe('Provider', () => {
187
196
  {
188
197
  method: 'DELETE',
189
198
  body: null,
199
+ signal: new AbortController().signal,
190
200
  headers: {
191
- 'Content-Type': 'application/json',
192
201
  Accept: 'application/json',
193
202
  'X-Custom-Provider-Header': 'value',
194
203
  'X-Provider-Credential-Header': 'apikey#1111',
@@ -225,7 +234,8 @@ describe('Provider', () => {
225
234
  const options = {
226
235
  credentials: { apiKey: 'apikey#1111' },
227
236
  logger: logger,
228
- additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
237
+ signal: new AbortController().signal,
238
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
229
239
  };
230
240
 
231
241
  const actualResponse = await rateLimitedProvider.delete('/endpoint/123', options);
@@ -238,8 +248,8 @@ describe('Provider', () => {
238
248
  {
239
249
  method: 'DELETE',
240
250
  body: null,
251
+ signal: new AbortController().signal,
241
252
  headers: {
242
- 'Content-Type': 'application/json',
243
253
  Accept: 'application/json',
244
254
  'X-Custom-Provider-Header': 'value',
245
255
  'X-Provider-Credential-Header': 'apikey#1111',
@@ -257,41 +267,132 @@ describe('Provider', () => {
257
267
 
258
268
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
259
269
 
260
- assert.rejects(() =>
261
- provider.get('/endpoint/123', {
270
+ let error;
271
+
272
+ try {
273
+ await provider.get('/endpoint/123', {
262
274
  credentials: { apiKey: 'apikey#1111' },
263
275
  logger: logger,
264
- }),
265
- );
276
+ signal: new AbortController().signal,
277
+ });
278
+ } catch (e) {
279
+ error = e;
280
+ }
281
+
282
+ assert.ok(error instanceof HttpErrors.BadRequestError);
283
+ assert.equal(error.message, 'Invalid JSON response');
266
284
  });
267
285
 
268
286
  it('throws on status 400', async context => {
269
- const response = new Response(undefined, {
287
+ const response = new Response('response body', {
270
288
  status: 400,
271
289
  });
272
290
 
273
291
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
274
292
 
275
- assert.rejects(() =>
276
- provider.get('/endpoint/123', {
293
+ let error;
294
+
295
+ try {
296
+ await provider.get('/endpoint/123', {
277
297
  credentials: { apiKey: 'apikey#1111' },
298
+ signal: new AbortController().signal,
278
299
  logger: logger,
279
- }),
280
- );
300
+ });
301
+ } catch (e) {
302
+ error = e;
303
+ }
304
+
305
+ assert.ok(error instanceof HttpErrors.BadRequestError);
306
+ assert.equal(error.message, 'response body');
307
+ });
308
+
309
+ it('throws on timeout', async context => {
310
+ context.mock.method(global, 'fetch', () => {
311
+ const error = new Error();
312
+ error.name = 'TimeoutError';
313
+ throw error;
314
+ });
315
+
316
+ let error;
317
+
318
+ try {
319
+ await provider.get('/endpoint/123', {
320
+ credentials: { apiKey: 'apikey#1111' },
321
+ signal: new AbortController().signal,
322
+ logger: logger,
323
+ });
324
+ } catch (e) {
325
+ error = e;
326
+ }
327
+
328
+ assert.ok(error instanceof HttpErrors.TimeoutError);
329
+ assert.equal(error.message, 'Request timeout');
330
+ });
331
+
332
+ it('throws on abort', async context => {
333
+ context.mock.method(global, 'fetch', () => {
334
+ const error = new Error();
335
+ error.name = 'AbortError';
336
+ throw error;
337
+ });
338
+
339
+ let error;
340
+
341
+ try {
342
+ await provider.get('/endpoint/123', {
343
+ credentials: { apiKey: 'apikey#1111' },
344
+ signal: new AbortController().signal,
345
+ logger: logger,
346
+ });
347
+ } catch (e) {
348
+ error = e;
349
+ }
350
+
351
+ assert.ok(error instanceof HttpErrors.TimeoutError);
352
+ assert.equal(error.message, 'Request aborted');
353
+ });
354
+
355
+ it('throws on unknown errors', async context => {
356
+ context.mock.method(global, 'fetch', () => {
357
+ throw new Error('foo');
358
+ });
359
+
360
+ let error;
361
+
362
+ try {
363
+ await provider.get('/endpoint/123', {
364
+ credentials: { apiKey: 'apikey#1111' },
365
+ signal: new AbortController().signal,
366
+ logger: logger,
367
+ });
368
+ } catch (e) {
369
+ error = e;
370
+ }
371
+
372
+ assert.ok(error instanceof HttpErrors.HttpError);
373
+ assert.equal(error.message, 'Unexpected error while calling the provider: "Error: foo"');
281
374
  });
282
375
 
283
376
  it('throws on status 429', async context => {
284
- const response = new Response(undefined, {
377
+ const response = new Response('response body', {
285
378
  status: 429,
286
379
  });
287
380
 
288
381
  context.mock.method(global, 'fetch', () => Promise.resolve(response));
289
382
 
290
- assert.rejects(() =>
291
- provider.get('/endpoint/123', {
383
+ let error;
384
+
385
+ try {
386
+ await provider.get('/endpoint/123', {
292
387
  credentials: { apiKey: 'apikey#1111' },
388
+ signal: new AbortController().signal,
293
389
  logger: logger,
294
- }),
295
- );
390
+ });
391
+ } catch (e) {
392
+ error = e;
393
+ }
394
+
395
+ assert.ok(error instanceof HttpErrors.RateLimitExceededError);
396
+ assert.equal(error.message, 'response body');
296
397
  });
297
398
  });