Home Reference Source

lib/implementations/BaseRestController.js

const debug = require('debug')('sequelize-resource:base-rest-controller');
const querystring = require('querystring');
const ResourceController = require('../resource-controller/ResourceController');
const ResourceError = require('../errors/ResourceError');

/**
 *
 * @class BaseRestController
 * @author Marco Villarreal
 */
class BaseRestController {
  /**
   *
   * @constructor
   * @param {Sequelize} sequelize
   * @param {Sequelize.Model} model
   */
  constructor(sequelize, model) {
    this.resource = new ResourceController(sequelize, model);
    this.model = model;
    this.baseURL = null;
  }

  checkJsonHeader(req) {
    debug(`Unimplemented checkJsonHeader method on request ${req}`);
    throw new ResourceError(ResourceError.ERRORS.INTERNAL, 'Unimplemented checkJsonHeader method');
  }

  async create(req, res) {
    if (this.checkJsonHeader(req)) {
      const { error, response } = await this.resource.create(req.body);
      if (error) {
        this.sendResponse(res, error.code, error);
      } else {
        this.sendResponse(res, 201, this.attachResourceLink(response.get({ plain: true }), req));
      }
    } else {
      const error = new ResourceError(
        ResourceError.ERRORS.FAILED_PRECONDITION,
        'Request body must be application/json'
      );
      this.sendResponse(res, error.code, error);
    }
  }

  /**
   *  Note: implemented a little polifyll to satisfy query params on express(a collission issue caused by restify)
   * @param {Request} req
   * @param {Response} res
   */
  async list(req, res) {
    let queryParams = req.query;
    if (typeof req.query === 'function') {
      queryParams = querystring.parse(req.query());
    }
    const { sortField, sortOrder } = queryParams;
    const requestArgs = { sortField, sortOrder };
    let { limit, offset } = queryParams;
    if (limit != null) {
      limit = parseInt(limit, 10);
      requestArgs.limit = limit;
    }

    if (offset != null) {
      offset = parseInt(offset, 0);
      requestArgs.offset = offset;
    }

    const { error, response } = await this.resource.findAll(requestArgs);

    if (error) {
      this.sendResponse(res, error.code, error);
    } else {
      this.sendResponse(res, 200, this.attachResourcesLinks(response, req));
    }
  }

  async getOne(req, res) {
    const { error, response } = await this.resource.findOne(req.params);
    if (error) {
      this.sendResponse(res, error.code, error);
    } else {
      const resource = this.attachResourceLink(response.get({ plain: true }), req);
      this.setHeaders(res, { Location: resource.links.self.href });
      this.sendResponse(res, 200, resource);
    }
  }

  async getOneMiddleware(req, res, next) {
    const { response, error } = await this.resource.findOne(req.params);
    if (error) {
      this.sendResponse(res, error.code, error);
    } else {
      req.resource = response;
      next();
    }
  }

  async destroy(req, res) {
    if (!req.resource) {
      this.setStatus(res, 412);
      this.response(res, null);
    }
    const { response, error } = await this.resource.destroy(req.resource);
    if (error) {
      this.sendResponse(res, error.code, error);
    } else {
      this.sendResponse(res, 200, response);
    }
  }

  async update(req, res) {
    if (this.checkJsonHeader(req)) {
      if (!req.resource) {
        this.setStatus(res, 412);
        this.response(res, null);
      } else {
        const { response, error } = await this.resource.update(req.resource, req.body);
        if (error) {
          this.sendResponse(res, error.code, error);
        } else {
          this.sendResponse(res, 200, this.attachResourceLink(response));
        }
      }
    } else {
      const error = new ResourceError(
        ResourceError.ERRORS.FAILED_PRECONDITION,
        'Request body must be application/json'
      );
      this.sendResponse(res, error.code, error);
    }
  }

  /**
   *
   * @method attachResourceLink
   * @param {Object} resource
   * @param {Request} requestObj
   * @return {Object}
   */
  attachResourceLink(resource, requestObj) {
    const linkedResource = resource;
    try {
      const baseURL = this.createHATEOABaseURL(requestObj);
      const primaryKeys = this.resource.primaryKey.map(attribute => linkedResource[attribute]);
      const href = `${baseURL}/${primaryKeys.join('/')}`;
      linkedResource.links = {
        self: { href }
      };
    } catch (error) {
      debug('Error found attaching resource link %o', error);
    }

    return linkedResource;
  }

  /**
   * @method attachResourcesLinks
   * @param {Object} paginatedResources
   * @param {Request} requestObj
   * @return {Object}
   */
  attachResourcesLinks(paginatedResources, requestObj) {
    const pagingInfo = paginatedResources.paging;
    const baseURL = this.createHATEOABaseURL(requestObj);
    const { first, next, previous } = pagingInfo;

    const firstLink =
      first != null ? `${baseURL}?limit=${first.limit}&offset=${pagingInfo.first.offset}` : null;
    const nextLink =
      next != null ? `${baseURL}?limit=${next.limit}&offset=${pagingInfo.next.offset}` : null;
    const previousLink =
      previous != null ? `${baseURL}?limit=${previous.limit}&offset=${previous.offset}` : null;

    const linkedResources = paginatedResources.rows.map(dataRow =>
      this.attachResourceLink(dataRow, requestObj)
    );

    return {
      rows: linkedResources,
      links: {
        first: firstLink,
        next: nextLink,
        previous: previousLink
      }
    };
  }
}

module.exports = BaseRestController;