@unito/integration-sdk 0.1.10 → 1.0.0
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.d.ts +39 -0
- package/dist/src/handler.js +9 -0
- package/dist/src/httpErrors.d.ts +29 -0
- package/dist/src/httpErrors.js +30 -0
- package/dist/src/index.cjs +289 -39
- package/dist/src/index.d.ts +2 -0
- package/dist/src/integration.d.ts +49 -0
- package/dist/src/integration.js +53 -17
- package/dist/src/middlewares/filters.d.ts +11 -2
- package/dist/src/middlewares/secrets.d.ts +5 -0
- package/dist/src/middlewares/signal.d.ts +15 -0
- package/dist/src/middlewares/signal.js +22 -0
- package/dist/src/resources/cache.d.ts +51 -2
- package/dist/src/resources/cache.js +49 -8
- package/dist/src/resources/context.d.ts +42 -13
- package/dist/src/resources/logger.d.ts +17 -0
- package/dist/src/resources/logger.js +17 -0
- package/dist/src/resources/provider.d.ts +93 -8
- package/dist/src/resources/provider.js +92 -11
- package/dist/test/middlewares/signal.test.d.ts +1 -0
- package/dist/test/middlewares/signal.test.js +20 -0
- package/dist/test/resources/cache.test.js +2 -2
- package/dist/test/resources/provider.test.js +116 -21
- package/package.json +4 -4
- package/src/handler.ts +48 -0
- package/src/httpErrors.ts +30 -0
- package/src/index.ts +2 -0
- package/src/integration.ts +53 -20
- package/src/middlewares/filters.ts +11 -2
- package/src/middlewares/secrets.ts +5 -0
- package/src/middlewares/signal.ts +41 -0
- package/src/resources/cache.ts +49 -11
- package/src/resources/context.ts +50 -33
- package/src/resources/logger.ts +17 -0
- package/src/resources/provider.ts +118 -19
- package/test/middlewares/signal.test.ts +28 -0
- package/test/resources/cache.test.ts +2 -2
- package/test/resources/provider.test.ts +122 -21
package/dist/src/index.cjs
CHANGED
|
@@ -33,6 +33,9 @@ var LogLevel;
|
|
|
33
33
|
LogLevel["LOG"] = "log";
|
|
34
34
|
LogLevel["DEBUG"] = "debug";
|
|
35
35
|
})(LogLevel || (LogLevel = {}));
|
|
36
|
+
/**
|
|
37
|
+
* Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
|
|
38
|
+
*/
|
|
36
39
|
class Logger {
|
|
37
40
|
metadata;
|
|
38
41
|
constructor(metadata = {}) {
|
|
@@ -85,12 +88,26 @@ class Logger {
|
|
|
85
88
|
decorate(metadata) {
|
|
86
89
|
this.metadata = { ...this.metadata, ...metadata };
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Return a copy of the Logger's metadata.
|
|
93
|
+
* @returns The {@link Metadata} associated with the logger.
|
|
94
|
+
*/
|
|
88
95
|
getMetadata() {
|
|
89
96
|
return structuredClone(this.metadata);
|
|
90
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Sets a key-value pair in the metadata. If the key already exists, it will be overwritten.
|
|
100
|
+
*
|
|
101
|
+
* @param key Key of the metadata to be set.
|
|
102
|
+
* May be any string other than 'message', which is reserved for the actual message logged.
|
|
103
|
+
* @param value Value of the metadata to be set.
|
|
104
|
+
*/
|
|
91
105
|
setMetadata(key, value) {
|
|
92
106
|
this.metadata[key] = value;
|
|
93
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Clears the Logger's metadata.
|
|
110
|
+
*/
|
|
94
111
|
clearMetadata() {
|
|
95
112
|
this.metadata = {};
|
|
96
113
|
}
|
|
@@ -119,42 +136,83 @@ class Logger {
|
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
/**
|
|
122
|
-
*
|
|
139
|
+
* The Cache class provides caching capabilities that can be used across your integration.
|
|
140
|
+
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
141
|
+
*
|
|
142
|
+
* @see {@link Cache.create}
|
|
123
143
|
*/
|
|
124
|
-
const caches = [];
|
|
125
|
-
const shutdownCaches = async () => {
|
|
126
|
-
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
127
|
-
};
|
|
128
144
|
class Cache {
|
|
129
145
|
cacheInstance;
|
|
130
146
|
constructor(cacheInstance) {
|
|
131
147
|
this.cacheInstance = cacheInstance;
|
|
132
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Get or fetch a value
|
|
151
|
+
*
|
|
152
|
+
* @param key The key of the value to get
|
|
153
|
+
* @param ttl The time to live of the value in seconds.
|
|
154
|
+
* @param fetchFn The function that can retrieve the original value
|
|
155
|
+
* @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
|
|
156
|
+
* If undefined, 0 or falsy, locking is not preformed
|
|
157
|
+
* @param shouldCacheError A callback being passed errors, controlling whether
|
|
158
|
+
* to cache or not errors. Defaults to never cache.
|
|
159
|
+
*
|
|
160
|
+
* @returns The cached or fetched value
|
|
161
|
+
*/
|
|
133
162
|
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
134
163
|
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
135
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Get a value from the cache.
|
|
167
|
+
*
|
|
168
|
+
* @param key The key of the value to get.
|
|
169
|
+
*
|
|
170
|
+
* @return The value associated with the key, or undefined if
|
|
171
|
+
* no such value exists.
|
|
172
|
+
*/
|
|
136
173
|
getValue(key) {
|
|
137
174
|
return this.cacheInstance.getValue(key);
|
|
138
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Set a value in the cache.
|
|
178
|
+
*
|
|
179
|
+
* @param key The key of the value to set.
|
|
180
|
+
* @param value The value to set.
|
|
181
|
+
* @param ttl The time to live of the value in seconds.
|
|
182
|
+
* By default, the value will not expire
|
|
183
|
+
*
|
|
184
|
+
* @return true if the value was stored, false otherwise.
|
|
185
|
+
*/
|
|
139
186
|
setValue(key, value, ttl) {
|
|
140
187
|
return this.cacheInstance.setValue(key, value, ttl);
|
|
141
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Delete a value from the cache.
|
|
191
|
+
* @param key — The key of the value to set.
|
|
192
|
+
*/
|
|
142
193
|
delValue(key) {
|
|
143
194
|
return this.cacheInstance.delValue(key);
|
|
144
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the TTL of an entry, in ms
|
|
198
|
+
*
|
|
199
|
+
* @param key The key of the entry whose ttl to retrieve
|
|
200
|
+
*
|
|
201
|
+
* @return The remaining TTL on the entry, in ms.
|
|
202
|
+
* undefined if the entry does not exist.
|
|
203
|
+
* 0 if the entry does not expire.
|
|
204
|
+
*/
|
|
145
205
|
getTtl(key) {
|
|
146
206
|
return this.cacheInstance.getTtl(key);
|
|
147
207
|
}
|
|
148
208
|
/**
|
|
149
|
-
* Initializes a
|
|
209
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
150
210
|
*
|
|
151
211
|
* @param redisUrl - The redis url to connect to (optional).
|
|
152
212
|
* @returns A cache instance.
|
|
153
213
|
*/
|
|
154
214
|
static create(redisUrl) {
|
|
155
215
|
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
216
|
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
159
217
|
const correlationId = uuid__namespace.v4();
|
|
160
218
|
const logger = new Logger({ correlation_id: correlationId });
|
|
@@ -172,43 +230,73 @@ class Cache {
|
|
|
172
230
|
}
|
|
173
231
|
}
|
|
174
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Error class meant to be returned by integrations in case of exceptions. These errors will be caught and handled
|
|
235
|
+
* appropriately. Any other error would result in an unhandled server error accompanied by a 500 status code.
|
|
236
|
+
*
|
|
237
|
+
* @field message - The error message
|
|
238
|
+
* @field status - The HTTP status code to return
|
|
239
|
+
*/
|
|
175
240
|
class HttpError extends Error {
|
|
176
241
|
status;
|
|
177
242
|
constructor(message, status) {
|
|
178
243
|
super(message);
|
|
179
244
|
this.status = status;
|
|
245
|
+
this.name = this.constructor.name;
|
|
180
246
|
}
|
|
181
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Used to generate a 400 Bad Request. Usually used when something is missing to properly handle the request.
|
|
250
|
+
*/
|
|
182
251
|
class BadRequestError extends HttpError {
|
|
183
252
|
constructor(message) {
|
|
184
253
|
super(message || 'Bad request', 400);
|
|
185
254
|
}
|
|
186
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Used to generate a 401 Unauthorized. Usually used when the credentials are missing or invalid.
|
|
258
|
+
*/
|
|
187
259
|
class UnauthorizedError extends HttpError {
|
|
188
260
|
constructor(message) {
|
|
189
261
|
super(message || 'Unauthorized', 401);
|
|
190
262
|
}
|
|
191
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Used to generate a 404 Not Found. Usually used when the requested `Item` is not found.
|
|
266
|
+
*/
|
|
192
267
|
class NotFoundError extends HttpError {
|
|
193
268
|
constructor(message) {
|
|
194
269
|
super(message || 'Not found', 404);
|
|
195
270
|
}
|
|
196
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Used to generate a 408 Timeout Error. Usually used when the call length exceeds the received Operation Deadline.
|
|
274
|
+
*/
|
|
197
275
|
class TimeoutError extends HttpError {
|
|
198
276
|
constructor(message) {
|
|
199
277
|
super(message || 'Not found', 408);
|
|
200
278
|
}
|
|
201
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Used to generate a 410 Resource Gone.
|
|
282
|
+
*/
|
|
202
283
|
class ResourceGoneError extends HttpError {
|
|
203
284
|
constructor(message) {
|
|
204
285
|
super(message || 'Resource gone or unavailable', 410);
|
|
205
286
|
}
|
|
206
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Used to generate a 422 Unprocessable Entity. Usually used when an operation is invalid.
|
|
290
|
+
*/
|
|
207
291
|
class UnprocessableEntityError extends HttpError {
|
|
208
292
|
constructor(message) {
|
|
209
293
|
super(message || 'Unprocessable Entity', 422);
|
|
210
294
|
}
|
|
211
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Used to generate a 429 Unprocessable Entity. Usually used when an operation triggers or would trigger a rate limit
|
|
298
|
+
* error on the provider's side.
|
|
299
|
+
*/
|
|
212
300
|
class RateLimitExceededError extends HttpError {
|
|
213
301
|
constructor(message) {
|
|
214
302
|
super(message || 'Rate Limit Exceeded', 429);
|
|
@@ -265,13 +353,13 @@ function buildHttpError(responseStatus, message) {
|
|
|
265
353
|
return httpError;
|
|
266
354
|
}
|
|
267
355
|
|
|
268
|
-
const middleware$
|
|
356
|
+
const middleware$8 = (req, res, next) => {
|
|
269
357
|
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? uuid__namespace.v4();
|
|
270
358
|
next();
|
|
271
359
|
};
|
|
272
360
|
|
|
273
361
|
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
274
|
-
const middleware$
|
|
362
|
+
const middleware$7 = (req, res, next) => {
|
|
275
363
|
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
276
364
|
res.locals.logger = logger;
|
|
277
365
|
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
@@ -288,7 +376,7 @@ const middleware$5 = (req, res, next) => {
|
|
|
288
376
|
};
|
|
289
377
|
|
|
290
378
|
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
291
|
-
const middleware$
|
|
379
|
+
const middleware$6 = (req, res, next) => {
|
|
292
380
|
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
293
381
|
if (credentialsHeader) {
|
|
294
382
|
let credentials;
|
|
@@ -303,6 +391,43 @@ const middleware$4 = (req, res, next) => {
|
|
|
303
391
|
next();
|
|
304
392
|
};
|
|
305
393
|
|
|
394
|
+
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
395
|
+
const middleware$5 = (req, res, next) => {
|
|
396
|
+
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
397
|
+
if (operationDeadlineHeader) {
|
|
398
|
+
// `operationDeadlineHeader` represents a timestamp in the future, in seconds.
|
|
399
|
+
// We need to convert it to a number of milliseconds.
|
|
400
|
+
const deadline = operationDeadlineHeader * 1000 - Date.now();
|
|
401
|
+
if (deadline > 0) {
|
|
402
|
+
res.locals.signal = AbortSignal.timeout(deadline);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
throw new TimeoutError('Request already timed out upon reception');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
// Default to 20s, which is the maximum time frame allowed for an operation by Unito.
|
|
410
|
+
res.locals.signal = AbortSignal.timeout(20000);
|
|
411
|
+
}
|
|
412
|
+
next();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
416
|
+
const middleware$4 = (req, res, next) => {
|
|
417
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
418
|
+
if (secretsHeader) {
|
|
419
|
+
let secrets;
|
|
420
|
+
try {
|
|
421
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
425
|
+
}
|
|
426
|
+
res.locals.secrets = secrets;
|
|
427
|
+
}
|
|
428
|
+
next();
|
|
429
|
+
};
|
|
430
|
+
|
|
306
431
|
const middleware$3 = (req, res, next) => {
|
|
307
432
|
const rawSelect = req.query.select;
|
|
308
433
|
if (typeof rawSelect === 'string') {
|
|
@@ -484,6 +609,7 @@ class Handler {
|
|
|
484
609
|
selects: res.locals.selects,
|
|
485
610
|
filters: res.locals.filters,
|
|
486
611
|
logger: res.locals.logger,
|
|
612
|
+
signal: res.locals.signal,
|
|
487
613
|
params: req.params,
|
|
488
614
|
query: req.query,
|
|
489
615
|
});
|
|
@@ -503,6 +629,7 @@ class Handler {
|
|
|
503
629
|
secrets: res.locals.secrets,
|
|
504
630
|
body: req.body,
|
|
505
631
|
logger: res.locals.logger,
|
|
632
|
+
signal: res.locals.signal,
|
|
506
633
|
params: req.params,
|
|
507
634
|
query: req.query,
|
|
508
635
|
});
|
|
@@ -520,6 +647,7 @@ class Handler {
|
|
|
520
647
|
credentials: res.locals.credentials,
|
|
521
648
|
secrets: res.locals.secrets,
|
|
522
649
|
logger: res.locals.logger,
|
|
650
|
+
signal: res.locals.signal,
|
|
523
651
|
params: req.params,
|
|
524
652
|
query: req.query,
|
|
525
653
|
});
|
|
@@ -539,6 +667,7 @@ class Handler {
|
|
|
539
667
|
secrets: res.locals.secrets,
|
|
540
668
|
body: req.body,
|
|
541
669
|
logger: res.locals.logger,
|
|
670
|
+
signal: res.locals.signal,
|
|
542
671
|
params: req.params,
|
|
543
672
|
query: req.query,
|
|
544
673
|
});
|
|
@@ -556,6 +685,7 @@ class Handler {
|
|
|
556
685
|
credentials: res.locals.credentials,
|
|
557
686
|
secrets: res.locals.secrets,
|
|
558
687
|
logger: res.locals.logger,
|
|
688
|
+
signal: res.locals.signal,
|
|
559
689
|
params: req.params,
|
|
560
690
|
query: req.query,
|
|
561
691
|
});
|
|
@@ -573,6 +703,7 @@ class Handler {
|
|
|
573
703
|
credentials: res.locals.credentials,
|
|
574
704
|
secrets: res.locals.secrets,
|
|
575
705
|
logger: res.locals.logger,
|
|
706
|
+
signal: res.locals.signal,
|
|
576
707
|
params: req.params,
|
|
577
708
|
query: req.query,
|
|
578
709
|
});
|
|
@@ -587,6 +718,7 @@ class Handler {
|
|
|
587
718
|
const response = await handler({
|
|
588
719
|
secrets: res.locals.secrets,
|
|
589
720
|
logger: res.locals.logger,
|
|
721
|
+
signal: res.locals.signal,
|
|
590
722
|
params: req.params,
|
|
591
723
|
query: req.query,
|
|
592
724
|
body: req.body,
|
|
@@ -602,6 +734,7 @@ class Handler {
|
|
|
602
734
|
const response = await handler({
|
|
603
735
|
secrets: res.locals.secrets,
|
|
604
736
|
logger: res.locals.logger,
|
|
737
|
+
signal: res.locals.signal,
|
|
605
738
|
params: req.params,
|
|
606
739
|
query: req.query,
|
|
607
740
|
body: req.body,
|
|
@@ -622,6 +755,7 @@ class Handler {
|
|
|
622
755
|
credentials: res.locals.credentials,
|
|
623
756
|
body: req.body,
|
|
624
757
|
logger: res.locals.logger,
|
|
758
|
+
signal: res.locals.signal,
|
|
625
759
|
params: req.params,
|
|
626
760
|
query: req.query,
|
|
627
761
|
});
|
|
@@ -638,14 +772,56 @@ function printErrorMessage(message) {
|
|
|
638
772
|
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
639
773
|
console.error(message);
|
|
640
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Main class for the Integration SDK providing an abstraction layer between the Integration's Graph definition
|
|
777
|
+
* and the underlying HTTP server.
|
|
778
|
+
*
|
|
779
|
+
* An `Integration` instance can have multiple handlers configured to handle different routes. Upon receiving a request,
|
|
780
|
+
* the Integration will parse the request to extract meaninful information, match the request to the appropriate handler
|
|
781
|
+
* method and forward that information in the form a {@link Context} object.
|
|
782
|
+
* The Integration also offer standardized error handling and logging to help you build a robust
|
|
783
|
+
* and reliable Integration.
|
|
784
|
+
*
|
|
785
|
+
* See our {@link https://dev.unito.io/docs/ | documentation} for more examples on how to build an integration.
|
|
786
|
+
*/
|
|
641
787
|
class Integration {
|
|
642
788
|
handlers;
|
|
643
789
|
instance = undefined;
|
|
644
790
|
port;
|
|
791
|
+
/**
|
|
792
|
+
* Creates a new Integration instance with default port set to 9200.
|
|
793
|
+
*
|
|
794
|
+
* @param options The {@link Options} to configure the Integration instance. Can be used to override the default port.
|
|
795
|
+
*/
|
|
645
796
|
constructor(options = {}) {
|
|
646
797
|
this.port = options.port || 9200;
|
|
647
798
|
this.handlers = [];
|
|
648
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Adds a group of common handlers to the integration.
|
|
802
|
+
*
|
|
803
|
+
* Handlers added to the integration can be one of the following:
|
|
804
|
+
* - `ItemHandlers`: A group of handlers defining the implementation of the Operations available for a given item.
|
|
805
|
+
* - `CredentialAccountHandlers`: A handler returning the CredentialAccount linked to the caller's credentials.
|
|
806
|
+
* - `ParseWebhookHandlers`: A handler parsing the content of an incoming webhook.
|
|
807
|
+
* - `WebhookSubscriptionHandlers`: A handler subscribing or unsubscribing to a particular webhook.
|
|
808
|
+
* - `AcknowledgeWebhookHandlers`: A handler acknowledging the reception of a webhook.
|
|
809
|
+
*
|
|
810
|
+
* To accomodate the fact that ItemHandlers may specify multiple operations, some at the collection level, some at the
|
|
811
|
+
* item level, we need a way to define the route for each of these operations.
|
|
812
|
+
* To achieve this, we assume that if the last part of the path is a variable, then it is the item identifier.
|
|
813
|
+
*
|
|
814
|
+
* @example The following path: `/trainer/:trainerId/pokemons/:pokemonId` will lead to the following
|
|
815
|
+
* routes:
|
|
816
|
+
* - getCollection will be called for `GET /trainer/:trainerId/pokemons/` requests
|
|
817
|
+
* - getItem will be called for `GET /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
818
|
+
* - createItem will be called for `POST /trainer/:trainerId/pokemons/` requests
|
|
819
|
+
* - updateItem will be called for `PATCH /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
820
|
+
* - deleteItem will be called for `DELETE /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
821
|
+
*
|
|
822
|
+
* @param path The path to be used as Route for the handlers.
|
|
823
|
+
* @param handlers The Handlers definition.
|
|
824
|
+
*/
|
|
649
825
|
addHandler(path, handlers) {
|
|
650
826
|
if (this.instance) {
|
|
651
827
|
printErrorMessage(`
|
|
@@ -678,6 +854,13 @@ class Integration {
|
|
|
678
854
|
process.exit(1);
|
|
679
855
|
}
|
|
680
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Starts the server and listens on the specified port (default to 9200).
|
|
859
|
+
*
|
|
860
|
+
* @remarks
|
|
861
|
+
* This function should be called after all the handlers have been added to the integration
|
|
862
|
+
* and any other configuration is completed.
|
|
863
|
+
*/
|
|
681
864
|
start() {
|
|
682
865
|
// Express Server initialization
|
|
683
866
|
const app = express();
|
|
@@ -686,10 +869,12 @@ class Integration {
|
|
|
686
869
|
app.use(express.json());
|
|
687
870
|
// Must be one of the first handlers (to catch all the errors).
|
|
688
871
|
app.use(middleware$1);
|
|
872
|
+
app.use(middleware$8);
|
|
873
|
+
app.use(middleware$7);
|
|
689
874
|
app.use(middleware$6);
|
|
690
|
-
app.use(middleware$5);
|
|
691
875
|
app.use(middleware$4);
|
|
692
876
|
app.use(middleware$3);
|
|
877
|
+
app.use(middleware$5);
|
|
693
878
|
// Load handlers as needed.
|
|
694
879
|
if (this.handlers.length) {
|
|
695
880
|
for (const handler of this.handlers) {
|
|
@@ -710,30 +895,24 @@ class Integration {
|
|
|
710
895
|
app.use(middleware);
|
|
711
896
|
// Start the server.
|
|
712
897
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
713
|
-
// Trap exit signals.
|
|
714
|
-
['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
|
|
715
|
-
process.once(signalType, async () => {
|
|
716
|
-
console.info(`Received termination signal ${signalType}. Exiting.`);
|
|
717
|
-
try {
|
|
718
|
-
if (this.instance) {
|
|
719
|
-
this.instance.close();
|
|
720
|
-
}
|
|
721
|
-
await shutdownCaches();
|
|
722
|
-
}
|
|
723
|
-
catch (e) {
|
|
724
|
-
console.error('Failed to gracefully exit', e);
|
|
725
|
-
}
|
|
726
|
-
process.exit();
|
|
727
|
-
});
|
|
728
|
-
});
|
|
729
898
|
}
|
|
730
899
|
}
|
|
731
900
|
|
|
901
|
+
/**
|
|
902
|
+
* The Provider class is a wrapper around the fetch function to call a provider's HTTP API.
|
|
903
|
+
*
|
|
904
|
+
* Defines methods for the following HTTP methods: GET, POST, PUT, PATCH, DELETE.
|
|
905
|
+
*
|
|
906
|
+
* Needs to be initialized with a prepareRequest function to define the Provider's base URL and any specific headers to
|
|
907
|
+
* add to the requests, and can also be configured to use a provided rate limiting function.
|
|
908
|
+
* @see {@link RateLimiter}
|
|
909
|
+
* @see {@link prepareRequest}
|
|
910
|
+
*/
|
|
732
911
|
class Provider {
|
|
733
912
|
rateLimiter = undefined;
|
|
734
913
|
prepareRequest;
|
|
735
914
|
/**
|
|
736
|
-
*
|
|
915
|
+
* Initializes a Provider with the given options.
|
|
737
916
|
*
|
|
738
917
|
* @property prepareRequest - function to define the Provider's base URL and specific headers to add to the request.
|
|
739
918
|
* @property rateLimiter - function to limit the rate of calls to the provider based on the caller's credentials.
|
|
@@ -742,52 +921,108 @@ class Provider {
|
|
|
742
921
|
this.prepareRequest = options.prepareRequest;
|
|
743
922
|
this.rateLimiter = options.rateLimiter;
|
|
744
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Performs a GET request to the provider.
|
|
926
|
+
*
|
|
927
|
+
* Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
|
|
928
|
+
* adds the following headers:
|
|
929
|
+
* - Accept: application/json
|
|
930
|
+
*
|
|
931
|
+
* @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
|
|
932
|
+
* @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
|
|
933
|
+
* @returns The {@link Response} extracted from the provider.
|
|
934
|
+
*/
|
|
745
935
|
async get(endpoint, options) {
|
|
746
936
|
return this.fetchWrapper(endpoint, null, {
|
|
747
937
|
...options,
|
|
748
938
|
method: 'GET',
|
|
749
939
|
defaultHeaders: {
|
|
750
|
-
'Content-Type': 'application/json',
|
|
751
940
|
Accept: 'application/json',
|
|
752
941
|
},
|
|
753
942
|
});
|
|
754
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* Performs a POST request to the provider.
|
|
946
|
+
*
|
|
947
|
+
* Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
|
|
948
|
+
* adds the following headers:
|
|
949
|
+
* - Content-Type: application/json',
|
|
950
|
+
* - Accept: application/json
|
|
951
|
+
*
|
|
952
|
+
* @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
|
|
953
|
+
* @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
|
|
954
|
+
* @returns The {@link Response} extracted from the provider.
|
|
955
|
+
*/
|
|
755
956
|
async post(endpoint, body, options) {
|
|
756
957
|
return this.fetchWrapper(endpoint, body, {
|
|
757
958
|
...options,
|
|
758
959
|
method: 'POST',
|
|
759
960
|
defaultHeaders: {
|
|
760
|
-
'Content-Type': 'application/
|
|
961
|
+
'Content-Type': 'application/json',
|
|
761
962
|
Accept: 'application/json',
|
|
762
963
|
},
|
|
763
964
|
});
|
|
764
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Performs a PUT request to the provider.
|
|
968
|
+
*
|
|
969
|
+
* Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
|
|
970
|
+
* adds the following headers:
|
|
971
|
+
* - Content-Type: application/json',
|
|
972
|
+
* - Accept: application/json
|
|
973
|
+
*
|
|
974
|
+
* @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
|
|
975
|
+
* @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
|
|
976
|
+
* @returns The {@link Response} extracted from the provider.
|
|
977
|
+
*/
|
|
765
978
|
async put(endpoint, body, options) {
|
|
766
979
|
return this.fetchWrapper(endpoint, body, {
|
|
767
980
|
...options,
|
|
768
981
|
method: 'PUT',
|
|
769
982
|
defaultHeaders: {
|
|
770
|
-
'Content-Type': 'application/
|
|
983
|
+
'Content-Type': 'application/json',
|
|
771
984
|
Accept: 'application/json',
|
|
772
985
|
},
|
|
773
986
|
});
|
|
774
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Performs a PATCH request to the provider.
|
|
990
|
+
*
|
|
991
|
+
* Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
|
|
992
|
+
* adds the following headers:
|
|
993
|
+
* - Content-Type: application/json',
|
|
994
|
+
* - Accept: application/json
|
|
995
|
+
*
|
|
996
|
+
* @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
|
|
997
|
+
* @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
|
|
998
|
+
* @returns The {@link Response} extracted from the provider.
|
|
999
|
+
*/
|
|
775
1000
|
async patch(endpoint, body, options) {
|
|
776
1001
|
return this.fetchWrapper(endpoint, body, {
|
|
777
1002
|
...options,
|
|
778
1003
|
method: 'PATCH',
|
|
779
1004
|
defaultHeaders: {
|
|
780
|
-
'Content-Type': 'application/
|
|
1005
|
+
'Content-Type': 'application/json',
|
|
781
1006
|
Accept: 'application/json',
|
|
782
1007
|
},
|
|
783
1008
|
});
|
|
784
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Performs a DELETE request to the provider.
|
|
1012
|
+
*
|
|
1013
|
+
* Uses the prepareRequest function to get the base URL and any specific headers to add to the request and by default
|
|
1014
|
+
* adds the following headers:
|
|
1015
|
+
* - Accept: application/json
|
|
1016
|
+
*
|
|
1017
|
+
* @param endpoint Path to the provider's resource. Will be added to the URL returned by the prepareRequest function.
|
|
1018
|
+
* @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
|
|
1019
|
+
* @returns The {@link Response} extracted from the provider.
|
|
1020
|
+
*/
|
|
785
1021
|
async delete(endpoint, options) {
|
|
786
1022
|
return this.fetchWrapper(endpoint, null, {
|
|
787
1023
|
...options,
|
|
788
1024
|
method: 'DELETE',
|
|
789
1025
|
defaultHeaders: {
|
|
790
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
791
1026
|
Accept: 'application/json',
|
|
792
1027
|
},
|
|
793
1028
|
});
|
|
@@ -809,11 +1044,26 @@ class Provider {
|
|
|
809
1044
|
}
|
|
810
1045
|
}
|
|
811
1046
|
const callToProvider = async () => {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1047
|
+
let response;
|
|
1048
|
+
try {
|
|
1049
|
+
response = await fetch(absoluteUrl, {
|
|
1050
|
+
method: options.method,
|
|
1051
|
+
signal: options.signal,
|
|
1052
|
+
headers,
|
|
1053
|
+
body: stringifiedBody,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
if (error instanceof Error) {
|
|
1058
|
+
switch (error.name) {
|
|
1059
|
+
case 'AbortError':
|
|
1060
|
+
throw buildHttpError(408, 'Request aborted');
|
|
1061
|
+
case 'TimeoutError':
|
|
1062
|
+
throw buildHttpError(408, 'Request timeout');
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
throw buildHttpError(500, `Unexpected error while calling the provider: "${error}"`);
|
|
1066
|
+
}
|
|
817
1067
|
if (response.status >= 400) {
|
|
818
1068
|
const textResult = await response.text();
|
|
819
1069
|
throw buildHttpError(response.status, textResult);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { Cache } from './resources/cache.js';
|
|
|
3
3
|
export { default as Integration } from './integration.js';
|
|
4
4
|
export * from './handler.js';
|
|
5
5
|
export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
|
|
6
|
+
export type { Secrets } from './middlewares/secrets.js';
|
|
6
7
|
export type { Credentials } from './middlewares/credentials.js';
|
|
7
8
|
export * as HttpErrors from './httpErrors.js';
|
|
8
9
|
export * from './resources/context.js';
|
|
10
|
+
export { type default as Logger } from './resources/logger.js';
|
|
@@ -2,12 +2,61 @@ import { HandlersInput } from './handler.js';
|
|
|
2
2
|
type Options = {
|
|
3
3
|
port?: number;
|
|
4
4
|
};
|
|
5
|
+
/**
|
|
6
|
+
* Main class for the Integration SDK providing an abstraction layer between the Integration's Graph definition
|
|
7
|
+
* and the underlying HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* An `Integration` instance can have multiple handlers configured to handle different routes. Upon receiving a request,
|
|
10
|
+
* the Integration will parse the request to extract meaninful information, match the request to the appropriate handler
|
|
11
|
+
* method and forward that information in the form a {@link Context} object.
|
|
12
|
+
* The Integration also offer standardized error handling and logging to help you build a robust
|
|
13
|
+
* and reliable Integration.
|
|
14
|
+
*
|
|
15
|
+
* See our {@link https://dev.unito.io/docs/ | documentation} for more examples on how to build an integration.
|
|
16
|
+
*/
|
|
5
17
|
export default class Integration {
|
|
6
18
|
private handlers;
|
|
7
19
|
private instance;
|
|
8
20
|
private port;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new Integration instance with default port set to 9200.
|
|
23
|
+
*
|
|
24
|
+
* @param options The {@link Options} to configure the Integration instance. Can be used to override the default port.
|
|
25
|
+
*/
|
|
9
26
|
constructor(options?: Options);
|
|
27
|
+
/**
|
|
28
|
+
* Adds a group of common handlers to the integration.
|
|
29
|
+
*
|
|
30
|
+
* Handlers added to the integration can be one of the following:
|
|
31
|
+
* - `ItemHandlers`: A group of handlers defining the implementation of the Operations available for a given item.
|
|
32
|
+
* - `CredentialAccountHandlers`: A handler returning the CredentialAccount linked to the caller's credentials.
|
|
33
|
+
* - `ParseWebhookHandlers`: A handler parsing the content of an incoming webhook.
|
|
34
|
+
* - `WebhookSubscriptionHandlers`: A handler subscribing or unsubscribing to a particular webhook.
|
|
35
|
+
* - `AcknowledgeWebhookHandlers`: A handler acknowledging the reception of a webhook.
|
|
36
|
+
*
|
|
37
|
+
* To accomodate the fact that ItemHandlers may specify multiple operations, some at the collection level, some at the
|
|
38
|
+
* item level, we need a way to define the route for each of these operations.
|
|
39
|
+
* To achieve this, we assume that if the last part of the path is a variable, then it is the item identifier.
|
|
40
|
+
*
|
|
41
|
+
* @example The following path: `/trainer/:trainerId/pokemons/:pokemonId` will lead to the following
|
|
42
|
+
* routes:
|
|
43
|
+
* - getCollection will be called for `GET /trainer/:trainerId/pokemons/` requests
|
|
44
|
+
* - getItem will be called for `GET /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
45
|
+
* - createItem will be called for `POST /trainer/:trainerId/pokemons/` requests
|
|
46
|
+
* - updateItem will be called for `PATCH /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
47
|
+
* - deleteItem will be called for `DELETE /trainer/:trainerId/pokemons/:pokemonId` requests
|
|
48
|
+
*
|
|
49
|
+
* @param path The path to be used as Route for the handlers.
|
|
50
|
+
* @param handlers The Handlers definition.
|
|
51
|
+
*/
|
|
10
52
|
addHandler(path: string, handlers: HandlersInput): void;
|
|
53
|
+
/**
|
|
54
|
+
* Starts the server and listens on the specified port (default to 9200).
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* This function should be called after all the handlers have been added to the integration
|
|
58
|
+
* and any other configuration is completed.
|
|
59
|
+
*/
|
|
11
60
|
start(): void;
|
|
12
61
|
}
|
|
13
62
|
export {};
|