update devDependencies, add jsdoc lint, fix lint issues
[squeep-api-dingus] / lib / dingus.js
index f060c18f00d251f610a97929fdce663c9f01de89..dfddaff577312857bdc695820ed768ab680d11bd 100644 (file)
@@ -6,10 +6,14 @@
  * Just a self-contained router and some request glue.
  */
 
+/**
+ * @typedef {import('node:http')} http
+ */
+
 require('./patches');
-const { promises: fsPromises } = require('fs');
-const path = require('path');
-const querystring = require('querystring');
+const { promises: fsPromises } = require('node:fs');
+const path = require('node:path');
+const querystring = require('node:querystring');
 const common = require('./common');
 const ContentNegotiation = require('./content-negotiation');
 const Enum = require('./enum');
@@ -39,17 +43,17 @@ const cookieSplitRE = /; */;
 
 class Dingus {
   /**
-   * @param {Object} logger object which implements logging methods
-   * @param {Object} options
-   * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
+   * @param {object} logger console-like object which implements logging methods
+   * @param {object} options options
+   * @param {boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
    * @param {string} options.proxyPrefix leading part of url path to strip
-   * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
+   * @param {boolean} options.strictAccept whether to error on unsupported Accept type
    * @param {string} options.selfBaseUrl for constructing links
-   * @param {Boolean} options.staticMetadata serve static headers with static files
-   * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
-   * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
-   * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
-   * @param {Object} options.querystring alternate qs parser to use
+   * @param {boolean} options.staticMetadata serve static headers with static files
+   * @param {boolean} options.trustProxy trust some header data to be provided by proxy
+   * @param {boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
+   * @param {boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
+   * @param {object} options.querystring alternate qs parser to use
    */
   constructor(logger = console, options = {}) {
     common.setOptions(this, defaultOptions, options);
@@ -73,6 +77,7 @@ class Dingus {
   /**
    * Resolve relative and empty paths in url
    * @param {string} p path
+   * @returns {string} normalized path
    */
   _normalizePath(p) {
     const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy...
@@ -84,6 +89,7 @@ class Dingus {
    * Remove a leading portion of url path
    * N.B. This method gets obliterated if there is no prefix defined at construction
    * @param {string} p path
+   * @returns {string} de-prefixed path
    */
   _stripPrefix(p) {
     if (p.startsWith(this.proxyPrefix)) {
@@ -95,7 +101,8 @@ class Dingus {
 
   /**
    * Returns the path part, and querystring object, from a request url.
-   * @param {string} url 
+   * @param {string} url url
+   * @returns {object} path string and query params
    */
   _splitUrl(url) {
     const [ p, qs ] = common.splitFirst(url, '?');
@@ -108,12 +115,13 @@ class Dingus {
 
   /**
    * Insert a new path handler
-   * @param {string} method 
-   * @param {string} urlPath 
-   * @param {fn} handler 
+   * @param {string|string[]} methods methods to match for this path, '*' allowed
+   * @param {string} urlPath request path to match
+   * @param {Function} handler handler to invoke on match
+   * @param {any[]} handlerArgs additional arguments for handler
    */
-  on(method, urlPath, handler, ...handlerArgs) {
-    this.router.on(method, urlPath, handler, handlerArgs);
+  on(methods, urlPath, handler, ...handlerArgs) {
+    this.router.on(methods, urlPath, handler, handlerArgs);
   }
 
 
@@ -123,9 +131,10 @@ class Dingus {
    * Sets requestId on ctx to a new uuid.
    * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them
    * on outgoing headers and sets on ctx.
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {string} id added request
    */
   static tagContext(req, res, ctx) {
     const requestId = common.requestId();
@@ -144,7 +153,8 @@ class Dingus {
 
   /**
    * 
-   * @param {http.ClientRequest} req 
+   * @param {http.ClientRequest} req request
+   * @returns {string} first forwarded header ip
    */
   _getAddress(req) {
     // TODO: RFC7239 Forwarded support
@@ -158,7 +168,8 @@ class Dingus {
 
   /**
    * 
-   * @param {http.ClientRequest} req 
+   * @param {http.ClientRequest} req request
+   * @returns {string} first forwarded header protocol
    */
   _getProtocol(req) {
     // TODO: RFC7239 Forwarded support
@@ -170,9 +181,9 @@ class Dingus {
 
   /**
    * Sets ctx.clientAddress and ctx.clientProtocol.
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   clientAddressContext(req, res, ctx) {
     ctx.clientAddress = this._getAddress(req);
@@ -182,9 +193,9 @@ class Dingus {
 
   /**
    * Sets ctx.cookie from Cookie header.
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   static ingestCookie(req, res, ctx) {
     ctx.cookie = {};
@@ -207,9 +218,9 @@ class Dingus {
   /**
    * Called before every request handler.
    * Sets tracking identifiers and client information on ctx.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async preHandler(req, res, ctx) {
     this.constructor.tagContext(req, res, ctx);
@@ -220,9 +231,9 @@ class Dingus {
 
   /**
    * Helper for collecting chunks as array of buffers.
-   * @param {Buffer[]} chunks 
-   * @param {string|Buffer} chunk 
-   * @param {string} encoding 
+   * @param {Buffer[]} chunks array to collect chunks in
+   * @param {string|Buffer} chunk chunk to collect
+   * @param {string} encoding chunk encoding
    */
   static pushBufChunk(chunks, chunk, encoding = 'utf8') {
     if (chunk) {
@@ -236,9 +247,9 @@ class Dingus {
 
   /**
    * Sets ctx.responseBody and calls handler upon res.end().
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    * @param {*} handler fn(req, res, ctx)
    */
   static setEndBodyHandler(req, res, ctx, handler) {
@@ -262,10 +273,10 @@ class Dingus {
    * Intercept writes for head requests, do not send to client,
    * but send length, and make body available in context.
    * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
-   * @param {Boolean} persistResponseBody
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {boolean} persistResponseBody store body buffer on context
    */
   static setHeadHandler(req, res, ctx, persistResponseBody = false) {
     if (req.method === 'HEAD') {
@@ -290,10 +301,10 @@ class Dingus {
 
   /**
    * Resolve the handler to invoke for a request.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
-   * @returns {object}
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {object} handler and args
    */
   _determineHandler(req, res, ctx) {
     const _scope = _fileScope('_determineHandler');
@@ -326,11 +337,11 @@ class Dingus {
 
   /**
    * For intrinsic HEAD requests, resolve the handler to invoke.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
-   * @param {string} pathPart
-   * @returns {object}
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {string} pathPart router path
+   * @returns {object} hander and handler args
    */
   _determineHeadHandler(req, res, ctx, pathPart) {
     const _scope = _fileScope('_determineHeadHandler');
@@ -352,9 +363,10 @@ class Dingus {
 
   /**
    * Dispatch the handler for a request
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {Promise<void>} handler promise
    */
   async dispatch(req, res, ctx = {}) {
     const { handler, handlerArgs } = this._determineHandler(req, res, ctx);
@@ -370,8 +382,8 @@ class Dingus {
 
   /**
    * Return normalized type, without any parameters.
-   * @param {http.ClientRequest} req
-   * @returns {string}
+   * @param {http.ClientRequest} req request
+   * @returns {string} bare content type
    */
   static getRequestContentType(req) {
     const contentType = req.getHeader(Enum.Header.ContentType);
@@ -381,10 +393,10 @@ class Dingus {
 
   /**
    * Parse rawBody as contentType into ctx.parsedBody.
-   * @param {string} contentType
-   * @param {object} ctx
-   * @param {string|buffer} rawBody
-  */
+   * @param {string} contentType type
+   * @param {object} ctx context
+   * @param {string|Buffer} rawBody body data
+   */
   parseBody(contentType, ctx, rawBody) {
     const _scope = _fileScope('parseBody');
 
@@ -411,9 +423,10 @@ class Dingus {
 
   /**
    * Return all body data from a request.
-   * @param {http.ClientRequest} req
-   * @param {Number=} maximumBodySize
-   * @param {Boolean=} toString
+   * @param {http.ClientRequest} req request
+   * @param {number=} maximumBodySize bytes
+   * @param {boolean=} toString whether to convert body data buffer to string
+   * @returns {Promise<Buffer|string>} body data
    */
   async bodyData(req, maximumBodySize, toString = true) {
     const _scope = _fileScope('bodyData');
@@ -443,12 +456,13 @@ class Dingus {
   /**
    * Read and parse request body data.
    * Sets ctx.parsedBody, and optionally ctx.rawBody.
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
-   * @param {object}
-   * @param {Boolean} .parseEmptyBody
-   * @param {Boolean} .persistRawBody
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {object} options options
+   * @param {boolean} options.parseEmptyBody attempt to parse an empty body, results may vary by encoding
+   * @param {boolean} options.persistRawBody store raw body buffer on context
+   * @param {number=} options.maximumBodySize bytes
    */
   async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) {
     const rawBody = await this.bodyData(req, maximumBodySize);
@@ -464,8 +478,9 @@ class Dingus {
 
   /**
    * Return the best matching response type.
-   * @param {string[]} responseTypes 
-   * @param {http.ClientRequest} req 
+   * @param {string[]} responseTypes available content types
+   * @param {http.ClientRequest} req request
+   * @returns {string} preferred content type
    */
   static getResponseContentType(responseTypes, req) {
     const acceptHeader = req.getHeader(Enum.Header.Accept);
@@ -475,8 +490,9 @@ class Dingus {
 
   /**
    * Returns a list of the most-preferred content encodings for the response.
-   * @param {string[]} responseEncodings
-   * @param {http.ClientRequest} req 
+   * @param {string[]} responseEncodings available encodings
+   * @param {http.ClientRequest} req request
+   * @returns {string} preferred encoding
    */
   static getResponseEncoding(responseEncodings, req) {
     const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding);
@@ -488,9 +504,9 @@ class Dingus {
    * Set the best content type for the response.
    * Sets ctx.responseType, and Content-Type header.
    * @param {string[]} responseTypes default first
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   setResponseType(responseTypes, req, res, ctx) {
     const _scope = _fileScope('setResponseType');
@@ -509,8 +525,8 @@ class Dingus {
 
   /**
    * Inserts an encoding into Content-Encoding header.
-   * @param {http.ServerResponse} res
-   * @param {string} encoding
+   * @param {http.ServerResponse} res response
+   * @param {string} encoding encoding
    */
   static addEncodingHeader(res, encoding) {
     const existingEncodings = res.getHeader(Enum.Header.ContentEncoding);
@@ -523,7 +539,8 @@ class Dingus {
 
   /**
    * Attempt to fetch both data and metadata for a file.
-   * @param {string} filePath 
+   * @param {string} filePath path to file
+   * @returns {Promise<[object=, object=]>} array with file contents and stats
    */
   async _readFileInfo(filePath) {
     const _scope = _fileScope('_readFileInfo');
@@ -547,9 +564,10 @@ class Dingus {
 
   /**
    * Potentially add additional headers from static file meta-file.
-   * @param {http.ServerResponse} res
-   * @param {string} directory
+   * @param {http.ServerResponse} res response
+   * @param {string} directory path to file
    * @param {string} fileName - already normalized and filtered
+   * @returns {boolean} if meta headers were added
    */
   async _serveFileMetaHeaders(res, directory, fileName) {
     const _scope = _fileScope('_serveFileMetaHeaders');
@@ -585,11 +603,12 @@ class Dingus {
   /**
    * Serve a file from a directory, with rudimentary cache awareness.
    * This will also serve pre-encoded variations if available and requested.
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
-   * @param {string} directory 
-   * @param {string} fileName 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {string} directory path to file
+   * @param {string} fileName file name
+   * @returns {Promise<void>=} serves file or renders status code
    */
   async serveFile(req, res, ctx, directory, fileName) {
     const _scope = _fileScope('serveFile');
@@ -681,11 +700,12 @@ class Dingus {
 
   /**
    * Return a content-type appropriate rendering of an errorResponse object.
-   * @param {string} type content-type of response
+   * @param {string} contentType content-type of response
    * @param {object} err either an Error object, or an error response
-   * @param {number} err.statusCode
-   * @param {string} err.errorMessage
-   * @param {string|string[]} err.details
+   * @param {number} err.statusCode htttp status code
+   * @param {string} err.errorMessage http status message
+   * @param {string|string[]} err.details additional details to include
+   * @returns {string} rendered err
    */
   // eslint-disable-next-line class-methods-use-this
   renderError(contentType, err) {
@@ -707,9 +727,9 @@ class Dingus {
    * Send an error response.  Terminal.
    * Logs any non-error-response errors as such.
    * @param {object} err either an Error object, or an error response
-   * @param {http.ClientRequest} req 
-   * @param {http.ServerResponse} res 
-   * @param {object} ctx 
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   sendErrorResponse(err, req, res, ctx) {
     const _scope = _fileScope('sendErrorResponse');
@@ -734,10 +754,10 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
-   * @param {String} file - override ctx.params.file
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {string} file - override ctx.params.file
    */
   async handlerGetStaticFile(req, res, ctx, file) {
     Dingus.setHeadHandler(req, res, ctx);
@@ -750,12 +770,12 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {String} newPath
-   * @param {Number} statusCode
-  */
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {string} newPath url to redirect to
+   * @param {number=} statusCode status code to use for redirect, default 307
+   */
   async handlerRedirect(req, res, ctx, newPath, statusCode = 307) {
     this.setResponseType(this.responseTypes, req, res, ctx);
     res.setHeader(Enum.Header.Location, newPath);
@@ -765,9 +785,9 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async handlerMethodNotAllowed(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);
@@ -776,9 +796,9 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async handlerNotFound(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);
@@ -787,9 +807,9 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async handlerBadRequest(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);
@@ -798,9 +818,9 @@ class Dingus {
 
 
   /**
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {object} ctx
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async handlerInternalServerError(req, res, ctx) {
     this.setResponseType(this.responseTypes, req, res, ctx);