@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,547 @@
1
+
2
+ const jsonGenericResponse = require('./generic.response.json');
3
+ const htmlGenericResponse = require('./generic.response.html');
4
+ const rssGenericResponse = require('./generic.response.rss');
5
+ const xmlGenericResponse = require('./generic.response.xml');
6
+ const textGenericResponse = require('./generic.response.text');
7
+ const ClientRequest = require('./ClientRequest.class');
8
+ const DebugAndLog = require('./DebugAndLog.class');
9
+
10
+ /*
11
+ Example Response
12
+ statusCode: 404,
13
+ headers: {
14
+ "Access-Control-Allow-Origin": "*",
15
+ "Content-Type": "application/json"
16
+ },
17
+ body: {
18
+ message: "Not Found"
19
+ }
20
+ */
21
+ /**
22
+ * Can be used to create a custom Response interface
23
+ */
24
+ class Response {
25
+
26
+ static #isInitialized = false;
27
+
28
+ static #jsonResponses = jsonGenericResponse;
29
+ static #htmlResponses = htmlGenericResponse;
30
+ static #rssResponses = rssGenericResponse;
31
+ static #xmlResponses = xmlGenericResponse;
32
+ static #textResponses = textGenericResponse;
33
+
34
+ static CONTENT_TYPE = {
35
+ JSON: Response.#jsonResponses.contentType,
36
+ HTML: Response.#htmlResponses.contentType,
37
+ XML: Response.#xmlResponses.contentType,
38
+ RSS: Response.#rssResponses.contentType,
39
+ TEXT: Response.#textResponses.contentType,
40
+ JAVASCRIPT: 'application/javascript',
41
+ CSS: 'text/css',
42
+ CSV: 'text/csv'
43
+ };
44
+
45
+ static #settings = {
46
+ errorExpirationInSeconds: (60 * 3),
47
+ routeExpirationInSeconds: 0,
48
+ contentType: Response.CONTENT_TYPE.JSON
49
+ };
50
+
51
+ _clientRequest = null;
52
+ _statusCode = 200;
53
+ _headers = {};
54
+ _body = null;
55
+
56
+ /**
57
+ * @param {ClientRequest} clientRequest
58
+ * @param {{statusCode: number, headers: object, body: string|number|object|array }} obj Default structure.
59
+ */
60
+ constructor(clientRequest, obj = {}, contentType = null) {
61
+ this._clientRequest = clientRequest;
62
+ this.reset(obj, contentType);
63
+ };
64
+
65
+ /**
66
+ * @typedef statusResponseObject
67
+ * @property {number} statusCode
68
+ * @property {object} headers
69
+ * @property {object|array} body
70
+ */
71
+
72
+ /**
73
+ * Initialize the Response class for all responses.
74
+ * Add Response.init(options) to the Config.init process or at the
75
+ * top of the main index.js file outside of the handler.
76
+ * @param {number} options.settings.errorExpirationInSeconds
77
+ * @param {number} options.settings.routeExpirationInSeconds
78
+ * @param {string} options.settings.contentType Any one of available Response.CONTENT_TYPE values
79
+ * @param {{response200: statusResponseObject, response404: statusResponseObject, response500: statusResponseObject}} options.jsonResponses
80
+ */
81
+ static init = (options) => {
82
+ if (!Response.#isInitialized) {
83
+
84
+ Response.#isInitialized = true;
85
+
86
+ if ( options?.settings ) {
87
+ // merge settings using assign object
88
+ //this.#settings = Object.assign({}, Response.#settings, options.settings);
89
+ Response.#settings = { ...Response.#settings, ...options.settings };
90
+ }
91
+ if ( options?.jsonResponses ) {
92
+ // merge settings using assign object
93
+ //Response.#jsonResponses = Object.assign({}, Response.#jsonResponses, options.jsonResponses);
94
+ Response.#jsonResponses = { ...Response.#jsonResponses, ...options.jsonResponses };
95
+ }
96
+
97
+ if ( options?.htmlResponses ) {
98
+ // merge settings using assign object
99
+ //Response.#htmlResponses = Object.assign({}, Response.#htmlResponses, options.htmlResponses);
100
+ Response.#htmlResponses = { ...Response.#htmlResponses, ...options.htmlResponses };
101
+ }
102
+
103
+ if ( options?.xmlResponses ) {
104
+ // merge settings using assign object
105
+ //Response.#xmlResponses = Object.assign({}, Response.#xmlResponses, options.xmlResponses);
106
+ Response.#htmlResponses = { ...Response.#xmlResponses, ...options.xmlResponses };
107
+ }
108
+
109
+ if ( options?.rssResponses ) {
110
+ // merge settings using assign object
111
+ //Response.#rssResponses = Object.assign({}, Response.#rssResponses, options.rssResponses);
112
+ Response.#rssResponses = { ...Response.#rssResponses, ...options.rssResponses };
113
+ }
114
+
115
+ if ( options?.textResponses ) {
116
+ // merge settings using assign object
117
+ //Response.#textResponses = Object.assign({}, Response.#textResponses, options.textResponses);
118
+ Response.#textResponses = { ...Response.#textResponses, ...options.textResponses };
119
+ }
120
+ }
121
+
122
+ };
123
+
124
+ /**
125
+ * Reset all properties of the response back to default values except for
126
+ * those properties specified in the object. Note that ClientRequest
127
+ * cannot be reset.
128
+ * @param {{statusCode: number|string, headers: object, body: string|number|object|array}} obj
129
+ * @param {string} contentType Accepted values may be obtained from Response.CONTENT_TYPES[JSON|HTML|XML|RSS|TEXT]
130
+ */
131
+ reset = (obj, contentType = null) => {
132
+
133
+ let newObj = {};
134
+
135
+ newObj.statusCode = obj?.statusCode ?? 200;
136
+
137
+ if (contentType === null) {
138
+ const result = Response.inspectContentType(obj);
139
+ contentType = (result !== null) ? result : Response.#settings.contentType;
140
+ }
141
+
142
+ const genericResponses = Response.getGenericResponses(contentType);
143
+
144
+ newObj.headers = obj?.headers ?? genericResponses.response(newObj.statusCode).headers;
145
+ newObj.body = obj?.body ?? genericResponses.response(newObj.statusCode).body;
146
+
147
+ this.set(newObj, contentType);
148
+ };
149
+
150
+ /**
151
+ * Set the properties of the response. This will overwrite only properties
152
+ * supplied in the new object. Use .reset if you wish to clear out all properties even
153
+ * if not explicitly set in the object. ClientRequest cannot be set.
154
+ * @param {{statusCode: number|string, headers: object, body: string|number|object|array}} obj
155
+ */
156
+ set = (obj, contentType = null) => {
157
+
158
+ if (contentType === null) {
159
+ const result = Response.inspectContentType(obj);
160
+ const thisResult = this.inspectContentType();
161
+ contentType = result || thisResult || Response.#settings.contentType;
162
+ }
163
+
164
+ if (obj?.statusCode) this._statusCode = parseInt(obj.statusCode);
165
+ if (obj?.headers) this._headers = obj.headers;
166
+ if (obj?.body) this._body = obj.body;
167
+
168
+ this.addHeader('Content-Type', contentType);
169
+ }
170
+
171
+ /**
172
+ *
173
+ * @returns {number} Current statusCode of the Response
174
+ */
175
+ getStatusCode = () => {
176
+ return this._statusCode;
177
+ };
178
+
179
+ /**
180
+ *
181
+ * @returns {object} Current headers of the Response
182
+ */
183
+ getHeaders = () => {
184
+ return this._headers;
185
+ };
186
+
187
+ /**
188
+ *
189
+ * @returns {object|array|string|number|null} Current body of the Response
190
+ */
191
+ getBody = () => {
192
+ return this._body;
193
+ };
194
+
195
+ /**
196
+ *
197
+ * @returns {string} Current ContentType of the Response
198
+ */
199
+ static getContentType() {
200
+ return Response.#settings.contentType;
201
+ };
202
+
203
+ /**
204
+ *
205
+ * @returns {number} Current errorExpirationInSeconds of the Response
206
+ */
207
+ static getErrorExpirationInSeconds() {
208
+ return Response.#settings.errorExpirationInSeconds;
209
+ };
210
+
211
+ /**
212
+ *
213
+ * @returns {number} Current routeExpirationInSeconds of the Response
214
+ */
215
+ static getRouteExpirationInSeconds() {
216
+ return Response.#settings.routeExpirationInSeconds;
217
+ };
218
+
219
+ /**
220
+ * Static method to inspect the body and headers to determine the ContentType. Used by the internal methods.
221
+ * @param {{headers: object, body: object|array|string|number|null}} obj Object to inspect
222
+ * @returns {string|null} The ContentType as determined after inspecting the headers and body
223
+ */
224
+ static inspectContentType = (obj) => {
225
+ const headerResult = Response.inspectHeaderContentType(obj.headers);
226
+ const bodyResult = Response.inspectBodyContentType(obj.body);
227
+ return (headerResult !== null) ? headerResult : bodyResult;
228
+ }
229
+
230
+ /**
231
+ * Static method to inspect the body to determine the ContentType. Used by the internal methods.
232
+ * @param {object|array|string|number|null} body
233
+ * @returns {string|null} The ContentType as determined after inspecting just the body
234
+ */
235
+ static inspectBodyContentType = (body) => {
236
+ if (body !== null) {
237
+ if (typeof body === 'string') {
238
+ if (body.includes('</html>')) {
239
+ return Response.CONTENT_TYPE.HTML;
240
+ } else if (body.includes('</rss>')) {
241
+ return Response.CONTENT_TYPE.RSS;
242
+ } else if (body.includes('<?xml')) {
243
+ return Response.CONTENT_TYPE.XML;
244
+ } else {
245
+ return Response.CONTENT_TYPE.TEXT;
246
+ }
247
+ } else {
248
+ return Response.CONTENT_TYPE.JSON;
249
+ }
250
+ }
251
+ return null;
252
+ }
253
+
254
+ /**
255
+ * Static method to inspect the headers to determine the ContentType. Used by the internal methods.
256
+ * @param {object} headers
257
+ * @returns {string|null} The ContentType as determined after inspecting just the headers
258
+ */
259
+ static inspectHeaderContentType = (headers) => {
260
+ return (headers && 'Content-Type' in headers ? headers['Content-Type'] : null);
261
+ }
262
+
263
+ /**
264
+ * Inspect the content type of this Response. Passes this headers and this body to the static method
265
+ * @returns {string|null} The ContentType as determined after inspecting the headers and body
266
+ */
267
+ inspectContentType = () => {
268
+ return Response.inspectContentType({headers: this._headers, body: this._body});
269
+ }
270
+
271
+ /**
272
+ * Inspect the body to determine the ContentType. Passes this body to the static method
273
+ * @returns {string} ContentType string value determined from the current body
274
+ */
275
+ inspectBodyContentType = () => {
276
+ return Response.inspectBodyContentType(this._body);
277
+ }
278
+
279
+ /**
280
+ * Inspect the headers to determine the ContentType. Passes this headers to the static method
281
+ * @returns {string} ContentType string value determined from the current headers
282
+ */
283
+ inspectHeaderContentType = () => {
284
+ return Response.inspectHeaderContentType(this._headers);
285
+ }
286
+
287
+ /**
288
+ * Get the current ContentType of the response. Inspects headers and body to determine ContentType. Returns the default from init if none is determined.
289
+ * @returns {string} ContentType string value determined from the header or current body
290
+ */
291
+ getContentType = () => {
292
+ // Default content type is JSON
293
+ let defaultContentType = Response.#settings.contentType;
294
+ let contentType = this.inspectContentType();
295
+ if (contentType === null) {
296
+ contentType = defaultContentType;
297
+ }
298
+ return contentType;
299
+ };
300
+
301
+ /**
302
+ * Get the content type code for the response. This is the key for the CONTENT_TYPE object.
303
+ * @returns {string}
304
+ */
305
+ getContentTypeCode = () => {
306
+ const contentTypeStr = this.getContentType();
307
+ const contentTypeCodes = Object.keys(Response.CONTENT_TYPE);
308
+ // loop through CONTENT_TYPE and find the index of the contentTypeStr
309
+ for (let i = 0; i < contentTypeCodes.length; i++) {
310
+ if (Response.CONTENT_TYPE[contentTypeCodes[i]] === contentTypeStr) {
311
+ return contentTypeCodes[i];
312
+ }
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Set the status code of the response. This will overwrite the status code of the response.
318
+ * @param {number} statusCode
319
+ */
320
+ setStatusCode = (statusCode) => {
321
+ this.set({statusCode: statusCode});
322
+ };
323
+
324
+ /**
325
+ * Set the headers of the response. This will overwrite the headers of the response.
326
+ * @param {object} headers
327
+ */
328
+ setHeaders = (headers) => {
329
+ this.set({headers: headers});
330
+ };
331
+
332
+ /**
333
+ * Set the body of the response. This will overwrite the body of the response.
334
+ * @param {string|number|object|array} body
335
+ */
336
+ setBody = (body) => {
337
+ this.set({body: body});
338
+ };
339
+
340
+ /**
341
+ * Get the generic response for the content type. Generic responses are either provided by default from Cache-Data or loaded in during Response.init()
342
+ * @param {string} contentType
343
+ * @returns {statusResponseObject}
344
+ */
345
+ static getGenericResponses = (contentType) => {
346
+ if (contentType === Response.CONTENT_TYPE.JSON || contentType === 'JSON') {
347
+ return Response.#jsonResponses;
348
+ } else if (contentType === Response.CONTENT_TYPE.HTML || contentType === 'HTML') {
349
+ return Response.#htmlResponses;
350
+ } else if (contentType === Response.CONTENT_TYPE.RSS || contentType === 'RSS') {
351
+ return Response.#rssResponses;
352
+ } else if (contentType === Response.CONTENT_TYPE.XML || contentType === 'XML') {
353
+ return Response.#xmlResponses;
354
+ } else if (contentType === Response.CONTENT_TYPE.TEXT || contentType === 'TEXT') {
355
+ return Response.#textResponses;
356
+ } else {
357
+ throw new Error(`Content Type: ${contentType} is not implemented for getResponses. Response.CONTENT_TYPES[JSON|HTML|XML|RSS|TEXT] must be used. Perform a custom implementation by extending the Response class.`);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Add a header if it does not exist, if it exists then update the value
363
+ * @param {string} key
364
+ * @param {string} value
365
+ */
366
+ addHeader = (key, value) => {
367
+ this._headers[key] = value;
368
+ };
369
+
370
+ /**
371
+ *
372
+ * @param {object} obj
373
+ */
374
+ addToJsonBody = (obj) => {
375
+ if (typeof this._body === 'object') {
376
+ this._body = Object.assign({}, this._body, obj);
377
+ }
378
+ };
379
+
380
+ /**
381
+ *
382
+ * @returns {{statusCode: number, headers: object, body: null|string|Array|object}}
383
+ */
384
+ toObject = () => {
385
+ return {
386
+ statusCode: this._statusCode,
387
+ headers: this._headers,
388
+ body: this._body
389
+ };
390
+ };
391
+
392
+ /**
393
+ *
394
+ * @returns {string} A string representation of the Response object
395
+ */
396
+ toString = () => {
397
+ return JSON.stringify(this.toObject());
398
+ };
399
+
400
+ /**
401
+ * Used by JSON.stringify to convert the response to a stringified object
402
+ * @returns {{statusCode: number, headers: object, body: null|string|Array|object}} this class in object form ready for use by JSON.stringify
403
+ */
404
+ toJSON = () => {
405
+ return this.toObject();
406
+ };
407
+
408
+ /**
409
+ * Send the response back to the client. If the body is an object or array, it will be stringified.
410
+ * If the body is a string or number and the Content-Type header is json, it will be placed as a single element in an array then stringified.
411
+ * If the body of the response is null it returns null
412
+ * A response log entry is also created and sent to CloudWatch.
413
+ * @returns {{statusCode: number, headers: object, body: string}} An object containing response data formatted to return from Lambda
414
+ */
415
+ finalize = () => {
416
+
417
+ let bodyAsString = null;
418
+
419
+ try {
420
+ // if the header response type is not set, determine from contents of body. default to json
421
+ if (!('Content-Type' in this._headers)) {
422
+ this._headers['Content-Type'] = this.getContentType();
423
+ }
424
+
425
+ // If body is of type error then set status to 500
426
+ if (this._body instanceof Error) {
427
+ this.reset({statusCode: 500});
428
+ }
429
+
430
+ if (this._body !== null) { // we'll keep null as null
431
+
432
+ // if response type is JSON we need to make sure we respond with stringified json
433
+ if (this._headers['Content-Type'] === Response.CONTENT_TYPE.JSON) {
434
+
435
+ // body is a string or number, place in array (unless the number is 404, then that signifies not found)
436
+ if (typeof this._body === 'string' || typeof this._body === 'number') {
437
+ if (this._body === 404) {
438
+ this.reset({statusCode: 404});
439
+ } else {
440
+ this._body = [this._body];
441
+ }
442
+ }
443
+
444
+ // body is presumably an object or array, so stringify
445
+ bodyAsString = JSON.stringify(this._body);
446
+
447
+ } else { // if response type is not json we need to respond with a string (or null but we already did a null check)
448
+ bodyAsString = `${this._body}`;
449
+ }
450
+ }
451
+
452
+ } catch (error) {
453
+ /* Log the error */
454
+ DebugAndLog.error(`Error Finalizing Response: ${error.message}`, error.stack);
455
+ this.reset({statusCode: 500});
456
+ bodyAsString = JSON.stringify(this._body); // we reset to 500 so stringify it
457
+ }
458
+
459
+ try {
460
+ if (ClientRequest.requiresValidReferrer()) {
461
+ this.addHeader("Referrer-Policy", "strict-origin-when-cross-origin");
462
+ this.addHeader("Vary", "Origin");
463
+ this.addHeader("Access-Control-Allow-Origin", `https://${this._clientRequest.getClientReferrer()}`);
464
+ } else {
465
+ this.addHeader("Access-Control-Allow-Origin", "*");
466
+ }
467
+
468
+ if (this._statusCode >= 400) {
469
+ this.addHeader("Expires", (new Date(Date.now() + ( Response.#settings.errorExpirationInSeconds * 1000))).toUTCString());
470
+ this.addHeader("Cache-Control", "max-age="+Response.#settings.errorExpirationInSeconds);
471
+ } else if (Response.#settings.routeExpirationInSeconds > 0 ) {
472
+ this.addHeader("Expires", (new Date(Date.now() + ( Response.#settings.routeExpirationInSeconds * 1000))).toUTCString());
473
+ this.addHeader("Cache-Control", "max-age="+Response.#settings.routeExpirationInSeconds);
474
+ }
475
+
476
+ this.addHeader('x-exec-ms', `${this._clientRequest.getFinalExecutionTime()}`);
477
+
478
+ this._log(bodyAsString);
479
+ } catch (error) {
480
+ DebugAndLog.error(`Error Finalizing Response: Header and Logging Block: ${error.message}`, error.stack);
481
+ this.reset({statusCode: 500});
482
+ bodyAsString = JSON.stringify(this._body); // we reset to 500 so stringify it
483
+ }
484
+
485
+ return {
486
+ statusCode: this._statusCode,
487
+ headers: this._headers,
488
+ body: bodyAsString
489
+ };
490
+
491
+ };
492
+
493
+ /**
494
+ * Log the ClientRequest and Response to CloudWatch
495
+ * Formats a log entry parsing in CloudWatch Dashboard.
496
+ */
497
+ _log(bodyAsString) {
498
+
499
+ try {
500
+
501
+ /* These are pushed onto the array in the same order that the CloudWatch
502
+ query is expecting to parse out.
503
+ -- NOTE: If you add any here, be sure to update the Dashboard template --
504
+ -- that parses response logs in template.yml !! --
505
+ -- loggingType, statusCode, bodySize, execTime, clientIP, userAgent, origin, referrer, route, params, key
506
+ */
507
+
508
+ const loggingType = "RESPONSE";
509
+ const statusCode = this._statusCode;
510
+ const bytes = this._body !== null ? Buffer.byteLength(bodyAsString, 'utf8') : 0; // calculate byte size of response.body
511
+ const contentType = this.getContentTypeCode();
512
+ const execms = this._clientRequest.getFinalExecutionTime();
513
+ const clientIp = this._clientRequest.getClientIp();
514
+ const userAgent = this._clientRequest.getClientUserAgent();
515
+ const origin = this._clientRequest.getClientOrigin();
516
+ const referrer = this._clientRequest.getClientReferrer(true);
517
+ const {resource, queryKeys, routeLog, queryLog, apiKey } = this._clientRequest.getRequestLog();
518
+
519
+ let logFields = [];
520
+ logFields.push(statusCode);
521
+ logFields.push(bytes);
522
+ logFields.push(contentType);
523
+ logFields.push(execms);
524
+ logFields.push(clientIp);
525
+ logFields.push( (( userAgent !== "" && userAgent !== null) ? userAgent : "-").replace(/|/g, "") ); // doubtful, but userAgent could have | which will mess with log fields
526
+ logFields.push( (( origin !== "" && origin !== null) ? origin : "-") );
527
+ logFields.push( (( referrer !== "" && referrer !== null) ? referrer : "-") );
528
+ logFields.push(resource); // path includes any path parameter keys (not values)
529
+ logFields.push(queryKeys ? queryKeys : "-"); // just the keys used in query string (no values)
530
+ logFields.push(routeLog ? routeLog : "-"); // custom set routePath with values
531
+ logFields.push(queryLog ? queryLog : "-"); // custom set keys with values
532
+ logFields.push(apiKey ? apiKey : "-");
533
+
534
+ /* Join array together into single text string delimited by ' | ' */
535
+ const msg = logFields.join(" | ");
536
+
537
+ /* send it to CloudWatch via DebugAndLog.log() */
538
+ DebugAndLog.log(msg, loggingType);
539
+
540
+ } catch (error) {
541
+ DebugAndLog.error(`Error Logging Response: ${error.message}`, error.stack);
542
+ }
543
+
544
+ };
545
+ };
546
+
547
+ module.exports = Response;