201-created/ember-data-hal-9000

View on GitHub
addon/mixin.js

Summary

Maintainability
A
2 hrs
Test Coverage
import Ember from 'ember';

// Reserved keys, per the HAL spec
let halReservedKeys = ['_embedded', '_links'],
  reservedKeys = halReservedKeys.concat(['meta']),
  keys = Object.keys;

const /*SINGLE_PAYLOAD_REQUEST_TYPES = [
    'findRecord',
    'findBelongsTo',
    'queryRecord',
    'createRecord',
    'deleteRecord',
    'updateRecord'
  ],*/
  COLLECTION_PAYLOAD_REQUEST_TYPES = [
    'findHasMany',
    'findMany',
    'query',
    'findAll'
  ];

/**
 * @see ember-data/system/coerce-id
 * @param id
 * @returns {*}
 */
function coerceId(id) {
  return id == null || id === '' ? null : id + '';
}

function halToJSONAPILink(link) {
  let converted,
    linkKeys = keys(link);

  if (linkKeys.length === 1) {
    converted = link.href;
  } else {
    converted = {href: link.href, meta: {}};
    linkKeys.forEach(key => {
      if (key !== 'href') {
        converted.meta[key] = link[key];
      }
    });
  }

  return converted;
}

function arrayFlatten(array) {
  let flattened = [];
  return flattened.concat.apply(flattened, array);
}

export default Ember.Mixin.create({
  keyForRelationship(relationshipKey/*, relationshipMeta */) {
    return relationshipKey;
  },
  keyForAttribute(attributeName/*, attributeMeta */) {
    return attributeName;
  },
  keyForLink(relationshipKey/*, relationshipMeta */) {
    return relationshipKey;
  },
  isSinglePayload(payload, requestType) {
    return COLLECTION_PAYLOAD_REQUEST_TYPES.indexOf(requestType) === -1;
  },

  extractLink(link) {
    return link.href;
  },

  /**
   * Use ember-data 1.13.5+ extractId method
   * @param modelClass
   * @param resourceHash
   * @returns {*}
   */
  extractId (modelClass, resourceHash) {
    var primaryKey = this.get('primaryKey');
    var id = resourceHash[primaryKey];
    return coerceId(id);
  },

  extractMeta (store, requestType, payload, primaryModelClass) {
    const meta = payload.meta || {},
      isSingle = this.isSinglePayload(payload, requestType);

    if (!isSingle) {
      keys(payload).forEach(key => {
        if (reservedKeys.indexOf(key) > -1) {
          return;
        }

        meta[key] = payload[key];
        delete payload[key];
      });

      if (payload._links) {
        meta.links = this.extractLinks(primaryModelClass, payload);
      }
    }

    return meta;
  },

  normalizeResponse (store, primaryModelClass, payload, id, requestType) {
    const isSingle = this.isSinglePayload(payload, requestType),
      documentHash = {},
      meta = this.extractMeta(store, requestType, payload, primaryModelClass),
      included = [];

    if (meta) {
      documentHash.meta = meta;
    }

    if (isSingle) {
      documentHash.data = this.normalize(primaryModelClass, payload, included);
    } else {
      documentHash.data = [];
      payload._embedded = payload._embedded || {};

      const normalizedEmbedded = Object.keys(payload._embedded).map(embeddedKey =>
        payload._embedded[embeddedKey].map(embeddedPayload =>
          this.normalize(primaryModelClass, embeddedPayload, included)));

      documentHash.data = arrayFlatten(normalizedEmbedded);
    }

    documentHash.included = included;
    return documentHash;
  },

  normalize(primaryModelClass, payload, included) {
    let data;

    if (payload) {
      const attributes = this.extractAttributes(primaryModelClass, payload),
        relationships = this.extractRelationships(primaryModelClass, payload, included);

      data = {
        id: this.extractId(primaryModelClass, payload),
        type: primaryModelClass.modelName
      };
      if (Object.keys(attributes).length > 0) {
        data.attributes = attributes;
      }
      if (Object.keys(relationships).length > 0) {
        data.relationships = relationships;
      }

      if (data.attributes) {
        this.applyTransforms(primaryModelClass, data.attributes);
      }
    }

    return data;
  },

  extractLinks(primaryModelClass, payload) {
    let links;

    if (payload._links) {
      links = {};
      Object.keys(payload._links).forEach(link => {
        links[link] = halToJSONAPILink(payload._links[link]);
      });
    }

    return links;
  },

  extractAttributes(primaryModelClass, payload) {
    let payloadKey,
      attributes = {};

    primaryModelClass.eachAttribute((attributeName, attributeMeta)=> {
      payloadKey = this.keyForAttribute(attributeName, attributeMeta);

      if (!payload.hasOwnProperty(payloadKey)) {
        return;
      }

      attributes[attributeName] = payload[payloadKey];
      delete payload[payloadKey];
    });

    if(payload._links) {
      attributes.links = this.extractLinks(primaryModelClass, payload);
    }

    return attributes;
  },

  extractRelationship(relationshipModelClass, payload, included) {
    if (Ember.isNone(payload)) {
      return undefined;
    }

    let relationshipModelName = relationshipModelClass.modelName,
      relationship;

    if (Ember.typeOf(payload) === 'object') {
      relationship = {
        id: coerceId(this.extractId({}, payload))
      };

      if (relationshipModelName) {
        relationship.type = this.modelNameFromPayloadKey(relationshipModelName);
        included.push(this.normalize(relationshipModelClass, payload, included));
      }
    } else {
      relationship = {
        id: coerceId(payload),
        type: relationshipModelName
      };
    }

    return relationship;
  },

  extractRelationships(primaryModelClass, payload, included) {
    let relationships = {},
      embedded = payload._embedded,
      links = payload._links;

    if (embedded || links) {
      primaryModelClass.eachRelationship((key, relationshipMeta) => {
        let relationship,
          relationshipKey = this.keyForRelationship(key, relationshipMeta),
          linkKey = this.keyForLink(key, relationshipMeta);

        if (embedded && embedded.hasOwnProperty(relationshipKey)) {
          let data,
            relationModelClass = this.store.modelFor(relationshipMeta.type);

          if (relationshipMeta.kind === 'belongsTo') {
            data = this.extractRelationship(relationModelClass, embedded[relationshipKey], included);
          } else if (relationshipMeta.kind === 'hasMany') {
            data = embedded[relationshipKey].map(item => {
              return this.extractRelationship(relationModelClass, item, included);
            });
          }

          relationship = {data};
        }

        if (links && links.hasOwnProperty(linkKey)) {
          relationship = relationship || {};

          const link = links[linkKey],
            useRelated = !relationship.data;

          relationship.links = {
            [useRelated ? 'related' : 'self']: this.extractLink(link)
          };
        }

        if (relationship) {
          relationships[key] = relationship;
        }
      }, this);
    }

    return relationships;
  }
});