mjackson/mach

View on GitHub
modules/extensions/server.js

Summary

Maintainability
C
1 day
Test Coverage
var fs = require('fs');
var d = require('describe-property');
var objectAssign = require('object-assign');
var getMimeType = require('../utils/getMimeType');
var filterProperties = require('../utils/filterProperties');
var stringifyCookie = require('../utils/stringifyCookie');
var saveToDisk = require('../utils/saveToDisk');

module.exports = function (mach) {
  mach.bind = require('../utils/bindApp');
  mach.createConnection = require('../utils/createConnection');
  mach.serve = require('../utils/serveApp');

  Object.defineProperties(mach.Connection.prototype, {

    /**
     * True if the request uses XMLHttpRequest, false otherwise.
     */
    isXHR: d.gs(function () {
      return this.request.headers['X-Requested-With'] === 'XMLHttpRequest';
    }),

    /**
     * A high-level method that returns a promise for an object that is the
     * union of parameters contained in the request body and query string.
     *
     * The paramTypes argument may be used to filter parameters. It functions
     * like a whitelist of acceptable parameters and increases the security of
     * your app by not returning any parameters that you do not specify.
     *
     *   // This function parses a list of comma-separated values in
     *   // a request parameter into an array.
     *   function parseList(value) {
     *     return value.split(',');
     *   }
     *
     *   function app(conn) {
     *     return conn.getParams({
     *       name: String,
     *       age: Number,
     *       hobbies: parseList
     *     }).then(function (params) {
     *       // params.name will be a string, params.age a number, and
     *       // params.hobbies an array if they were provided in the
     *       // request. params won't contain any other properties.
     *     });
     *   }
     *
     * Of course, paramTypes may be omitted entirely to get a hash of all parameters.
     * 
     * The maxLength argument is passed directly to the request's parseContent method.
     *
     *   var maxUploadLimit = Math.pow(2, 20); // 1 mb
     *
     *   function app(conn) {
     *     return conn.getParams(maxUploadLimit).then(function (params) {
     *       // params is the union of query and request content params
     *     });
     *   }
     *
     * Note: Content parameters take precedence over query parameters with the same name.
     */
    getParams: d(function (paramTypes, maxLength) {
      if (typeof paramTypes !== 'object') {
        maxLength = paramTypes;
        paramTypes = null;
      }

      var request = this.request;
      var queryParams = objectAssign({}, this.query);

      return request.parseContent(maxLength).then(function (contentParams) {
        // Content params take precedence over query params.
        var params = objectAssign(queryParams, contentParams);
        return paramTypes ? filterProperties(params, paramTypes) : params;
      });
    }),

    /**
     * Redirects the client to the given location. If status is not
     * given, it defaults to 302 Found.
     */
    redirect: d(function (status, location) {
      if (typeof status !== 'number') {
        location = status;
        status = 302;
      }

      this.status = status;
      this.response.headers['Location'] = location;
    }),

    /**
     * Redirects the client back to the URL they just came from, or
     * to the given location if it isn't known.
     */
    back: d(function (location) {
      this.redirect(this.request.headers['Referer'] || location || '/');
    }),

    /**
     * A quick way to write the status and/or content to the response.
     *
     * Examples:
     *
     *   conn.send(404);
     *   conn.send(404, 'Not Found');
     *   conn.send('Hello world');
     *   conn.send(fs.createReadStream('welcome.txt'));
     */
    send: d(function (status, content) {
      if (typeof status === 'number') {
        this.status = status;
      } else {
        content = status;
      }

      if (content != null)
        this.response.content = content;
    }),

    /**
     * Sends the given text in a text/plain response.
     */
    text: d(function (status, text) {
      this.response.contentType = 'text/plain';
      this.send(status, text);
    }),

    /**
     * Sends the given HTML in a text/html response.
     */
    html: d(function (status, html) {
      this.response.contentType = 'text/html';
      this.send(status, html);
    }),

    /**
     * Sends the given JSON in an application/json response.
     */
    json: d(function (status, json) {
      this.response.contentType = 'application/json';

      if (typeof status === 'number') {
        this.status = status;
      } else {
        json = status;
      }

      if (json != null)
        this.response.content = typeof json === 'string' ? json : JSON.stringify(json);
    }),

    /**
     * Sends a file to the client with the given options. The following
     * options are available:
     *
     * - content/path   The raw file content as a string, Buffer, stream, or
     *                  path to a file on disk
     * - type           The Content-Type of the file. Defaults to a guess based
     *                  on the file extension when a file path is given
     * - length/size    The Content-Length of the file, if it's known. Defaults
     *                  to the size of the file when a file path is given
     *
     * Examples:
     *
     *   response.file('path/to/file.txt');
     *   response.file(200, 'path/to/file.txt');
     */
    file: d(function (status, options) {
      if (typeof status === 'number') {
        this.status = status;
      } else {
        options = status;
      }

      var response = this.response;

      if (typeof options === 'string')
        options = { path: options };

      if (options.content) {
        response.content = options.content;
      } else if (typeof options.path === 'string') {
        response.content = fs.createReadStream(options.path);
      } else {
        throw new Error('Missing file content/path');
      }

      if (options.type || options.path)
        response.headers['Content-Type'] = options.type || getMimeType(options.path);

      if (options.length || options.size) {
        response.headers['Content-Length'] = options.length || options.size;
      } else if (typeof options.path === 'string') {
        response.headers['Content-Length'] = fs.statSync(options.path).size;
      }
    })

  });

  mach.extend(require('./multipart'));

  var _handlePart = mach.Message.prototype.handlePart;

  Object.defineProperties(mach.Message.prototype, {

    /**
     * Sets a cookie with the given name and options.
     */
    setCookie: d(function (name, options) {
      this.addHeader('Set-Cookie', stringifyCookie(name, options));
    }),

    /**
     * Override the multipart extension's Message#handlePart to enable
     * streaming file uploads to disk when parsing multipart messages.
     */
    handlePart: d(function (part) {
      return part.filename ? saveToDisk(part, 'MachUpload-') : _handlePart.apply(this, arguments);
    })

  });
};