endpoints/endpoints

View on GitHub
src/request-handler/index.js

Summary

Maintainability
D
2 days
Test Coverage
import _ from 'lodash';

import throwIfModel from './lib/throw_if_model';
import throwIfNoModel from './lib/throw_if_no_model';
import verifyContentType from './lib/verify_content_type';
import verifyDataObject from './lib/verify_data_object';
import splitStringProps from './lib/split_string_props';
import verifyClientGeneratedId from './lib/verify_client_generated_id';
import verifyFullReplacement from './lib/verify_full_replacement';
import collapseInclude from './lib/collapse_include';

/**
  Provides methods for pulling out json-api relevant data from
  express or hapi request instances. Also provides route level
  validation.
*/
class RequestHandler {

  /**
    The constructor.

    @constructs RequestHandler
    @param {Endpoints.Store} store
  */
  constructor (config={}) {
    this.config = config;
  }

  get store() {
    return this.config.store;
  }

  get model() {
    return this.config.model;
  }

  /**
    A function that, given a request, validates the request.

    @returns {Object} An object containing errors, if any.
  */
  validate (request) {

    var err;
    var validators = [];

    if (request.body && request.body.data) {
      var clientIdCheck =
        request.method === 'POST' &&
        // posting to a relation endpoint is for appending
        // relationships and and such is allowed (must have, really)
        // ids
        !request.params.relation &&
        !this.config.allowClientGeneratedIds;

      // this applies for both "base" and relation endpoints
      var fullReplacementCheck =
        request.method === 'PATCH' &&
        !this.config.allowToManyFullReplacement;

      if (clientIdCheck) {
        validators.push(verifyClientGeneratedId);
      }
      if (fullReplacementCheck) {
        validators.push(verifyFullReplacement);
      }
      validators = validators.concat([
        verifyContentType,
        verifyDataObject
      ]);
    }

    // does this.validators needs a better name? controllerValidator, userValidators?
    validators = validators.concat(this.config.validators);

    for (var validate in validators) {
      err = validators[validate](request, this);
      if (err) {
        break;
      }
    }
    return err;
  }

  /**
    Given a request, build a query object.

    @returns {Object} The query object on a request.
   */
  query (request) {
    // bits down the chain can mutate this config
    // on a per-request basis, so we need to clone
    const config = _.cloneDeep(_.omit(this.config, ['store', 'model']));
    const {include, filter, fields, sort} = request.query;
    return {
      include: include ? collapseInclude(include.split(',')) : config.include,
      filter: filter ? splitStringProps(filter) : config.filter,
      fields: fields ? splitStringProps(fields) : config.fields,
      sort: sort ? sort.split(',') : config.sort
    };
  }

  /**
    Given a request, create a new record in the underlying store.

    @returns {Promise(Bookshelf.Model)} Newly created instance of the model.
  */
  create (request) {
    const {store, model} = this;
    const data = request.body.data;
    if (data && data.id) {
      return store.byId(model, data.id)
        .then(throwIfModel)
        .then(function() {
          return store.create(model, data);
        }
      );
    } else {
      return store.create(model, data);
    }
  }

  createRelation (request) {
    const store = this.store;
    const relationName = request.params.relation;
    return store.byId(this.model, request.params.id, [relationName])
      .then(throwIfNoModel)
      .then((model) => {
        return store.createRelation(model, relationName, request.body.data);
      });
  }

  /**
    Queries the store for matching models.

    @returns {Promise(Bookshelf.Model)|Promise(Bookshelf.Collection)}
  */
  read (request) {
    const id = request.params.id;
    const query = this.query(request);
    if (id) {
      // FIXME: this could collide with filter[id]=#
      query.filter.id = id;
      query.singleResult = true;
    }
    return this.store.read(this.model, query);
  }

  readRelated (request) {
    const id = request.params.id;
    const query = this.query(request);
    const relatedName = request.params.related;
    return this.store.readRelated(this.model, id, relatedName, query);
  }

  readRelation (request) {
    const id = request.params.id;
    const query = this.query(request);
    const relationName = request.params.relation;
    return this.store.readRelation(this.model, id, relationName, query);
  }

  update (request) {
    const store = this.store;
    return store.byId(this.model, request.params.id).
      then(throwIfNoModel).
      then((model) => {
        return store.update(model, request.body.data);
      });
  }

  updateRelation (request) {
    const store = this.store;
    const relationName = request.params.relation;
    return store.byId(this.model, request.params.id, [relationName])
      .then(throwIfNoModel)
      .then((model) => {
        return store.update(model, {
          relationships: {
            [relationName]: {
              data: request.body.data
            }
          }
        });
      });
  }

  /**
    Deletes a model.

    @returns {Promise(Bookshelf.Model)}
  */
  destroy (request) {
    const store = this.store;
    const id = request.params.id;
    return store.byId(this.model, id).then((model) => {
      if (model) {
        return store.destroy(model);
      }
    });
  }

  destroyRelation (request) {
    const store = this.store;
    const relationName = request.params.relation;
    return store.byId(this.model, request.params.id, [relationName])
      .then(throwIfNoModel)
      .then((model) => {
        return store.destroyRelation(model, {
          relationships: {
            [relationName]: {
              data: request.body.data
            }
          }
        });
      });
  }

}

export default RequestHandler;