@unito/integration-sdk 0.1.0 → 0.1.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 (69) hide show
  1. package/dist/src/errors.d.ts +3 -2
  2. package/dist/src/errors.js +11 -9
  3. package/dist/src/httpErrors.d.ts +3 -0
  4. package/dist/src/httpErrors.js +6 -1
  5. package/dist/src/index.d.ts +2 -1
  6. package/dist/src/index.js +2 -0
  7. package/dist/src/middlewares/credentials.d.ts +4 -1
  8. package/dist/src/resources/provider.d.ts +35 -12
  9. package/dist/src/resources/provider.js +59 -34
  10. package/dist/test/errors.test.js +7 -8
  11. package/dist/test/handler.test.js +1 -1
  12. package/dist/test/resources/provider.test.d.ts +1 -0
  13. package/dist/test/resources/provider.test.js +229 -0
  14. package/package.json +1 -3
  15. package/src/errors.ts +12 -9
  16. package/src/httpErrors.ts +7 -1
  17. package/src/index.ts +4 -0
  18. package/src/middlewares/credentials.ts +1 -1
  19. package/src/resources/provider.ts +89 -47
  20. package/test/errors.test.ts +7 -8
  21. package/test/handler.test.ts +1 -1
  22. package/test/resources/provider.test.ts +285 -0
  23. package/dist/src/api/index.d.ts +0 -2
  24. package/dist/src/api/index.d.ts.map +0 -1
  25. package/dist/src/api/index.js +0 -2
  26. package/dist/src/api/index.js.map +0 -1
  27. package/dist/src/app/errors/HTTPError.d.ts +0 -5
  28. package/dist/src/app/errors/HTTPError.d.ts.map +0 -1
  29. package/dist/src/app/errors/HTTPError.js +0 -8
  30. package/dist/src/app/errors/HTTPError.js.map +0 -1
  31. package/dist/src/app/errors/HTTPNotFoundError.d.ts +0 -5
  32. package/dist/src/app/errors/HTTPNotFoundError.d.ts.map +0 -1
  33. package/dist/src/app/errors/HTTPNotFoundError.js +0 -7
  34. package/dist/src/app/errors/HTTPNotFoundError.js.map +0 -1
  35. package/dist/src/app/errors/HTTPUnprocessableEntityError.d.ts +0 -5
  36. package/dist/src/app/errors/HTTPUnprocessableEntityError.d.ts.map +0 -1
  37. package/dist/src/app/errors/HTTPUnprocessableEntityError.js +0 -7
  38. package/dist/src/app/errors/HTTPUnprocessableEntityError.js.map +0 -1
  39. package/dist/src/app/errors/index.d.ts +0 -4
  40. package/dist/src/app/errors/index.d.ts.map +0 -1
  41. package/dist/src/app/errors/index.js +0 -4
  42. package/dist/src/app/errors/index.js.map +0 -1
  43. package/dist/src/app/index.d.ts +0 -6
  44. package/dist/src/app/index.d.ts.map +0 -1
  45. package/dist/src/app/index.js +0 -80
  46. package/dist/src/app/index.js.map +0 -1
  47. package/dist/src/app/integration.d.ts +0 -5
  48. package/dist/src/app/integration.d.ts.map +0 -1
  49. package/dist/src/app/integration.js +0 -86
  50. package/dist/src/app/integration.js.map +0 -1
  51. package/dist/src/app/itemNode.d.ts +0 -8
  52. package/dist/src/app/itemNode.d.ts.map +0 -1
  53. package/dist/src/app/itemNode.js +0 -13
  54. package/dist/src/app/itemNode.js.map +0 -1
  55. package/dist/src/app/middlewares/withCorrelationId.d.ts +0 -11
  56. package/dist/src/app/middlewares/withCorrelationId.d.ts.map +0 -1
  57. package/dist/src/app/middlewares/withCorrelationId.js +0 -8
  58. package/dist/src/app/middlewares/withCorrelationId.js.map +0 -1
  59. package/dist/src/app/middlewares/withLogger.d.ts +0 -12
  60. package/dist/src/app/middlewares/withLogger.d.ts.map +0 -1
  61. package/dist/src/app/middlewares/withLogger.js +0 -18
  62. package/dist/src/app/middlewares/withLogger.js.map +0 -1
  63. package/dist/src/index.d.ts.map +0 -1
  64. package/dist/src/index.js.map +0 -1
  65. package/dist/src/resources/cache.d.ts.map +0 -1
  66. package/dist/src/resources/cache.js.map +0 -1
  67. package/dist/src/resources/logger.d.ts.map +0 -1
  68. package/dist/src/resources/logger.js.map +0 -1
  69. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -1,3 +1,4 @@
