@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
|
@@ -41,7 +41,7 @@ describe('Provider', () => {
|
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
]);
|
|
44
|
-
assert.deepEqual(actualResponse, { status: 200, headers: response.headers,
|
|
44
|
+
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
45
45
|
});
|
|
46
46
|
it('post with url encoded body', async (context) => {
|
|
47
47
|
const response = new Response('{"data": "value"}', {
|
|
@@ -71,7 +71,7 @@ describe('Provider', () => {
|
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
]);
|
|
74
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
74
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
75
75
|
});
|
|
76
76
|
it('put with json body', async (context) => {
|
|
77
77
|
const response = new Response('{"data": "value"}', {
|
|
@@ -102,7 +102,7 @@ describe('Provider', () => {
|
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
104
|
]);
|
|
105
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
105
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
106
106
|
});
|
|
107
107
|
it('patch with query params', async (context) => {
|
|
108
108
|
const response = new Response('{"data": "value"}', {
|
|
@@ -133,7 +133,7 @@ describe('Provider', () => {
|
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
135
|
]);
|
|
136
|
-
assert.deepEqual(actualResponse, { status: 201, headers: response.headers,
|
|
136
|
+
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
137
137
|
});
|
|
138
138
|
it('delete', async (context) => {
|
|
139
139
|
const response = new Response(undefined, {
|
|
@@ -161,7 +161,7 @@ describe('Provider', () => {
|
|
|
161
161
|
},
|
|
162
162
|
},
|
|
163
163
|
]);
|
|
164
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
164
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
165
165
|
});
|
|
166
166
|
it('uses rate limiter if provided', async (context) => {
|
|
167
167
|
const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request()));
|
|
@@ -205,7 +205,7 @@ describe('Provider', () => {
|
|
|
205
205
|
},
|
|
206
206
|
},
|
|
207
207
|
]);
|
|
208
|
-
assert.deepEqual(actualResponse, { status: 204, headers: response.headers,
|
|
208
|
+
assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined });
|
|
209
209
|
});
|
|
210
210
|
it('throws on invalid json response', async (context) => {
|
|
211
211
|
const response = new Response('{invalidJSON}', {
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unito/integration-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Integration SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
"./package.json": "./package.json",
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/src/index.js",
|
|
11
|
+
"require": "./dist/src/index.cjs"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"license": "LicenseRef-LICENSE",
|
|
12
15
|
"engines": {
|
|
@@ -21,8 +24,10 @@
|
|
|
21
24
|
"prepublishOnly": "npm run lint && npm run test",
|
|
22
25
|
"prepare": "npm run compile",
|
|
23
26
|
"lint": "eslint --fix src test --ext .ts && prettier --write src test",
|
|
24
|
-
"compile": "
|
|
25
|
-
"compile:watch": "
|
|
27
|
+
"compile": "npm run compile:esm && npm run compile:cjs",
|
|
28
|
+
"compile:watch": "nodemon --watch \"src/**\" --ext js,ts --exec \"npm run compile\"",
|
|
29
|
+
"compile:esm": "tsc --build",
|
|
30
|
+
"compile:cjs": "rollup dist/src/index.js --file dist/src/index.cjs --format cjs",
|
|
26
31
|
"test": "NODE_ENV=test node --test --no-warnings --loader ts-node/esm --test-name-pattern=${ONLY:-.*} $(find test -type f -name '*.test.ts')",
|
|
27
32
|
"test:debug": "NODE_ENV=test node --test --loader ts-node/esm --test-name-pattern=${ONLY:-.*} --inspect-brk $(find test -type f -name '*.test.ts')",
|
|
28
33
|
"ci:test": "npm run test"
|
|
@@ -36,7 +41,9 @@
|
|
|
36
41
|
"c8": "9.x",
|
|
37
42
|
"eslint": "8.x",
|
|
38
43
|
"prettier": "3.x",
|
|
44
|
+
"rollup": "4.x",
|
|
39
45
|
"ts-node": "10.x",
|
|
46
|
+
"nodemon": "3.x",
|
|
40
47
|
"typescript": "5.x"
|
|
41
48
|
},
|
|
42
49
|
"dependencies": {
|
package/src/errors.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import * as HttpErrors from './httpErrors.js';
|
|
2
2
|
|
|
3
|
-
export class NoIntegrationFoundError extends Error {}
|
|
4
|
-
|
|
5
|
-
export class NoConfigurationFileError extends Error {}
|
|
6
|
-
|
|
7
|
-
export class ConfigurationMalformed extends Error {}
|
|
8
|
-
|
|
9
3
|
export class InvalidHandler extends Error {}
|
|
10
4
|
|
|
11
5
|
/**
|
|
@@ -25,6 +19,8 @@ export function buildHttpError(responseStatus: number, message: string): HttpErr
|
|
|
25
19
|
httpError = new HttpErrors.NotFoundError(message);
|
|
26
20
|
} else if (responseStatus === 408) {
|
|
27
21
|
httpError = new HttpErrors.TimeoutError(message);
|
|
22
|
+
} else if (responseStatus === 410) {
|
|
23
|
+
httpError = new HttpErrors.ResourceGoneError(message);
|
|
28
24
|
} else if (responseStatus === 422) {
|
|
29
25
|
httpError = new HttpErrors.UnprocessableEntityError(message);
|
|
30
26
|
} else if (responseStatus === 429) {
|
package/src/httpErrors.ts
CHANGED
|
@@ -31,20 +31,20 @@ export class TimeoutError extends HttpError {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export class
|
|
34
|
+
export class ResourceGoneError extends HttpError {
|
|
35
35
|
constructor(message?: string) {
|
|
36
|
-
super(message || '
|
|
36
|
+
super(message || 'Resource gone or unavailable', 410);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export class
|
|
40
|
+
export class UnprocessableEntityError extends HttpError {
|
|
41
41
|
constructor(message?: string) {
|
|
42
|
-
super(message || '
|
|
42
|
+
super(message || 'Unprocessable Entity', 422);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export class
|
|
46
|
+
export class RateLimitExceededError extends HttpError {
|
|
47
47
|
constructor(message?: string) {
|
|
48
|
-
super(message || '
|
|
48
|
+
super(message || 'Rate Limit Exceeded', 429);
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/index.ts
CHANGED
package/src/integration.ts
CHANGED
|
@@ -9,7 +9,7 @@ import selectsMiddleware from './middlewares/selects.js';
|
|
|
9
9
|
import errorsMiddleware from './middlewares/errors.js';
|
|
10
10
|
import finishMiddleware from './middlewares/finish.js';
|
|
11
11
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
12
|
-
import {
|
|
12
|
+
import { shutdownCaches } from './resources/cache.js';
|
|
13
13
|
import { HandlersInput, Handler } from './handler.js';
|
|
14
14
|
|
|
15
15
|
function printErrorMessage(message: string) {
|
|
@@ -27,8 +27,6 @@ export default class Integration {
|
|
|
27
27
|
|
|
28
28
|
private instance: Server<typeof IncomingMessage, typeof ServerResponse> | undefined = undefined;
|
|
29
29
|
|
|
30
|
-
private cache: Cache | undefined = undefined;
|
|
31
|
-
|
|
32
30
|
private port: number;
|
|
33
31
|
|
|
34
32
|
constructor(options: Options = {}) {
|
|
@@ -69,9 +67,6 @@ export default class Integration {
|
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
public start() {
|
|
72
|
-
// Initialize the cache.
|
|
73
|
-
this.cache = initializeCache();
|
|
74
|
-
|
|
75
70
|
// Express Server initialization
|
|
76
71
|
const app: express.Application = express();
|
|
77
72
|
|
|
@@ -121,9 +116,7 @@ export default class Integration {
|
|
|
121
116
|
this.instance.close();
|
|
122
117
|
}
|
|
123
118
|
|
|
124
|
-
|
|
125
|
-
await this.cache.quit();
|
|
126
|
-
}
|
|
119
|
+
await shutdownCaches();
|
|
127
120
|
} catch (e) {
|
|
128
121
|
console.error('Failed to gracefully exit', e);
|
|
129
122
|
}
|
|
@@ -1,30 +1,55 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { Error as
|
|
2
|
+
import { Error as ApiError } from '@unito/integration-api';
|
|
3
|
+
|
|
3
4
|
import { HttpError } from '../httpErrors.js';
|
|
4
5
|
|
|
6
|
+
declare global {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Locals {
|
|
10
|
+
error?: ApiError;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
const middleware = (err: Error, _req: Request, res: Response, next: NextFunction) => {
|
|
6
16
|
if (res.headersSent) {
|
|
7
17
|
return next(err);
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
res.locals.logger.error(err);
|
|
12
|
-
}
|
|
20
|
+
let error: ApiError;
|
|
13
21
|
|
|
14
22
|
if (err instanceof HttpError) {
|
|
15
|
-
|
|
23
|
+
error = {
|
|
24
|
+
code: err.status.toString(),
|
|
25
|
+
message: err.message,
|
|
26
|
+
details: {
|
|
27
|
+
stack: err.stack,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
error = {
|
|
32
|
+
code: '500',
|
|
33
|
+
message: 'Oops! Something went wrong',
|
|
34
|
+
originalError: {
|
|
35
|
+
code: err.name,
|
|
36
|
+
message: err.message,
|
|
37
|
+
details: {
|
|
38
|
+
stack: err.stack,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
16
42
|
}
|
|
17
43
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
res.locals.error = structuredClone(error);
|
|
45
|
+
|
|
46
|
+
// Keep the stack details in development for the Debugger
|
|
47
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
48
|
+
delete error.details;
|
|
49
|
+
delete error.originalError?.details;
|
|
50
|
+
}
|
|
22
51
|
|
|
23
|
-
res.status(
|
|
24
|
-
code: '500',
|
|
25
|
-
message: 'Oops! Something went wrong',
|
|
26
|
-
originalError,
|
|
27
|
-
} as APIError);
|
|
52
|
+
res.status(Number(error.code)).json(error);
|
|
28
53
|
};
|
|
29
54
|
|
|
30
55
|
export default middleware;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import
|
|
2
|
+
import { Error as ApiError } from '@unito/integration-api';
|
|
3
|
+
import { default as Logger, Metadata } from '../resources/logger.js';
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
5
6
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
7
|
namespace Express {
|
|
7
8
|
interface Locals {
|
|
8
9
|
logger: Logger;
|
|
10
|
+
error?: ApiError;
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
}
|
|
@@ -13,8 +15,31 @@ declare global {
|
|
|
13
15
|
const middleware = (req: Request, res: Response, next: NextFunction) => {
|
|
14
16
|
if (req.originalUrl !== '/health') {
|
|
15
17
|
res.on('finish', function () {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
+
const error = res.locals.error;
|
|
19
|
+
|
|
20
|
+
const message = `${req.method} ${req.originalUrl} ${res.statusCode}`;
|
|
21
|
+
const metadata: Metadata = {
|
|
22
|
+
// Use reserved and standard attributes of Datadog
|
|
23
|
+
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
24
|
+
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
25
|
+
...(error
|
|
26
|
+
? {
|
|
27
|
+
error: {
|
|
28
|
+
kind: error.message,
|
|
29
|
+
stack: (error.originalError?.details?.stack ?? error.details?.stack) as string,
|
|
30
|
+
message: error.originalError?.message ?? error.message,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if ([404, 429].includes(res.statusCode)) {
|
|
37
|
+
res.locals.logger.warn(message, metadata);
|
|
38
|
+
} else if (res.statusCode >= 400) {
|
|
39
|
+
res.locals.logger.error(message, metadata);
|
|
40
|
+
} else {
|
|
41
|
+
res.locals.logger.info(message, metadata);
|
|
42
|
+
}
|
|
18
43
|
});
|
|
19
44
|
}
|
|
20
45
|
|
package/src/resources/cache.ts
CHANGED
|
@@ -1,34 +1,77 @@
|
|
|
1
|
-
import { WriteThroughCache, LocalCache, CacheInstance } from 'cachette';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { WriteThroughCache, LocalCache, CacheInstance, FetchingFunction, CachableValue } from 'cachette';
|
|
3
2
|
import * as uuid from 'uuid';
|
|
4
3
|
import Logger from './logger.js';
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Array of created caches kept to allow for graceful shutdown on exit signals.
|
|
7
|
+
*/
|
|
8
|
+
const caches: CacheInstance[] = [];
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
+
export const shutdownCaches = async () => {
|
|
11
|
+
return Promise.allSettled(caches.map(cache => cache.quit()));
|
|
12
|
+
};
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
export class Cache {
|
|
15
|
+
private cacheInstance: CacheInstance;
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
private constructor(cacheInstance: CacheInstance) {
|
|
18
|
+
this.cacheInstance = cacheInstance;
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
});
|
|
21
|
+
public getOrFetchValue<F extends FetchingFunction = FetchingFunction>(
|
|
22
|
+
key: string,
|
|
23
|
+
ttl: number,
|
|
24
|
+
fetcher: F,
|
|
25
|
+
lockTtl?: number,
|
|
26
|
+
shouldCacheError?: (err: Error) => boolean,
|
|
27
|
+
): Promise<ReturnType<F>> {
|
|
28
|
+
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
29
|
+
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
public getValue(key: string): Promise<CachableValue> {
|
|
32
|
+
return this.cacheInstance.getValue(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean> {
|
|
36
|
+
return this.cacheInstance.setValue(key, value, ttl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public delValue(key: string): Promise<void> {
|
|
40
|
+
return this.cacheInstance.delValue(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public getTtl(key: string): Promise<number | undefined> {
|
|
44
|
+
return this.cacheInstance.getTtl(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initializes a WriteThroughCache instance with the provided redis url if present, or a LocalCache otherwise.
|
|
49
|
+
*
|
|
50
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
51
|
+
* @returns A cache instance.
|
|
52
|
+
*/
|
|
53
|
+
public static create(redisUrl?: string): Cache {
|
|
54
|
+
const cacheInstance: CacheInstance = redisUrl ? new WriteThroughCache(redisUrl) : new LocalCache();
|
|
55
|
+
|
|
56
|
+
// Push to the array of caches for graceful shutdown on exit signals.
|
|
57
|
+
caches.push(cacheInstance);
|
|
58
|
+
|
|
59
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
60
|
+
const correlationId = uuid.v4();
|
|
61
|
+
|
|
62
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
29
63
|
|
|
30
|
-
|
|
64
|
+
cacheInstance
|
|
65
|
+
.on('info', message => {
|
|
66
|
+
logger.info(message);
|
|
67
|
+
})
|
|
68
|
+
.on('warn', message => {
|
|
69
|
+
logger.warn(message);
|
|
70
|
+
})
|
|
71
|
+
.on('error', message => {
|
|
72
|
+
logger.error(message);
|
|
73
|
+
});
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
return new Cache(cacheInstance);
|
|
76
|
+
}
|
|
34
77
|
}
|
package/src/resources/logger.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
enum LogLevel {
|
|
1
|
+
const enum LogLevel {
|
|
2
2
|
ERROR = 'error',
|
|
3
3
|
WARN = 'warn',
|
|
4
4
|
INFO = 'info',
|
|
@@ -6,52 +6,112 @@ enum LogLevel {
|
|
|
6
6
|
DEBUG = 'debug',
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
10
|
+
type Value = {
|
|
11
|
+
[key: string]: PrimitiveValue | Value;
|
|
12
|
+
};
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.metadata = structuredClone(metadata);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
14
|
+
export type Metadata = Value & { message?: never };
|
|
15
|
+
type ForbidenMetadataKey = 'message';
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export default class Logger {
|
|
18
|
+
private metadata: Metadata;
|
|
19
|
+
|
|
20
|
+
constructor(metadata: Metadata = {}) {
|
|
21
|
+
this.metadata = structuredClone(metadata);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Logs a message with the 'log' log level.
|
|
26
|
+
* @param message The message to be logged.
|
|
27
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
28
|
+
*/
|
|
29
|
+
public log(message: string, metadata?: Metadata): void {
|
|
30
|
+
this.send(LogLevel.LOG, message, metadata);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Logs an error message with the 'error' log level.
|
|
35
|
+
* @param message The error message to be logged.
|
|
36
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
37
|
+
*/
|
|
38
|
+
public error(message: string, metadata?: Metadata): void {
|
|
39
|
+
this.send(LogLevel.ERROR, message, metadata);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Logs a warning message with the 'warn' log level.
|
|
44
|
+
* @param message The warning message to be logged.
|
|
45
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
46
|
+
*/
|
|
47
|
+
public warn(message: string, metadata?: Metadata): void {
|
|
48
|
+
this.send(LogLevel.WARN, message, metadata);
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Logs an informational message with the 'info' log level.
|
|
53
|
+
* @param message The informational message to be logged.
|
|
54
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
55
|
+
*/
|
|
56
|
+
public info(message: string, metadata?: Metadata): void {
|
|
57
|
+
this.send(LogLevel.INFO, message, metadata);
|
|
36
58
|
}
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Logs a debug message with the 'debug' log level.
|
|
62
|
+
* @param message The debug message to be logged.
|
|
63
|
+
* @param metadata Optional metadata to be associated with the log message.
|
|
64
|
+
*/
|
|
65
|
+
public debug(message: string, metadata?: Metadata): void {
|
|
66
|
+
this.send(LogLevel.DEBUG, message, metadata);
|
|
40
67
|
}
|
|
41
68
|
|
|
42
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Decorates the logger with additional metadata.
|
|
71
|
+
* @param metadata Additional metadata to be added to the logger.
|
|
72
|
+
*/
|
|
73
|
+
public decorate(metadata: Metadata): void {
|
|
43
74
|
this.metadata = { ...this.metadata, ...metadata };
|
|
44
75
|
}
|
|
45
76
|
|
|
46
|
-
public getMetadata() {
|
|
77
|
+
public getMetadata(): Metadata {
|
|
47
78
|
return structuredClone(this.metadata);
|
|
48
79
|
}
|
|
49
80
|
|
|
50
|
-
public setMetadata
|
|
81
|
+
public setMetadata<Key extends string>(
|
|
82
|
+
key: Key extends ForbidenMetadataKey ? never : Key,
|
|
83
|
+
value: PrimitiveValue | Value,
|
|
84
|
+
): void {
|
|
51
85
|
this.metadata[key] = value;
|
|
52
86
|
}
|
|
53
87
|
|
|
54
|
-
public clearMetadata() {
|
|
88
|
+
public clearMetadata(): void {
|
|
55
89
|
this.metadata = {};
|
|
56
90
|
}
|
|
91
|
+
|
|
92
|
+
private send(logLevel: LogLevel, message: string, metadata?: Metadata): void {
|
|
93
|
+
const processedMessage = this.snakifyKeys({
|
|
94
|
+
...this.metadata,
|
|
95
|
+
...metadata,
|
|
96
|
+
message,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (process.env.NODE_ENV === 'development') {
|
|
100
|
+
console[logLevel](JSON.stringify(processedMessage, null, 2));
|
|
101
|
+
} else {
|
|
102
|
+
console[logLevel](JSON.stringify(processedMessage));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private snakifyKeys<T extends Value>(value: T): T {
|
|
107
|
+
const result: Value = {};
|
|
108
|
+
|
|
109
|
+
for (const key in value) {
|
|
110
|
+
const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key] as Value) : value[key];
|
|
111
|
+
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
112
|
+
result[snakifiedKey] = deepValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result as T;
|
|
116
|
+
}
|
|
57
117
|
}
|
|
@@ -31,7 +31,7 @@ export interface RequestOptions {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface Response<T> {
|
|
34
|
-
|
|
34
|
+
body: T;
|
|
35
35
|
status: number;
|
|
36
36
|
headers: Headers;
|
|
37
37
|
}
|
|
@@ -151,8 +151,8 @@ export class Provider {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
|
-
const
|
|
155
|
-
return { status: response.status, headers: response.headers,
|
|
154
|
+
const body: T = response.body ? await response.json() : undefined;
|
|
155
|
+
return { status: response.status, headers: response.headers, body };
|
|
156
156
|
} catch {
|
|
157
157
|
throw buildHttpError(400, 'Invalid JSON response');
|
|
158
158
|
}
|
package/test/errors.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ describe('handleErrorResponse', () => {
|
|
|
9
9
|
assert.ok(errors.buildHttpError(403, 'forbidden') instanceof httpErrors.UnauthorizedError);
|
|
10
10
|
assert.ok(errors.buildHttpError(404, 'not found') instanceof httpErrors.NotFoundError);
|
|
11
11
|
assert.ok(errors.buildHttpError(408, 'timeout') instanceof httpErrors.TimeoutError);
|
|
12
|
+
assert.ok(errors.buildHttpError(410, 'resource gone') instanceof httpErrors.ResourceGoneError);
|
|
12
13
|
assert.ok(errors.buildHttpError(422, 'unprocessable entity') instanceof httpErrors.UnprocessableEntityError);
|
|
13
14
|
assert.ok(errors.buildHttpError(429, 'rate limit exceeded') instanceof httpErrors.RateLimitExceededError);
|
|
14
15
|
assert.ok(errors.buildHttpError(500, 'internal server error') instanceof httpErrors.HttpError);
|
|
@@ -49,7 +49,7 @@ describe('errors middleware', () => {
|
|
|
49
49
|
middleware(new HttpError('httpError', 429), {} as express.Request, response, () => {});
|
|
50
50
|
|
|
51
51
|
assert.strictEqual(actualStatus, 429);
|
|
52
|
-
assert.
|
|
52
|
+
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('handles other error', () => {
|
|
@@ -1,31 +1,21 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { LocalCache } from 'cachette';
|
|
4
|
-
import {
|
|
4
|
+
import { Cache, shutdownCaches } from '../../src/resources/cache.js';
|
|
5
5
|
|
|
6
6
|
describe('Cache', () => {
|
|
7
7
|
describe('initializeCache', () => {
|
|
8
|
-
it('no redis url returns LocalCache', () => {
|
|
9
|
-
const cache =
|
|
8
|
+
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
9
|
+
const cache = Cache.create();
|
|
10
10
|
|
|
11
|
-
assert.
|
|
11
|
+
assert.ok(cache instanceof Cache);
|
|
12
|
+
assert.ok(cache['cacheInstance'] instanceof LocalCache);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
await shutdownCaches();
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
it('redis url returns (tries) WriteThroughCache', () => {
|
|
17
|
-
|
|
18
|
-
assert.throws(() => initializeCache(), Error, 'Invalid redis url fakereis.');
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('generateCacheKey', () => {
|
|
23
|
-
it('hashes string and returns hex value', () => {
|
|
24
|
-
const value = 'test';
|
|
25
|
-
const hash = generateCacheKey(value);
|
|
26
|
-
|
|
27
|
-
assert.equal(typeof hash, 'string');
|
|
28
|
-
assert.equal(hash.length, 64);
|
|
18
|
+
assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
|
|
29
19
|
});
|
|
30
20
|
});
|
|
31
21
|
});
|