const Enum = require('./enum');
/**
- * Return a function which combines a part of the filename with a scope, for use in logging.
- * @param {string} filename
+ * @callback ScopeFn
+ * @param {String} scope
+ * @returns {String}
+ */
+/**
+ * Return a function which prefixes a provided scope with the most-
+ * relevant part of the filename, for use in logging.
+ * @param {String} filename
+ * @returns {ScopeFn}
*/
const fileScope = (filename) => {
let fScope = path.basename(filename, '.js');
/**
* Simple ETag from data.
- * @param {string} filePath
- * @param {object} fileStat
- * @param {*} fileData
+ * @param {String} filePath (currently unused)
+ * @param {fs.Stats} fileStat
+ * @param {crypto.BinaryLike} fileData content
+ * @returns {String}
*/
const generateETag = (_filePath, fileStat, fileData) => {
const hash = crypto.createHash('sha256');
};
/**
- * @param {object} obj
- * @param {string} prop
- * @param {*} def
+ * Access property with default.
+ * @param {Object} obj
+ * @param {String} prop
+ * @param {*} def default value if prop does not exist for obj
+ * @return {*}
*/
const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def;
/**
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
- * @param {object} ctx
+ * @param {Object} ctx
+ * @deprecated after v1.2.5 (integrated into logger module)
*/
const handlerLogData = (req, res, ctx) => ({
req: requestLogData(req),
});
/**
- *
+ * Determine whether a client has already requested a resource,
+ * based on If-Modified-Since and If-None-Match headers.
* @param {http.ClientRequest} req
* @param {Number} modifiedTimeMs
- * @param {string} eTag
+ * @param {String} eTag
+ * @returns {Boolean}
*/
const isClientCached = (req, modifiedTimeMs, eTag) => {
let clientCached = false;
* Expects only one-level deep, is not recursive!
* @param {Object} origEnum
* @param {Object} additionalEnum
+ * @returns {Object}
*/
const mergeEnum = (origEnum, additionalEnum) => {
for (const e of Object.keys(additionalEnum)) {
const _isObject = (obj) => obj && typeof obj === 'object';
const _isArray = (obj) => Array.isArray(obj);
/**
- * Return a new object with all objects combined.
- * @param {...any} objects
- * @returns
+ * Return a new object with all objects combined, later properties taking precedence.
+ * @param {...Object} objects
+ * @returns {Object}
*/
const mergeDeep = (...objects) => {
return objects.reduce((acc, obj) => {
/**
* Return a new object with selected props.
* @param {Object} obj
- * @param {string[]} props
+ * @param {String[]} props
+ * @returns {Object}
*/
const pick = (obj, props) => {
const picked = {};
* Return a subset of a request object, suitable for logging.
* Obscures sensitive header values.
* @param {http.ClientRequest} req
+ * @deprecated after v1.2.5 (integrated into logger module)
*/
const requestLogData = (req) => {
const data = pick(req, [
* Remove sensitive header data.
* @param {Object} data
* @param {Object} data.headers
+ * @deprecated after v1.2.5 (integrated into logger module)
*/
const scrubHeaderObject = (data) => {
if (data && data.headers && 'authorization' in data.headers) {
* Hide sensitive part of an Authorization header.
* @param {String} authHeader
* @returns {String}
+ * @deprecated after v1.2.5 (integrated into logger module)
*/
const obscureAuthorizationHeader = (authHeader) => {
if (!authHeader) {
/**
* Return a subset of a response object, suitable for logging.
* @param {http.ServerResponse} res
+ * @deprecated after v1.2.5 (integrated into logger module)
*/
const responseLogData = (res) => {
const response = pick(res, [
/**
- * Store updates to defaultOptions, but no new properties.
+ * Store all properties in defaultOptions on target from either options or defaultOptions.
* @param {Object} target
* @param {Object} defaultOptions
* @param {Object} options
};
/**
- * Return a list of source split at first delimiter.
- * @param {string} src
- * @param {string} delimiter
- * @param {string} fill trailing stand-in if no delimiter in src
+ * Return a two-item list of src, split at first delimiter encountered.
+ * @param {String} src
+ * @param {String} delimiter
+ * @param {String} fill trailing stand-in if no delimiter in src
*/
const splitFirst = (src, delimiter, fill) => {
const idx = src.indexOf(delimiter);
};
/**
- * Generate a new request identifier.
+ * Generate a new request identifier, a time/host-based uuid.
* @returns {String}
*/
const requestId = () => {
return uuid.v1();
};
+/**
+ * Do nothing.
+ */
const nop = () => { /**/ };
+
+/**
+ * A logger object which does nothing.
+ */
const nullLogger = {
error: nop,
warn: nop,
};
/**
- * Populates any absent logger levels.
+ * Populates any absent logger level functions on a logger object.
* @param {Object} logger
+ * @returns {Object}
*/
const ensureLoggerLevels = (logger = {}) => {
for (const level in nullLogger) {
/**
* Merges folded header lines
* @param {String[]} lines
+ * @returns {String}
*/
const unfoldHeaderLines = (lines) => {
const foldedLineRE = /^(\t| +)(.*)$/;
/**
* Welp, here we are, already into the crazy.
- * Per documentation (https://nodejs.org/docs/latest-v12.x/api/http.html#http_request_getheader_name)
- * this should exist, yet (as of 12.18.4) it does not. So let us change this pitch up, and patch
-*/
+ * This originally existed due to a misinterpretation of the documentation,
+ * but it's really quite useful, so it stays in our world.
+ */
/* istanbul ignore else */
if (typeof IncomingMessage.getHeader !== 'function') {
IncomingMessage.prototype.getHeader = function (name) {
paramPrefix: ':',
};
+/**
+ * A naïve router which maps incoming requests to handler functions
+ * by way of url path and request method.
+ *
+ * Regex parsing of paths was eschewed, as a design decision.
+ *
+ * Instead, each path to be searched for is deconstructed into a list
+ * of its constituent parts as strings or objects, for invariant or
+ * parameterized parts respectively. Each search path is assigned a
+ * mapping of methods to handler functions.
+ *
+ * @property {Object} pathsByLength index to registered paths by number of parts
+ * @property {Symbol} METHODS key to method:handler map on search paths
+ * @property {Symbol} PARAM key to parameter name in search path parts
+ */
class Router {
/**
* @param {Object} options
- * @param {Boolean} options.ignoreTrailingSlash
- * @param {Boolean} options.paramPrefix
+ * @param {Boolean} options.ignoreTrailingSlash discard any trailing slashes when registering and comparing paths (default: false)
+ * @param {String} options.paramPrefix prefix of a path part denoting a named parameter when registering paths (default: ':')
*/
constructor(options = {}) {
common.setOptions(this, defaultOptions, options);
this.PARAM = PARAM;
}
-
+ /**
+ * @typedef {Object} Router~ParamPart
+ * @property {String} PARAM (symbol key)
+ */
+ /**
+ * @typedef {Array<String|Router~ParamPart>} Router~SearchPath
+ * @property {Object} METHODS (symbol key)
+ */
/**
* Prepare a path for insertion into search list.
- * A searchable path is a list of path parts, with a property of method handlers.
- * @param {string} pathDefinition
+ * A searchable path is a list of path parts, with a property of an object of method handlers.
+ * @param {String} pathDefinition
+ * @returns {Router~SearchPath}
+ * @private
*/
_pathDefinitionToPathMatch(pathDefinition) {
- const pathMatch = pathDefinition.split('/').map((p) => p.startsWith(this.paramPrefix) ? { [PARAM]: p.slice(this.paramPrefix.length) } : p);
+ const pathMatch = pathDefinition
+ .split('/')
+ .map((p) => p.startsWith(this.paramPrefix) ? { [PARAM]: p.slice(this.paramPrefix.length) } : p);
if (this.ignoreTrailingSlash
&& pathMatch[pathMatch.length - 1] === '') {
pathMatch.pop();
/**
* Compare checkPath to fixedPath, no param substitution, params must match.
- * @param {*} fixedPath
- * @param {*} checkPath
+ * @param {Router~SearchPath} fixedPath
+ * @param {Router~SearchPath} checkPath
+ * @returns {Boolean}
+ * @private
*/
static _pathCompareExact(fixedPath, checkPath) {
if (fixedPath.length !== checkPath.length) {
/**
* Compare checkPath to fixedPath, populating params.
- * @param {*} fixedPath
- * @param {*} checkPath
- * @param {*} returnParams
+ * @param {Router~SearchPath} fixedPath
+ * @param {Array<String>} checkPath
+ * @param {Object} returnParams
+ * @returns {Boolean}
+ * @private
*/
static _pathCompareParam(fixedPath, checkPath, returnParams = {}) {
const params = {};
}
+ /**
+ * @typedef Router~MatchedPath
+ * @property {Object} pathParams populated param fields
+ * @property {Router~SearchPath} matchedPath
+ */
/**
* Search for an existing path, return matched path and path parameters.
- * @param {Array} matchParts
+ * @param {Array<String>} matchParts
+ * @returns {Router~MatchedPath}
+ * @private
*/
_pathFind(matchParts) {
const result = {
/**
* Return a matching path, no param substitution, params must match
- * @param {*} matchParts
+ * @param {Router~SearchPath} matchParts
+ * @returns {Router~SearchPath=}
+ * @private
*/
_pathFindExact(matchParts) {
const pathsByLength = this.pathsByLength[matchParts.length];
}
+ /**
+ * @callback Router~HandlerFn
+ */
+ /**
+ * @typedef {Object} Router~PathHandler
+ * @property {Router~HandlerFn} handler
+ * @property {any[]} handlerArgs
+ */
/**
* Insert a new path handler.
* @param {string|string[]} methods
* @param {string} urlPath
- * @param {fn} handler
- * @param {*[]} handlerArgs
+ * @param {Router~HandlerFn} handler
+ * @param {any[]} handlerArgs
*/
on(methods, urlPath, handler, handlerArgs = []) {
const matchParts = this._pathDefinitionToPathMatch(urlPath);
/**
* Return an object, which contains a matching handler and any extra
* arguments, for a requested url.
- * Also sets path parameters on context.
+ * Also sets path named-parameters as #params and the matched path as
+ * #matchedPath on the context.
* @param {string} method
* @param {string[]} urlPath
* @param {object} ctx
- * @returns {object}
+ * @returns {Router~PathHandler}
*/
lookup(method, urlPath, ctx = {}) {
const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part));
});
}); // responseLogData
+ describe('handlerLogData', function () {
+ it('covers', function () {
+ const req = {
+ method: 'GET',
+ somethingElse: 'blah',
+ };
+ const res = {
+ getHeaders: () => ({}),
+ statusCode: 200,
+ blah: 'blah',
+ };
+ const ctx = {};
+ const result = common.handlerLogData(req, res, ctx);
+ assert.deepStrictEqual(result, {
+ req: {
+ method: 'GET',
+ },
+ res: {
+ headers: {},
+ statusCode: 200,
+ },
+ ctx: {},
+ });
+ });
+ }); // handlerLogData
+
describe('setOptions', function () {
it('sets options', function () {
const expected = {