1
+ import * as HttpErrors from './httpErrors.js';
1
2
  export declare class NoIntegrationFoundError extends Error {
2
3
  }
3
4
  export declare class NoConfigurationFileError extends Error {
@@ -7,9 +8,9 @@ export declare class ConfigurationMalformed extends Error {
7
8
  export declare class InvalidHandler extends Error {
8
9
  }
9
10
  /**
10
- * Processes provider response codes and throw corresponding errors to be translated further in our responses
11
+ * Processes provider response codes and returns the corresponding errors to be translated further in our responses
11
12
  *
12
13
  * @param responseStatus the reponseStatus of the request. Any HTTP response code passed here will result in an error!
13
14
  * @param message The message returned by the provider
14
15
  */
15
- export declare function handleErrorResponse(responseStatus: number, message: string): void;
16
+ export declare function buildHttpError(responseStatus: number, message: string): HttpErrors.HttpError;
@@ -8,32 +8,34 @@ export class ConfigurationMalformed extends Error {
8
8
  export class InvalidHandler extends Error {
9
9
  }
10
10
  /**
11
- * Processes provider response codes and throw corresponding errors to be translated further in our responses
11
+ * Processes provider response codes and returns the corresponding errors to be translated further in our responses
12
12
  *
13
13
  * @param responseStatus the reponseStatus of the request. Any HTTP response code passed here will result in an error!
14
14
  * @param message The message returned by the provider
15
15
  */
16
16
  // Keep in errors.ts instead of httpErrors.ts because we do not need to export it outside of the sdk
17
- export function handleErrorResponse(responseStatus, message) {
17
+ export function buildHttpError(responseStatus, message) {
18
+ let httpError;
18
19
  if (responseStatus === 400) {
19
- throw new HttpErrors.BadRequestError(message);
20
+ httpError = new HttpErrors.BadRequestError(message);
20
21
  }
21
22
  else if (responseStatus === 401 || responseStatus === 403) {
22
- throw new HttpErrors.UnauthorizedError(message);
23
+ httpError = new HttpErrors.UnauthorizedError(message);
23
24
  }
24
25
  else if (responseStatus === 404) {
25
- throw new HttpErrors.NotFoundError(message);
26
+ httpError = new HttpErrors.NotFoundError(message);
26
27
  }
27
28
  else if (responseStatus === 408) {
28
- throw new HttpErrors.TimeoutError(message);
29
+ httpError = new HttpErrors.TimeoutError(message);
29
30
  }
30
31
  else if (responseStatus === 422) {
31
- throw new HttpErrors.UnprocessableEntityError(message);
32
+ httpError = new HttpErrors.UnprocessableEntityError(message);
32
33
  }
33
34
  else if (responseStatus === 429) {
34
- throw new HttpErrors.RateLimitExceededError(message);
35
+ httpError = new HttpErrors.RateLimitExceededError(message);
35
36
  }
36
37
  else {
37
- throw new HttpErrors.HttpError(message, responseStatus);
38
+ httpError = new HttpErrors.HttpError(message, responseStatus);
38
39
  }
40
+ return httpError;
39
41
  }
@@ -20,3 +20,6 @@ export declare class UnprocessableEntityError extends HttpError {
20
20
  export declare class RateLimitExceededError extends HttpError {
21
21
  constructor(message?: string);
22
22
  }
23
+ export declare class WouldExceedLimitError extends HttpError {
24
+ constructor(message?: string);
25
+ }
@@ -32,6 +32,11 @@ export class UnprocessableEntityError extends HttpError {
32
32
  }
33
33
  export class RateLimitExceededError extends HttpError {
34
34
  constructor(message) {
35
- super(message || 'Unprocessable Entity', 429);
35
+ super(message || 'Rate Limit Exceeded', 429);
36
+ }
37
+ }
38
+ export class WouldExceedLimitError extends HttpError {
39
+ constructor(message) {
40
+ super(message || 'Would Exceed Limit', 429);
36
41
  }
37
42
  }
@@ -1,6 +1,7 @@
1
1
  export * as Api from '@unito/integration-api';
2
2
  export { default as Integration } from './integration.js';
3
3
  export * from './handler.js';
4
- export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, } from './resources/provider.js';
4
+ export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
5
+ export type { Credentials } from './middlewares/credentials.js';
5
6
  export * as HttpErrors from './httpErrors.js';
6
7
  export * from './resources/context.js';
package/dist/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ /* c8 ignore start */
1
2
  export * as Api from '@unito/integration-api';
2
3
  export { default as Integration } from './integration.js';
3
4
  export * from './handler.js';
4
5
  export { Provider, } from './resources/provider.js';
5
6
  export * as HttpErrors from './httpErrors.js';
6
7
  export * from './resources/context.js';
8
+ /* c8 ignore stop */
@@ -6,6 +6,9 @@ declare global {
6
6
  }
7
7
  }
8
8
  }
9
- export type Credentials = Record<string, unknown>;
9
+ export type Credentials = {
10
+ accessToken?: string;
11
+ [keys: string]: unknown;
12
+ };
10
13
  declare const middleware: (req: Request, res: Response, next: NextFunction) => void;
11
14
  export default middleware;
@@ -1,36 +1,59 @@
1
1
  import { Credentials } from '../middlewares/credentials.js';
2
+ /**
3
+ * RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
4
+ * caller's credentials.
5
+ *
6
+ * When necessary, the Provider's Response headers can be inspected to update the rate limit before being returned.
7
+ *
8
+ * NOTE: make sure to return one of the supported HttpErrors from the SDK, otherwise the error will be translated to a
9
+ * generic server (500) error.
10
+ *
11
+ * @param context - The credentials of the caller.
12
+ * @param targetFunction - The function to call the provider.
13
+ * @returns The response from the provider.
14
+ * @throws RateLimitExceededError when the rate limit is exceeded.
15
+ * @throws WouldExceedRateLimitError when the next call would exceed the rate limit.
16
+ * @throws HttpError when the provider returns an error.
17
+ */
18
+ export type RateLimiter = <T>(context: {
19
+ credentials: Credentials;
20
+ }, targetFunction: () => Promise<Response<T>>) => Promise<Response<T>>;
2
21
  export interface RequestOptions {
3
22
  credentials: Credentials;
4
23
  queryParams?: {
5
24
  [key: string]: string;
6
25
  };
7
- body?: Record<string, unknown>;
8
26
  additionnalheaders?: {
9
27
  [key: string]: string;
10
28
  };
11
29
  }
12
30
  export interface Response<T> {
13
- data: T;
31
+ data: T | undefined;
32
+ status: number;
14
33
  headers: Headers;
15
34
  }
16
35
  export declare class Provider {
17
- protected prepareRequest: ((context: {
36
+ protected rateLimiter: RateLimiter | undefined;
37
+ protected prepareRequest: (context: {
18
38
  credentials: Credentials;
19
39
  }) => {
20
- /**
21
- * The base URL of the provider.
22
- */
23
40
  url: string;
24
- /**
25
- * The additional headers to add to the request.
26
- */
27
41
  headers: Record<string, string>;
28
- }) | undefined;
42
+ };
43
+ /**
44
+ * Initialize a Provider with the given options.
45
+ *
46
+ * @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
47
+ * @property rateLimiter - function to limit the rate of calls to the provider based on the caller's credentials.
48
+ */
29
49
  constructor(options: {
30
50
  prepareRequest: typeof Provider.prototype.prepareRequest;
51
+ rateLimiter?: RateLimiter;
31
52
  });
32
53
  get<T>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
33
- post<T>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
34
- patch<T>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
54
+ post<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
55
+ put<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
56
+ patch<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
57
+ delete<T>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
35
58
  private fetchWrapper;
36
59
  }
@@ -1,11 +1,19 @@
1
- import { handleErrorResponse as throwHttpError } from '../errors.js';
1
+ import { buildHttpError } from '../errors.js';
2
2
  export class Provider {
3
- prepareRequest = undefined;
3
+ rateLimiter = undefined;
4
+ prepareRequest;
5
+ /**
6
+ * Initialize a Provider with the given options.
7
+ *
8
+ * @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
9
+ * @property rateLimiter - function to limit the rate of calls to the provider based on the caller's credentials.
10
+ */
4
11
  constructor(options) {
5
12
  this.prepareRequest = options.prepareRequest;
13
+ this.rateLimiter = options.rateLimiter;
6
14
  }
7
15
  async get(endpoint, options) {
8
- return this.fetchWrapper(endpoint, {
16
+ return this.fetchWrapper(endpoint, null, {
9
17
  ...options,
10
18
  method: 'GET',
11
19
  defaultHeaders: {
@@ -14,8 +22,8 @@ export class Provider {
14
22
  },
15
23
  });
16
24
  }
17
- async post(endpoint, options) {
18
- return this.fetchWrapper(endpoint, {
25
+ async post(endpoint, body, options) {
26
+ return this.fetchWrapper(endpoint, body, {
19
27
  ...options,
20
28
  method: 'POST',
21
29
  defaultHeaders: {
@@ -24,8 +32,18 @@ export class Provider {
24
32
  },
25
33
  });
26
34
  }
27
- async patch(endpoint, options) {
28
- return this.fetchWrapper(endpoint, {
35
+ async put(endpoint, body, options) {
36
+ return this.fetchWrapper(endpoint, body, {
37
+ ...options,
38
+ method: 'PUT',
39
+ defaultHeaders: {
40
+ 'Content-Type': 'application/x-www-form-urlencoded',
41
+ Accept: 'application/json',
42
+ },
43
+ });
44
+ }
45
+ async patch(endpoint, body, options) {
46
+ return this.fetchWrapper(endpoint, body, {
29
47
  ...options,
30
48
  method: 'PATCH',
31
49
  defaultHeaders: {
@@ -34,43 +52,50 @@ export class Provider {
34
52
  },
35
53
  });
36
54
  }
37
- async fetchWrapper(endpoint, options) {
38
- if (!this.prepareRequest) {
39
- throw new Error('Provider not initialized');
40
- }
55
+ async delete(endpoint, options) {
56
+ return this.fetchWrapper(endpoint, null, {
57
+ ...options,
58
+ method: 'DELETE',
59
+ defaultHeaders: {
60
+ 'Content-Type': 'application/x-www-form-urlencoded',
61
+ Accept: 'application/json',
62
+ },
63
+ });
64
+ }
65
+ async fetchWrapper(endpoint, body, options) {
41
66
  const { url: providerUrl, headers: providerHeaders } = this.prepareRequest({ credentials: options.credentials });
42
67
  let absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
43
68
  if (options.queryParams) {
44
69
  absoluteUrl = `${absoluteUrl}?${new URLSearchParams(options.queryParams)}`;
45
70
  }
46
71
  const headers = { ...options.defaultHeaders, ...providerHeaders, ...options.additionnalheaders };
47
- let body = null;
48
- if (options.body) {
72
+ let stringifiedBody = null;
73
+ if (body) {
49
74
  if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
50
- body = new URLSearchParams(options.body).toString(); // this doesn't support objects!
75
+ stringifiedBody = new URLSearchParams(body).toString();
51
76
  }
52
77
  else if (headers['Content-Type'] === 'application/json') {
53
- body = JSON.stringify(options.body);
78
+ stringifiedBody = JSON.stringify(body);
54
79
  }
55
80
  }
56
- const callToProvider = async () => await fetch(absoluteUrl, {
57
- method: options.method,
58
- headers,
59
- body: body,
60
- });
61
- // TODO: add back rate limiter
62
- const response = await callToProvider();
63
- if (response.status >= 400) {
64
- const textResult = await response.text();
65
- throwHttpError(response.status, textResult);
66
- }
67
- let data;
68
- try {
69
- data = await response.json();
70
- }
71
- catch {
72
- throwHttpError(400, 'Invalid JSON response');
73
- }
74
- return { headers: response.headers, data };
81
+ const callToProvider = async () => {
82
+ const response = await fetch(absoluteUrl, {
83
+ method: options.method,
84
+ headers,
85
+ body: stringifiedBody,
86
+ });
87
+ if (response.status >= 400) {
88
+ const textResult = await response.text();
89
+ throw buildHttpError(response.status, textResult);
90
+ }
91
+ try {
92
+ const data = response.body ? await response.json() : undefined;
93
+ return { status: response.status, headers: response.headers, data };
94
+ }
95
+ catch {
96
+ throw buildHttpError(400, 'Invalid JSON response');
97
+ }
98
+ };
99
+ return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
75
100
  }
76
101
  }
@@ -4,13 +4,12 @@ import * as errors from '../src/errors.js';
4
4
  import * as httpErrors from '../src/httpErrors.js';
5
5
  describe('handleErrorResponse', () => {
6
6
  it('returns correct httpError given status code', () => {
7
- assert.throws(() => errors.handleErrorResponse(400, 'bad request'), httpErrors.BadRequestError);
8
- assert.throws(() => errors.handleErrorResponse(401, 'unauthorized'), httpErrors.UnauthorizedError);
9
- assert.throws(() => errors.handleErrorResponse(403, 'forbidden'), httpErrors.UnauthorizedError);
10
- assert.throws(() => errors.handleErrorResponse(404, 'not found'), httpErrors.NotFoundError);
11
- assert.throws(() => errors.handleErrorResponse(408, 'timeout'), httpErrors.TimeoutError);
12
- assert.throws(() => errors.handleErrorResponse(422, 'unprocessable entity'), httpErrors.UnprocessableEntityError);
13
- assert.throws(() => errors.handleErrorResponse(429, 'rate limit exceeded'), httpErrors.RateLimitExceededError);
14
- assert.throws(() => errors.handleErrorResponse(500, 'internal server error'), httpErrors.HttpError);
7
+ assert.ok(errors.buildHttpError(401, 'unauthorized') instanceof httpErrors.UnauthorizedError);
8
+ assert.ok(errors.buildHttpError(403, 'forbidden') instanceof httpErrors.UnauthorizedError);
9
+ assert.ok(errors.buildHttpError(404, 'not found') instanceof httpErrors.NotFoundError);
10
+ assert.ok(errors.buildHttpError(408, 'timeout') instanceof httpErrors.TimeoutError);
11
+ assert.ok(errors.buildHttpError(422, 'unprocessable entity') instanceof httpErrors.UnprocessableEntityError);
12
+ assert.ok(errors.buildHttpError(429, 'rate limit exceeded') instanceof httpErrors.RateLimitExceededError);
13
+ assert.ok(errors.buildHttpError(500, 'internal server error') instanceof httpErrors.HttpError);
15
14
  });
16
15
  });
@@ -13,7 +13,7 @@ describe('Handler', () => {
13
13
  describe('constructor', () => {
14
14
  it('returns a Handler', () => {
15
15
  const itemHandler = new Handler('/', {});
16
- assert.equal(itemHandler instanceof Handler, true);
16
+ assert.ok(itemHandler instanceof Handler);
17
17
  });
18
18
  it('validates path', () => {
19
19
  const paths = [
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,229 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { Provider } from '../../src/resources/provider.js';
4
+ describe('Provider', () => {
5
+ const provider = new Provider({
6
+ prepareRequest: requestOptions => {
7
+ return {
8
+ url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
9
+ headers: {
10
+ 'X-Custom-Provider-Header': 'value',
11
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
12
+ },
13
+ };
14
+ },
15
+ });
16
+ it('get', async (context) => {
17
+ const response = new Response('{"data": "value"}', {
18
+ status: 200,
19
+ headers: { 'Content-Type': 'application/json' },
20
+ });
21
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
22
+ const actualResponse = await provider.get('/endpoint', {
23
+ credentials: { apiKey: 'apikey#1111' },
24
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
25
+ });
26
+ assert.equal(fetchMock.mock.calls.length, 1);
27
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
28
+ 'www.myApi.com/endpoint',
29
+ {
30
+ method: 'GET',
31
+ body: null,
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Accept: 'application/json',
35
+ 'X-Custom-Provider-Header': 'value',
36
+ 'X-Provider-Credential-Header': 'apikey#1111',
37
+ 'X-Additional-Header': 'value1',
38
+ },
39
+ },
40
+ ]);
41
+ assert.deepEqual(actualResponse, { status: 200, headers: response.headers, data: { data: 'value' } });
42
+ });
43
+ it('post with url encoded body', async (context) => {
44
+ const response = new Response('{"data": "value"}', {
45
+ status: 201,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ });
48
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
49
+ const actualResponse = await provider.post('/endpoint', {
50
+ data: 'createdItemInfo',
51
+ }, {
52
+ credentials: { apiKey: 'apikey#1111' },
53
+ additionnalheaders: { 'X-Additional-Header': 'value1' },
54
+ });
55
+ assert.equal(fetchMock.mock.calls.length, 1);
56
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
57
+ 'www.myApi.com/endpoint',
58
+ {
59
+ method: 'POST',
60
+ body: 'data=createdItemInfo',
61
+ headers: {
62
+ 'Content-Type': 'application/x-www-form-urlencoded',
63
+ Accept: 'application/json',
64
+ 'X-Custom-Provider-Header': 'value',
65
+ 'X-Provider-Credential-Header': 'apikey#1111',
66
+ 'X-Additional-Header': 'value1',
67
+ },
68
+ },
69
+ ]);
70
+ assert.deepEqual(actualResponse, { status: 201, headers: response.headers, data: { data: 'value' } });
71
+ });
72
+ it('put with json body', async (context) => {
73
+ const response = new Response('{"data": "value"}', {
74
+ status: 201,
75
+ headers: { 'Content-Type': 'application/json' },
76
+ });
77
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
78
+ // Removing leading '/' on endpoint to make sure we support both cases
79
+ const actualResponse = await provider.put('endpoint/123', {
80
+ data: 'updatedItemInfo',
81
+ }, {
82
+ credentials: { apiKey: 'apikey#1111' },
83
+ additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
84
+ });
85
+ assert.equal(fetchMock.mock.calls.length, 1);
86
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
87
+ 'www.myApi.com/endpoint/123',
88
+ {
89
+ method: 'PUT',
90
+ body: JSON.stringify({ data: 'updatedItemInfo' }),
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ Accept: 'application/json',
94
+ 'X-Custom-Provider-Header': 'value',
95
+ 'X-Provider-Credential-Header': 'apikey#1111',
96
+ 'X-Additional-Header': 'value1',
97
+ },
98
+ },
99
+ ]);
100
+ assert.deepEqual(actualResponse, { status: 201, headers: response.headers, data: { data: 'value' } });
101
+ });
102
+ it('patch with query params', async (context) => {
103
+ const response = new Response('{"data": "value"}', {
104
+ status: 201,
105
+ headers: { 'Content-Type': 'application/json' },
106
+ });
107
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
108
+ const actualResponse = await provider.patch('/endpoint/123', {
109
+ data: 'updatedItemInfo',
110
+ }, {
111
+ credentials: { apiKey: 'apikey#1111' },
112
+ queryParams: { param1: 'value1', param2: 'value2' },
113
+ additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
114
+ });
115
+ assert.equal(fetchMock.mock.calls.length, 1);
116
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
117
+ 'www.myApi.com/endpoint/123?param1=value1&param2=value2',
118
+ {
119
+ method: 'PATCH',
120
+ body: JSON.stringify({ data: 'updatedItemInfo' }),
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ Accept: 'application/json',
124
+ 'X-Custom-Provider-Header': 'value',
125
+ 'X-Provider-Credential-Header': 'apikey#1111',
126
+ 'X-Additional-Header': 'value1',
127
+ },
128
+ },
129
+ ]);
130
+ assert.deepEqual(actualResponse, { status: 201, headers: response.headers, data: { data: 'value' } });
131
+ });
132
+ it('delete', async (context) => {
133
+ const response = new Response(undefined, {
134
+ status: 204,
135
+ headers: { 'Content-Type': 'application/json' },
136
+ });
137
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
138
+ const actualResponse = await provider.delete('/endpoint/123', {
139
+ credentials: { apiKey: 'apikey#1111' },
140
+ additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
141
+ });
142
+ assert.equal(fetchMock.mock.calls.length, 1);
143
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
144
+ 'www.myApi.com/endpoint/123',
145
+ {
146
+ method: 'DELETE',
147
+ body: null,
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ Accept: 'application/json',
151
+ 'X-Custom-Provider-Header': 'value',
152
+ 'X-Provider-Credential-Header': 'apikey#1111',
153
+ 'X-Additional-Header': 'value1',
154
+ },
155
+ },
156
+ ]);
157
+ assert.deepEqual(actualResponse, { status: 204, headers: response.headers, data: undefined });
158
+ });
159
+ it('uses rate limiter if provided', async (context) => {
160
+ const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));
161
+ const rateLimitedProvider = new Provider({
162
+ prepareRequest: requestOptions => {
163
+ return {
164
+ url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
165
+ headers: {
166
+ 'X-Custom-Provider-Header': 'value',
167
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
168
+ },
169
+ };
170
+ },
171
+ rateLimiter: mockRateLimiter,
172
+ });
173
+ const response = new Response(undefined, {
174
+ status: 204,
175
+ headers: { 'Content-Type': 'application/json' },
176
+ });
177
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
178
+ const options = {
179
+ credentials: { apiKey: 'apikey#1111' },
180
+ additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
181
+ };
182
+ const actualResponse = await rateLimitedProvider.delete('/endpoint/123', options);
183
+ assert.equal(mockRateLimiter.mock.calls.length, 1);
184
+ assert.deepEqual(mockRateLimiter.mock.calls[0]?.arguments[0]?.credentials, options.credentials);
185
+ assert.equal(fetchMock.mock.calls.length, 1);
186
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
187
+ 'www.myApi.com/endpoint/123',
188
+ {
189
+ method: 'DELETE',
190
+ body: null,
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ Accept: 'application/json',
194
+ 'X-Custom-Provider-Header': 'value',
195
+ 'X-Provider-Credential-Header': 'apikey#1111',
196
+ 'X-Additional-Header': 'value1',
197
+ },
198
+ },
199
+ ]);
200
+ assert.deepEqual(actualResponse, { status: 204, headers: response.headers, data: undefined });
201
+ });
202
+ it('throws on invalid json response', async (context) => {
203
+ const response = new Response('{invalidJSON}', {
204
+ status: 200,
205
+ });
206
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
207
+ assert.rejects(() => provider.get('/endpoint/123', {
208
+ credentials: { apiKey: 'apikey#1111' },
209
+ }));
210
+ });
211
+ it('throws on status 400', async (context) => {
212
+ const response = new Response(undefined, {
213
+ status: 400,
214
+ });
215
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
216
+ assert.rejects(() => provider.get('/endpoint/123', {
217
+ credentials: { apiKey: 'apikey#1111' },
218
+ }));
219
+ });
220
+ it('throws on status 429', async (context) => {
221
+ const response = new Response(undefined, {
222
+ status: 429,
223
+ });
224
+ context.mock.method(global, 'fetch', () => Promise.resolve(response));
225
+ assert.rejects(() => provider.get('/endpoint/123', {
226
+ credentials: { apiKey: 'apikey#1111' },
227
+ }));
228
+ });
229
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -30,13 +30,11 @@
30
30
  "devDependencies": {
31
31
  "@types/express": "4.x",
32
32
  "@types/node": "20.x",
33
- "@types/prettier": "2.x",
34
33
  "@types/uuid": "9.x",
35
34
  "@typescript-eslint/eslint-plugin": "6.x",
36
35
  "@typescript-eslint/parser": "6.x",
37
36
  "c8": "9.x",
38
37
  "eslint": "8.x",
39
- "json-schema-to-typescript": "13.x",
40
38
  "prettier": "3.x",
41
39
  "ts-node": "10.x",
42
40
  "typescript": "5.x"
package/src/errors.ts CHANGED
@@ -9,26 +9,29 @@ export class ConfigurationMalformed extends Error {}
9
9
  export class InvalidHandler extends Error {}
10
10
 
11
11
  /**
12
- * Processes provider response codes and throw corresponding errors to be translated further in our responses
12
+ * Processes provider response codes and returns the corresponding errors to be translated further in our responses
13
13
  *
14
14
  * @param responseStatus the reponseStatus of the request. Any HTTP response code passed here will result in an error!
15
15
  * @param message The message returned by the provider
16
16
  */
17
17
  // Keep in errors.ts instead of httpErrors.ts because we do not need to export it outside of the sdk
18
- export function handleErrorResponse(responseStatus: number, message: string): void {
18
+ export function buildHttpError(responseStatus: number, message: string): HttpErrors.HttpError {
19
+ let httpError: HttpErrors.HttpError;
19
20
  if (responseStatus === 400) {
20
- throw new HttpErrors.BadRequestError(message);
21
+ httpError = new HttpErrors.BadRequestError(message);
21
22
  } else if (responseStatus === 401 || responseStatus === 403) {
22
- throw new HttpErrors.UnauthorizedError(message);
23
+ httpError = new HttpErrors.UnauthorizedError(message);
23
24
  } else if (responseStatus === 404) {
24
- throw new HttpErrors.NotFoundError(message);
25
+ httpError = new HttpErrors.NotFoundError(message);
25
26
  } else if (responseStatus === 408) {
26
- throw new HttpErrors.TimeoutError(message);
27
+ httpError = new HttpErrors.TimeoutError(message);
27
28
  } else if (responseStatus === 422) {
28
- throw new HttpErrors.UnprocessableEntityError(message);
29
+ httpError = new HttpErrors.UnprocessableEntityError(message);
29
30
  } else if (responseStatus === 429) {
30
- throw new HttpErrors.RateLimitExceededError(message);
31
+ httpError = new HttpErrors.RateLimitExceededError(message);
31
32
  } else {
32
- throw new HttpErrors.HttpError(message, responseStatus);
33
+ httpError = new HttpErrors.HttpError(message, responseStatus);
33
34
  }
35
+
36
+ return httpError;
34
37
  }