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 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,2 @@
1
+ import { AxiosAdapter } from 'axios';
2
+ export declare function createDebounceRequestAdapter(requestAdapter: AxiosAdapter, requestKeyProvider: (AxiosRequestConfig: any) => string): AxiosAdapter;
@@ -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 { Options as CacheOptions } from 'axios-extensions/lib/cacheAdapterEnhancer';
3
- import { Options as RetryOptions } from 'axios-extensions/lib/retryAdapterEnhancer';
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/kuitos/axios-extensions#cacheadapterenhancer
85
+ * @link https://github.com/RasCarlito/axios-cache-adapter/blob/master/axios-cache-adapter.d.ts#L26
85
86
  */
86
- cacheOptions?: 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/kuitos/axios-extensions#cacheadapterenhancer
94
+ * @link https://github.com/JustinBeckwith/retry-axios/blob/v2.6.0/src/index.ts#L11
94
95
  */
95
- retryOptions?: 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 cacheAdapterEnhancer_1 = __importDefault(require("axios-extensions/lib/cacheAdapterEnhancer"));
10
- const retryAdapterEnhancer_1 = __importDefault(require("axios-extensions/lib/retryAdapterEnhancer"));
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
- adapters = cacheAdapterEnhancer_1.default(adapters, options === null || options === void 0 ? void 0 : options.cacheOptions);
42
- }
43
- if (this.enableRetry) {
44
- adapters = retryAdapterEnhancer_1.default(adapters, options === null || options === void 0 ? void 0 : options.retryOptions);
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 = headers !== null && headers !== void 0 ? 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 : (_f = jsonwebtoken_1.default.decode(this.userToken)) === null || _f === void 0 ? void 0 : _f[this.canonicalIdKey];
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": "2.2.2",
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-extensions": "3.1.3",
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": {