Home Identifier Source Repository

lib/resources/Base.js

"use strict";

import Actions       from "../utilities/Actions";
import Api           from "../utilities/WebAPI";
import APIDispatcher from "../dispatcher/APIDispatcher";
import URLHelpers    from "../utilities/URLHelpers";

let DEFAULT_API_METHODS = {
  countable:    ["fetchCount"],
  createable:   ["create", "update"],
  destroyable:  ["destroy"]
};

function isPrimitive(value) {
  return ["number", "string"].indexOf(typeof value) >= 0;
}

/**
 * A base class for API resources. This class is meant to be subclassed and have it's `apiMethods` method overridden to
 * include all methods that should be made available to consumers.
 *
 * @example
 * import Base from "./Base";
 *
 * class Thing extends Base {
 *   constructor() {
 *     super("thing")
 *   }
 *
 *   get apiMethods() {
 *     return super.apiMethods.concat(["fetchAllTheThings"]);
 *   }
 *
 *   fetchAllTheThings() {
 *     return this.fetch("/admin/things.json");
 *   }
 * }
 */
export default class {
  /**
   * Using the supplied `resourceName`, this constructor will generate the singular and plural names for this resource
   * which will be used to infer the merge, count, and clear actions to be dispatched.
   *
   * @param {string} resourceName - The singular name for the resource (e.g. `product`)
   * @param {object} [options] - Options for this resource
   * @param {boolean} options.countable - Whether or not this resource is countable (has a /count.json endpoint)
   * @param {boolean} options.createable - Whether or not this resource can be created
   * @param {boolean} options.destroyable - Whether or not this resource can be deleted
   */
  constructor(resourceName, options = {}) {
    this._resource  = resourceName;
    this._resources = `${resourceName}s`;
    this._options   = options;
  }

  /**
   * An array of methods to be exposed via {@link APIProxy}
   *
   * @return {Array<string>} The names of all methods to be made available
   */
  get apiMethods() {
    let exportedMethods = ["clear", "fetch"];

    for (let type of Object.keys(DEFAULT_API_METHODS)) {
      if (this._options[type]) {
        exportedMethods.push(...DEFAULT_API_METHODS[type]);
      }
    }

    return exportedMethods;
  }

  /**
   * Returns an object that supplies the clear, count and merge actions for this resource
   *
   * @return {Actions}
   */
  get actions() {
    return new Actions(this._resources);
  }

  /**
   * Performs a GET request for the specified query.
   *
   * @param {ResourceQuery} [query]
   *
   * @return {RequestPromise}
   */
  fetch(query) {
    let url      = this.urlFor(query);
    let dispatch = this._dispatchMerge.bind(this);
    let resource = isPrimitive(query) ? this._resource : this._resources;

    return Api.get(url).then(response => dispatch(response[resource]));
  }

  /**
   * Performs a GET request for the resource's count endpoint
   *
   * @param {ResourceQuery} [query]
   *
   * @return {RequestPromise}
   */
  fetchCount(query) {
    let url      = this.urlFor("count", query);
    let dispatch = this._dispatchCount.bind(this);

    return Api.get(url).then(response => dispatch(response.count, query));
  }

  /**
   * Performs a POST request, sending the resource along as the request body
   *
   * @param {object} resource - The resource to be created
   * @param {object} [options] - Options to be passed along to the request
   *
   * @return {RequestPromise}
   */
  create(resource, options) {
    let body     = { [this._resource]: resource };
    let url      = this.urlFor();
    let dispatch = this._dispatchMerge.bind(this);
    let property = this._resource;

    return Api.post(url, body, options).then(response => dispatch(response[property]));
  }

  /**
   * Updates the given resource
   *
   * @param {number|string} id - The id of the resource
   * @param {object} resource - The resource to be updated
   * @param {object} [options] - Options to be passed along with the request
   *
   * @return {RequestPromise}
   */
  update(id, resource, options) {
    let body     = { [this._resource]: resource };
    let url      = this.urlFor(id);
    let dispatch = this._dispatchMerge.bind(this);
    let property = this._resource;

    return Api.put(url, body, options).then(response => dispatch(response[property]));
  }

  /**
   * Destroys the resource
   *
   * @param {number|string} id - The id of the resource
   * @param {object} [options] - Options to be passed along with the request
   *
   * @return {RequestPromise}
   */
  destroy(id, options) {
    let url      = this.urlFor(id);
    let dispatch = this._dispatchDestroy.bind(this);

    return Api.delete(url, options).then(() => dispatch(id));
  }

  /**
   * Dispatches the clear action for this resource. The action will be handled by the appropriate store.
   */
  clear() {
    APIDispatcher.dispatch({ actionType: this.actions.clearAction });
  }

  /**
   * @protected
   * Generates a resource URL for the specified query. The query can be an id, or an object to be converted to a query
   * string.
   *
   * @param {ResourceQuery} [query]
   * @param {ResourceQuery} [params] - Query string parameters (only used when query is a number or string)
   *
   * @return {string} The resource URL for the specified query
   *
   * @example
   * import Base from "./Base";
   *
   * let instance = new Base("product");
   *
   * instance.urlFor();                            // `/admin/products.json`
   * instance.urlFor(1);                           // `/admin/products/1.json`
   * instance.urlFor("1");                         // `/admin/products/1.json`
   * instance.urlFor(1, { type: "test" })          // `/admin/products/1.json?type=test
   * instance.urlFor({ fields: ["id", "title"] }); // `/admin/products.json?fields=id,title
   */
  urlFor(query, params) {
    var url         = `/admin/${this._resources}.json`;
    var queryString = URLHelpers.toQueryString(query);

    if (isPrimitive(query)) {
      url         = `/admin/${this._resources}/${query}.json`;
      queryString = URLHelpers.toQueryString(params);
    }

    return queryString ? `${url}?${queryString}` : url;
  }

  _dispatchMerge(items) {
    if (!Array.isArray(items)) {
      items = [items];
    }

    let actionType = this.actions.mergeAction;
    APIDispatcher.dispatch({ actionType, items });

    return items;
  }

  _dispatchCount(count, query) {
    let actionType = this.actions.countAction;
    APIDispatcher.dispatch({ actionType, count, query });

    return count;
  }

  _dispatchDestroy(id) {
    let actionType = this.actions.destroyAction;
    APIDispatcher.dispatch({ actionType, id });
  }
}