@unito/integration-sdk 0.1.6 → 0.1.8
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/.eslintrc.cjs +2 -0
- package/dist/src/errors.d.ts +0 -6
- package/dist/src/errors.js +3 -6
- package/dist/src/httpErrors.d.ts +3 -3
- package/dist/src/httpErrors.js +5 -5
- package/dist/src/index.cjs +829 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/integration.d.ts +0 -1
- package/dist/src/integration.js +2 -7
- package/dist/src/middlewares/errors.d.ts +9 -1
- package/dist/src/middlewares/errors.js +28 -13
- package/dist/src/middlewares/finish.d.ts +3 -1
- package/dist/src/middlewares/finish.js +25 -2
- package/dist/src/resources/cache.d.ts +18 -4
- package/dist/src/resources/cache.js +52 -21
- package/dist/src/resources/logger.d.ts +49 -10
- package/dist/src/resources/logger.js +64 -18
- package/dist/src/resources/provider.d.ts +1 -1
- package/dist/src/resources/provider.js +2 -2
- package/dist/test/errors.test.js +1 -0
- package/dist/test/middlewares/errors.test.js +1 -1
- package/dist/test/resources/cache.test.js +7 -15
- package/dist/test/resources/logger.test.js +53 -6
- package/dist/test/resources/provider.test.js +6 -6
- package/package.json +11 -4
- package/src/errors.ts +2 -6
- package/src/httpErrors.ts +6 -6
- package/src/index.ts +1 -0
- package/src/integration.ts +2 -9
- package/src/middlewares/errors.ts +39 -14
- package/src/middlewares/finish.ts +28 -3
- package/src/resources/cache.ts +66 -23
- package/src/resources/logger.ts +84 -24
- package/src/resources/provider.ts +3 -3
- package/test/errors.test.ts +1 -0
- package/test/middlewares/errors.test.ts +1 -1
- package/test/resources/cache.test.ts +7 -17
- package/test/resources/logger.test.ts +60 -7
- package/test/resources/provider.test.ts +6 -6
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var integrationApi = require('@unito/integration-api');
|
|
4
|
+
var cachette = require('cachette');
|
|
5
|
+
var uuid = require('uuid');
|
|
6
|
+
var express = require('express');
|
|
7
|
+
|
|
8
|
+
function _interopNamespaceDefault(e) {
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var integrationApi__namespace = /*#__PURE__*/_interopNamespaceDefault(integrationApi);
|
|
26
|
+
var uuid__namespace = /*#__PURE__*/_interopNamespaceDefault(uuid);
|
|
27
|
+
|
|
28
|
+
var LogLevel;
|
|
29
|
+
(function (LogLevel) {
|
|
30
|
+
LogLevel["ERROR"] = "error";
|
|
31
|
+
LogLevel["WARN"] = "warn";
|
|
32
|
+
LogLevel["INFO"] = "info";
|
|
33
|
+
LogLevel["LOG"] = "log";
|
|
34
|
+
LogLevel["DEBUG"] = "debug";
|
|
35
|
+
})(LogLevel || (LogLevel = {}));
|
|
36
|
+
class Logger {
|
|
37
|
+
metadata;
|
|
38
|
+
constructor(metadata = {}) {
|
|
39
|
+
this.metadata = structuredClone(metadata);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Logs a message with the 'log' log level.
|
|
43
|
+
* @param message The message to be logged.
|
|
44
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
45
|
+
*/
|
|
46
|
+
log(message, metadata) {
|
|
47
|
+
this.send(LogLevel.LOG, message, metadata);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Logs an error message with the 'error' log level.
|
|
51
|
+
* @param message The error message to be logged.
|
|
52
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
53
|
+
*/
|
|
54
|
+
error(message, metadata) {
|
|
55
|
+
this.send(LogLevel.ERROR, message, metadata);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Logs a warning message with the 'warn' log level.
|
|
59
|
+
* @param message The warning message to be logged.
|
|
60
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
61
|
+
*/
|
|
62
|
+
warn(message, metadata) {
|
|
63
|
+
this.send(LogLevel.WARN, message, metadata);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Logs an informational message with the 'info' log level.
|
|
67
|
+
* @param message The informational message to be logged.
|
|
68
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
69
|
+
*/
|
|
70
|
+
info(message, metadata) {
|
|
71
|
+
this.send(LogLevel.INFO, message, metadata);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Logs a debug message with the 'debug' log level.
|
|
75
|
+
* @param message The debug message to be logged.
|
|
76
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
77
|
+
*/
|
|
78
|
+
debug(message, metadata) {
|
|
79
|
+
this.send(LogLevel.DEBUG, message, metadata);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Decorates the logger with additional metadata.
|
|
83
|
+
* @param metadata Additional metadata to be added to the logger.
|
|
84
|
+
*/
|
|
85
|
+
decorate(metadata) {
|
|
86
|
+
this.metadata = { ...this.metadata, ...metadata };
|
|
87
|
+
}
|
|
88
|
+
getMetadata() {
|
|
89
|
+
return structuredClone(this.metadata);
|
|
90
|
+
}
|
|
91
|
+
setMetadata(key, value) {
|
|
92
|
+
this.metadata[key] = value;
|
|
93
|
+
}
|
|
94
|
+
clearMetadata() {
|
|
95
|
+
this.metadata = {};
|
|
96
|
+
}
|
|
97
|
+
send(logLevel, message, metadata) {
|
|
98
|
+
const processedMessage = this.snakifyKeys({
|
|
99
|
+
...this.metadata,
|
|
100
|
+
...metadata,
|
|
101
|
+
message,
|
|
102
|
+
});
|
|
103
|
+
if (process.env.NODE_ENV === 'development') {
|
|
104
|
+
console[logLevel](JSON.stringify(processedMessage, null, 2));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console[logLevel](JSON.stringify(processedMessage));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
snakifyKeys(value) {
|
|
111
|
+
const result = {};
|
|
112
|
+
for (const key in value) {
|
|
113
|
+
const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
|
|
114
|
+
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
115
|
+
result[snakifiedKey] = deepValue;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
123
|
+
*/
|
|
124
|
+
const caches = [];
|
|
125
|
+
const shutdownCaches = async () => {
|
|
126
|
+
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
127
|
+
};
|
|
128
|
+
class Cache {
|
|
129
|
+
cacheInstance;
|
|
130
|
+
constructor(cacheInstance) {
|
|
131
|
+
this.cacheInstance = cacheInstance;
|
|
132
|
+
}
|
|
133
|
+
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
134
|
+
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
135
|
+
}
|
|
136
|
+
getValue(key) {
|
|
137
|
+
return this.cacheInstance.getValue(key);
|
|
138
|
+
}
|
|
139
|
+
setValue(key, value, ttl) {
|
|
140
|
+
return this.cacheInstance.setValue(key, value, ttl);
|
|
141
|
+
}
|
|
142
|
+
delValue(key) {
|
|
143
|
+
return this.cacheInstance.delValue(key);
|
|
144
|
+
}
|
|
145
|
+
getTtl(key) {
|
|
146
|
+
return this.cacheInstance.getTtl(key);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Initializes a WriteThroughCache instance with the provided redis url if present, or a LocalCache otherwise.
|
|
150
|
+
*
|
|
151
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
152
|
+
* @returns A cache instance.
|
|
153
|
+
*/
|
|
154
|
+
static create(redisUrl) {
|
|
155
|
+
const cacheInstance = redisUrl ? new cachette.WriteThroughCache(redisUrl) : new cachette.LocalCache();
|
|
156
|
+
// Push to the array of caches for graceful shutdown on exit signals.
|
|
157
|
+
caches.push(cacheInstance);
|
|
158
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
159
|
+
const correlationId = uuid__namespace.v4();
|
|
160
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
161
|
+
cacheInstance
|
|
162
|
+
.on('info', message => {
|
|
163
|
+
logger.info(message);
|
|
164
|
+
})
|
|
165
|
+
.on('warn', message => {
|
|
166
|
+
logger.warn(message);
|
|
167
|
+
})
|
|
168
|
+
.on('error', message => {
|
|
169
|
+
logger.error(message);
|
|
170
|
+
});
|
|
171
|
+
return new Cache(cacheInstance);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class HttpError extends Error {
|
|
176
|
+
status;
|
|
177
|
+
constructor(message, status) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.status = status;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
class BadRequestError extends HttpError {
|
|
183
|
+
constructor(message) {
|
|
184
|
+
super(message || 'Bad request', 400);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
class UnauthorizedError extends HttpError {
|
|
188
|
+
constructor(message) {
|
|
189
|
+
super(message || 'Unauthorized', 401);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
class NotFoundError extends HttpError {
|
|
193
|
+
constructor(message) {
|
|
194
|
+
super(message || 'Not found', 404);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
class TimeoutError extends HttpError {
|
|
198
|
+
constructor(message) {
|
|
199
|
+
super(message || 'Not found', 408);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
class ResourceGoneError extends HttpError {
|
|
203
|
+
constructor(message) {
|
|
204
|
+
super(message || 'Resource gone or unavailable', 410);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
class UnprocessableEntityError extends HttpError {
|
|
208
|
+
constructor(message) {
|
|
209
|
+
super(message || 'Unprocessable Entity', 422);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
class RateLimitExceededError extends HttpError {
|
|
213
|
+
constructor(message) {
|
|
214
|
+
super(message || 'Rate Limit Exceeded', 429);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
var httpErrors = /*#__PURE__*/Object.freeze({
|
|
219
|
+
__proto__: null,
|
|
220
|
+
BadRequestError: BadRequestError,
|
|
221
|
+
HttpError: HttpError,
|
|
222
|
+
NotFoundError: NotFoundError,
|
|
223
|
+
RateLimitExceededError: RateLimitExceededError,
|
|
224
|
+
ResourceGoneError: ResourceGoneError,
|
|
225
|
+
TimeoutError: TimeoutError,
|
|
226
|
+
UnauthorizedError: UnauthorizedError,
|
|
227
|
+
UnprocessableEntityError: UnprocessableEntityError
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
class InvalidHandler extends Error {
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Processes provider response codes and returns the corresponding errors to be translated further in our responses
|
|
234
|
+
*
|
|
235
|
+
* @param responseStatus the reponseStatus of the request. Any HTTP response code passed here will result in an error!
|
|
236
|
+
* @param message The message returned by the provider
|
|
237
|
+
*/
|
|
238
|
+
// Keep in errors.ts instead of httpErrors.ts because we do not need to export it outside of the sdk
|
|
239
|
+
function buildHttpError(responseStatus, message) {
|
|
240
|
+
let httpError;
|
|
241
|
+
if (responseStatus === 400) {
|
|
242
|
+
httpError = new BadRequestError(message);
|
|
243
|
+
}
|
|
244
|
+
else if (responseStatus === 401 || responseStatus === 403) {
|
|
245
|
+
httpError = new UnauthorizedError(message);
|
|
246
|
+
}
|
|
247
|
+
else if (responseStatus === 404) {
|
|
248
|
+
httpError = new NotFoundError(message);
|
|
249
|
+
}
|
|
250
|
+
else if (responseStatus === 408) {
|
|
251
|
+
httpError = new TimeoutError(message);
|
|
252
|
+
}
|
|
253
|
+
else if (responseStatus === 410) {
|
|
254
|
+
httpError = new ResourceGoneError(message);
|
|
255
|
+
}
|
|
256
|
+
else if (responseStatus === 422) {
|
|
257
|
+
httpError = new UnprocessableEntityError(message);
|
|
258
|
+
}
|
|
259
|
+
else if (responseStatus === 429) {
|
|
260
|
+
httpError = new RateLimitExceededError(message);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
httpError = new HttpError(message, responseStatus);
|
|
264
|
+
}
|
|
265
|
+
return httpError;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const middleware$6 = (req, res, next) => {
|
|
269
|
+
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? uuid__namespace.v4();
|
|
270
|
+
next();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
274
|
+
const middleware$5 = (req, res, next) => {
|
|
275
|
+
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
276
|
+
res.locals.logger = logger;
|
|
277
|
+
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
278
|
+
if (typeof rawAdditionalContext === 'string') {
|
|
279
|
+
try {
|
|
280
|
+
const additionalContext = JSON.parse(rawAdditionalContext);
|
|
281
|
+
logger.decorate(additionalContext);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
logger.warn(`Failed parsing header ${ADDITIONAL_CONTEXT_HEADER}: ${rawAdditionalContext}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
next();
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
291
|
+
const middleware$4 = (req, res, next) => {
|
|
292
|
+
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
293
|
+
if (credentialsHeader) {
|
|
294
|
+
let credentials;
|
|
295
|
+
try {
|
|
296
|
+
credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
|
|
300
|
+
}
|
|
301
|
+
res.locals.credentials = credentials;
|
|
302
|
+
}
|
|
303
|
+
next();
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const middleware$3 = (req, res, next) => {
|
|
307
|
+
const rawSelect = req.query.select;
|
|
308
|
+
if (typeof rawSelect === 'string') {
|
|
309
|
+
res.locals.selects = rawSelect.split(',');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
res.locals.selects = [];
|
|
313
|
+
}
|
|
314
|
+
next();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const middleware$2 = (err, _req, res, next) => {
|
|
318
|
+
if (res.headersSent) {
|
|
319
|
+
return next(err);
|
|
320
|
+
}
|
|
321
|
+
let error;
|
|
322
|
+
if (err instanceof HttpError) {
|
|
323
|
+
error = {
|
|
324
|
+
code: err.status.toString(),
|
|
325
|
+
message: err.message,
|
|
326
|
+
details: {
|
|
327
|
+
stack: err.stack,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
error = {
|
|
333
|
+
code: '500',
|
|
334
|
+
message: 'Oops! Something went wrong',
|
|
335
|
+
originalError: {
|
|
336
|
+
code: err.name,
|
|
337
|
+
message: err.message,
|
|
338
|
+
details: {
|
|
339
|
+
stack: err.stack,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
res.locals.error = structuredClone(error);
|
|
345
|
+
// Keep the stack details in development for the Debugger
|
|
346
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
347
|
+
delete error.details;
|
|
348
|
+
delete error.originalError?.details;
|
|
349
|
+
}
|
|
350
|
+
res.status(Number(error.code)).json(error);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const middleware$1 = (req, res, next) => {
|
|
354
|
+
if (req.originalUrl !== '/health') {
|
|
355
|
+
res.on('finish', function () {
|
|
356
|
+
const error = res.locals.error;
|
|
357
|
+
const message = `${req.method} ${req.originalUrl} ${res.statusCode}`;
|
|
358
|
+
const metadata = {
|
|
359
|
+
// Use reserved and standard attributes of Datadog
|
|
360
|
+
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
361
|
+
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
362
|
+
...(error
|
|
363
|
+
? {
|
|
364
|
+
error: {
|
|
365
|
+
kind: error.message,
|
|
366
|
+
stack: (error.originalError?.details?.stack ?? error.details?.stack),
|
|
367
|
+
message: error.originalError?.message ?? error.message,
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
: {}),
|
|
371
|
+
};
|
|
372
|
+
if ([404, 429].includes(res.statusCode)) {
|
|
373
|
+
res.locals.logger.warn(message, metadata);
|
|
374
|
+
}
|
|
375
|
+
else if (res.statusCode >= 400) {
|
|
376
|
+
res.locals.logger.error(message, metadata);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
res.locals.logger.info(message, metadata);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
next();
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const middleware = (req, res, _next) => {
|
|
387
|
+
const error = {
|
|
388
|
+
code: '404',
|
|
389
|
+
message: `Path ${req.path} not found.`,
|
|
390
|
+
};
|
|
391
|
+
res.status(404).json(error);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
function assertValidPath(path) {
|
|
395
|
+
if (!path.startsWith('/')) {
|
|
396
|
+
throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
|
|
397
|
+
}
|
|
398
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
399
|
+
throw new InvalidHandler(`The provided path '${path}' is invalid. Paths must not end with a '/'.`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function assertValidConfiguration(path, pathWithIdentifier, handlers) {
|
|
403
|
+
if (path === pathWithIdentifier) {
|
|
404
|
+
const individualHandlers = ['getItem', 'updateItem', 'deleteItem'];
|
|
405
|
+
const collectionHandlers = ['getCollection', 'createItem'];
|
|
406
|
+
const hasIndividualHandlers = individualHandlers.some(handler => handler in handlers);
|
|
407
|
+
const hasCollectionHandlers = collectionHandlers.some(handler => handler in handlers);
|
|
408
|
+
if (hasIndividualHandlers && hasCollectionHandlers) {
|
|
409
|
+
throw new InvalidHandler(`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function parsePath(path) {
|
|
414
|
+
const pathParts = path.split('/');
|
|
415
|
+
const lastPart = pathParts.at(-1);
|
|
416
|
+
if (pathParts.length > 1 && lastPart && lastPart.startsWith(':')) {
|
|
417
|
+
pathParts.pop();
|
|
418
|
+
}
|
|
419
|
+
return { pathWithIdentifier: path, path: pathParts.join('/') };
|
|
420
|
+
}
|
|
421
|
+
function assertCreateItemRequestPayload(body) {
|
|
422
|
+
if (typeof body !== 'object' || body === null) {
|
|
423
|
+
throw new BadRequestError('Invalid CreateItemRequestPayload');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function assertUpdateItemRequestPayload(body) {
|
|
427
|
+
if (typeof body !== 'object' || body === null) {
|
|
428
|
+
throw new BadRequestError('Invalid UpdateItemRequestPayload');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function assertWebhookParseRequestPayload(body) {
|
|
432
|
+
if (typeof body !== 'object' || body === null) {
|
|
433
|
+
throw new BadRequestError('Invalid WebhookParseRequestPayload');
|
|
434
|
+
}
|
|
435
|
+
if (!('payload' in body) || body.payload !== 'string') {
|
|
436
|
+
throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
|
|
437
|
+
}
|
|
438
|
+
if (!('url' in body) || body.url !== 'string') {
|
|
439
|
+
throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function assertWebhookSubscriptionRequestPayload(body) {
|
|
443
|
+
if (typeof body !== 'object' || body === null) {
|
|
444
|
+
throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
|
|
445
|
+
}
|
|
446
|
+
if (!('itemPath' in body) || body.itemPath !== 'string') {
|
|
447
|
+
throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
|
|
448
|
+
}
|
|
449
|
+
if (!('targetUrl' in body) || body.targetUrl !== 'string') {
|
|
450
|
+
throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
|
|
451
|
+
}
|
|
452
|
+
if (!('action' in body) || body.action !== 'string') {
|
|
453
|
+
throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
|
|
454
|
+
}
|
|
455
|
+
if (!['start', 'stop'].includes(body.action)) {
|
|
456
|
+
throw new BadRequestError("Invalid value for 'action' property in WebhookSubscriptionRequestPayload");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
class Handler {
|
|
460
|
+
path;
|
|
461
|
+
pathWithIdentifier;
|
|
462
|
+
handlers;
|
|
463
|
+
constructor(inputPath, handlers) {
|
|
464
|
+
assertValidPath(inputPath);
|
|
465
|
+
const { pathWithIdentifier, path } = parsePath(inputPath);
|
|
466
|
+
assertValidConfiguration(path, pathWithIdentifier, handlers);
|
|
467
|
+
this.pathWithIdentifier = pathWithIdentifier;
|
|
468
|
+
this.path = path;
|
|
469
|
+
this.handlers = handlers;
|
|
470
|
+
}
|
|
471
|
+
generate() {
|
|
472
|
+
const router = express.Router({ caseSensitive: true });
|
|
473
|
+
console.debug(`\x1b[33mMounting handler at path ${this.pathWithIdentifier}`);
|
|
474
|
+
if (this.handlers.getCollection) {
|
|
475
|
+
const handler = this.handlers.getCollection;
|
|
476
|
+
console.debug(` Enabling getCollection at GET ${this.path}`);
|
|
477
|
+
router.get(this.path, async (req, res) => {
|
|
478
|
+
if (!res.locals.credentials) {
|
|
479
|
+
throw new UnauthorizedError();
|
|
480
|
+
}
|
|
481
|
+
const collection = await handler({
|
|
482
|
+
credentials: res.locals.credentials,
|
|
483
|
+
selects: res.locals.selects,
|
|
484
|
+
filters: res.locals.filters,
|
|
485
|
+
logger: res.locals.logger,
|
|
486
|
+
params: req.params,
|
|
487
|
+
query: req.query,
|
|
488
|
+
});
|
|
489
|
+
res.status(200).send(collection);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (this.handlers.createItem) {
|
|
493
|
+
const handler = this.handlers.createItem;
|
|
494
|
+
console.debug(` Enabling createItem at POST ${this.path}`);
|
|
495
|
+
router.post(this.path, async (req, res) => {
|
|
496
|
+
if (!res.locals.credentials) {
|
|
497
|
+
throw new UnauthorizedError();
|
|
498
|
+
}
|
|
499
|
+
assertCreateItemRequestPayload(req.body);
|
|
500
|
+
const createItemSummary = await handler({
|
|
501
|
+
credentials: res.locals.credentials,
|
|
502
|
+
body: req.body,
|
|
503
|
+
logger: res.locals.logger,
|
|
504
|
+
params: req.params,
|
|
505
|
+
query: req.query,
|
|
506
|
+
});
|
|
507
|
+
res.status(201).send(createItemSummary);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (this.handlers.getItem) {
|
|
511
|
+
const handler = this.handlers.getItem;
|
|
512
|
+
console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`);
|
|
513
|
+
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
514
|
+
if (!res.locals.credentials) {
|
|
515
|
+
throw new UnauthorizedError();
|
|
516
|
+
}
|
|
517
|
+
const item = await handler({
|
|
518
|
+
credentials: res.locals.credentials,
|
|
519
|
+
logger: res.locals.logger,
|
|
520
|
+
params: req.params,
|
|
521
|
+
query: req.query,
|
|
522
|
+
});
|
|
523
|
+
res.status(200).send(item);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (this.handlers.updateItem) {
|
|
527
|
+
const handler = this.handlers.updateItem;
|
|
528
|
+
console.debug(` Enabling updateItem at PATCH ${this.pathWithIdentifier}`);
|
|
529
|
+
router.patch(this.pathWithIdentifier, async (req, res) => {
|
|
530
|
+
if (!res.locals.credentials) {
|
|
531
|
+
throw new UnauthorizedError();
|
|
532
|
+
}
|
|
533
|
+
assertUpdateItemRequestPayload(req.body);
|
|
534
|
+
const item = await handler({
|
|
535
|
+
credentials: res.locals.credentials,
|
|
536
|
+
body: req.body,
|
|
537
|
+
logger: res.locals.logger,
|
|
538
|
+
params: req.params,
|
|
539
|
+
query: req.query,
|
|
540
|
+
});
|
|
541
|
+
res.status(200).send(item);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (this.handlers.deleteItem) {
|
|
545
|
+
const handler = this.handlers.deleteItem;
|
|
546
|
+
console.debug(` Enabling deleteItem at DELETE ${this.pathWithIdentifier}`);
|
|
547
|
+
router.delete(this.pathWithIdentifier, async (req, res) => {
|
|
548
|
+
if (!res.locals.credentials) {
|
|
549
|
+
throw new UnauthorizedError();
|
|
550
|
+
}
|
|
551
|
+
await handler({
|
|
552
|
+
credentials: res.locals.credentials,
|
|
553
|
+
logger: res.locals.logger,
|
|
554
|
+
params: req.params,
|
|
555
|
+
query: req.query,
|
|
556
|
+
});
|
|
557
|
+
res.status(204).send(null);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (this.handlers.getCredentialAccount) {
|
|
561
|
+
const handler = this.handlers.getCredentialAccount;
|
|
562
|
+
console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`);
|
|
563
|
+
router.get(this.pathWithIdentifier, async (req, res) => {
|
|
564
|
+
if (!res.locals.credentials) {
|
|
565
|
+
throw new UnauthorizedError();
|
|
566
|
+
}
|
|
567
|
+
const credentialAccount = await handler({
|
|
568
|
+
credentials: res.locals.credentials,
|
|
569
|
+
logger: res.locals.logger,
|
|
570
|
+
params: req.params,
|
|
571
|
+
query: req.query,
|
|
572
|
+
});
|
|
573
|
+
res.status(200).send(credentialAccount);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (this.handlers.acknowledgeWebhooks) {
|
|
577
|
+
const handler = this.handlers.acknowledgeWebhooks;
|
|
578
|
+
console.debug(` Enabling acknowledgeWebhooks at POST ${this.pathWithIdentifier}`);
|
|
579
|
+
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
580
|
+
assertWebhookParseRequestPayload(req.body);
|
|
581
|
+
const response = await handler({
|
|
582
|
+
logger: res.locals.logger,
|
|
583
|
+
params: req.params,
|
|
584
|
+
query: req.query,
|
|
585
|
+
body: req.body,
|
|
586
|
+
});
|
|
587
|
+
res.status(200).send(response);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (this.handlers.parseWebhooks) {
|
|
591
|
+
const handler = this.handlers.parseWebhooks;
|
|
592
|
+
console.debug(` Enabling parseWebhooks at POST ${this.pathWithIdentifier}`);
|
|
593
|
+
router.post(this.pathWithIdentifier, async (req, res) => {
|
|
594
|
+
assertWebhookParseRequestPayload(req.body);
|
|
595
|
+
const response = await handler({
|
|
596
|
+
logger: res.locals.logger,
|
|
597
|
+
params: req.params,
|
|
598
|
+
query: req.query,
|
|
599
|
+
body: req.body,
|
|
600
|
+
});
|
|
601
|
+
res.status(200).send(response);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (this.handlers.updateWebhookSubscriptions) {
|
|
605
|
+
const handler = this.handlers.updateWebhookSubscriptions;
|
|
606
|
+
console.debug(` Enabling updateWebhookSubscriptions at PUT ${this.pathWithIdentifier}`);
|
|
607
|
+
router.put(this.pathWithIdentifier, async (req, res) => {
|
|
608
|
+
if (!res.locals.credentials) {
|
|
609
|
+
throw new UnauthorizedError();
|
|
610
|
+
}
|
|
611
|
+
assertWebhookSubscriptionRequestPayload(req.body);
|
|
612
|
+
const response = await handler({
|
|
613
|
+
credentials: res.locals.credentials,
|
|
614
|
+
body: req.body,
|
|
615
|
+
logger: res.locals.logger,
|
|
616
|
+
params: req.params,
|
|
617
|
+
query: req.query,
|
|
618
|
+
});
|
|
619
|
+
res.status(204).send(response);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
console.debug(`\x1b[0m`);
|
|
623
|
+
return router;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function printErrorMessage(message) {
|
|
628
|
+
console.error();
|
|
629
|
+
console.error(`\x1b[31m Oups! Something went wrong! \x1b[0m`);
|
|
630
|
+
console.error(message);
|
|
631
|
+
}
|
|
632
|
+
class Integration {
|
|
633
|
+
handlers;
|
|
634
|
+
instance = undefined;
|
|
635
|
+
port;
|
|
636
|
+
constructor(options = {}) {
|
|
637
|
+
this.port = options.port || 9200;
|
|
638
|
+
this.handlers = [];
|
|
639
|
+
}
|
|
640
|
+
addHandler(path, handlers) {
|
|
641
|
+
if (this.instance) {
|
|
642
|
+
printErrorMessage(`
|
|
643
|
+
It seems like you're trying to add a handler after the server has already started. This is probably
|
|
644
|
+
a mistake as calling the start() function essentially starts the server and ignore any further change.
|
|
645
|
+
To fix this error, move all your addHandler() calls before the start() function.
|
|
646
|
+
`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
this.handlers.push(new Handler(path, handlers));
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
if (error instanceof InvalidHandler) {
|
|
654
|
+
printErrorMessage(`
|
|
655
|
+
It seems like you're trying to add an invalid handler. The exact error message is:
|
|
656
|
+
|
|
657
|
+
> ${error.message}
|
|
658
|
+
|
|
659
|
+
You must address this issue before trying again.
|
|
660
|
+
`);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
printErrorMessage(`
|
|
664
|
+
An unexpected error happened as we were trying to add your handler. The exact error message is;
|
|
665
|
+
|
|
666
|
+
> ${error.message}
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
start() {
|
|
673
|
+
// Express Server initialization
|
|
674
|
+
const app = express();
|
|
675
|
+
// Parse query strings with https://github.com/ljharb/qs.
|
|
676
|
+
app.set('query parser', 'extended');
|
|
677
|
+
app.use(express.json());
|
|
678
|
+
// Must be one of the first handlers (to catch all the errors).
|
|
679
|
+
app.use(middleware$1);
|
|
680
|
+
app.use(middleware$6);
|
|
681
|
+
app.use(middleware$5);
|
|
682
|
+
app.use(middleware$4);
|
|
683
|
+
app.use(middleware$3);
|
|
684
|
+
// Load handlers as needed.
|
|
685
|
+
if (this.handlers.length) {
|
|
686
|
+
for (const handler of this.handlers) {
|
|
687
|
+
app.use(handler.generate());
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
printErrorMessage(`
|
|
692
|
+
It seems like you're trying to start the server without any handler. This is probably a mistake as the
|
|
693
|
+
server wouldn't expose any route. To fix this error, add at least one handler before calling the start()
|
|
694
|
+
function.
|
|
695
|
+
`);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
// Must be the (last - 1) handler.
|
|
699
|
+
app.use(middleware$2);
|
|
700
|
+
// Must be the last handler.
|
|
701
|
+
app.use(middleware);
|
|
702
|
+
// Start the server.
|
|
703
|
+
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
704
|
+
// Trap exit signals.
|
|
705
|
+
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
706
|
+
process.once(signalType, async () => {
|
|
707
|
+
console.info(`Received termination signal ${signalType}. Exiting.`);
|
|
708
|
+
try {
|
|
709
|
+
if (this.instance) {
|
|
710
|
+
this.instance.close();
|
|
711
|
+
}
|
|
712
|
+
await shutdownCaches();
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
console.error('Failed to gracefully exit', e);
|
|
716
|
+
}
|
|
717
|
+
process.exit();
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
class Provider {
|
|
724
|
+
rateLimiter = undefined;
|
|
725
|
+
prepareRequest;
|
|
726
|
+
/**
|
|
727
|
+
* Initialize a Provider with the given options.
|
|
728
|
+
*
|
|
729
|
+
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
730
|
+
* @property rateLimiter - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
731
|
+
*/
|
|
732
|
+
constructor(options) {
|
|
733
|
+
this.prepareRequest = options.prepareRequest;
|
|
734
|
+
this.rateLimiter = options.rateLimiter;
|
|
735
|
+
}
|
|
736
|
+
async get(endpoint, options) {
|
|
737
|
+
return this.fetchWrapper(endpoint, null, {
|
|
738
|
+
...options,
|
|
739
|
+
method: 'GET',
|
|
740
|
+
defaultHeaders: {
|
|
741
|
+
'Content-Type': 'application/json',
|
|
742
|
+
Accept: 'application/json',
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async post(endpoint, body, options) {
|
|
747
|
+
return this.fetchWrapper(endpoint, body, {
|
|
748
|
+
...options,
|
|
749
|
+
method: 'POST',
|
|
750
|
+
defaultHeaders: {
|
|
751
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
752
|
+
Accept: 'application/json',
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
async put(endpoint, body, options) {
|
|
757
|
+
return this.fetchWrapper(endpoint, body, {
|
|
758
|
+
...options,
|
|
759
|
+
method: 'PUT',
|
|
760
|
+
defaultHeaders: {
|
|
761
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
762
|
+
Accept: 'application/json',
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async patch(endpoint, body, options) {
|
|
767
|
+
return this.fetchWrapper(endpoint, body, {
|
|
768
|
+
...options,
|
|
769
|
+
method: 'PATCH',
|
|
770
|
+
defaultHeaders: {
|
|
771
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
772
|
+
Accept: 'application/json',
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
async delete(endpoint, options) {
|
|
777
|
+
return this.fetchWrapper(endpoint, null, {
|
|
778
|
+
...options,
|
|
779
|
+
method: 'DELETE',
|
|
780
|
+
defaultHeaders: {
|
|
781
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
782
|
+
Accept: 'application/json',
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async fetchWrapper(endpoint, body, options) {
|
|
787
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
788
|
+
let absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
789
|
+
if (options.queryParams) {
|
|
790
|
+
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(options.queryParams)}`;
|
|
791
|
+
}
|
|
792
|
+
const headers = { ...options.defaultHeaders, ...providerHeaders, ...options.additionnalheaders };
|
|
793
|
+
let stringifiedBody = null;
|
|
794
|
+
if (body) {
|
|
795
|
+
if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
|
|
796
|
+
stringifiedBody = new URLSearchParams(body).toString();
|
|
797
|
+
}
|
|
798
|
+
else if (headers['Content-Type'] === 'application/json') {
|
|
799
|
+
stringifiedBody = JSON.stringify(body);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const callToProvider = async () => {
|
|
803
|
+
const response = await fetch(absoluteUrl, {
|
|
804
|
+
method: options.method,
|
|
805
|
+
headers,
|
|
806
|
+
body: stringifiedBody,
|
|
807
|
+
});
|
|
808
|
+
if (response.status >= 400) {
|
|
809
|
+
const textResult = await response.text();
|
|
810
|
+
throw buildHttpError(response.status, textResult);
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
const body = response.body ? await response.json() : undefined;
|
|
814
|
+
return { status: response.status, headers: response.headers, body };
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
throw buildHttpError(400, 'Invalid JSON response');
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
exports.Api = integrationApi__namespace;
|
|
825
|
+
exports.Cache = Cache;
|
|
826
|
+
exports.Handler = Handler;
|
|
827
|
+
exports.HttpErrors = httpErrors;
|
|
828
|
+
exports.Integration = Integration;
|
|
829
|
+
exports.Provider = Provider;
|