All files / server/api/utils api.js

68.97% Statements 40/58
55.26% Branches 21/38
58.33% Functions 7/12
71.7% Lines 38/53
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171          1x   1x 1x                                                                             1x 2x   2x   2x   2x   2x       2x 2x 2x 2x 2x 2x 2x   2x 2x 22x             2x 2x 2x         2x 22x   22x     22x 22x 4x       18x     18x     18x         18x 18x         18x                                                 1x 7x 34x                                                 1x          
/**
 * API utilities.
 *
 * @module server/api/utils/api
 */
const _ = require('lodash');
 
const db = require('../../db');
const errors = require('./errors');
 
/**
 * Creates a middleware function that will fetch the record identified by the
 * current URL and attach it to the request. If no record is found, an HTTP 404
 * Not Found response will be sent.
 *
 *     const fetcher = require('./fetcher');
 *     const User = require('./models/user');
 *
 *     const fetchUser = fetcher({
 *       model: User,
 *       resourceName: 'user'
 *     });
 *
 *     router.get('/api/users/:id', fetchUser, (req, res) => {
 *       // req.user has been fetched, or HTTP 404 not found sent
 *       res.send(req.user);
 *     });
 *
 * @param {object} options - Fetcher options.
 *
 * @param {function} options.model - The database model to use to fetch the resource.
 *
 * @param {string} options.resourceName - The name of the API resource (used in error messages).
 *
 * @param {string} [options.column] - The database column containing the identifier (defaults to `api_id`).
 *
 * @param {string} [options.urlParameter] - The URL parameter containing the resource identifier (defaults to `id`).
 *
 * @param {function} [options.queryHandler] - An optional function to modify the database query (it will receive
 *   the query and the request as arguments, and should return the updated query).
 *
 * @param {string} [options.requestProperty] - The request property to attach the fetched record to (defaults to `options.resourceName`).
 *
 * @param {string[]} [options.eagerLoad] - Relations to eager-load when fetching the resource.
 *
 * @returns {function} A middleware function.
 */
exports.fetcher = function(options) {
  Iif (!_.isObject(options)) {
    throw new Error('An options object is required');
  } else Iif (!_.isFunction(options.model)) {
    throw new Error('The "model" option must be a database model');
  } else Iif (_.has(options, 'queryHandler') && !_.isFunction(options.queryHandler)) {
    throw new Error('The "queryHandler" option must be a function');
  } else Iif (!_.isString(options.resourceName)) {
    throw new Error('The "resourceName" option must be a string (e.g. the name of the model)');
  } else Iif (_.has(options, 'requestProperty') && !_.isString(options.requestProperty)) {
    throw new Error('The "requestProperty" option must be a string');
  }
 
  const Model = options.model;
  const column = options.column || 'api_id';
  const urlParameter = options.urlParameter || 'id';
  const queryHandler = options.queryHandler;
  const resourceName = options.resourceName;
  const requestProperty = options.requestProperty || resourceName;
  const eagerLoad = options.eagerLoad || [];
 
  let validate = () => true;
  Eif (options.validate == 'uuid') {
    validate = id => !!id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
  } else if (_.isFunction(options.validate)) {
    validate = options.validate;
  } else if (options.validate !== undefined) {
    throw new Error('The "validate" option must be a function or the string "uuid"');
  }
 
  let coerce = value => value;
  Eif (_.isFunction(options.coerce)) {
    coerce = options.coerce;
  } else if (options.coerce !== undefined) {
    throw new Error(`The "coerce" option must be a function`);
  }
 
  return function(req, res, next) {
    Promise.resolve().then(async () => {
 
      const resourceId = req.params[urlParameter];
 
      // Make sure the ID is valid.
      const resourceIdValid = await Promise.resolve(validate(resourceId));
      if (!resourceIdValid) {
        throw errors.recordNotFound(resourceName, resourceId);
      }
 
      // Coerce the ID.
      const coercedResourceId = await Promise.resolve(coerce(resourceId));
 
      // Prepare the query to fetch the record.
      let query = new Model({ [column]: coercedResourceId });
 
      // Pass the query through the handler (if any).
      Iif (_.isFunction(queryHandler)) {
        query = queryHandler(query, req);
      }
 
      // Perform the query.
      const record = await query.fetch({ withRelated: eagerLoad })
      Iif (!record) {
        throw errors.recordNotFound(resourceName, resourceId);
      }
 
      // Attach the record to the request object.
      req[requestProperty] = record;
    }).then(next).catch(next);
  };
}
 
/**
 * Converts a promise-based function into an Express middleware function.
 *
 *     route(async (req, res) => {
 *
 *       // Asynchronous code
 *       const data = await fetchData();
 *
 *       // Errors caught by promise chain and automatically passed to next(err)
 *       if (!data) {
 *         throw new Error('No data available');
 *       }
 *
 *       res.send(data);
 *     });
 *
 * @param {function} routeFunc - The asynchronous route implementation.
 *
 * @returns {function} A middleware function.
 */
exports.route = function makeRoute(routeFunc) {
  return function(req, res, next) {
    Promise.resolve().then(() => routeFunc(req, res, next)).catch(next);
  };
};
 
/**
 * Converts a promise-based function into an Express middleware function (like
 * `route`), and wraps the route into a database transaction.
 *
 * Any error thrown will cause the transaction to be rolled back.
 *
 *     transactionalRoute(async (req, res) => {
 *
 *       // Asynchronous code
 *       const user = await fetchUser();
 *
 *       // Any error will roll back the transaction.
 *       await user.save(req.body);
 *
 *       res.send(user);
 *     });
 *
 * @param {function} routeFunc - The asynchronous route implementation.
 *
 * @returns {function} A middleware function.
 */
exports.transactionalRoute = function makeTransactionalRoute(routeFunc) {
  return makeRoute(function(req, res, next) {
    return db.transaction(() => routeFunc(req, res, next));
  });
};