API Docs for: 0.2.1
Show:

File: addon/serializers/application.js

/**
  @module ember-jsonapi-resources
  @submodule serializers
**/

import Ember from 'ember';
import { singularize, pluralize } from 'ember-inflector';

/**
  Serializer/Deserializer for a JSON API resource object, used by adapter.

  When extending use a mixin or define transform methods to serialize and/or
  deserializer attributes based on the name or the type of attribute.

  The methods use a naming convention:

    - '[de]serialize' + 'AttrName' or 'TypeName' + 'Attribute'
    - E.g. use `serializeNameAttribute` and `deserializeNameAttribute` in
      a generated serializer for use with `name: attr()`
    - Or, redefine `serializeDateAttribute` and `deserializeDateAttribute`
      to use your own data transformation with `attr('date')` the default,
      Date type [de]serialize methods transfrom to/from ISO Format.
    - Transform methods based on the name of the attribute will be called
      instead of any transform methods based on the type of the attribute.

  @class ApplicationSerializer
  @static
*/
export default Ember.Object.extend({

  /**
    Serialize resource for the request payload

    @method serialize
    @param {Resource|Array} resource - object to serialize
    @return {Object|Array}
  */
  serialize(resources) {
    const json = { data: null };
    if (Array.isArray(resources)) {
      json.data = this.serializeResources(resources);
    } else if (typeof resources === 'object') {
      json.data = this.serializeResource(resources);
    } else {
      json.data = {};
    }
    return json;
  },

  /**
    @method serializeResources
    @param {Array} collection
    @return {Array}
  */
  serializeResources(collection) {
    for (let i = 0; i < collection.length; i++) {
      collection[i] = this.serializeResource(collection[i]);
    }
    return collection;
  },

  /**
    Serialize a resource object

    @method serializeResource
    @param {Resource} object to serialize
    @return {Object}
  */
  serializeResource(resource) {
    let json = resource.getProperties('type', 'attributes', 'relationships');
    json = this.transformAttributes(json, 'serialize');
    for (let relationship in json.relationships) {
      if (json.relationships.hasOwnProperty(relationship)) {
        delete json.relationships[relationship].links;
        if (!json.relationships[relationship].data) {
          delete json.relationships[relationship];
        }
      }
    }
    return json;
  },

  /**
    Serialize a resource object, but only the changed attributes

    @method serializeChanged
    @param {Resource} object to serialize
    @return {Object}
  */
  serializeChanged(resource) {
    let json = resource.getProperties('id', 'type', 'changedAttributes');
    if (Ember.isEmpty(Object.keys(json.changedAttributes))) { return null; }
    let serialized = {
      data: {
        type: json.type,
        id: json.id,
        attributes: json.changedAttributes
      }
    };
    serialized.data = this.transformAttributes(serialized.data, 'serialize');
    return serialized;
  },

  /**
    @method serializeRelationships
    @param {Resource} resource with relationships to serialize
    @param {Array} relationships list of {String} relationship properties
    @return {Object} the serialized `relationship` node for the JSON payload
  */
  serializeRelationships(resource, relationships) {
    if (!relationships || relationships.length === 0) {
      return null;
    }
    let relations = Object.keys(resource.get('relationships'));
    relations = this._intersection(relations, relationships);
    relationships = {};
    relations.forEach((relationship) => {
      relationships[relationship] = this.serializeRelationship(resource, relationship);
    });
    return relationships;
  },

  /**
    @method serializeRelationship
    @param {Resource} resource instance, has URLs via it's relationships property
    @param {String} relationship name
    @param {String|undefined} id (optional) of the related resource
    @return {Object} payload
  */
  serializeRelationship(resource, relationship, id) {
    resource = resource.get('content') || resource;
    // The actual resource type of this relationship is found in related-proxy's meta.
    let meta = resource.relationMetadata(relationship);
    let data = resource.get(['relationships', meta.relation, 'data'].join('.'));
    if (id === undefined) {
      return { data: data };
    }
    let resourceObject = { type: pluralize(meta.type), id: id.toString() };
    return { data: (Array.isArray(data)) ? [resourceObject] : resourceObject };
  },

  /**
    @private
    @method _intersection
    @param {Array} first
    @param {Array} second
    @return {Array}
  */
  _intersection(first, second) {
    if (!Array.isArray(first) || !Array.isArray(second)) {
      return [];
    }
    if (second.length > first.length) {
      let tmp = second;
      second = first;
      first = tmp;
    }
    return first.filter( (item) => {
      return (second.indexOf(item) !== -1);
    });
  },

  /**
    Deserialize response objects from the request payload

    @method deserialize
    @param {Object} json - response object, extract resource(s)
    @return {Ember.Array|Resource}
  */
  deserialize(json) {
    if (Array.isArray(json.data)) {
      return this.deserializeResources(Ember.A(json.data));
    } else if (typeof json.data === 'object') {
      return this.deserializeResource(json.data);
    } else {
      return null;
    }
  },

  /**
    Deserialize response objects in the payload data

    @method deserializeResources
    @param {Array} collection
    @return {Object|Ember.Array}
  */
  deserializeResources(collection) {
    for (let i = 0; i < collection.length; i++) {
      collection[i] = this.deserializeResource(collection[i]);
    }
    return collection;
  },

  /**
    Deserialize response an object in the payload data

    @method deserializeResource
    @param {Object} json
    @return {Resource}
  */
  deserializeResource(json) {
    json = this.transformAttributes(json);
    return this._createResourceInstance(json);
  },

  /**
    Deserialize optional included array of payload and add to service cache

    @method deserializeIncluded
    @param {Array} related
    @param {Object} resp (optional) e.g. headers, meta, etc.
  */
  deserializeIncluded(related, resp) {
    if (!related) { return; }
    let resource, service;
    for (let i = 0; i < related.length; i++) {
      service = Ember.getOwner(this).lookup('service:' + pluralize(related[i].type));
      if (service && service.cache && service.cache.data) {
        resource = service.serializer.deserializeResource(related[i]);
        service.cacheResource({ meta: resp.meta, data: resource, headers: resp.headers});
      }
    }
  },

  /**
    Transform attributes, serialize or deserialize by specified method or
    per type of attribute, e.g. date.

    Your serializer may define a specific method for a type of attribute,
    i.e. `serializeDateAttribute` and/or `deserializeDateAttribute`. Likewise,
    your serializer may define a specific method for the name of an attribute,
    like `serializeUpdatedAtAttribute` and `deserializeUpdatedAtAttribute`.

    During transformation a method based on the name of the attribute takes
    priority over a transform method based on the type of attribute, e.g. date.

    @method transformAttributes
    @param {Object} json with attributes hash of resource properties to be transformed
    @param {String} [operation='deserialize'] perform a serialize or deserialize
      operation, the default operation is to deserialize when not passed
    @return {Object} json
  */
  transformAttributes(json, operation = 'deserialize') {
    assertTranformOperation(operation);
    let transformMethod, factory, meta;
    for (let attr in json.attributes) {
      transformMethod = transformMethodName(operation, attr);
      if (typeof this[transformMethod] === 'function') {
        json.attributes[attr] = this[transformMethod](json.attributes[attr]);
      } else {
        try {
          factory = this._lookupFactory(json.type);
          meta = factory.metaForProperty(attr);
          transformMethod = transformMethodName(operation, meta.type);
          if (typeof this[transformMethod] === 'function') {
            json.attributes[attr] = this[transformMethod](json.attributes[attr]);
          }
        } catch (e) {
          continue; // metaForProperty has an assertion that may throw
        }
      }
    }
    return json;
  },

  /**
    Create a Resource from a JSON API Resource Object

    See <http://jsonapi.org/format/#document-resource-objects>

    @private
    @method _createResourceInstance
    @param {Object} json
    @return (Resource) instance
  */
  _createResourceInstance(json) {
    let resource = {
      'type': json.type,
      'id': json.id,
      'attributes': json.attributes,
      'relationships': json.relationships,
      'links': json.links,
      'meta': json.meta
    };
    for (let prop in resource) {
      if (!resource[prop]) {
        delete resource[prop];
      }
    }
    return this._lookupFactory(json.type).create(resource);
  },

  /**
    @private
    @method _lookupFactory
    @param {String} type
    @return {Function} factory for creating resource instances
  */
  _lookupFactory(type) {
    return Ember.getOwner(this).lookup('model:' + singularize(type));
  }
});

const tranformOperations = ['serialize', 'deserialize'];

function assertTranformOperation(operation) {
  Ember.assert(`${operation} is not a valid transform operation`, tranformOperations.indexOf(operation) > -1);
}

function transformMethodName(operation, attrName) {
  return [operation, Ember.String.classify(attrName), 'Attribute'].join('');
}