router_Stack.js

/**
 *  lambda-lambda-lambda/router
 *  AWS Lambda@Edge serverless application router.
 *
 *  Copyright 2021-2023, Marc S. Brooks (https://mbrooks.info)
 *  Licensed under the MIT license:
 *  http://www.opensource.org/licenses/mit-license.php
 */

'use strict';

// Local modules.
const {RouterError} = require('./Error');
const {
  isAsyncFunc,
  isValidFunc,
  promiseEvents
} = require('./Utils');

/**
 * Provides Stack item handler and methods.
 */
class RouterStack {

  /**
   * Create new stack instance.
   */
  constructor() {
    this.middleware = [];
    this.routes     = [];
    this.resources  = [];
    this.fallback   = null;
  }

  /**
   * Add new function to stack items.
   *
   * Stack function types
   *  - middleware
   *  - route:<method>
   *  - resource:<method>
   *  - fallback
   *
   * @param {Function} func
   *   Route/middleware function.
   *
   * @example
   * const func1 = function(req, res, next) {
   *   if (req.method() === 'POST') {
   *     res.status(405).send();
   *   } else {
   *     next();
   *   }
   * };
   *
   * setFuncName(func1, 'middleware');
   * stack.add(func1);
   *
   *   ..
   *
   * const func2 = function(req, res) {
   *   res.setHeader('Content-Type', 'text/html');
   *   res.status(200).send('Hello World');
   * };
   *
   * setFuncName(func2, 'route:get');
   * stack.add(func2);
   */
  add(func) {
    if (isValidFunc(func)) {
      const name = func.name;

      switch (true) {
        case /^middleware/.test(name):
          this.middleware.push(func);
          break;

        case /^route:/.test(name):
          this.routes.push(func);
          break;

        case /^resource:/.test(name):
          this.resources.push(func);
          break;

        default:
          this.fallback = func;
      }
    }
  }

  /**
   * Execute stored functions (a)synchronously.
   *
   * Order by priority.
   * > Middleware, Routes, Resources, fallback
   *
   * @param {RouterRequest} req
   *   Request instance.
   *
   * @param {RouterResponse} res
   *   Response instance.
   *
   * @return {Promise|undefined}
   *
   * @example
   * stack.exec(req, res);
   *
   * // updated instance
   * res.data();
   *
   *   ..
   *
   * stack.exec(req, res)
   *   .then(() => res.data())
   */
  exec(req, res) {
    const funcs = [].concat(this.middleware, this.routes, this.resources, [this.fallback]);

    let lastItem = true;
    let nextItem = true;
    let promises = [];

    funcs.forEach((func, index) => {
      if (nextItem) {
        lastItem = index++ === funcs.length;
        nextItem = false;

        if (isAsyncFunc(func)) {

          // Asynchronous handling.
          promises.push(() => {
            return func(req, res, function() {
              throw new RouterError('Middleware next() is unsupported in async');
            });
          });

          nextItem = !lastItem;

        } else if (isValidFunc(func)) {

          // Synchronous handling.
          func(req, res, () => nextItem = !lastItem);
        }
      }
    });

    if (promises.length) {
      return promiseEvents(promises)
        .catch(err => {
          if (err instanceof RouterError) {
            throw err;
          } else if (err) {
            console.info(err);
          }
        });
    }
  }
};

module.exports = RouterStack;