@63klabs/cache-data 1.2.2

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,567 @@
1
+ const RequestInfo = require('./RequestInfo.class');
2
+ const Timer = require('./Timer.class');
3
+ const DebugAndLog = require('./DebugAndLog.class');
4
+
5
+
6
+ /**
7
+ * Extends RequestInfo
8
+ * Can be used to create a custom ClientRequest object
9
+ */
10
+ class ClientRequest extends RequestInfo {
11
+
12
+ static #validations = {
13
+ referrers: ['*'],
14
+ parameters: {}
15
+ };
16
+
17
+ static #authenticationIsRequired = false; // is it a public API (no authentication required) or authenticated API? (if both, set to false and use authorizations and roles)
18
+ static #unauthenticatedAuthorizations = (ClientRequest.#authenticationIsRequired) ? ['none'] : ['all']; // change from 'all' if there is a mix of public and authenticated access
19
+
20
+ /* we would need to add valid roles and authorizations as well as static */
21
+
22
+ /* What and who of the request */
23
+ #event = null;
24
+ #context = null;
25
+ #authorizations = JSON.parse(JSON.stringify(ClientRequest.#unauthenticatedAuthorizations));
26
+ #roles = [];
27
+
28
+ /* The request data */
29
+ #props = {};
30
+
31
+ /* Logging */
32
+ #timer = null;
33
+ #logs = {
34
+ pathLog: [],
35
+ queryLog: [],
36
+ apiKey: null
37
+ }
38
+
39
+
40
+ /**
41
+ * Initializes the request data based on the event. Also sets the
42
+ * validity of the request so it may be checked by .isValid()
43
+ * @param {object} event object from Lambda
44
+ */
45
+ constructor(event, context) {
46
+ super(event);
47
+
48
+ this.#timer = new Timer("ClientRequest", true);
49
+
50
+ this.#event = event;
51
+ this.#context = context;
52
+
53
+ this.#authenticate();
54
+
55
+ const { resource, resourceArray, path, pathArray } = this.#extractResourceAndPath();
56
+
57
+ this.#props = {
58
+ method: this.#event.httpMethod,
59
+ path,
60
+ pathArray,
61
+ resource,
62
+ resourceArray,
63
+ pathParameters: {},
64
+ queryStringParameters: {},
65
+ headerParameters: {},
66
+ cookieParameters: {},
67
+ bodyPayload: this.#event?.body || null, // from body
68
+ client: {
69
+ isAuthenticated: this.isAuthenticated(),
70
+ isGuest: this.isGuest(),
71
+ authorizations: this.getAuthorizations(),
72
+ roles: this.getRoles()
73
+ },
74
+ deadline: (this.deadline() - 500),
75
+ calcMsToDeadline: this.calcMsToDeadline
76
+ };
77
+
78
+ this.#validate();
79
+
80
+ };
81
+
82
+ /**
83
+ * This is used to initialize the ClientRequest class for all requests.
84
+ * Add ClientRequest.init(options) to the Config.init process or at the
85
+ * top of the main index.js file outside of the handler.
86
+ * @param {Array<string>} options.validations.referrers An array of accepted referrers. String matching goes from right to left, so ['example.com'] will allow example.com and subdomain.example.com
87
+ * @param {object} options.validations.parameters An object containing functions for validating request parameters (path, querystring, headers, cookies, etc).
88
+ */
89
+ static init(options) {
90
+ if (typeof options === 'object') {
91
+ if ('validations' in options) {
92
+ if ('referrers' in options.validations) {
93
+ ClientRequest.#validations.referrers = options.validations.referrers;
94
+ }
95
+ if ('parameters' in options.validations) {
96
+ ClientRequest.#validations.parameters = options.validations.parameters;
97
+ }
98
+ }
99
+ } else {
100
+ const errMsg = 'Application Configuration Error. Invalid options passed to ClientRequest.init(). Received:';
101
+ DebugAndLog.error(errMsg, options);
102
+ throw new Error(errMsg, options);
103
+ }
104
+
105
+ };
106
+
107
+ static getReferrerWhiteList() {
108
+ return ClientRequest.#validations.referrers;
109
+ };
110
+
111
+ static getParameterValidations() {
112
+ return ClientRequest.#validations.parameters;
113
+ };
114
+
115
+ /**
116
+ * Used in the constructor to set validity of the request
117
+ * This method may be customized to meet your validation needs
118
+ */
119
+ #validate() {
120
+
121
+ let valid = false;
122
+
123
+ // add your additional validations here
124
+ valid = this.isAuthorizedReferrer() && this.#hasValidPathParameters() && this.#hasValidQueryStringParameters() && this.#hasValidHeaderParameters() && this.#hasValidCookieParameters();
125
+
126
+ // set the variable
127
+ super._isValid = valid;
128
+
129
+ };
130
+
131
+ #hasValidParameters(paramValidations, clientParameters) {
132
+
133
+ let rValue = {
134
+ isValid: true,
135
+ params: {}
136
+ }
137
+
138
+ if (clientParameters && paramValidations) {
139
+ // Use a for...of loop instead of forEach for better control flow
140
+ for (const [key, value] of Object.entries(clientParameters)) {
141
+ const paramKey = key.replace(/^\/|\/$/g, '');
142
+ const paramValue = value;
143
+
144
+ if (paramKey in paramValidations) {
145
+ const validationFunc = paramValidations[paramKey];
146
+ if (typeof validationFunc === 'function' && validationFunc(paramValue)) {
147
+ rValue.params[paramKey] = paramValue;
148
+ } else {
149
+ DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
150
+ rValue.isValid = false;
151
+ rValue.params = {};
152
+ return rValue; // exit right away
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return rValue;
158
+ }
159
+
160
+ #hasValidPathParameters() {
161
+ const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.pathParameters, this.#event?.pathParameters);
162
+ this.#props.pathParameters = params;
163
+ return isValid;
164
+ }
165
+
166
+ #hasValidQueryStringParameters() {
167
+ // lowercase all the this.#event.queryStringParameters keys
168
+ const qs = {};
169
+ for (const key in this.#event.queryStringParameters) {
170
+ qs[key.toLowerCase()] = this.#event.queryStringParameters[key];
171
+ }
172
+ const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.queryStringParameters, qs);
173
+ this.#props.queryStringParameters = params;
174
+ return isValid;
175
+ }
176
+
177
+ #hasValidHeaderParameters() {
178
+ // camel case all the this.#event.headers keys and remove hyphens
179
+ const headers = {};
180
+ for (const key in this.#event.headers) {
181
+ const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
182
+ headers[camelCaseKey] = this.#event.headers[key];
183
+ }
184
+ const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.headerParameters, headers);
185
+ this.#props.headerParameters = params;
186
+ return isValid;
187
+ }
188
+
189
+ #hasValidCookieParameters() {
190
+ const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.cookiearameters, this.#event?.cookie); // TODO
191
+ this.#props.cookieParameters = params;
192
+ return isValid;
193
+ }
194
+
195
+
196
+ /**
197
+ * Utility function for getPathArray and getResourceArray
198
+ * @param {array<string>} arr array to slice
199
+ * @param {number} n number of elements to return
200
+ * @returns {array<string>} array of elements
201
+ */
202
+ #getArray(arr, n = 0) {
203
+ if (n === 0 || arr.length <= n || (n < 0 && arr.length <= (n*-1))) {
204
+ return arr;
205
+ } else if (n > 0) {
206
+ return arr.slice(0, n);
207
+ } else {
208
+ // Handle negative indices by counting from the end
209
+ return arr.slice(n);
210
+ }
211
+ };
212
+
213
+ #getElementAt(arr, n = 0) {
214
+ if (arr.length <= n || (n < 0 && arr.length <= (n*-1)-1)) return null;
215
+ if (n < 0) {
216
+ // Handle negative indices by counting from the end
217
+ return arr[arr.length + n];
218
+ } else {
219
+ return arr[n];
220
+ }
221
+ };
222
+
223
+ /**
224
+ * Get the first n path elements as a string.
225
+ * If n is 0, the whole path will be provided
226
+ * If n is a negative number, the last n elements will be provided
227
+ * The return value is a string with each element separated by a slash.
228
+ * @param {number} n number of elements to return.
229
+ * @returns {string} path elements
230
+ */
231
+ getPath(n = 0) {
232
+ return this.getPathArray(n).join('/');
233
+ }
234
+
235
+ /**
236
+ * Get the first n path elements as an array.
237
+ * If n is 0, the whole path will be provided
238
+ * If n is a negative number, the last n elements will be provided
239
+ * The return value is an array of strings.
240
+ * @param {number} n number of elements to return.
241
+ * @returns {array<string>} path elements
242
+ */
243
+ getPathArray(n = 0) {
244
+ return this.#getArray(this.#props.pathArray, n);
245
+ }
246
+
247
+
248
+ /**
249
+ * Get the path element at the specified index. If n is a negative number then return the nth element from the end.
250
+ * @param {number} n index of the resource to return
251
+ * @returns {string} path element
252
+ */
253
+ getPathAt(n = 0) {
254
+ return this.#getElementAt(this.#props.pathArray, n);
255
+ }
256
+
257
+ /**
258
+ * Get the first n resource elements as a string.
259
+ * If n is 0, the whole resource will be provided
260
+ * If n is a negative number, the last n elements will be provided
261
+ * The return value is a string with each element separated by a slash.
262
+ * @param {number} n number of elements to return.
263
+ * @returns {string} resource elements
264
+ */
265
+ getResource(n = 0) {
266
+ return this.getResourceArray(n).join('/');
267
+ }
268
+
269
+ /**
270
+ * Get the first n resource elements as an array.
271
+ * If n is 0, the whole resource will be provided
272
+ * If n is a negative number, the last n elements will be provided
273
+ * The return value is an array of strings.
274
+ * @param {number} n number of elements to return.
275
+ * @returns {array<string>} resource elements
276
+ */
277
+ getResourceArray(n = 0) {
278
+ return this.#getArray(this.#props.resourceArray, n);
279
+ }
280
+
281
+ /**
282
+ * Get the resource element at the specified index. If n is a negative number then return the nth element from the end.
283
+ * @param {number} n index of the resource to return
284
+ * @returns {string} resource element
285
+ */
286
+ getResourceAt(n = 0) {
287
+ return this.#getElementAt(this.#props.resourceArray, n);
288
+ }
289
+
290
+ /**
291
+ * Returns the path parameters received in the request.
292
+ * Path parameters are defined in the API's path definition and validated in the applications validation functions.
293
+ * @returns {object} path parameters
294
+ */
295
+ getPathParameters() {
296
+ return this.#props.pathParameters;
297
+ };
298
+
299
+ /**
300
+ * Returns the query string parameters received in the request.
301
+ * Query string parameters are validated in the applications validation functions.
302
+ * @returns {object} query string parameters
303
+ */
304
+ getQueryStringParameters() {
305
+ return this.#props.queryStringParameters;
306
+ };
307
+
308
+ /**
309
+ * Returns the header parameters received in the request.
310
+ * Only headers validated in the applications validation functions are returned.
311
+ * @returns {object} header parameters
312
+ */
313
+ getHeaderParameters() {
314
+ return this.#props.headerParameters;
315
+ };
316
+
317
+ /**
318
+ * Returns the cookie parameters received in the request.
319
+ * Only cookies validated in the applications validation functions are returned.
320
+ * @returns {object} cookie parameters
321
+ */
322
+ getCookieParameters() {
323
+ return this.#props.cookieParameters;
324
+ };
325
+
326
+ #authenticate() {
327
+ // add your authentication logic here
328
+ this.authenticated = false; // anonymous
329
+ };
330
+
331
+ isAuthenticated() {
332
+ return (ClientRequest.#authenticationIsRequired && this.authenticated);
333
+ };
334
+
335
+ isGuest() {
336
+ return (!ClientRequest.#authenticationIsRequired && !this.authenticated);
337
+ };
338
+
339
+ isAuthorizedToPerform(action="all") {
340
+ return ( this.getAuthorizations().includes(action) || this.getAuthorizations().includes('all'));
341
+ };
342
+
343
+ getRoles() {
344
+ if (this.isAuthenticated()) {
345
+ return this.#roles;
346
+ } else {
347
+ return ['guest'];
348
+ }
349
+ };
350
+
351
+ getAuthorizations() {
352
+ if (this.isAuthenticated()) {
353
+ return this.#authorizations;
354
+ } else {
355
+ return JSON.parse(JSON.stringify(ClientRequest.#unauthenticatedAuthorizations));
356
+ }
357
+ };
358
+
359
+ isAuthorizedReferrer() {
360
+ /* Check the array of valid referrers */
361
+ /* Check if the array includes a wildcard (*) OR if one of the whitelisted referrers matches the end of the referrer */
362
+ if (ClientRequest.requiresValidReferrer()) {
363
+ return true;
364
+ } else {
365
+ for (let i = 0; i < ClientRequest.#validations.referrers.length; i++) {
366
+ if (this.getClientReferer().endsWith(ClientRequest.#validations.referrers[i])) {
367
+ return true;
368
+ }
369
+ }
370
+ }
371
+ return false;
372
+ };
373
+
374
+ static requiresValidReferrer() {
375
+ return !ClientRequest.#validations.referrers.includes('*');
376
+ };
377
+
378
+ hasNoAuthorization() {
379
+ return (this.getAuthorizations().includes('none') || !this.isAuthorizedReferrer() );
380
+ };
381
+
382
+
383
+ getExecutionTime() {
384
+ return this.#timer.elapsed();
385
+ };
386
+
387
+ getFinalExecutionTime() {
388
+ return this.#timer.stop();
389
+ }
390
+
391
+ /**
392
+ * Get the _processed_ request properties. These are the properties that
393
+ * the ClientRequest object took from the event sent to Lambda, validated,
394
+ * supplemented, and makes available to controllers.
395
+ * @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray[], pathParameters: {}, queryStringParameters: {}, headerParameters: {}, cookieParameters: {}, bodyPayload: string, client: {isAuthenticated: boolean, isGuest: boolean, authorizations: string[], roles: string[]}, deadline: number, calcMsToDeadline: number}
396
+ */
397
+ getProps() {
398
+ return this.#props;
399
+ };
400
+
401
+ /**
402
+ * Add one or more path notations to the log.
403
+ * These are used for logging and monitoring. When a response is finalized the route
404
+ * is recorded in CloudWatch logs along with the status and other information.
405
+ * Do not send sensitive information in the path notation, use placeholders instead.
406
+ * For example, /user/{id}/profile instead of /user/123/profile
407
+ * However, /city/Chicago is acceptable because it is not a sensitive identifier.
408
+ * Only add meaningful parameters. You can abbreviate and rewrite long parameters.
409
+ * For example, /format/jpg can be coded as /f:jpg or /user/123/profile/privacy as /userProfile/privacy
410
+ * @param {string|Array<string>} path
411
+ */
412
+ addPathLog(path = null) {
413
+ if (path === null) {
414
+ path = `${this.#props.method}:${this.#props.pathArray.join("/")}`;
415
+ }
416
+ if (typeof path === 'string') {
417
+ this.#logs.pathLog.push(path);
418
+ } else if (Array.isArray(path)) {
419
+ this.#logs.pathLog = this.#logs.pathLog.concat(path);
420
+ }
421
+ };
422
+
423
+ /**
424
+ * Add one or more query notations to the query log.
425
+ * These are used for logging and monitoring. When a response is finalized the
426
+ * parameters are recorded in CloudWatch logs along with the status and other
427
+ * information.
428
+ * Do not send sensitive information in the query notation, use placeholders instead.
429
+ * For example, user instead of user=123
430
+ * However, city=Chicago is acceptable because it is not a sensitive query.
431
+ * Only add meaningful parameters. You can abbreviate long parameters.
432
+ * For example, format=jpg can be coded as f:jpg
433
+ * @param {string|Array<string>} query
434
+ */
435
+ addQueryLog(query) {
436
+ if (typeof query === 'string') {
437
+ this.#logs.queryLog.push(query);
438
+ } else if (Array.isArray(query)) {
439
+ this.#logs.queryLog = this.#logs.queryLog.concat(query);
440
+ }
441
+ };
442
+
443
+ /**
444
+ * Get the request log entries
445
+ * resource: http method and resource path with path parameter keys (no values)
446
+ * queryKeys: query string keys (no values)
447
+ * pathLog: custom route path with values (set by application using addPathLog())
448
+ * queryLog: custom query with or without values (set by addQueryLog())
449
+ * apiKey: last 6 characters of api key if present
450
+ * @returns {resource: string, queryKeys: string, routeLog: string, queryLog: string, apiKey: string}
451
+ */
452
+ getRequestLog() {
453
+ return {
454
+ resource: `${this.#props.method}:${this.#props.resourceArray.join('/')}`,
455
+ // put queryString keys in alpha order and join with &
456
+ queryKeys: Object.keys(this.#props.queryStringParameters).sort().map(key => `${key}=${this.#props.queryStringParameters[key]}`).join('&'),
457
+ routeLog: this.#logs.pathLog.join('/'),
458
+ // put logs.params in alpha order and join with &
459
+ queryLog: this.#logs.queryLog.sort().join('&'),
460
+ // only show last 6 characters of this.apiKey
461
+ apiKey: (this.#logs.apiKey !== null) ? this.#logs.apiKey.substring(this.#logs.apiKey.length - 6) : null
462
+ };
463
+ };
464
+
465
+ timerStop() {
466
+ return this.#timer?.stop() || 0;
467
+ };
468
+
469
+ /**
470
+ *
471
+ * @returns {number} The remaining time before Lambda times out. 1000 if context is not set in ClientRequest object.
472
+ */
473
+ getRemainingTimeInMillis() {
474
+ return this.getContext().getRemainingTimeInMillis() || 1000;
475
+ };
476
+
477
+ /**
478
+ * Get the number of milliseconds remaining and deduct the headroom given.
479
+ * Useful when you want to set a timeout on a function (such as an http request)
480
+ * that may take longer than our function has time for.
481
+ * @param {number} headroomInMillis number in milliseconds to deduct from Remaining Time
482
+ * @returns {number} greater than or equal to 0
483
+ */
484
+ calcRemainingTimeInMillis(headroomInMillis = 0) {
485
+ let rt = this.getRemainingTimeInMillis() - headroomInMillis;
486
+ return (rt > 0 ? rt : 0);
487
+ };
488
+
489
+ /**
490
+ *
491
+ * @returns timestamp for when the remaining time is up
492
+ */
493
+ deadline() {
494
+ return Date.now() + this.getRemainingTimeInMillis();
495
+ };
496
+
497
+ /**
498
+ *
499
+ * @returns Milliseconds to Deadline
500
+ */
501
+ calcMsToDeadline(deadline) {
502
+ if (!deadline) {
503
+ deadline = Date.now() - 500;
504
+ }
505
+ return deadline - Date.now();
506
+ };
507
+
508
+ getContext() {
509
+ if (this.#context === null) {
510
+ DebugAndLog.warn("Context for request is null but was requested. Set context along with event when constructing ClientRequest object");
511
+ }
512
+ return this.#context;
513
+ };
514
+
515
+ getEvent() {
516
+ return this.#event;
517
+ };
518
+
519
+ #extractResourceAndPath() {
520
+ const {resource, path} = this.getEvent();
521
+
522
+ let resourceIndex = [];
523
+
524
+ const resourcesAndPaths = {
525
+ resource: '',
526
+ resourceArray: [],
527
+ path: '',
528
+ pathArray: []
529
+ };
530
+
531
+ /* We want to use reqContext.resourcePath to create a resourcePath and resourceArray, and we want to use path to create a path and pathArray
532
+ For resourcePathArray, we want to split resourcePath on / and remove any empty strings. We also want to lowercase any element that is not surrounded with {}
533
+ We want to add the index of any resource element that is surrounded with {} to the resourceIndex array.
534
+ For pathArray we want to split on / and remove any empty strings. We also want to lowercase any element that is not at an index listed in the resourceIndex array
535
+ */
536
+ if (resource) {
537
+ const resourceArray = resource.split('/').filter((element) => element !== '');
538
+ resourceArray.forEach((element, index) => {
539
+ if (element.startsWith('{') && element.endsWith('}')) {
540
+ resourceIndex.push(index);
541
+ resourcesAndPaths.resourceArray.push(element);
542
+ } else {
543
+ resourcesAndPaths.resourceArray.push(element.toLowerCase());
544
+ }
545
+ });
546
+ resourcesAndPaths.resource = resourcesAndPaths.resourceArray.join('/');
547
+ }
548
+
549
+ if (path) {
550
+ const pathArray = path.split('/').filter((element) => element !== '');
551
+ pathArray.forEach((element, index) => {
552
+ if (!resourceIndex.includes(index)) {
553
+ resourcesAndPaths.pathArray.push(element.toLowerCase());
554
+ } else {
555
+ resourcesAndPaths.pathArray.push(element);
556
+ }
557
+ });
558
+ resourcesAndPaths.path = resourcesAndPaths.pathArray.join('/');
559
+ }
560
+
561
+ return resourcesAndPaths;
562
+
563
+ }
564
+
565
+ };
566
+
567
+ module.exports = ClientRequest;