@unito/integration-sdk 0.1.5 → 0.1.7
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 +10 -10
- package/dist/src/integration.d.ts +6 -1
- package/dist/src/integration.js +5 -2
- package/dist/src/resources/provider.d.ts +5 -1
- package/dist/src/resources/provider.js +3 -3
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +26 -0
- package/dist/test/middlewares/filters.test.js +8 -0
- package/dist/test/resources/provider.test.js +17 -6
- package/package.json +1 -1
- package/src/handler.ts +10 -10
- package/src/integration.ts +10 -4
- package/src/resources/provider.ts +8 -6
- package/test/integration.test.ts +34 -0
- package/test/middlewares/filters.test.ts +17 -0
- package/test/resources/provider.test.ts +18 -6
package/dist/src/handler.js
CHANGED
|
@@ -16,7 +16,7 @@ function assertValidConfiguration(path, pathWithIdentifier, handlers) {
|
|
|
16
16
|
const hasIndividualHandlers = individualHandlers.some(handler => handler in handlers);
|
|
17
17
|
const hasCollectionHandlers = collectionHandlers.some(handler => handler in handlers);
|
|
18
18
|
if (hasIndividualHandlers && hasCollectionHandlers) {
|
|
19
|
-
throw new InvalidHandler(`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both
|
|
19
|
+
throw new InvalidHandler(`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both.`);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -83,7 +83,7 @@ export class Handler {
|
|
|
83
83
|
console.debug(`\x1b[33mMounting handler at path ${this.pathWithIdentifier}`);
|
|
84
84
|
if (this.handlers.getCollection) {
|
|
85
85
|
const handler = this.handlers.getCollection;
|
|
86
|
-
console.debug(` Enabling GET ${this.path}`);
|
|
86
|
+
console.debug(` Enabling getCollection at GET ${this.path}`);
|
|
87
87
|
router.get(this.path, async (req, res) => {
|
|
88
88
|
if (!res.locals.credentials) {
|
|
89
89
|
throw new UnauthorizedError();
|
|
@@ -101,7 +101,7 @@ export class Handler {
|
|
|
101
101
|
}
|
|
102
102
|
if (this.handlers.createItem) {
|
|
103
103
|
const handler = this.handlers.createItem;
|
|
104
|
-
console.debug(` Enabling POST ${this.path}`);
|
|
104
|
+
console.debug(` Enabling createItem at POST ${this.path}`);
|
|
105
105
|
router.post(this.path, async (req, res) => {
|
|
106
106
|
if (!res.locals.credentials) {
|
|
107
107
|
throw new UnauthorizedError();
|
|
@@ -119,7 +119,7 @@ export class Handler {
|
|
|
119
119
|
}
|
|
120
120
|
if (this.handlers.getItem) {
|
|
121
121
|
const handler = this.handlers.getItem;
|
|
122
|
-
console.debug(` Enabling GET ${this.pathWithIdentifier}`);
|
|
122
|
+
console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`);
|
|
123
123
|
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
124
124
|
if (!res.locals.credentials) {
|
|
125
125
|
throw new UnauthorizedError();
|
|
@@ -135,7 +135,7 @@ export class Handler {
|
|
|
135
135
|
}
|
|
136
136
|
if (this.handlers.updateItem) {
|
|
137
137
|
const handler = this.handlers.updateItem;
|
|
138
|
-
console.debug(` Enabling PATCH ${this.pathWithIdentifier}`);
|
|
138
|
+
console.debug(` Enabling updateItem at PATCH ${this.pathWithIdentifier}`);
|
|
139
139
|
router.patch(this.pathWithIdentifier, async (req, res) => {
|
|
140
140
|
if (!res.locals.credentials) {
|
|
141
141
|
throw new UnauthorizedError();
|
|
@@ -153,7 +153,7 @@ export class Handler {
|
|
|
153
153
|
}
|
|
154
154
|
if (this.handlers.deleteItem) {
|
|
155
155
|
const handler = this.handlers.deleteItem;
|
|
156
|
-
console.debug(` Enabling DELETE ${this.pathWithIdentifier}`);
|
|
156
|
+
console.debug(` Enabling deleteItem at DELETE ${this.pathWithIdentifier}`);
|
|
157
157
|
router.delete(this.pathWithIdentifier, async (req, res) => {
|
|
158
158
|
if (!res.locals.credentials) {
|
|
159
159
|
throw new UnauthorizedError();
|
|
@@ -169,7 +169,7 @@ export class Handler {
|
|
|
169
169
|
}
|
|
170
170
|
if (this.handlers.getCredentialAccount) {
|
|
171
171
|
const handler = this.handlers.getCredentialAccount;
|
|
172
|
-
console.debug(` Enabling GET ${this.pathWithIdentifier}`);
|
|
172
|
+
console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);
|
|
173
173
|
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
174
174
|
if (!res.locals.credentials) {
|
|
175
175
|
throw new UnauthorizedError();
|
|
@@ -185,7 +185,7 @@ export class Handler {
|
|
|
185
185
|
}
|
|
186
186
|
if (this.handlers.acknowledgeWebhooks) {
|
|
187
187
|
const handler = this.handlers.acknowledgeWebhooks;
|
|
188
|
-
console.debug(` Enabling POST ${this.pathWithIdentifier}`);
|
|
188
|
+
console.debug(` Enabling acknowledgeWebhooks at POST ${this.pathWithIdentifier}`);
|
|
189
189
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
190
190
|
assertWebhookParseRequestPayload(req.body);
|
|
191
191
|
const response = await handler({
|
|
@@ -199,7 +199,7 @@ export class Handler {
|
|
|
199
199
|
}
|
|
200
200
|
if (this.handlers.parseWebhooks) {
|
|
201
201
|
const handler = this.handlers.parseWebhooks;
|
|
202
|
-
console.debug(` Enabling POST ${this.pathWithIdentifier}`);
|
|
202
|
+
console.debug(` Enabling parseWebhooks at POST ${this.pathWithIdentifier}`);
|
|
203
203
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
204
204
|
assertWebhookParseRequestPayload(req.body);
|
|
205
205
|
const response = await handler({
|
|
@@ -213,7 +213,7 @@ export class Handler {
|
|
|
213
213
|
}
|
|
214
214
|
if (this.handlers.updateWebhookSubscriptions) {
|
|
215
215
|
const handler = this.handlers.updateWebhookSubscriptions;
|
|
216
|
-
console.debug(` Enabling PUT ${this.pathWithIdentifier}`);
|
|
216
|
+
console.debug(` Enabling updateWebhookSubscriptions at PUT ${this.pathWithIdentifier}`);
|
|
217
217
|
router.put(this.pathWithIdentifier, async (req, res) => {
|
|
218
218
|
if (!res.locals.credentials) {
|
|
219
219
|
throw new UnauthorizedError();
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { HandlersInput } from './handler.js';
|
|
2
|
+
type Options = {
|
|
3
|
+
port?: number;
|
|
4
|
+
};
|
|
2
5
|
export default class Integration {
|
|
3
6
|
private handlers;
|
|
4
7
|
private instance;
|
|
5
8
|
private cache;
|
|
6
|
-
|
|
9
|
+
private port;
|
|
10
|
+
constructor(options?: Options);
|
|
7
11
|
addHandler(path: string, handlers: HandlersInput): void;
|
|
8
12
|
start(): void;
|
|
9
13
|
}
|
|
14
|
+
export {};
|
package/dist/src/integration.js
CHANGED
|
@@ -18,7 +18,9 @@ export default class Integration {
|
|
|
18
18
|
handlers;
|
|
19
19
|
instance = undefined;
|
|
20
20
|
cache = undefined;
|
|
21
|
-
|
|
21
|
+
port;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.port = options.port || 9200;
|
|
22
24
|
this.handlers = [];
|
|
23
25
|
}
|
|
24
26
|
addHandler(path, handlers) {
|
|
@@ -85,7 +87,8 @@ export default class Integration {
|
|
|
85
87
|
app.use(errorsMiddleware);
|
|
86
88
|
// Must be the last handler.
|
|
87
89
|
app.use(notFoundMiddleware);
|
|
88
|
-
|
|
90
|
+
// Start the server.
|
|
91
|
+
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
89
92
|
// Trap exit signals.
|
|
90
93
|
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
91
94
|
process.once(signalType, async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Credentials } from '../middlewares/credentials.js';
|
|
2
|
+
import Logger from '../resources/logger.js';
|
|
2
3
|
/**
|
|
3
4
|
* RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
|
|
4
5
|
* caller's credentials.
|
|
@@ -17,9 +18,11 @@ import { Credentials } from '../middlewares/credentials.js';
|
|
|
17
18
|
*/
|
|
18
19
|
export type RateLimiter = <T>(context: {
|
|
19
20
|
credentials: Credentials;
|
|
21
|
+
logger: Logger;
|
|
20
22
|
}, targetFunction: () => Promise<Response<T>>) => Promise<Response<T>>;
|
|
21
23
|
export interface RequestOptions {
|
|
22
24
|
credentials: Credentials;
|
|
25
|
+
logger: Logger;
|
|
23
26
|
queryParams?: {
|
|
24
27
|
[key: string]: string;
|
|
25
28
|
};
|
|
@@ -28,7 +31,7 @@ export interface RequestOptions {
|
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
33
|
export interface Response<T> {
|
|
31
|
-
|
|
34
|
+
body: T;
|
|
32
35
|
status: number;
|
|
33
36
|
headers: Headers;
|
|
34
37
|
}
|
|
@@ -36,6 +39,7 @@ export declare class Provider {
|
|
|
36
39
|
protected rateLimiter: RateLimiter | undefined;
|
|
37
40
|
protected prepareRequest: (context: {
|
|
38
41
|
credentials: Credentials;
|
|
42
|
+
logger: Logger;
|
|
39
43
|
}) => {
|
|
40
44
|
url: string;
|
|
41
45
|
headers: Record<string, string>;
|
|
@@ -63,7 +63,7 @@ export class Provider {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
async fetchWrapper(endpoint, body, options) {
|
|
66
|
-
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(
|
|
66
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
67
67
|
let absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
68
68
|
if (options.queryParams) {
|
|
69
69
|
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(options.queryParams)}`;
|
|
@@ -89,8 +89,8 @@ export class Provider {
|
|
|
89
89
|
throw buildHttpError(response.status, textResult);
|
|
90
90
|
}
|
|
91
91
|
try {
|
|
92
|
-
const
|
|
93
|
-
return { status: response.status, headers: response.headers,
|
|
92
|
+
const body = response.body ? await response.json() : undefined;
|
|
93
|
+
return { status: response.status, headers: response.headers, body };
|
|
94
94
|
}
|
|
95
95
|
catch {
|
|
96
96
|
throw buildHttpError(400, 'Invalid JSON response');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import Integration from '../src/integration.js';
|
|
4
|
+
describe('Integration', () => {
|
|
5
|
+
describe('constructor', () => {
|
|
6
|
+
it('defaults to port 9200', () => {
|
|
7
|
+
const integration = new Integration();
|
|
8
|
+
assert.equal(integration['port'], 9200);
|
|
9
|
+
});
|
|
10
|
+
it('accepts an optional port', () => {
|
|
11
|
+
const integration = new Integration({ port: 1234 });
|
|
12
|
+
assert.equal(integration['port'], 1234);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('addHandler', () => {
|
|
16
|
+
it('works', () => {
|
|
17
|
+
const integration = new Integration();
|
|
18
|
+
integration.addHandler('/', {});
|
|
19
|
+
const handler = integration['handlers'][0];
|
|
20
|
+
assert.ok(handler);
|
|
21
|
+
assert.equal(handler['path'], '/');
|
|
22
|
+
assert.equal(handler['pathWithIdentifier'], '/');
|
|
23
|
+
assert.deepEqual(handler['handlers'], {});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -15,6 +15,14 @@ describe('filters middleware', () => {
|
|
|
15
15
|
],
|
|
16
16
|
});
|
|
17
17
|
});
|
|
18
|
+
it('decodes URI components', () => {
|
|
19
|
+
const request = { query: { filter: 'status=foo%2Cbar!!%2C%3Fbaz%3D!%3Equx' } };
|
|
20
|
+
const response = { locals: {} };
|
|
21
|
+
middleware(request, response, () => { });
|
|
22
|
+
assert.deepEqual(response.locals, {
|
|
23
|
+
filters: [{ field: 'status', operator: OperatorType.EQUAL, values: ['foo,bar!!,?baz=!>qux'] }],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
18
26
|
it('no data', () => {
|
|
19
27
|
const request = { query: {} };
|
|
20
28
|
const response = { locals: {} };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { Provider } from '../../src/resources/provider.js';
|
|
4
|
+
import Logger from '../../src/resources/logger.js';
|
|
4
5
|
describe('Provider', () => {
|
|
5
6
|
const provider = new Provider({
|
|
6
7
|
prepareRequest: requestOptions => {
|
|
@@ -13,6 +14,7 @@ describe('Provider', () => {
|
|
|
13
14
|
};
|
|
14
15
|
},
|
|
15
16
|
});
|
|
17
|
+
const logger = new Logger();
|
|
16
18
|
it('get', async (context) => {
|
|
17
19
|
const response = new Response('{"data": "value"}', {
|
|
18
20
|
status: 200,
|
|
@@ -21,6 +23,7 @@ describe('Provider', () => {
|
|
|
21
23
|
const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
22
24
|
const actualResponse = await provider.get('/endpoint', {
|
|
23
25
|
credentials: { apiKey: 'apikey#1111' },
|
|
26
|
+
logger: logger,
|
|
24
27
|
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
25
28
|
});
|
|
26
29
|
assert.equal(fetchMock.mock.calls.length, 1);
|
|
@@ -38,7 +41,7 @@ describe('Provider', () => {
|
|
|
38
41
|
},
|
|
39
42
|
},
|
|
40
43
|
]);
|
|
41
|
-
assert.deepEqual(actualResponse, { status: 200, headers: response.headers,
|
|
44
|
+
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
42
45
|
});
|
|
43
46
|
it('post with url encoded body', async (context) => {
|
|
44
47
|
const response = new Response('{"data": "value"}', {
|
|
@@ -50,6 +53,7 @@ describe('Provider', () => {
|
|
|
50
53
|
data: 'createdItemInfo',
|
|
51
54
|
}, {
|
|
52
55
|
credentials: { apiKey: 'apikey#1111' },
|
|
56
|
+
logger: logger,
|
|
53
57
|
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
54
58
|
});
|
|
55
59
|
assert.equal(fetchMock.mock.calls.length, 1);
|
|
@@ -67,7 +71,7 @@ describe('Provider', () => {
|
|
|
67
71
|
},
|
|
68
72
|
},
|
|
69
73
|
]);
|
|
70
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
74
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
71
75
|
});
|
|
72
76
|
it('put with json body', async (context) => {
|
|
73
77
|
const response = new Response('{"data": "value"}', {
|
|
@@ -80,6 +84,7 @@ describe('Provider', () => {
|
|
|
80
84
|
data: 'updatedItemInfo',
|
|
81
85
|
}, {
|
|
82
86
|
credentials: { apiKey: 'apikey#1111' },
|
|
87
|
+
logger: logger,
|
|
83
88
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
84
89
|
});
|
|
85
90
|
assert.equal(fetchMock.mock.calls.length, 1);
|
|
@@ -97,7 +102,7 @@ describe('Provider', () => {
|
|
|
97
102
|
},
|
|
98
103
|
},
|
|
99
104
|
]);
|
|
100
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
105
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
101
106
|
});
|
|
102
107
|
it('patch with query params', async (context) => {
|
|
103
108
|
const response = new Response('{"data": "value"}', {
|
|
@@ -109,6 +114,7 @@ describe('Provider', () => {
|
|
|
109
114
|
data: 'updatedItemInfo',
|
|
110
115
|
}, {
|
|
111
116
|
credentials: { apiKey: 'apikey#1111' },
|
|
117
|
+
logger: logger,
|
|
112
118
|
queryParams: { param1: 'value1', param2: 'value2' },
|
|
113
119
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
114
120
|
});
|
|
@@ -127,7 +133,7 @@ describe('Provider', () => {
|
|
|
127
133
|
},
|
|
128
134
|
},
|
|
129
135
|
]);
|
|
130
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
136
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
131
137
|
});
|
|
132
138
|
it('delete', async (context) => {
|
|
133
139
|
const response = new Response(undefined, {
|
|
@@ -137,6 +143,7 @@ describe('Provider', () => {
|
|
|
137
143
|
const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
138
144
|
const actualResponse = await provider.delete('/endpoint/123', {
|
|
139
145
|
credentials: { apiKey: 'apikey#1111' },
|
|
146
|
+
logger: logger,
|
|
140
147
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
141
148
|
});
|
|
142
149
|
assert.equal(fetchMock.mock.calls.length, 1);
|
|
@@ -154,7 +161,7 @@ describe('Provider', () => {
|
|
|
154
161
|
},
|
|
155
162
|
},
|
|
156
163
|
]);
|
|
157
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
164
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
158
165
|
});
|
|
159
166
|
it('uses rate limiter if provided', async (context) => {
|
|
160
167
|
const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));
|
|
@@ -177,6 +184,7 @@ describe('Provider', () => {
|
|
|
177
184
|
const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
178
185
|
const options = {
|
|
179
186
|
credentials: { apiKey: 'apikey#1111' },
|
|
187
|
+
logger: logger,
|
|
180
188
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
181
189
|
};
|
|
182
190
|
const actualResponse = await rateLimitedProvider.delete('/endpoint/123', options);
|
|
@@ -197,7 +205,7 @@ describe('Provider', () => {
|
|
|
197
205
|
},
|
|
198
206
|
},
|
|
199
207
|
]);
|
|
200
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
208
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
201
209
|
});
|
|
202
210
|
it('throws on invalid json response', async (context) => {
|
|
203
211
|
const response = new Response('{invalidJSON}', {
|
|
@@ -206,6 +214,7 @@ describe('Provider', () => {
|
|
|
206
214
|
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
207
215
|
assert.rejects(() => provider.get('/endpoint/123', {
|
|
208
216
|
credentials: { apiKey: 'apikey#1111' },
|
|
217
|
+
logger: logger,
|
|
209
218
|
}));
|
|
210
219
|
});
|
|
211
220
|
it('throws on status 400', async (context) => {
|
|
@@ -215,6 +224,7 @@ describe('Provider', () => {
|
|
|
215
224
|
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
216
225
|
assert.rejects(() => provider.get('/endpoint/123', {
|
|
217
226
|
credentials: { apiKey: 'apikey#1111' },
|
|
227
|
+
logger: logger,
|
|
218
228
|
}));
|
|
219
229
|
});
|
|
220
230
|
it('throws on status 429', async (context) => {
|
|
@@ -224,6 +234,7 @@ describe('Provider', () => {
|
|
|
224
234
|
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
225
235
|
assert.rejects(() => provider.get('/endpoint/123', {
|
|
226
236
|
credentials: { apiKey: 'apikey#1111' },
|
|
237
|
+
logger: logger,
|
|
227
238
|
}));
|
|
228
239
|
});
|
|
229
240
|
});
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -128,7 +128,7 @@ function assertValidConfiguration(path: Path, pathWithIdentifier: Path, handlers
|
|
|
128
128
|
|
|
129
129
|
if (hasIndividualHandlers && hasCollectionHandlers) {
|
|
130
130
|
throw new InvalidHandler(
|
|
131
|
-
`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both
|
|
131
|
+
`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both.`,
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -221,7 +221,7 @@ export class Handler {
|
|
|
221
221
|
if (this.handlers.getCollection) {
|
|
222
222
|
const handler = this.handlers.getCollection;
|
|
223
223
|
|
|
224
|
-
console.debug(` Enabling GET ${this.path}`);
|
|
224
|
+
console.debug(` Enabling getCollection at GET ${this.path}`);
|
|
225
225
|
|
|
226
226
|
router.get(this.path, async (req, res) => {
|
|
227
227
|
if (!res.locals.credentials) {
|
|
@@ -244,7 +244,7 @@ export class Handler {
|
|
|
244
244
|
if (this.handlers.createItem) {
|
|
245
245
|
const handler = this.handlers.createItem;
|
|
246
246
|
|
|
247
|
-
console.debug(` Enabling POST ${this.path}`);
|
|
247
|
+
console.debug(` Enabling createItem at POST ${this.path}`);
|
|
248
248
|
|
|
249
249
|
router.post(this.path, async (req, res) => {
|
|
250
250
|
if (!res.locals.credentials) {
|
|
@@ -268,7 +268,7 @@ export class Handler {
|
|
|
268
268
|
if (this.handlers.getItem) {
|
|
269
269
|
const handler = this.handlers.getItem;
|
|
270
270
|
|
|
271
|
-
console.debug(` Enabling GET ${this.pathWithIdentifier}`);
|
|
271
|
+
console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`);
|
|
272
272
|
|
|
273
273
|
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
274
274
|
if (!res.locals.credentials) {
|
|
@@ -289,7 +289,7 @@ export class Handler {
|
|
|
289
289
|
if (this.handlers.updateItem) {
|
|
290
290
|
const handler = this.handlers.updateItem;
|
|
291
291
|
|
|
292
|
-
console.debug(` Enabling PATCH ${this.pathWithIdentifier}`);
|
|
292
|
+
console.debug(` Enabling updateItem at PATCH ${this.pathWithIdentifier}`);
|
|
293
293
|
|
|
294
294
|
router.patch(this.pathWithIdentifier, async (req, res) => {
|
|
295
295
|
if (!res.locals.credentials) {
|
|
@@ -313,7 +313,7 @@ export class Handler {
|
|
|
313
313
|
if (this.handlers.deleteItem) {
|
|
314
314
|
const handler = this.handlers.deleteItem;
|
|
315
315
|
|
|
316
|
-
console.debug(` Enabling DELETE ${this.pathWithIdentifier}`);
|
|
316
|
+
console.debug(` Enabling deleteItem at DELETE ${this.pathWithIdentifier}`);
|
|
317
317
|
|
|
318
318
|
router.delete(this.pathWithIdentifier, async (req, res) => {
|
|
319
319
|
if (!res.locals.credentials) {
|
|
@@ -334,7 +334,7 @@ export class Handler {
|
|
|
334
334
|
if (this.handlers.getCredentialAccount) {
|
|
335
335
|
const handler = this.handlers.getCredentialAccount;
|
|
336
336
|
|
|
337
|
-
console.debug(` Enabling GET ${this.pathWithIdentifier}`);
|
|
337
|
+
console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);
|
|
338
338
|
|
|
339
339
|
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
340
340
|
if (!res.locals.credentials) {
|
|
@@ -355,7 +355,7 @@ export class Handler {
|
|
|
355
355
|
if (this.handlers.acknowledgeWebhooks) {
|
|
356
356
|
const handler = this.handlers.acknowledgeWebhooks;
|
|
357
357
|
|
|
358
|
-
console.debug(` Enabling POST ${this.pathWithIdentifier}`);
|
|
358
|
+
console.debug(` Enabling acknowledgeWebhooks at POST ${this.pathWithIdentifier}`);
|
|
359
359
|
|
|
360
360
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
361
361
|
assertWebhookParseRequestPayload(req.body);
|
|
@@ -374,7 +374,7 @@ export class Handler {
|
|
|
374
374
|
if (this.handlers.parseWebhooks) {
|
|
375
375
|
const handler = this.handlers.parseWebhooks;
|
|
376
376
|
|
|
377
|
-
console.debug(` Enabling POST ${this.pathWithIdentifier}`);
|
|
377
|
+
console.debug(` Enabling parseWebhooks at POST ${this.pathWithIdentifier}`);
|
|
378
378
|
|
|
379
379
|
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
380
380
|
assertWebhookParseRequestPayload(req.body);
|
|
@@ -393,7 +393,7 @@ export class Handler {
|
|
|
393
393
|
if (this.handlers.updateWebhookSubscriptions) {
|
|
394
394
|
const handler = this.handlers.updateWebhookSubscriptions;
|
|
395
395
|
|
|
396
|
-
console.debug(` Enabling PUT ${this.pathWithIdentifier}`);
|
|
396
|
+
console.debug(` Enabling updateWebhookSubscriptions at PUT ${this.pathWithIdentifier}`);
|
|
397
397
|
|
|
398
398
|
router.put(this.pathWithIdentifier, async (req, res) => {
|
|
399
399
|
if (!res.locals.credentials) {
|
package/src/integration.ts
CHANGED
|
@@ -18,6 +18,10 @@ function printErrorMessage(message: string) {
|
|
|
18
18
|
console.error(message);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
type Options = {
|
|
22
|
+
port?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
21
25
|
export default class Integration {
|
|
22
26
|
private handlers: Handler[];
|
|
23
27
|
|
|
@@ -25,7 +29,10 @@ export default class Integration {
|
|
|
25
29
|
|
|
26
30
|
private cache: Cache | undefined = undefined;
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
private port: number;
|
|
33
|
+
|
|
34
|
+
constructor(options: Options = {}) {
|
|
35
|
+
this.port = options.port || 9200;
|
|
29
36
|
this.handlers = [];
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -101,9 +108,8 @@ export default class Integration {
|
|
|
101
108
|
// Must be the last handler.
|
|
102
109
|
app.use(notFoundMiddleware);
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
111
|
+
// Start the server.
|
|
112
|
+
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
107
113
|
|
|
108
114
|
// Trap exit signals.
|
|
109
115
|
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildHttpError } from '../errors.js';
|
|
2
2
|
import { Credentials } from '../middlewares/credentials.js';
|
|
3
|
+
import Logger from '../resources/logger.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* RateLimiter is a wrapper function that you can provide to limit the rate of calls to the provider based on the
|
|
@@ -18,25 +19,26 @@ import { Credentials } from '../middlewares/credentials.js';
|
|
|
18
19
|
* @throws HttpError when the provider returns an error.
|
|
19
20
|
*/
|
|
20
21
|
export type RateLimiter = <T>(
|
|
21
|
-
context: { credentials: Credentials },
|
|
22
|
+
context: { credentials: Credentials; logger: Logger },
|
|
22
23
|
targetFunction: () => Promise<Response<T>>,
|
|
23
24
|
) => Promise<Response<T>>;
|
|
24
25
|
|
|
25
26
|
export interface RequestOptions {
|
|
26
27
|
credentials: Credentials;
|
|
28
|
+
logger: Logger;
|
|
27
29
|
queryParams?: { [key: string]: string };
|
|
28
30
|
additionnalheaders?: { [key: string]: string };
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface Response<T> {
|
|
32
|
-
|
|
34
|
+
body: T;
|
|
33
35
|
status: number;
|
|
34
36
|
headers: Headers;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export class Provider {
|
|
38
40
|
protected rateLimiter: RateLimiter | undefined = undefined;
|
|
39
|
-
protected prepareRequest: (context: { credentials: Credentials }) => {
|
|
41
|
+
protected prepareRequest: (context: { credentials: Credentials; logger: Logger }) => {
|
|
40
42
|
url: string;
|
|
41
43
|
headers: Record<string, string>;
|
|
42
44
|
};
|
|
@@ -116,7 +118,7 @@ export class Provider {
|
|
|
116
118
|
body: Record<string, unknown> | null,
|
|
117
119
|
options: RequestOptions & { defaultHeaders: { 'Content-Type': string; Accept: string }; method: string },
|
|
118
120
|
): Promise<Response<T>> {
|
|
119
|
-
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(
|
|
121
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
120
122
|
|
|
121
123
|
let absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
122
124
|
|
|
@@ -149,8 +151,8 @@ export class Provider {
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
try {
|
|
152
|
-
const
|
|
153
|
-
return { status: response.status, headers: response.headers,
|
|
154
|
+
const body: T = response.body ? await response.json() : undefined;
|
|
155
|
+
return { status: response.status, headers: response.headers, body };
|
|
154
156
|
} catch {
|
|
155
157
|
throw buildHttpError(400, 'Invalid JSON response');
|
|
156
158
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import Integration from '../src/integration.js';
|
|
4
|
+
|
|
5
|
+
describe('Integration', () => {
|
|
6
|
+
describe('constructor', () => {
|
|
7
|
+
it('defaults to port 9200', () => {
|
|
8
|
+
const integration = new Integration();
|
|
9
|
+
|
|
10
|
+
assert.equal(integration['port'], 9200);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('accepts an optional port', () => {
|
|
14
|
+
const integration = new Integration({ port: 1234 });
|
|
15
|
+
|
|
16
|
+
assert.equal(integration['port'], 1234);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('addHandler', () => {
|
|
21
|
+
it('works', () => {
|
|
22
|
+
const integration = new Integration();
|
|
23
|
+
|
|
24
|
+
integration.addHandler('/', {});
|
|
25
|
+
|
|
26
|
+
const handler = integration['handlers'][0];
|
|
27
|
+
|
|
28
|
+
assert.ok(handler);
|
|
29
|
+
assert.equal(handler['path'], '/');
|
|
30
|
+
assert.equal(handler['pathWithIdentifier'], '/');
|
|
31
|
+
assert.deepEqual(handler['handlers'], {});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -26,6 +26,23 @@ describe('filters middleware', () => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it('decodes URI components', () => {
|
|
30
|
+
const request = { query: { filter: 'status=foo%2Cbar!!%2C%3Fbaz%3D!%3Equx' } } as express.Request<
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
any,
|
|
33
|
+
object,
|
|
34
|
+
object,
|
|
35
|
+
{ filter: string }
|
|
36
|
+
>;
|
|
37
|
+
const response = { locals: {} } as express.Response;
|
|
38
|
+
|
|
39
|
+
middleware(request, response, () => {});
|
|
40
|
+
|
|
41
|
+
assert.deepEqual(response.locals, {
|
|
42
|
+
filters: [{ field: 'status', operator: OperatorType.EQUAL, values: ['foo,bar!!,?baz=!>qux'] }],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
29
46
|
it('no data', () => {
|
|
30
47
|
const request = { query: {} } as express.Request;
|
|
31
48
|
const response = { locals: {} } as express.Response;
|
|
@@ -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 Logger from '../../src/resources/logger.js';
|
|
5
6
|
|
|
6
7
|
describe('Provider', () => {
|
|
7
8
|
const provider = new Provider({
|
|
@@ -16,6 +17,8 @@ describe('Provider', () => {
|
|
|
16
17
|
},
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
const logger = new Logger();
|
|
21
|
+
|
|
19
22
|
it('get', async context => {
|
|
20
23
|
const response = new Response('{"data": "value"}', {
|
|
21
24
|
status: 200,
|
|
@@ -26,6 +29,7 @@ describe('Provider', () => {
|
|
|
26
29
|
|
|
27
30
|
const actualResponse = await provider.get('/endpoint', {
|
|
28
31
|
credentials: { apiKey: 'apikey#1111' },
|
|
32
|
+
logger: logger,
|
|
29
33
|
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
30
34
|
});
|
|
31
35
|
|
|
@@ -44,7 +48,7 @@ describe('Provider', () => {
|
|
|
44
48
|
},
|
|
45
49
|
},
|
|
46
50
|
]);
|
|
47
|
-
assert.deepEqual(actualResponse, { status: 200, headers: response.headers,
|
|
51
|
+
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
it('post with url encoded body', async context => {
|
|
@@ -62,6 +66,7 @@ describe('Provider', () => {
|
|
|
62
66
|
},
|
|
63
67
|
{
|
|
64
68
|
credentials: { apiKey: 'apikey#1111' },
|
|
69
|
+
logger: logger,
|
|
65
70
|
additionnalheaders: { 'X-Additional-Header': 'value1' },
|
|
66
71
|
},
|
|
67
72
|
);
|
|
@@ -81,7 +86,7 @@ describe('Provider', () => {
|
|
|
81
86
|
},
|
|
82
87
|
},
|
|
83
88
|
]);
|
|
84
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
89
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
85
90
|
});
|
|
86
91
|
|
|
87
92
|
it('put with json body', async context => {
|
|
@@ -100,6 +105,7 @@ describe('Provider', () => {
|
|
|
100
105
|
},
|
|
101
106
|
{
|
|
102
107
|
credentials: { apiKey: 'apikey#1111' },
|
|
108
|
+
logger: logger,
|
|
103
109
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
104
110
|
},
|
|
105
111
|
);
|
|
@@ -119,7 +125,7 @@ describe('Provider', () => {
|
|
|
119
125
|
},
|
|
120
126
|
},
|
|
121
127
|
]);
|
|
122
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
128
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
123
129
|
});
|
|
124
130
|
|
|
125
131
|
it('patch with query params', async context => {
|
|
@@ -137,6 +143,7 @@ describe('Provider', () => {
|
|
|
137
143
|
},
|
|
138
144
|
{
|
|
139
145
|
credentials: { apiKey: 'apikey#1111' },
|
|
146
|
+
logger: logger,
|
|
140
147
|
queryParams: { param1: 'value1', param2: 'value2' },
|
|
141
148
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
142
149
|
},
|
|
@@ -157,7 +164,7 @@ describe('Provider', () => {
|
|
|
157
164
|
},
|
|
158
165
|
},
|
|
159
166
|
]);
|
|
160
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
167
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
161
168
|
});
|
|
162
169
|
|
|
163
170
|
it('delete', async context => {
|
|
@@ -170,6 +177,7 @@ describe('Provider', () => {
|
|
|
170
177
|
|
|
171
178
|
const actualResponse = await provider.delete('/endpoint/123', {
|
|
172
179
|
credentials: { apiKey: 'apikey#1111' },
|
|
180
|
+
logger: logger,
|
|
173
181
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
174
182
|
});
|
|
175
183
|
|
|
@@ -188,7 +196,7 @@ describe('Provider', () => {
|
|
|
188
196
|
},
|
|
189
197
|
},
|
|
190
198
|
]);
|
|
191
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
199
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
192
200
|
});
|
|
193
201
|
|
|
194
202
|
it('uses rate limiter if provided', async context => {
|
|
@@ -216,6 +224,7 @@ describe('Provider', () => {
|
|
|
216
224
|
|
|
217
225
|
const options = {
|
|
218
226
|
credentials: { apiKey: 'apikey#1111' },
|
|
227
|
+
logger: logger,
|
|
219
228
|
additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
|
|
220
229
|
};
|
|
221
230
|
|
|
@@ -238,7 +247,7 @@ describe('Provider', () => {
|
|
|
238
247
|
},
|
|
239
248
|
},
|
|
240
249
|
]);
|
|
241
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
250
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
242
251
|
});
|
|
243
252
|
|
|
244
253
|
it('throws on invalid json response', async context => {
|
|
@@ -251,6 +260,7 @@ describe('Provider', () => {
|
|
|
251
260
|
assert.rejects(() =>
|
|
252
261
|
provider.get('/endpoint/123', {
|
|
253
262
|
credentials: { apiKey: 'apikey#1111' },
|
|
263
|
+
logger: logger,
|
|
254
264
|
}),
|
|
255
265
|
);
|
|
256
266
|
});
|
|
@@ -265,6 +275,7 @@ describe('Provider', () => {
|
|
|
265
275
|
assert.rejects(() =>
|
|
266
276
|
provider.get('/endpoint/123', {
|
|
267
277
|
credentials: { apiKey: 'apikey#1111' },
|
|
278
|
+
logger: logger,
|
|
268
279
|
}),
|
|
269
280
|
);
|
|
270
281
|
});
|
|
@@ -279,6 +290,7 @@ describe('Provider', () => {
|
|
|
279
290
|
assert.rejects(() =>
|
|
280
291
|
provider.get('/endpoint/123', {
|
|
281
292
|
credentials: { apiKey: 'apikey#1111' },
|
|
293
|
+
logger: logger,
|
|
282
294
|
}),
|
|
283
295
|
);
|
|
284
296
|
});
|