collection_json.js

'use strict';

var _ = require('lodash');
var FieldUtils = require('./field_utils');
var Form = require('./form');
var WebLink = require('./web_link');
var Resource = require('./resource');
var LinkCollection = require('./link_collection');

var cjObjectLinkParser = function(obj, headers, context) {

  var links = (obj.links || []).concat([{ rel: 'self', href: obj.href }]);

  return _(links)
    .map(function(l) { return new WebLink(l, context); })
    .groupBy('rel')
    .mapValues(function(links) { return LinkCollection.fromArray(links); })
    .value();
};

var CollectionJsonItemExtension = function(parentCollection) {
  this.applies = _.constant(true);

  this.linkParser = cjObjectLinkParser;

  this.dataParser = function(data) {
    return data.data || [];
  };

  this.formParser = function(data, headers, context) {
    var templateData = _.get(parentCollection, 'collection.template.data') || [];
    // Depend on indexBy using the *last* item to generate a key as the value to
    // have the item's data, if present, override the template's data.
    var fields = _(templateData.concat(data.data || [])).indexBy('name').values().value();

    return {
      'edit-form': [
        new Form({
          href: data.href,
          method: 'PUT',
          type: 'application/vnd.collection+json',
          fields: fields
        }, context)
      ]
    };
  };
};

/**
 * Create the Collection+JSON extension
 *
 * @constructor
 * @implements {Extension}
 * @arg {Array} [mediaTypes] Media types in addition to
 * `application/vnd.collection+json` that should be handled by this extensions.
 * This allows for custom media types based on Collection+JSON to be handled
 * properly.
 *
 * @classdesc
 * Extension for processing
 * [Collection+JSON](http://amundsen.com/media-types/collection/format/).
 * By default, the extension will only process links and embedded
 * resources in responses if the HTTP response `Content-Type` header
 * equals `application/vnd.collection+json`. If you have a custom media type that
 * extends C+J, you can register it by passing it in the `mediaTypes`
 * parameter.
 *
 * C+J queries are exposed as forms, and can be accessed using {@link Resource#$form}
 * or {@link Resource#$forms}. For adding items, a form is accessible using the
 * `create-form` IANA standard link relation.
 *
 * Collection items can be extracted using the `item` standard link relation using
 * {@link Resource#$sub} or {@link Resource#$subs}.
 *
 * A given embedded item can be edited by using the form with the `edit-form` standard
 * link relation.
 *
 * @example <caption>Example editing an existing item</caption>
 * new Root('http://localhost/posts', axios, [new CollectionJsonExtension()]).follow().then(function(coll) {
 *   var firstItem = coll.$subs('item')[0];
 *   var editForm = firstItem.$form('edit-form');
 *   editForm.field('title').value = 'Edited Title';
 *   var newFirstItem = editForm.submit().$followOne('item');
 * });
 *
 */
var CollectionJsonExtension = function(mediaTypes) {
  var mediaTypeSet = { 'application/vnd.collection+json': true };

  mediaTypes = mediaTypes || [];
  for (var i = 0; i < mediaTypes.length; i++) {
    mediaTypeSet[mediaTypes[i]] = true;
  }

  this.encoders = {
    'application/vnd.collection+json': function(data) {
      return JSON.stringify({
        template: {
          data: FieldUtils.extractFields(data)
        }
      });
    }
  };

  this.mediaTypes = _.keys(mediaTypeSet);

  this.applies = function(data, headers) {
    var h = headers['content-type'];
    if (!h) {
      return false;
    }

    // Handle parameters, e.g. application/vnd.collection+json; charset=UTF-8
    var type = h.split(';')[0];
    return mediaTypeSet[type] !==  undefined;
  };

  this.dataParser = function(data) {
    // The data parser really only applies when parsing
    // an included item for use as an embedded resource,
    // so here we don't expect to be nested under "collection".
    return data.data || [];
  };

  this.linkParser = function(data, headers, context) {
    return cjObjectLinkParser(data.collection, headers, context);
  };

  var queryFormDefaults = {
    method: 'GET',
    type: 'application/x-www-form-urlencoded'
  };

  this.formParser = function(data, headers, context) {
    var coll = data.collection;

    var formFactory = function(q) {
      var q2 = _.clone(q);
      q2.fields = q2.data;
      delete q2.data;
      return new Form(_.defaults(q2, queryFormDefaults), context);
    };

    var forms = _.groupBy(_.map((coll.queries || []), formFactory), 'rel');

    if (coll.template) {
      forms['create-form'] = [
        new Form({
          href: coll.href,
          method: 'POST',
          type: 'application/vnd.collection+json',
          fields: coll.template.data
        }, context)
      ];
    }
    return forms;
  };

  this.embeddedParser = function(data, headers, context, parent) {
    return {
      item: Resource.embeddedCollection(
        _.cloneDeep(data.collection.items),
        headers,
        context.withExtensions([new CollectionJsonItemExtension(data)]),
        parent
      )
    };
  };
};

module.exports = CollectionJsonExtension;