json_api/response.js

import { parseQueryParams } from '../utils/url_helper'

import isArray from 'lodash/isArray'
import toPairs from 'lodash/toPairs'
import camelCase from 'lodash/camelCase'
import isString from 'lodash/isString'
import get from 'lodash/get'

import { toCamelCaseDeep } from '../utils/case_converter'

/**
 * JSON API response wrapper.
 *
 * JSON API spec: http: //jsonapi.org/
 *
 * @class
 */
export class JsonApiResponse {
  /**
   * Wrap a raw axios.js response object.
   *
   * @constructor
   * @param {object} rawResponse Raw axios.js response object.
   * @param {ShelfNetwork} sdk ShelfNetwork instance.
   */
  constructor (rawResponse, sdk, server) {
    this._rawResponse = rawResponse
    this._sdk = sdk
    this._server = server
    this._parseResponse(rawResponse)
  }

  /**
   * Get response data.
   */
  get data () {
    return this._data
  }

  /**
   * Get response HTTP status.
   */
  get httpStatus () {
    return this._rawResponse.status
  }

  /**
   * Get response headers.
   */
  get headers () {
    return this._rawResponse.headers
  }

  /**
   * Get response metadata.
   */
  get meta () {
    return this._meta
  }

  /**
   * Override JSON serialization.
   *
   * @return {object} Data to be serialized.
   */
  toJSON () {
    return this.data
  }

  _parseResponse (response) {
    // NOTE: Horizon follows the JSON API spec
    if (response.status === 204) {
      return
    }

    let body = response.data

    if (!body.data) {
      this._data = null
    } else if (isArray(body.data)) {
      this._data = body.data.map((item) => {
        let parsed = this._parseResourceObjectData(item)
        this._resolveRelationships(item, parsed)
        this._resolveLinks(item, parsed)

        return parsed
      })
    } else {
      this._data = this._parseResourceObjectData(body.data)
      this._resolveRelationships(body.data, this)
    }

    this._resolveLinks(body, this)
    this._data = toCamelCaseDeep(this._data)
    this._meta = toCamelCaseDeep(body.meta)
  }

  _parseResourceObjectData (resourceObject) {
    let data = {
      id: resourceObject.id,
      resourceType: resourceObject.type
    }

    return Object.assign(data, resourceObject.attributes)
  }

  _resolveLinks (source, target) {
    if (!source.links) {
      return
    }

    toPairs(source.links).forEach((pair) => {
      let key = pair[0]
      let value = pair[1]
      let methodName = camelCase('fetch_' + key)
      let link = isString(value) ? value : value.href
      target[methodName] = this._makeLinkCaller(link)
    })

    delete source.links
  }

  _makeLinkCaller (link) {
    return () => {
      let query = parseQueryParams(link)

      let callBuilder = this._server._makeCallBuilder()
        .appendUrlSegment(link)

      if (this._sdk.token || this._sdk.wallet) {
        callBuilder = callBuilder.withCredentials()
      }

      return callBuilder.get(query)
    }
  }

  _resolveRelationships (source, target) {
    if (!source.relationships) {
      return
    }

    let relationships = {}
    Object.keys(source.relationships).forEach((key) => {
      let value = source.relationships[key]
      let parsed

      if (isArray(value.data)) {
        parsed = value.data.map(item => {
          let parsedItem = this._parseRelationshipData(item)
          this._resolveLinks(item, parsedItem)
          this._resolveRelationships(parsedItem, parsedItem)

          return parsedItem
        })
      } else {
        parsed = this._parseRelationshipData(value.data)
        this._resolveLinks(value, parsed)
        this._resolveRelationships(parsed, parsed)
      }

      relationships[camelCase(key)] = parsed
    })

    delete source.relationships
    target.relationships = relationships
  }

  _parseRelationshipData (relationship) {
    let data = {
      id: relationship.id,
      resourceType: relationship.type
    }

    let included = this._getIncluded(relationship.type, relationship.id)
    if (included) {
      data = Object.assign(data, toCamelCaseDeep(included.attributes))
      if (included.relationships) {
        data.relationships = included.relationships
      }
    }

    return data
  }

  _getIncluded (type, id) {
    let included = get(this._rawResponse, 'data.included')
    if (!included) {
      return null
    }

    return included.find(x => x.type === type && x.id === id)
  }
}