@unito/integration-sdk 2.3.15 → 2.4.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/httpErrors.d.ts +11 -3
- package/dist/src/httpErrors.js +8 -5
- package/dist/src/index.cjs +31 -95
- package/dist/src/index.d.ts +1 -1
- package/dist/src/middlewares/errors.js +3 -0
- package/dist/src/resources/cache.d.ts +9 -64
- package/dist/src/resources/cache.js +20 -90
- package/dist/src/resources/provider.d.ts +0 -1
- package/dist/test/middlewares/errors.test.js +24 -1
- package/dist/test/resources/cache.test.js +2 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/httpErrors.ts +8 -5
- package/src/index.ts +1 -1
- package/src/middlewares/errors.ts +4 -0
- package/src/resources/cache.ts +27 -108
- package/src/resources/provider.ts +0 -1
- package/test/middlewares/errors.test.ts +28 -1
- package/test/resources/cache.test.ts +2 -3
package/dist/src/httpErrors.d.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @field message - The error message
|
|
6
6
|
* @field status - The HTTP status code to return
|
|
7
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
7
8
|
*/
|
|
8
9
|
export declare class HttpError extends Error {
|
|
9
10
|
readonly status: number;
|
|
10
|
-
|
|
11
|
+
readonly retryAfter: number | undefined;
|
|
12
|
+
constructor(message: string, status: number, options?: {
|
|
13
|
+
retryAfter?: number;
|
|
14
|
+
});
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Used to generate a 400 Bad Request. Usually used when something is missing to properly handle the request.
|
|
@@ -57,12 +61,16 @@ export declare class UnprocessableEntityError extends HttpError {
|
|
|
57
61
|
* Used to generate a 423 Provider Instance Locked.
|
|
58
62
|
*/
|
|
59
63
|
export declare class ProviderInstanceLockedError extends HttpError {
|
|
60
|
-
constructor(message?: string
|
|
64
|
+
constructor(message?: string, options?: {
|
|
65
|
+
retryAfter?: number;
|
|
66
|
+
});
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Used to generate a 429 Rate Limit Exceeded. Usually used when an operation triggers or would trigger a rate limit
|
|
64
70
|
* error on the provider's side.
|
|
65
71
|
*/
|
|
66
72
|
export declare class RateLimitExceededError extends HttpError {
|
|
67
|
-
constructor(message?: string
|
|
73
|
+
constructor(message?: string, options?: {
|
|
74
|
+
retryAfter?: number;
|
|
75
|
+
});
|
|
68
76
|
}
|
package/dist/src/httpErrors.js
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @field message - The error message
|
|
6
6
|
* @field status - The HTTP status code to return
|
|
7
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
7
8
|
*/
|
|
8
9
|
export class HttpError extends Error {
|
|
9
10
|
status;
|
|
10
|
-
|
|
11
|
+
retryAfter = undefined;
|
|
12
|
+
constructor(message, status, options = {}) {
|
|
11
13
|
super(message);
|
|
12
14
|
this.status = status;
|
|
13
15
|
this.name = this.constructor.name;
|
|
16
|
+
this.retryAfter = options.retryAfter;
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
@@ -75,8 +78,8 @@ export class UnprocessableEntityError extends HttpError {
|
|
|
75
78
|
* Used to generate a 423 Provider Instance Locked.
|
|
76
79
|
*/
|
|
77
80
|
export class ProviderInstanceLockedError extends HttpError {
|
|
78
|
-
constructor(message) {
|
|
79
|
-
super(message || 'Provider instance locked or unavailable', 423);
|
|
81
|
+
constructor(message, options = {}) {
|
|
82
|
+
super(message || 'Provider instance locked or unavailable', 423, options);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
@@ -84,7 +87,7 @@ export class ProviderInstanceLockedError extends HttpError {
|
|
|
84
87
|
* error on the provider's side.
|
|
85
88
|
*/
|
|
86
89
|
export class RateLimitExceededError extends HttpError {
|
|
87
|
-
constructor(message) {
|
|
88
|
-
super(message || 'Rate Limit Exceeded', 429);
|
|
90
|
+
constructor(message, options = {}) {
|
|
91
|
+
super(message || 'Rate Limit Exceeded', 429, options);
|
|
89
92
|
}
|
|
90
93
|
}
|
package/dist/src/index.cjs
CHANGED
|
@@ -264,99 +264,29 @@ class Logger {
|
|
|
264
264
|
const NULL_LOGGER = new Logger({}, true);
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
268
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
267
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
269
268
|
*
|
|
270
|
-
* @
|
|
269
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
270
|
+
* @returns A cache instance.
|
|
271
271
|
*/
|
|
272
|
-
|
|
273
|
-
cacheInstance;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
* @returns The cached or fetched value
|
|
289
|
-
*/
|
|
290
|
-
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
291
|
-
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Get a value from the cache.
|
|
295
|
-
*
|
|
296
|
-
* @param key The key of the value to get.
|
|
297
|
-
*
|
|
298
|
-
* @return The value associated with the key, or undefined if
|
|
299
|
-
* no such value exists.
|
|
300
|
-
*/
|
|
301
|
-
getValue(key) {
|
|
302
|
-
return this.cacheInstance.getValue(key);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Set a value in the cache.
|
|
306
|
-
*
|
|
307
|
-
* @param key The key of the value to set.
|
|
308
|
-
* @param value The value to set.
|
|
309
|
-
* @param ttl The time to live of the value in seconds.
|
|
310
|
-
* By default, the value will not expire
|
|
311
|
-
*
|
|
312
|
-
* @return true if the value was stored, false otherwise.
|
|
313
|
-
*/
|
|
314
|
-
setValue(key, value, ttl) {
|
|
315
|
-
return this.cacheInstance.setValue(key, value, ttl);
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Delete a value from the cache.
|
|
319
|
-
* @param key — The key of the value to set.
|
|
320
|
-
*/
|
|
321
|
-
delValue(key) {
|
|
322
|
-
return this.cacheInstance.delValue(key);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Get the TTL of an entry, in ms
|
|
326
|
-
*
|
|
327
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
328
|
-
*
|
|
329
|
-
* @return The remaining TTL on the entry, in ms.
|
|
330
|
-
* undefined if the entry does not exist.
|
|
331
|
-
* 0 if the entry does not expire.
|
|
332
|
-
*/
|
|
333
|
-
getTtl(key) {
|
|
334
|
-
return this.cacheInstance.getTtl(key);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
338
|
-
*
|
|
339
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
340
|
-
* @returns A cache instance.
|
|
341
|
-
*/
|
|
342
|
-
static create(redisUrl) {
|
|
343
|
-
const cacheInstance = redisUrl ? new cachette.RedisCache(redisUrl) : new cachette.LocalCache();
|
|
344
|
-
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
345
|
-
const correlationId = crypto.randomUUID();
|
|
346
|
-
const logger = new Logger({ correlation_id: correlationId });
|
|
347
|
-
cacheInstance
|
|
348
|
-
.on('info', message => {
|
|
349
|
-
logger.info(message);
|
|
350
|
-
})
|
|
351
|
-
.on('warn', message => {
|
|
352
|
-
logger.warn(message);
|
|
353
|
-
})
|
|
354
|
-
.on('error', message => {
|
|
355
|
-
logger.error(message);
|
|
356
|
-
});
|
|
357
|
-
return new Cache(cacheInstance);
|
|
358
|
-
}
|
|
272
|
+
function create(redisUrl) {
|
|
273
|
+
const cacheInstance = redisUrl ? new cachette.RedisCache(redisUrl) : new cachette.LocalCache();
|
|
274
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
275
|
+
const correlationId = crypto.randomUUID();
|
|
276
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
277
|
+
cacheInstance
|
|
278
|
+
.on('info', message => {
|
|
279
|
+
logger.info(message);
|
|
280
|
+
})
|
|
281
|
+
.on('warn', message => {
|
|
282
|
+
logger.warn(message);
|
|
283
|
+
})
|
|
284
|
+
.on('error', message => {
|
|
285
|
+
logger.error(message);
|
|
286
|
+
});
|
|
287
|
+
return cacheInstance;
|
|
359
288
|
}
|
|
289
|
+
const Cache = { create };
|
|
360
290
|
|
|
361
291
|
/**
|
|
362
292
|
* Error class meant to be returned by integrations in case of exceptions. These errors will be caught and handled
|
|
@@ -364,13 +294,16 @@ class Cache {
|
|
|
364
294
|
*
|
|
365
295
|
* @field message - The error message
|
|
366
296
|
* @field status - The HTTP status code to return
|
|
297
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
367
298
|
*/
|
|
368
299
|
class HttpError extends Error {
|
|
369
300
|
status;
|
|
370
|
-
|
|
301
|
+
retryAfter = undefined;
|
|
302
|
+
constructor(message, status, options = {}) {
|
|
371
303
|
super(message);
|
|
372
304
|
this.status = status;
|
|
373
305
|
this.name = this.constructor.name;
|
|
306
|
+
this.retryAfter = options.retryAfter;
|
|
374
307
|
}
|
|
375
308
|
}
|
|
376
309
|
/**
|
|
@@ -435,8 +368,8 @@ class UnprocessableEntityError extends HttpError {
|
|
|
435
368
|
* Used to generate a 423 Provider Instance Locked.
|
|
436
369
|
*/
|
|
437
370
|
class ProviderInstanceLockedError extends HttpError {
|
|
438
|
-
constructor(message) {
|
|
439
|
-
super(message || 'Provider instance locked or unavailable', 423);
|
|
371
|
+
constructor(message, options = {}) {
|
|
372
|
+
super(message || 'Provider instance locked or unavailable', 423, options);
|
|
440
373
|
}
|
|
441
374
|
}
|
|
442
375
|
/**
|
|
@@ -444,8 +377,8 @@ class ProviderInstanceLockedError extends HttpError {
|
|
|
444
377
|
* error on the provider's side.
|
|
445
378
|
*/
|
|
446
379
|
class RateLimitExceededError extends HttpError {
|
|
447
|
-
constructor(message) {
|
|
448
|
-
super(message || 'Rate Limit Exceeded', 429);
|
|
380
|
+
constructor(message, options = {}) {
|
|
381
|
+
super(message || 'Rate Limit Exceeded', 429, options);
|
|
449
382
|
}
|
|
450
383
|
}
|
|
451
384
|
|
|
@@ -876,6 +809,9 @@ function onError(err, _req, res, next) {
|
|
|
876
809
|
}
|
|
877
810
|
let error;
|
|
878
811
|
if (err instanceof HttpError) {
|
|
812
|
+
if (err.retryAfter) {
|
|
813
|
+
res.setHeader('Retry-After', String(err.retryAfter));
|
|
814
|
+
}
|
|
879
815
|
error = {
|
|
880
816
|
code: err.status.toString(),
|
|
881
817
|
message: err.message,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as Api from '@unito/integration-api';
|
|
2
|
-
export { Cache } from './resources/cache.js';
|
|
2
|
+
export { Cache, type CacheInstance } 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';
|
|
@@ -1,67 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CacheInstance } from 'cachette';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
3
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
5
4
|
*
|
|
6
|
-
* @
|
|
5
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
6
|
+
* @returns A cache instance.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* @param key The key of the value to get
|
|
15
|
-
* @param ttl The time to live of the value in seconds.
|
|
16
|
-
* @param fetchFn The function that can retrieve the original value
|
|
17
|
-
* @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
|
|
18
|
-
* If undefined, 0 or falsy, locking is not preformed
|
|
19
|
-
* @param shouldCacheError A callback being passed errors, controlling whether
|
|
20
|
-
* to cache or not errors. Defaults to never cache.
|
|
21
|
-
*
|
|
22
|
-
* @returns The cached or fetched value
|
|
23
|
-
*/
|
|
24
|
-
getOrFetchValue<F extends FetchingFunction = FetchingFunction>(key: string, ttl: number, fetcher: F, lockTtl?: number, shouldCacheError?: (err: Error) => boolean): Promise<ReturnType<F>>;
|
|
25
|
-
/**
|
|
26
|
-
* Get a value from the cache.
|
|
27
|
-
*
|
|
28
|
-
* @param key The key of the value to get.
|
|
29
|
-
*
|
|
30
|
-
* @return The value associated with the key, or undefined if
|
|
31
|
-
* no such value exists.
|
|
32
|
-
*/
|
|
33
|
-
getValue(key: string): Promise<CachableValue>;
|
|
34
|
-
/**
|
|
35
|
-
* Set a value in the cache.
|
|
36
|
-
*
|
|
37
|
-
* @param key The key of the value to set.
|
|
38
|
-
* @param value The value to set.
|
|
39
|
-
* @param ttl The time to live of the value in seconds.
|
|
40
|
-
* By default, the value will not expire
|
|
41
|
-
*
|
|
42
|
-
* @return true if the value was stored, false otherwise.
|
|
43
|
-
*/
|
|
44
|
-
setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean>;
|
|
45
|
-
/**
|
|
46
|
-
* Delete a value from the cache.
|
|
47
|
-
* @param key — The key of the value to set.
|
|
48
|
-
*/
|
|
49
|
-
delValue(key: string): Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
* Get the TTL of an entry, in ms
|
|
52
|
-
*
|
|
53
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
54
|
-
*
|
|
55
|
-
* @return The remaining TTL on the entry, in ms.
|
|
56
|
-
* undefined if the entry does not exist.
|
|
57
|
-
* 0 if the entry does not expire.
|
|
58
|
-
*/
|
|
59
|
-
getTtl(key: string): Promise<number | undefined>;
|
|
60
|
-
/**
|
|
61
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
62
|
-
*
|
|
63
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
64
|
-
* @returns A cache instance.
|
|
65
|
-
*/
|
|
66
|
-
static create(redisUrl?: string): Cache;
|
|
67
|
-
}
|
|
8
|
+
declare function create(redisUrl?: string): CacheInstance;
|
|
9
|
+
export declare const Cache: {
|
|
10
|
+
create: typeof create;
|
|
11
|
+
};
|
|
12
|
+
export type { CacheInstance };
|
|
@@ -2,96 +2,26 @@ import { LocalCache, RedisCache } from 'cachette';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
5
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
7
6
|
*
|
|
8
|
-
* @
|
|
7
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
8
|
+
* @returns A cache instance.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
cacheInstance;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* @returns The cached or fetched value
|
|
27
|
-
*/
|
|
28
|
-
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
29
|
-
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get a value from the cache.
|
|
33
|
-
*
|
|
34
|
-
* @param key The key of the value to get.
|
|
35
|
-
*
|
|
36
|
-
* @return The value associated with the key, or undefined if
|
|
37
|
-
* no such value exists.
|
|
38
|
-
*/
|
|
39
|
-
getValue(key) {
|
|
40
|
-
return this.cacheInstance.getValue(key);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Set a value in the cache.
|
|
44
|
-
*
|
|
45
|
-
* @param key The key of the value to set.
|
|
46
|
-
* @param value The value to set.
|
|
47
|
-
* @param ttl The time to live of the value in seconds.
|
|
48
|
-
* By default, the value will not expire
|
|
49
|
-
*
|
|
50
|
-
* @return true if the value was stored, false otherwise.
|
|
51
|
-
*/
|
|
52
|
-
setValue(key, value, ttl) {
|
|
53
|
-
return this.cacheInstance.setValue(key, value, ttl);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Delete a value from the cache.
|
|
57
|
-
* @param key — The key of the value to set.
|
|
58
|
-
*/
|
|
59
|
-
delValue(key) {
|
|
60
|
-
return this.cacheInstance.delValue(key);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Get the TTL of an entry, in ms
|
|
64
|
-
*
|
|
65
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
66
|
-
*
|
|
67
|
-
* @return The remaining TTL on the entry, in ms.
|
|
68
|
-
* undefined if the entry does not exist.
|
|
69
|
-
* 0 if the entry does not expire.
|
|
70
|
-
*/
|
|
71
|
-
getTtl(key) {
|
|
72
|
-
return this.cacheInstance.getTtl(key);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
76
|
-
*
|
|
77
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
78
|
-
* @returns A cache instance.
|
|
79
|
-
*/
|
|
80
|
-
static create(redisUrl) {
|
|
81
|
-
const cacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
82
|
-
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
83
|
-
const correlationId = crypto.randomUUID();
|
|
84
|
-
const logger = new Logger({ correlation_id: correlationId });
|
|
85
|
-
cacheInstance
|
|
86
|
-
.on('info', message => {
|
|
87
|
-
logger.info(message);
|
|
88
|
-
})
|
|
89
|
-
.on('warn', message => {
|
|
90
|
-
logger.warn(message);
|
|
91
|
-
})
|
|
92
|
-
.on('error', message => {
|
|
93
|
-
logger.error(message);
|
|
94
|
-
});
|
|
95
|
-
return new Cache(cacheInstance);
|
|
96
|
-
}
|
|
10
|
+
function create(redisUrl) {
|
|
11
|
+
const cacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
12
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
13
|
+
const correlationId = crypto.randomUUID();
|
|
14
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
15
|
+
cacheInstance
|
|
16
|
+
.on('info', message => {
|
|
17
|
+
logger.info(message);
|
|
18
|
+
})
|
|
19
|
+
.on('warn', message => {
|
|
20
|
+
logger.warn(message);
|
|
21
|
+
})
|
|
22
|
+
.on('error', message => {
|
|
23
|
+
logger.error(message);
|
|
24
|
+
});
|
|
25
|
+
return cacheInstance;
|
|
97
26
|
}
|
|
27
|
+
export const Cache = { create };
|
|
@@ -17,7 +17,6 @@ import Logger from '../resources/logger.js';
|
|
|
17
17
|
* @param targetFunction - The function to call the provider.
|
|
18
18
|
* @returns The response from the provider.
|
|
19
19
|
* @throws RateLimitExceededError when the rate limit is exceeded.
|
|
20
|
-
* @throws WouldExceedRateLimitError when the next call would exceed the rate limit.
|
|
21
20
|
* @throws HttpError when the provider returns an error.
|
|
22
21
|
*/
|
|
23
22
|
export type RateLimiter = <T>(options: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import onError from '../../src/middlewares/errors.js';
|
|
4
|
-
import { HttpError } from '../../src/httpErrors.js';
|
|
4
|
+
import { HttpError, RateLimitExceededError } from '../../src/httpErrors.js';
|
|
5
5
|
describe('errors middleware', () => {
|
|
6
6
|
it('headers sent, do nothing', () => {
|
|
7
7
|
let actualStatus;
|
|
@@ -41,6 +41,29 @@ describe('errors middleware', () => {
|
|
|
41
41
|
assert.strictEqual(actualStatus, 429);
|
|
42
42
|
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
43
43
|
});
|
|
44
|
+
it('handles retry-after header', () => {
|
|
45
|
+
let actualStatus;
|
|
46
|
+
let actualJson;
|
|
47
|
+
const response = {
|
|
48
|
+
headersSent: false,
|
|
49
|
+
status: (code) => {
|
|
50
|
+
actualStatus = code;
|
|
51
|
+
return {
|
|
52
|
+
json: (json) => {
|
|
53
|
+
actualJson = json;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
locals: { logger: { error: () => undefined } },
|
|
58
|
+
setHeader: (name, value) => {
|
|
59
|
+
assert.strictEqual(name, 'Retry-After');
|
|
60
|
+
assert.strictEqual(value, '1234');
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
onError(new RateLimitExceededError('httpError', { retryAfter: 1234 }), {}, response, () => { });
|
|
64
|
+
assert.strictEqual(actualStatus, 429);
|
|
65
|
+
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
66
|
+
});
|
|
44
67
|
it('handles other error', () => {
|
|
45
68
|
let actualStatus;
|
|
46
69
|
let actualJson;
|
|
@@ -6,9 +6,8 @@ describe('Cache', () => {
|
|
|
6
6
|
describe('initializeCache', () => {
|
|
7
7
|
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
8
8
|
const cache = Cache.create();
|
|
9
|
-
assert.ok(cache instanceof
|
|
10
|
-
|
|
11
|
-
await cache['cacheInstance'].quit();
|
|
9
|
+
assert.ok(cache instanceof LocalCache);
|
|
10
|
+
await cache.quit();
|
|
12
11
|
});
|
|
13
12
|
it('redis url tries to return a RedisCache', () => {
|
|
14
13
|
assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts"],"version":"5.
|
|
1
|
+
{"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
package/src/httpErrors.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @field message - The error message
|
|
6
6
|
* @field status - The HTTP status code to return
|
|
7
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
7
8
|
*/
|
|
8
9
|
export class HttpError extends Error {
|
|
9
10
|
readonly status: number;
|
|
11
|
+
readonly retryAfter: number | undefined = undefined;
|
|
10
12
|
|
|
11
|
-
constructor(message: string, status: number) {
|
|
13
|
+
constructor(message: string, status: number, options: { retryAfter?: number } = {}) {
|
|
12
14
|
super(message);
|
|
13
15
|
this.status = status;
|
|
14
16
|
this.name = this.constructor.name;
|
|
17
|
+
this.retryAfter = options.retryAfter;
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -84,8 +87,8 @@ export class UnprocessableEntityError extends HttpError {
|
|
|
84
87
|
* Used to generate a 423 Provider Instance Locked.
|
|
85
88
|
*/
|
|
86
89
|
export class ProviderInstanceLockedError extends HttpError {
|
|
87
|
-
constructor(message?: string) {
|
|
88
|
-
super(message || 'Provider instance locked or unavailable', 423);
|
|
90
|
+
constructor(message?: string, options: { retryAfter?: number } = {}) {
|
|
91
|
+
super(message || 'Provider instance locked or unavailable', 423, options);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -94,7 +97,7 @@ export class ProviderInstanceLockedError extends HttpError {
|
|
|
94
97
|
* error on the provider's side.
|
|
95
98
|
*/
|
|
96
99
|
export class RateLimitExceededError extends HttpError {
|
|
97
|
-
constructor(message?: string) {
|
|
98
|
-
super(message || 'Rate Limit Exceeded', 429);
|
|
100
|
+
constructor(message?: string, options: { retryAfter?: number } = {}) {
|
|
101
|
+
super(message || 'Rate Limit Exceeded', 429, options);
|
|
99
102
|
}
|
|
100
103
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* c8 ignore start */
|
|
2
2
|
export * as Api from '@unito/integration-api';
|
|
3
3
|
|
|
4
|
-
export { Cache } from './resources/cache.js';
|
|
4
|
+
export { Cache, type CacheInstance } from './resources/cache.js';
|
|
5
5
|
export { default as Integration } from './integration.js';
|
|
6
6
|
export * from './handler.js';
|
|
7
7
|
export {
|
|
@@ -20,6 +20,10 @@ function onError(err: Error, _req: Request, res: Response, next: NextFunction) {
|
|
|
20
20
|
let error: ApiError;
|
|
21
21
|
|
|
22
22
|
if (err instanceof HttpError) {
|
|
23
|
+
if (err.retryAfter) {
|
|
24
|
+
res.setHeader('Retry-After', String(err.retryAfter));
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
error = {
|
|
24
28
|
code: err.status.toString(),
|
|
25
29
|
message: err.message,
|
package/src/resources/cache.ts
CHANGED
|
@@ -1,115 +1,34 @@
|
|
|
1
|
-
import { LocalCache, CacheInstance,
|
|
1
|
+
import { LocalCache, CacheInstance, RedisCache } from 'cachette';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
6
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
8
7
|
*
|
|
9
|
-
* @
|
|
8
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
9
|
+
* @returns A cache instance.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
public getOrFetchValue<F extends FetchingFunction = FetchingFunction>(
|
|
32
|
-
key: string,
|
|
33
|
-
ttl: number,
|
|
34
|
-
fetcher: F,
|
|
35
|
-
lockTtl?: number,
|
|
36
|
-
shouldCacheError?: (err: Error) => boolean,
|
|
37
|
-
): Promise<ReturnType<F>> {
|
|
38
|
-
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get a value from the cache.
|
|
43
|
-
*
|
|
44
|
-
* @param key The key of the value to get.
|
|
45
|
-
*
|
|
46
|
-
* @return The value associated with the key, or undefined if
|
|
47
|
-
* no such value exists.
|
|
48
|
-
*/
|
|
49
|
-
public getValue(key: string): Promise<CachableValue> {
|
|
50
|
-
return this.cacheInstance.getValue(key);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Set a value in the cache.
|
|
55
|
-
*
|
|
56
|
-
* @param key The key of the value to set.
|
|
57
|
-
* @param value The value to set.
|
|
58
|
-
* @param ttl The time to live of the value in seconds.
|
|
59
|
-
* By default, the value will not expire
|
|
60
|
-
*
|
|
61
|
-
* @return true if the value was stored, false otherwise.
|
|
62
|
-
*/
|
|
63
|
-
public setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean> {
|
|
64
|
-
return this.cacheInstance.setValue(key, value, ttl);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Delete a value from the cache.
|
|
69
|
-
* @param key — The key of the value to set.
|
|
70
|
-
*/
|
|
71
|
-
public delValue(key: string): Promise<void> {
|
|
72
|
-
return this.cacheInstance.delValue(key);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get the TTL of an entry, in ms
|
|
77
|
-
*
|
|
78
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
79
|
-
*
|
|
80
|
-
* @return The remaining TTL on the entry, in ms.
|
|
81
|
-
* undefined if the entry does not exist.
|
|
82
|
-
* 0 if the entry does not expire.
|
|
83
|
-
*/
|
|
84
|
-
public getTtl(key: string): Promise<number | undefined> {
|
|
85
|
-
return this.cacheInstance.getTtl(key);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
90
|
-
*
|
|
91
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
92
|
-
* @returns A cache instance.
|
|
93
|
-
*/
|
|
94
|
-
public static create(redisUrl?: string): Cache {
|
|
95
|
-
const cacheInstance: CacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
96
|
-
|
|
97
|
-
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
98
|
-
const correlationId = crypto.randomUUID();
|
|
99
|
-
|
|
100
|
-
const logger = new Logger({ correlation_id: correlationId });
|
|
101
|
-
|
|
102
|
-
cacheInstance
|
|
103
|
-
.on('info', message => {
|
|
104
|
-
logger.info(message);
|
|
105
|
-
})
|
|
106
|
-
.on('warn', message => {
|
|
107
|
-
logger.warn(message);
|
|
108
|
-
})
|
|
109
|
-
.on('error', message => {
|
|
110
|
-
logger.error(message);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return new Cache(cacheInstance);
|
|
114
|
-
}
|
|
11
|
+
function create(redisUrl?: string): CacheInstance {
|
|
12
|
+
const cacheInstance: CacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
13
|
+
|
|
14
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
15
|
+
const correlationId = crypto.randomUUID();
|
|
16
|
+
|
|
17
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
18
|
+
|
|
19
|
+
cacheInstance
|
|
20
|
+
.on('info', message => {
|
|
21
|
+
logger.info(message);
|
|
22
|
+
})
|
|
23
|
+
.on('warn', message => {
|
|
24
|
+
logger.warn(message);
|
|
25
|
+
})
|
|
26
|
+
.on('error', message => {
|
|
27
|
+
logger.error(message);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return cacheInstance;
|
|
115
31
|
}
|
|
32
|
+
|
|
33
|
+
export const Cache = { create };
|
|
34
|
+
export type { CacheInstance };
|
|
@@ -21,7 +21,6 @@ import Logger from '../resources/logger.js';
|
|
|
21
21
|
* @param targetFunction - The function to call the provider.
|
|
22
22
|
* @returns The response from the provider.
|
|
23
23
|
* @throws RateLimitExceededError when the rate limit is exceeded.
|
|
24
|
-
* @throws WouldExceedRateLimitError when the next call would exceed the rate limit.
|
|
25
24
|
* @throws HttpError when the provider returns an error.
|
|
26
25
|
*/
|
|
27
26
|
export type RateLimiter = <T>(
|
|
@@ -2,7 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
4
|
import onError from '../../src/middlewares/errors.js';
|
|
5
|
-
import { HttpError } from '../../src/httpErrors.js';
|
|
5
|
+
import { HttpError, RateLimitExceededError } from '../../src/httpErrors.js';
|
|
6
6
|
|
|
7
7
|
describe('errors middleware', () => {
|
|
8
8
|
it('headers sent, do nothing', () => {
|
|
@@ -52,6 +52,33 @@ describe('errors middleware', () => {
|
|
|
52
52
|
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it('handles retry-after header', () => {
|
|
56
|
+
let actualStatus: number | undefined;
|
|
57
|
+
let actualJson: Record<string, unknown> | undefined;
|
|
58
|
+
|
|
59
|
+
const response = {
|
|
60
|
+
headersSent: false,
|
|
61
|
+
status: (code: number) => {
|
|
62
|
+
actualStatus = code;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
json: (json: Record<string, unknown>) => {
|
|
66
|
+
actualJson = json;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
locals: { logger: { error: () => undefined } },
|
|
71
|
+
setHeader: (name: string, value: string) => {
|
|
72
|
+
assert.strictEqual(name, 'Retry-After');
|
|
73
|
+
assert.strictEqual(value, '1234');
|
|
74
|
+
},
|
|
75
|
+
} as unknown as express.Response;
|
|
76
|
+
onError(new RateLimitExceededError('httpError', { retryAfter: 1234 }), {} as express.Request, response, () => {});
|
|
77
|
+
|
|
78
|
+
assert.strictEqual(actualStatus, 429);
|
|
79
|
+
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
80
|
+
});
|
|
81
|
+
|
|
55
82
|
it('handles other error', () => {
|
|
56
83
|
let actualStatus: number | undefined;
|
|
57
84
|
let actualJson: Record<string, unknown> | undefined;
|
|
@@ -8,10 +8,9 @@ describe('Cache', () => {
|
|
|
8
8
|
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
9
9
|
const cache = Cache.create();
|
|
10
10
|
|
|
11
|
-
assert.ok(cache instanceof
|
|
12
|
-
assert.ok(cache['cacheInstance'] instanceof LocalCache);
|
|
11
|
+
assert.ok(cache instanceof LocalCache);
|
|
13
12
|
|
|
14
|
-
await cache
|
|
13
|
+
await cache.quit();
|
|
15
14
|
});
|
|
16
15
|
|
|
17
16
|
it('redis url tries to return a RedisCache', () => {
|