lambda-essentials-ts 2.2.2 → 4.1.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/CHANGELOG.md +35 -0
- package/README.md +17 -0
- package/lib/httpClient/deduplicateRequestAdapter.d.ts +2 -0
- package/lib/httpClient/deduplicateRequestAdapter.js +20 -0
- package/lib/httpClient/httpClient.d.ts +7 -6
- package/lib/httpClient/httpClient.js +69 -6
- package/lib/openApi/apiResponseModel.js +1 -1
- package/lib/openApi/openApiWrapper.js +1 -2
- package/lib/util.d.ts +1 -0
- package/lib/util.js +15 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
|
|
7
|
+
## [4.1.0] - 2021-11-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `ApiResponse` default content-type header was changed from `application/links+json` to `application/hal+json`
|
|
12
|
+
|
|
13
|
+
## [4.0.0] - 2021-11-12
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `HttpClient` the [retryAdapterEnhancer axios adapter](https://github.com/kuitos/axios-extensions#cacheadapterenhancer)
|
|
18
|
+
was replaced by the more flexible [axios-cache-adapter](https://github.com/RasCarlito/axios-cache-adapter).
|
|
19
|
+
- **[Breaking change]** `HttpClientOptions.cacheOptions` now accepts [extensive cache configuration](https://github.com/RasCarlito/axios-cache-adapter/blob/master/axios-cache-adapter.d.ts#L26).
|
|
20
|
+
- The cache is now partitioned by `canonical_id` JWT claim.
|
|
21
|
+
|
|
22
|
+
## [3.0.1] - 2021-09-13
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Downgraded Axios to 0.21.1 due to response interceptors not being applied correctly in 0.21.2. [There has been a fix to
|
|
27
|
+
axios but a version with the fix is not available yet.](https://github.com/axios/axios/commit/83ae3830e4070adbcdcdcdd6e8afbac568afd708)
|
|
28
|
+
|
|
29
|
+
## [3.0.0] - 2021-09-10
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `HttpClient` the [retryAdapterEnhancer axios adapter](https://github.com/kuitos/axios-extensions#retryadapterenhancer)
|
|
34
|
+
was replaced by the more flexible [retry-axios interceptor](https://github.com/JustinBeckwith/retry-axios).
|
|
35
|
+
- **[Breaking change]** `HttpClientOptions.retryOptions` now accepts [extensive retry configuration](https://github.com/JustinBeckwith/retry-axios/blob/v2.6.0/src/index.ts#L11)
|
|
36
|
+
such as specifying HTTP status codes that should be retried.
|
|
37
|
+
- **[Breaking change]** All HTTP status codes are no longer retried by default. The new default are these ranges:
|
|
38
|
+
- [100, 199] Informational, request still processing
|
|
39
|
+
- [429, 429] Too Many Requests
|
|
40
|
+
- [500, 599] Server errors
|
|
41
|
+
|
|
7
42
|
## [2.2.2] - 2021-07-08
|
|
8
43
|
|
|
9
44
|
### Fixed bugs
|
package/README.md
CHANGED
|
@@ -52,6 +52,23 @@ let httpClient = new HttpClient({
|
|
|
52
52
|
logFunction: (msg) => logger.log(msg),
|
|
53
53
|
logOptions: { enabledLogs: [HttpLogType.requests] },
|
|
54
54
|
tokenResolver: () => Promise.resolve('exampleAccessToken'),
|
|
55
|
+
enableRetry: true,
|
|
56
|
+
retryOptions: {
|
|
57
|
+
retry: 3,
|
|
58
|
+
statusCodesToRetry: [
|
|
59
|
+
[100, 199],
|
|
60
|
+
[429, 429],
|
|
61
|
+
[500, 599],
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
enableCache: true,
|
|
65
|
+
cacheOptions: {
|
|
66
|
+
maxAge: 5 * 60 * 1000,
|
|
67
|
+
readOnError: true,
|
|
68
|
+
exclude: {
|
|
69
|
+
query: false, // also cache requests with query parameters
|
|
70
|
+
},
|
|
71
|
+
},
|
|
55
72
|
});
|
|
56
73
|
|
|
57
74
|
let headers = {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDebounceRequestAdapter = void 0;
|
|
4
|
+
// adopted from https://github.com/RasCarlito/axios-cache-adapter/issues/231#issuecomment-880288436
|
|
5
|
+
function createDebounceRequestAdapter(requestAdapter, requestKeyProvider) {
|
|
6
|
+
const runningRequests = {};
|
|
7
|
+
return (req) => {
|
|
8
|
+
const cacheKey = requestKeyProvider(req);
|
|
9
|
+
// Add the request to runningRequests. If it is already there, drop the duplicated request.
|
|
10
|
+
if (!runningRequests[cacheKey]) {
|
|
11
|
+
runningRequests[cacheKey] = requestAdapter(req);
|
|
12
|
+
}
|
|
13
|
+
// Return the response promise
|
|
14
|
+
return runningRequests[cacheKey].finally(() => {
|
|
15
|
+
// Finally, delete the request from the runningRequests whether there's error or not
|
|
16
|
+
delete runningRequests[cacheKey];
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
exports.createDebounceRequestAdapter = createDebounceRequestAdapter;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { IAxiosCacheAdapterOptions } from 'axios-cache-adapter';
|
|
3
|
+
import { RetryConfig } from 'retry-axios';
|
|
4
4
|
/**
|
|
5
5
|
* Allows to specify which http data should be logged.
|
|
6
6
|
*/
|
|
@@ -21,6 +21,7 @@ export default class HttpClient {
|
|
|
21
21
|
*/
|
|
22
22
|
constructor(options?: HttpClientOptions);
|
|
23
23
|
private static extractRequestLogData;
|
|
24
|
+
static generateCacheKey(req: AxiosRequestConfig): string;
|
|
24
25
|
/**
|
|
25
26
|
* Resolves the token with the token provider and adds it to the headers
|
|
26
27
|
*/
|
|
@@ -81,18 +82,18 @@ export interface HttpClientOptions {
|
|
|
81
82
|
enableCache?: boolean;
|
|
82
83
|
/**
|
|
83
84
|
* Cache options
|
|
84
|
-
* @link https://github.com/
|
|
85
|
+
* @link https://github.com/RasCarlito/axios-cache-adapter/blob/master/axios-cache-adapter.d.ts#L26
|
|
85
86
|
*/
|
|
86
|
-
cacheOptions?:
|
|
87
|
+
cacheOptions?: IAxiosCacheAdapterOptions;
|
|
87
88
|
/**
|
|
88
89
|
* Enable automatic retries
|
|
89
90
|
*/
|
|
90
91
|
enableRetry?: boolean;
|
|
91
92
|
/**
|
|
92
93
|
* Retry options
|
|
93
|
-
* @link https://github.com/
|
|
94
|
+
* @link https://github.com/JustinBeckwith/retry-axios/blob/v2.6.0/src/index.ts#L11
|
|
94
95
|
*/
|
|
95
|
-
retryOptions?:
|
|
96
|
+
retryOptions?: RetryConfig;
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
99
|
* Log options object.
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
2
21
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
23
|
};
|
|
5
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
25
|
exports.HttpLogType = void 0;
|
|
26
|
+
const uuid = __importStar(require("uuid"));
|
|
7
27
|
const url_1 = require("url");
|
|
8
28
|
const axios_1 = __importDefault(require("axios"));
|
|
9
|
-
const
|
|
10
|
-
const
|
|
29
|
+
const axios_cache_adapter_1 = require("axios-cache-adapter");
|
|
30
|
+
const rax = __importStar(require("retry-axios"));
|
|
31
|
+
const md5_1 = __importDefault(require("md5"));
|
|
11
32
|
const util_1 = require("../util");
|
|
12
33
|
const internalException_1 = require("../exceptions/internalException");
|
|
13
34
|
const clientException_1 = require("../exceptions/clientException");
|
|
14
35
|
const shared_1 = require("../shared");
|
|
36
|
+
const deduplicateRequestAdapter_1 = require("./deduplicateRequestAdapter");
|
|
15
37
|
const invalidToken = 'Invalid token';
|
|
16
38
|
/**
|
|
17
39
|
* Allows to specify which http data should be logged.
|
|
@@ -38,14 +60,41 @@ class HttpClient {
|
|
|
38
60
|
adapter: (() => {
|
|
39
61
|
let adapters = axios_1.default.defaults.adapter;
|
|
40
62
|
if (this.enableCache) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
const cache = axios_cache_adapter_1.setupCache({
|
|
64
|
+
maxAge: 5 * 60 * 1000,
|
|
65
|
+
readHeaders: false,
|
|
66
|
+
readOnError: true,
|
|
67
|
+
exclude: {
|
|
68
|
+
query: false,
|
|
69
|
+
},
|
|
70
|
+
...options === null || options === void 0 ? void 0 : options.cacheOptions,
|
|
71
|
+
key: (req) => HttpClient.generateCacheKey(req),
|
|
72
|
+
});
|
|
73
|
+
// debounce concurrent calls with the same cacheKey so that only one HTTP request is made
|
|
74
|
+
adapters = deduplicateRequestAdapter_1.createDebounceRequestAdapter(cache.adapter, HttpClient.generateCacheKey);
|
|
45
75
|
}
|
|
46
76
|
return adapters;
|
|
47
77
|
})(),
|
|
48
78
|
});
|
|
79
|
+
if (this.enableRetry) {
|
|
80
|
+
this.client.defaults.raxConfig = {
|
|
81
|
+
...options === null || options === void 0 ? void 0 : options.retryOptions,
|
|
82
|
+
instance: this.client,
|
|
83
|
+
httpMethodsToRetry: ['GET', 'HEAD', 'OPTIONS', 'DELETE', 'PUT', 'POST'],
|
|
84
|
+
onRetryAttempt: (err) => {
|
|
85
|
+
var _a, _b;
|
|
86
|
+
this.logFunction({
|
|
87
|
+
title: 'HTTP Response Error Retry',
|
|
88
|
+
level: 'INFO',
|
|
89
|
+
retryAttempt: (_b = (_a = err.config) === null || _a === void 0 ? void 0 : _a.raxConfig) === null || _b === void 0 ? void 0 : _b.currentRetryAttempt,
|
|
90
|
+
...HttpClient.extractRequestLogData(err.config),
|
|
91
|
+
error: util_1.serializeAxiosError(err),
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
// attach retry-axios
|
|
96
|
+
rax.attach(this.client);
|
|
97
|
+
}
|
|
49
98
|
this.client.interceptors.request.use((config) => {
|
|
50
99
|
if (this.logOptions.enabledLogs.includes(HttpLogType.requests)) {
|
|
51
100
|
this.logFunction({
|
|
@@ -82,6 +131,11 @@ class HttpClient {
|
|
|
82
131
|
return response;
|
|
83
132
|
}, (error) => {
|
|
84
133
|
var _a;
|
|
134
|
+
// when retries are configured, this middleware gets triggered for each retry
|
|
135
|
+
// it changes the error object to ClientException and therefore the transformation can be run only once
|
|
136
|
+
if (error instanceof clientException_1.ClientException) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
85
139
|
const serializedAxiosError = util_1.serializeAxiosError(error);
|
|
86
140
|
if (error.message === invalidToken) {
|
|
87
141
|
this.logFunction({
|
|
@@ -116,6 +170,15 @@ class HttpClient {
|
|
|
116
170
|
correlationId: (_a = requestConfig.headers) === null || _a === void 0 ? void 0 : _a[shared_1.orionCorrelationIdRoot],
|
|
117
171
|
};
|
|
118
172
|
}
|
|
173
|
+
// implemented based on https://github.com/RasCarlito/axios-cache-adapter/blob/master/src/cache.js#L77
|
|
174
|
+
static generateCacheKey(req) {
|
|
175
|
+
var _a, _b;
|
|
176
|
+
const prefix = ((_a = req.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ? (_b = util_1.safeJwtCanonicalIdParse(req.headers.Authorization.replace('Bearer ', ''))) !== null && _b !== void 0 ? _b : uuid.v4() : 'shared';
|
|
177
|
+
const url = `${req.baseURL ? req.baseURL : ''}${req.url}`;
|
|
178
|
+
const query = req.params ? JSON.stringify(req.params) : ''; // possible improvement: optimize cache-hit ratio by sorting the query params
|
|
179
|
+
const key = `${prefix}/${url}${query}`;
|
|
180
|
+
return `${key}${req.data ? md5_1.default(req.data) : ''}`;
|
|
181
|
+
}
|
|
119
182
|
/**
|
|
120
183
|
* Resolves the token with the token provider and adds it to the headers
|
|
121
184
|
*/
|
|
@@ -6,7 +6,7 @@ class ApiResponse {
|
|
|
6
6
|
constructor(statusCode, body, headers) {
|
|
7
7
|
this.body = body;
|
|
8
8
|
this.statusCode = statusCode;
|
|
9
|
-
this.headers =
|
|
9
|
+
this.headers = { 'Content-type': 'application/hal+json', ...headers };
|
|
10
10
|
}
|
|
11
11
|
withCorrelationId(correlationId) {
|
|
12
12
|
this.headers[shared_1.orionCorrelationIdRoot] = correlationId;
|
|
@@ -23,7 +23,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
const openapi_factory_1 = __importDefault(require("openapi-factory"));
|
|
26
|
-
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
27
26
|
const uuid = __importStar(require("uuid"));
|
|
28
27
|
const apiResponseModel_1 = require("./apiResponseModel");
|
|
29
28
|
const exception_1 = require("../exceptions/exception");
|
|
@@ -45,7 +44,7 @@ class OpenApiWrapper {
|
|
|
45
44
|
requestLogger.startInvocation(null, correlationId);
|
|
46
45
|
// TODO: restrict the alternative way of resolving token and principal only for localhost
|
|
47
46
|
this.userToken = (_b = (_a = request.requestContext.authorizer) === null || _a === void 0 ? void 0 : _a.jwt) !== null && _b !== void 0 ? _b : (_c = request.headers.Authorization) === null || _c === void 0 ? void 0 : _c.split(' ')[1];
|
|
48
|
-
this.userPrincipal = (_e = (_d = request.requestContext.authorizer) === null || _d === void 0 ? void 0 : _d.canonicalId) !== null && _e !== void 0 ? _e :
|
|
47
|
+
this.userPrincipal = (_f = (_e = (_d = request.requestContext.authorizer) === null || _d === void 0 ? void 0 : _d.canonicalId) !== null && _e !== void 0 ? _e : util_1.safeJwtCanonicalIdParse(this.userToken)) !== null && _f !== void 0 ? _f : 'unknown';
|
|
49
48
|
this.requestId = request.requestContext.requestId;
|
|
50
49
|
requestLogger.log({
|
|
51
50
|
title: 'RequestLogger',
|
package/lib/util.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiosError } from 'axios';
|
|
2
|
+
export declare function safeJwtCanonicalIdParse(jwtToken: string): string | undefined;
|
|
2
3
|
export declare function safeJsonParse(input: any, defaultValue: unknown): unknown;
|
|
3
4
|
export declare function serializeObject(obj: unknown): object;
|
|
4
5
|
export declare function serializeAxiosError(error: AxiosError): SerializedAxiosError | undefined;
|
package/lib/util.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.serializeAxiosError = exports.serializeObject = exports.safeJsonParse = void 0;
|
|
6
|
+
exports.serializeAxiosError = exports.serializeObject = exports.safeJsonParse = exports.safeJwtCanonicalIdParse = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
function safeJwtCanonicalIdParse(jwtToken) {
|
|
9
|
+
var _a;
|
|
10
|
+
try {
|
|
11
|
+
return (_a = jsonwebtoken_1.default.decode(jwtToken)) === null || _a === void 0 ? void 0 : _a['https://claims.cimpress.io/canonical_id'];
|
|
12
|
+
}
|
|
13
|
+
catch (_b) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.safeJwtCanonicalIdParse = safeJwtCanonicalIdParse;
|
|
4
18
|
function safeJsonParse(input, defaultValue) {
|
|
5
19
|
try {
|
|
6
20
|
return JSON.parse(input);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambda-essentials-ts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "A selection of the finest modules supporting authorization, API routing, error handling, logging and sending HTTP requests.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -28,11 +28,13 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"aws-sdk": "2.939.0",
|
|
30
30
|
"axios": "0.21.1",
|
|
31
|
-
"axios-
|
|
31
|
+
"axios-cache-adapter": "2.7.3",
|
|
32
32
|
"fast-safe-stringify": "2.0.7",
|
|
33
33
|
"is-error": "2.2.2",
|
|
34
34
|
"jsonwebtoken": "8.5.1",
|
|
35
|
+
"md5": "2.3.0",
|
|
35
36
|
"openapi-factory": "4.4.247",
|
|
37
|
+
"retry-axios": "2.6.0",
|
|
36
38
|
"uuid": "8.3.2"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|