284427a7bd769742eafda2838b66275951a25de3
[squeep-api-dingus] / lib / common.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 /**
5 * Utility and miscellaneous functions.
6 */
7
8 const path = require('path');
9 const crypto = require('crypto');
10 const uuid = require('uuid');
11 const Enum = require('./enum');
12
13 /**
14 * Return a function which combines a part of the filename with a scope, for use in logging.
15 * @param {string} filename
16 */
17 const fileScope = (filename) => {
18 let fScope = path.basename(filename, '.js');
19 if (fScope === 'index') {
20 fScope = path.basename(path.dirname(filename));
21 }
22 return (scope) => `${fScope}:${scope}`;
23 }
24
25 /**
26 * Simple ETag from data.
27 * @param {string} filePath
28 * @param {object} fileStat
29 * @param {*} fileData
30 */
31 const generateETag = (_filePath, fileStat, fileData) => {
32 const hash = crypto.createHash('sha256');
33 if (fileStat && fileStat.mtimeMs) {
34 hash.update(fileStat.mtimeMs.toString());
35 }
36 hash.update(fileData);
37 const digest = hash.digest('base64').replace('=', '');
38 return `"${digest}"`;
39 };
40
41 /**
42 * @param {object} obj
43 * @param {string} prop
44 * @param {*} def
45 */
46 const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def;
47
48 /**
49 * @param {http.ClientRequest} req
50 * @param {http.ServerResponse} res
51 * @param {object} ctx
52 */
53 const handlerLogData = (req, res, ctx) => ({
54 req: requestLogData(req),
55 res: responseLogData(res),
56 ctx,
57 });
58
59 /**
60 *
61 * @param {http.ClientRequest} req
62 * @param {Number} modifiedTimeMs
63 * @param {string} eTag
64 */
65 const isClientCached = (req, modifiedTimeMs, eTag) => {
66 let clientCached = false;
67
68 const ifModifiedSince = req.getHeader(Enum.Header.IfModifiedSince);
69 if (ifModifiedSince) {
70 const ifModifiedSinceMs = Date.parse(ifModifiedSince);
71 if (modifiedTimeMs < ifModifiedSinceMs) {
72 clientCached = true;
73 }
74 }
75
76 const ifNoneMatch = req.getHeader(Enum.Header.IfNoneMatch);
77 if (ifNoneMatch) {
78 const matches = ifNoneMatch.split(',').map((m) => m.trim());
79 if (matches.includes(eTag)
80 || (ifNoneMatch === '*' && eTag)) {
81 clientCached = true;
82 } else {
83 // If if-none-matched header is present, it takes precedence over modified-since.
84 clientCached = false;
85 }
86 }
87
88 return clientCached;
89 };
90
91 /**
92 * Shallow merge for enums, to be called by derived constructor.
93 * Expects only one-level deep, is not recursive!
94 * @param {Object} origEnum
95 * @param {Object} additionalEnum
96 */
97 const mergeEnum = (origEnum, additionalEnum) => {
98 for (const e of Object.keys(additionalEnum)) {
99 if (typeof additionalEnum[e] === 'object') {
100 if (! (e in origEnum)) {
101 origEnum[e] = {};
102 }
103 Object.assign(origEnum[e], additionalEnum[e]);
104 } else {
105 origEnum[e] = additionalEnum[e];
106 }
107 }
108 return origEnum;
109 };
110
111 /**
112 * Isolate the general category of an http status code.
113 * @param {Number} statusCode
114 * @returns {Number}
115 */
116 const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100);
117
118 const _isObject = (obj) => obj && typeof obj === 'object';
119 const _isArray = (obj) => Array.isArray(obj);
120 /**
121 * Return a new object with all objects combined.
122 * @param {...any} objects
123 * @returns
124 */
125 const mergeDeep = (...objects) => {
126 return objects.reduce((acc, obj) => {
127 const objectProperties = [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)];
128 objectProperties.forEach((k) => {
129 const aVal = acc[k];
130 const oVal = obj[k];
131 if (_isArray(oVal)) {
132 acc[k] = (_isArray(aVal) ? aVal : []).concat(oVal);
133 } else if (_isObject(oVal)) {
134 acc[k] = mergeDeep(_isObject(aVal) ? aVal : {}, oVal);
135 } else {
136 acc[k] = oVal;
137 }
138 });
139 return acc;
140 }, {});
141 };
142
143
144 /**
145 * Return a new object with selected props.
146 * @param {Object} obj
147 * @param {string[]} props
148 */
149 const pick = (obj, props) => {
150 const picked = {};
151 props.forEach((prop) => {
152 if (prop in obj) {
153 picked[prop] = obj[prop];
154 }
155 });
156 return picked;
157 };
158
159 /**
160 * Return a subset of a request object, suitable for logging.
161 * @param {http.ClientRequest} req
162 */
163 const requestLogData = (req) => {
164 return pick(req, [
165 'method',
166 'url',
167 'httpVersion',
168 'headers',
169 'trailers',
170 ]);
171 };
172
173
174 /**
175 * Return a subset of a response object, suitable for logging.
176 * @param {http.ServerResponse} res
177 */
178 const responseLogData = (res) => {
179 const response = pick(res, [
180 'statusCode',
181 'statusMessage',
182 ]);
183 response.headers = res.getHeaders();
184 return response;
185 };
186
187
188 /**
189 * Store updates to defaultOptions, but no new properties.
190 * @param {Object} target
191 * @param {Object} defaultOptions
192 * @param {Object} options
193 */
194 const setOptions = (target, defaultOptions, options) => {
195 Object.assign(target, defaultOptions, pick(options, Object.keys(defaultOptions)));
196 };
197
198 /**
199 * Return a list of source split at first delimiter.
200 * @param {string} src
201 * @param {string} delimiter
202 * @param {string} fill trailing stand-in if no delimiter in src
203 */
204 const splitFirst = (src, delimiter, fill) => {
205 const idx = src.indexOf(delimiter);
206 if (idx >= 0) {
207 return [ src.slice(0, idx), src.slice(idx + 1) ];
208 } else {
209 return [ src, fill ];
210 }
211 };
212
213 /**
214 * Generate a new request identifier.
215 * @returns {String}
216 */
217 const requestId = () => {
218 return uuid.v1();
219 };
220
221 const nop = () => { /**/ };
222 const nullLogger = {
223 error: nop,
224 warn: nop,
225 info: nop,
226 log: nop,
227 debug: nop,
228 };
229
230 const ensureLoggerLevels = (logger = {}) => {
231 for (const level in nullLogger) {
232 if (! (level in logger)) {
233 logger[level] = nullLogger[level];
234 }
235 }
236 return logger;
237 };
238
239 module.exports = {
240 fileScope,
241 generateETag,
242 get,
243 handlerLogData,
244 isClientCached,
245 httpStatusCodeClass,
246 mergeDeep,
247 mergeEnum,
248 nop,
249 nullLogger,
250 ensureLoggerLevels,
251 pick,
252 requestId,
253 requestLogData,
254 responseLogData,
255 setOptions,
256 splitFirst,
257 };