@felloh-org/lambda-wrapper 1.0.0 → 1.0.1

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.
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _epsagon = _interopRequireDefault(require("epsagon"));
9
+
10
+ var _winston = _interopRequireDefault(require("winston"));
11
+
12
+ var _dependencyAware = _interopRequireDefault(require("../dependency-injection/dependency-aware"));
13
+
14
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
+
16
+ /**
17
+ * LoggerService class
18
+ */
19
+ class Logger extends _dependencyAware.default {
20
+ constructor(di) {
21
+ super(di);
22
+ this.winston = null;
23
+ }
24
+ /**
25
+ * Returns a Winston logger object
26
+ * configured for our lambdas.
27
+ *
28
+ * Note:
29
+ *
30
+ * If the lambda is executed
31
+ * in a `serverless-offline` context
32
+ * the log output to console will be pretty printed.
33
+ */
34
+
35
+
36
+ getLogger() {
37
+ const loggerFormats = [_winston.default.format.json({
38
+ replacer: (key, value) => {
39
+ if (value instanceof Buffer) {
40
+ return value.toString('base64');
41
+ }
42
+
43
+ if (value instanceof Error) {
44
+ const error = {};
45
+ Object.getOwnPropertyNames(value).forEach(objectKey => {
46
+ error[objectKey] = value[objectKey];
47
+ });
48
+ return error;
49
+ }
50
+
51
+ return value;
52
+ }
53
+ })];
54
+
55
+ if (this.getContainer().isOffline) {
56
+ loggerFormats.push(_winston.default.format.prettyPrint());
57
+ }
58
+
59
+ return _winston.default.createLogger({
60
+ level: 'info',
61
+ format: _winston.default.format.combine(...loggerFormats),
62
+ transports: [new _winston.default.transports.Console()]
63
+ });
64
+ }
65
+ /**
66
+ * Returns the logger.
67
+ *
68
+ * Uses a cached `Winston` object
69
+ * if it has been already generated,
70
+ * otherwise it generates one.
71
+ */
72
+
73
+
74
+ get logger() {
75
+ if (!this.winston) {
76
+ this.winston = this.getLogger();
77
+ }
78
+
79
+ return this.winston;
80
+ }
81
+ /**
82
+ * While handling an error, lambda wrapper should
83
+ * recognise axios errors and trim down the information.
84
+ *
85
+ * Keep the following keys:
86
+ * - message.config
87
+ * - message.message
88
+ * - message.response?.status
89
+ * - message.response?.data
90
+ *
91
+ * @param {object} error
92
+ */
93
+
94
+
95
+ static processAxiosError(error) {
96
+ const processed = {
97
+ config: error.config,
98
+ message: error.message
99
+ }; // It's pretty common for axios errors
100
+ // to not have.response e.g.when there's
101
+ // a network error or timeout.
102
+ // These errors will have .request but not .response.
103
+
104
+ if (error.response) {
105
+ processed.response = {
106
+ status: error.response.status,
107
+ data: error.response.data
108
+ };
109
+ }
110
+
111
+ return processed;
112
+ }
113
+ /**
114
+ * Transform the original message
115
+ * before it is passed to the winston logger
116
+ *
117
+ * @param {string|object} message
118
+ */
119
+
120
+
121
+ processMessage(message = '') {
122
+ let processed = message;
123
+
124
+ if (processed && processed.isAxiosError) {
125
+ processed = this.constructor.processAxiosError(processed);
126
+ }
127
+
128
+ return processed;
129
+ }
130
+ /**
131
+ * Log Error Message
132
+ *
133
+ * @param error object
134
+ * @param message string
135
+ */
136
+
137
+
138
+ error(error, message = '') {
139
+ if (typeof process.env.EPSAGON_TOKEN === 'string' && process.env.EPSAGON_TOKEN !== 'undefined' && typeof process.env.EPSAGON_SERVICE_NAME === 'string' && process.env.EPSAGON_SERVICE_NAME !== 'undefined' && error instanceof Error) {
140
+ _epsagon.default.setError(error);
141
+ }
142
+
143
+ this.logger.log('error', message, {
144
+ error: this.processMessage(error)
145
+ });
146
+ this.label('error', true);
147
+ this.metric('error', 'error', true);
148
+ }
149
+ /**
150
+ * Log Information Message
151
+ *
152
+ * @param message string
153
+ */
154
+
155
+
156
+ info(message) {
157
+ this.logger.log('info', this.processMessage(message));
158
+ }
159
+ /**
160
+ * Logs an error, using `LoggerService.error`
161
+ * or `LoggerService.info` based on
162
+ * `process.env.LOGGER_SOFT_WARNING`.
163
+ *
164
+ * Please note that `LoggerService.error` and `LoggerService.info`
165
+ * have different signatures. The function uses the shared argument
166
+ * instead of introducing ambiguity.
167
+ *
168
+ * @param error
169
+ */
170
+
171
+
172
+ warning(error) {
173
+ const softWarningValues = ['true', '1'];
174
+
175
+ if (softWarningValues.includes(process.env.LOGGER_SOFT_WARNING)) {
176
+ return this.info(error);
177
+ }
178
+
179
+ return this.error(error);
180
+ }
181
+ /**
182
+ * Add a label
183
+ *
184
+ * @param descriptor string
185
+ * @param silent boolean
186
+ */
187
+
188
+
189
+ label(descriptor, silent = false) {
190
+ if (typeof process.env.EPSAGON_TOKEN === 'string' && process.env.EPSAGON_TOKEN !== 'undefined' && typeof process.env.EPSAGON_SERVICE_NAME === 'string' && process.env.EPSAGON_SERVICE_NAME !== 'undefined') {
191
+ _epsagon.default.label(descriptor);
192
+ }
193
+
194
+ if (silent === false) {
195
+ this.logger.log('info', `label - ${descriptor}`);
196
+ }
197
+ }
198
+ /**
199
+ * Add a metric
200
+ *
201
+ * @param descriptor string
202
+ * @param stat integer | string
203
+ * @param silent boolean
204
+ */
205
+
206
+
207
+ metric(descriptor, stat, silent = false) {
208
+ if (typeof process.env.EPSAGON_TOKEN === 'string' && process.env.EPSAGON_TOKEN !== 'undefined' && typeof process.env.EPSAGON_SERVICE_NAME === 'string' && process.env.EPSAGON_SERVICE_NAME !== 'undefined') {
209
+ _epsagon.default.label(descriptor, stat);
210
+ }
211
+
212
+ if (silent === false) {
213
+ this.logger.log('info', `metric - ${descriptor} - ${stat}`);
214
+ }
215
+ }
216
+ /**
217
+ * Logs an object so that it can be inspected
218
+ *
219
+ * @param action - What are we doing with the object, i.e. 'Processing'
220
+ * @param object - The object to be stored in logs
221
+ * @param level - 'error', 'warning' or 'info'
222
+ */
223
+
224
+
225
+ object(action, object, level = 'info') {
226
+ if (!['error', 'warning', 'info'].includes(level)) {
227
+ throw new Error('Unrecognised log level');
228
+ }
229
+
230
+ const payload = JSON.stringify(object, null, 4);
231
+ return this[level](`${action}: '${payload}'`);
232
+ }
233
+
234
+ }
235
+
236
+ exports.default = Logger;
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.REQUEST_TYPES = exports.HTTP_METHODS_WITH_PAYLOADS = exports.HTTP_METHODS_WITHOUT_PAYLOADS = exports.ERROR_TYPES = void 0;
7
+
8
+ var _querystring = _interopRequireDefault(require("querystring"));
9
+
10
+ var _useragent = _interopRequireDefault(require("useragent"));
11
+
12
+ var _validate = _interopRequireDefault(require("validate.js/validate"));
13
+
14
+ var _xml2js = _interopRequireDefault(require("xml2js"));
15
+
16
+ var _dependencies = require("../config/dependencies");
17
+
18
+ var _dependencyAware = _interopRequireDefault(require("../dependency-injection/dependency-aware"));
19
+
20
+ var _response = _interopRequireDefault(require("../model/response"));
21
+
22
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23
+
24
+ /* eslint-disable class-methods-use-this */
25
+
26
+ /* eslint-disable sonarjs/no-duplicate-string */
27
+ const REQUEST_TYPES = {
28
+ DELETE: 'DELETE',
29
+ GET: 'GET',
30
+ HEAD: 'HEAD',
31
+ OPTIONS: 'OPTIONS',
32
+ PATCH: 'PATCH',
33
+ POST: 'POST',
34
+ PUT: 'PUT'
35
+ };
36
+ exports.REQUEST_TYPES = REQUEST_TYPES;
37
+ const HTTP_METHODS_WITHOUT_PAYLOADS = [REQUEST_TYPES.DELETE, REQUEST_TYPES.GET, REQUEST_TYPES.HEAD, REQUEST_TYPES.OPTIONS];
38
+ exports.HTTP_METHODS_WITHOUT_PAYLOADS = HTTP_METHODS_WITHOUT_PAYLOADS;
39
+ const HTTP_METHODS_WITH_PAYLOADS = [REQUEST_TYPES.PATCH, REQUEST_TYPES.POST, REQUEST_TYPES.PUT]; // Define action specific error types
40
+
41
+ exports.HTTP_METHODS_WITH_PAYLOADS = HTTP_METHODS_WITH_PAYLOADS;
42
+ const ERROR_TYPES = {
43
+ VALIDATION_ERROR: new _response.default({}, 400, 'required fields are missing')
44
+ };
45
+ /**
46
+ * RequestService class
47
+ */
48
+
49
+ exports.ERROR_TYPES = ERROR_TYPES;
50
+
51
+ class Request extends _dependencyAware.default {
52
+ /**
53
+ * Get a parameter from the request.
54
+ *
55
+ * @param parameter
56
+ * @param ifNull
57
+ * @param requestType
58
+ */
59
+ get(parameter, ifNull = null, requestType = null) {
60
+ const queryParameters = this.getAll(requestType);
61
+
62
+ if (queryParameters === null) {
63
+ return ifNull;
64
+ }
65
+
66
+ return typeof queryParameters[parameter] !== 'undefined' ? queryParameters[parameter] : ifNull;
67
+ }
68
+ /**
69
+ * Get all HTTP headers included in the request.
70
+ *
71
+ * @returns {object} An object with a key for each header.
72
+ */
73
+
74
+
75
+ getAllHeaders() {
76
+ return { ...this.getContainer().getEvent().headers
77
+ };
78
+ }
79
+ /**
80
+ * Get an HTTP header from the request.
81
+ *
82
+ * The header name is case-insensitive.
83
+ *
84
+ * @param {string} name The name of the header.
85
+ * @param {string} [whenMissing] Value to return if the header is missing.
86
+ * (default: empty string)
87
+ *
88
+ * @returns {string}
89
+ */
90
+
91
+
92
+ getHeader(name, whenMissing = '') {
93
+ const headers = this.getAllHeaders();
94
+
95
+ if (!headers) {
96
+ return whenMissing;
97
+ }
98
+
99
+ const lowerName = name.toLowerCase();
100
+ const key = Object.keys(headers).find(k => k.toLowerCase() === lowerName);
101
+ return key && headers[key] || whenMissing;
102
+ }
103
+ /**
104
+ * Get authorization token
105
+ *
106
+ * @returns {*}
107
+ */
108
+
109
+
110
+ getAuthorizationToken() {
111
+ const authorization = this.getHeader('Authorization');
112
+
113
+ if (!authorization) {
114
+ return null;
115
+ }
116
+
117
+ const tokenParts = authorization.split(' ');
118
+ const tokenValue = tokenParts[1];
119
+
120
+ if (!(tokenParts[0].toLowerCase() === 'bearer' && tokenValue)) {
121
+ return null;
122
+ }
123
+
124
+ return tokenValue;
125
+ }
126
+ /**
127
+ * Get a path parameter
128
+ *
129
+ * @param parameter
130
+ * @param ifNull mixed
131
+ */
132
+
133
+
134
+ getPathParameter(parameter = null, ifNull = {}) {
135
+ const event = this.getContainer().getEvent(); // If no parameter has been requested, return all path parameters
136
+
137
+ if (parameter === null && typeof event.pathParameters === 'object') {
138
+ return event.pathParameters;
139
+ } // If a specifc parameter has been requested, return the parameter if it exists
140
+
141
+
142
+ if (typeof parameter === 'string' && typeof event.pathParameters === 'object' && event.pathParameters !== null && typeof event.pathParameters[parameter] !== 'undefined') {
143
+ return event.pathParameters[parameter];
144
+ }
145
+
146
+ return ifNull;
147
+ }
148
+ /**
149
+ * Get all request parameters
150
+ *
151
+ * @param requestType
152
+ * @returns {{}}
153
+ */
154
+ // eslint-disable-next-line sonarjs/cognitive-complexity
155
+
156
+
157
+ getAll(requestType = null) {
158
+ const event = this.getContainer().getEvent();
159
+
160
+ if (HTTP_METHODS_WITHOUT_PAYLOADS.includes(event.httpMethod) || HTTP_METHODS_WITHOUT_PAYLOADS.includes(requestType)) {
161
+ // get simple parameters
162
+ const params = { ...event.queryStringParameters
163
+ }; // add array parameters as arrays
164
+
165
+ Object.keys(params).filter(key => key.endsWith('[]')).forEach(key => {
166
+ params[key] = event.multiValueQueryStringParameters[key];
167
+ });
168
+ return params;
169
+ }
170
+
171
+ if (HTTP_METHODS_WITH_PAYLOADS.includes(event.httpMethod) || HTTP_METHODS_WITH_PAYLOADS.includes(requestType)) {
172
+ const contentType = this.getHeader('Content-Type');
173
+ let queryParameters = {};
174
+
175
+ if (contentType.includes('application/x-www-form-urlencoded')) {
176
+ queryParameters = _querystring.default.parse(event.body);
177
+ }
178
+
179
+ if (contentType.includes('application/json')) {
180
+ try {
181
+ queryParameters = JSON.parse(event.body);
182
+ } catch (error) {
183
+ queryParameters = {};
184
+ }
185
+ }
186
+
187
+ if (contentType.includes('text/xml')) {
188
+ _xml2js.default.parseString(event.body, (error, result) => {
189
+ queryParameters = error ? {} : result;
190
+ });
191
+ }
192
+
193
+ if (contentType.includes('multipart/form-data')) {
194
+ queryParameters = this.parseForm(true);
195
+ }
196
+
197
+ return typeof queryParameters !== 'undefined' ? queryParameters : {};
198
+ }
199
+
200
+ return null;
201
+ }
202
+ /**
203
+ * Fetch the request IP address
204
+ *
205
+ * @returns {*}
206
+ */
207
+
208
+
209
+ getIp() {
210
+ const event = this.getContainer().getEvent();
211
+
212
+ if (typeof event.requestContext !== 'undefined' && typeof event.requestContext.identity !== 'undefined' && typeof event.requestContext.identity.sourceIp !== 'undefined') {
213
+ return event.requestContext.identity.sourceIp;
214
+ }
215
+
216
+ return null;
217
+ }
218
+ /**
219
+ * Get user agent
220
+ *
221
+ * @returns {*}
222
+ */
223
+
224
+
225
+ getUserBrowserAndDevice() {
226
+ const userAgent = this.getHeader('user-agent', null);
227
+
228
+ if (userAgent === null) {
229
+ return null;
230
+ }
231
+
232
+ try {
233
+ const agent = _useragent.default.parse(userAgent);
234
+
235
+ const os = agent.os.toJSON();
236
+ return {
237
+ 'browser-type': agent.family,
238
+ 'browser-version': agent.toVersion(),
239
+ 'device-type': agent.device.family,
240
+ 'operating-system': os.family,
241
+ 'operating-system-version': agent.os.toVersion()
242
+ };
243
+ } catch (error) {
244
+ this.getContainer().get(_dependencies.DEFINITIONS.LOGGER).label('user-agent-parsing-failed');
245
+ return null;
246
+ }
247
+ }
248
+ /**
249
+ * Test a request against validation constraints
250
+ *
251
+ * @param constraints
252
+ * @returns {Promise<any>}
253
+ */
254
+
255
+
256
+ validateAgainstConstraints(constraints) {
257
+ const Logger = this.getContainer().get(_dependencies.DEFINITIONS.LOGGER);
258
+ return new Promise((resolve, reject) => {
259
+ const validation = (0, _validate.default)(this.getAll(), constraints);
260
+
261
+ if (typeof validation === 'undefined') {
262
+ resolve();
263
+ } else {
264
+ Logger.label('request-validation-failed');
265
+ const validationErrorResponse = ERROR_TYPES.VALIDATION_ERROR;
266
+ validationErrorResponse.setBodyVariable('validation_errors', validation);
267
+ reject(validationErrorResponse);
268
+ }
269
+ });
270
+ }
271
+ /**
272
+ * Fetch the request multipart form
273
+ *
274
+ * @param useBuffer
275
+ * @returns {*}
276
+ */
277
+
278
+
279
+ parseForm(useBuffer) {
280
+ const event = this.getContainer().getEvent();
281
+ const boundary = this.getBoundary(event);
282
+ const body = event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('binary').trim() : event.body;
283
+ const result = {};
284
+ body.split(boundary).forEach(item => {
285
+ if (/filename=".+"/g.test(item)) {
286
+ result[item.match(/name=".+";/g)[0].slice(6, -2)] = {
287
+ type: 'file',
288
+ filename: item.match(/filename=".+"/g)[0].slice(10, -1),
289
+ contentType: item.match(/Content-Type:\s.+/g)[0].slice(14),
290
+ content: useBuffer ? Buffer.from(item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4), 'binary') : item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4)
291
+ };
292
+ } else if (/name=".+"/g.test(item)) {
293
+ result[item.match(/name=".+"/g)[0].slice(6, -1)] = item.slice(item.search(/name=".+"/g) + item.match(/name=".+"/g)[0].length + 4, -4);
294
+ }
295
+ });
296
+ return result;
297
+ }
298
+ /**
299
+ * Fetch the request AWS event Records
300
+ *
301
+ * @returns {*}
302
+ */
303
+
304
+
305
+ getAWSRecords() {
306
+ const event = this.getContainer().getEvent();
307
+ const eventRecord = event.Records && event.Records[0];
308
+
309
+ if (typeof event.Records !== 'undefined' && typeof event.Records[0] !== 'undefined' && typeof eventRecord.eventSource !== 'undefined') {
310
+ return eventRecord;
311
+ }
312
+
313
+ return null;
314
+ }
315
+ /**
316
+ * Gets a value independently from
317
+ * the case of the key
318
+ *
319
+ * @param object
320
+ * @param key
321
+ */
322
+
323
+
324
+ getValueIgnoringKeyCase(object, key) {
325
+ const foundKey = Object.keys(object).find(currentKey => currentKey.toLocaleLowerCase() === key.toLowerCase());
326
+ return object[foundKey];
327
+ }
328
+ /**
329
+ * Returns the content type
330
+ * assoiated with the request
331
+ *
332
+ * @param event
333
+ */
334
+
335
+
336
+ getBoundary(event) {
337
+ return this.getValueIgnoringKeyCase(event.headers, 'Content-Type').split('=')[1];
338
+ }
339
+
340
+ }
341
+
342
+ exports.default = Request;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ /**
9
+ *
10
+ */
11
+ class LambdaTermination extends Error {
12
+ /**
13
+ * Triggers a Lambda Termination.
14
+ * Offers developer details (that are logged)
15
+ * an code for the Lambda and a front facing
16
+ * consumer message.
17
+ *
18
+ * @param {object|string} internal
19
+ * @param {number?} code
20
+ * @param {object|string?} body
21
+ * @param details
22
+ */
23
+ constructor(internal, code = 500, body = null, details = 'unknown error') {
24
+ let stringified = internal;
25
+
26
+ if (typeof internal !== 'string') {
27
+ stringified = JSON.stringify(internal);
28
+ }
29
+
30
+ super(stringified);
31
+ this.internal = internal;
32
+ this.code = code;
33
+ this.body = body || 'unknown error';
34
+ this.details = details;
35
+ }
36
+
37
+ }
38
+
39
+ exports.default = LambdaTermination;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ const STANDARD_LATENCY_DELAYS = {
8
+ 2000: 70,
9
+ 3500: 15,
10
+ 4000: 10,
11
+ 5000: 5
12
+ };
13
+ const HIGH_LATENCY_DELAYS = {
14
+ 2000: 65,
15
+ 3500: 15,
16
+ 4000: 9,
17
+ 5000: 5,
18
+ 10000: 5,
19
+ 20000: 1
20
+ };
21
+ /**
22
+ * PromisifiedDelay class
23
+ */
24
+
25
+ class PromisifiedDelay {
26
+ /**
27
+ * PromisifiedDelay constructor
28
+ *
29
+ * @param highLatency
30
+ */
31
+ constructor(highLatency = true) {
32
+ this.delays = [];
33
+ const delayArray = highLatency === true ? HIGH_LATENCY_DELAYS : STANDARD_LATENCY_DELAYS;
34
+ Object.keys(delayArray).forEach(delayDuration => {
35
+ const delayIterations = delayArray[delayDuration];
36
+
37
+ for (let i = 0; i < delayIterations; i += 1) {
38
+ this.delays.push(delayDuration);
39
+ }
40
+ });
41
+ }
42
+ /**
43
+ * Create a promisified delay
44
+ *
45
+ * @returns {Promise<any>}
46
+ */
47
+
48
+
49
+ get() {
50
+ return new Promise(resolve => {
51
+ setTimeout(() => {
52
+ resolve();
53
+ }, this.delays[Math.floor(Math.random() * this.delays.length)]);
54
+ });
55
+ }
56
+
57
+ }
58
+
59
+ exports.default = PromisifiedDelay;