Source: actions.js

/**
 * Pinia actions, used via `this.$store.dispatch`, e.g.:
 * `this.$store.dispatch('jv/get', <args>)`
 *
 * `args` can be either a string or an object representing the item(s) required,
 * or it can be an array of string/object and an optional axios config object.
 * @namespace actions
 * @memberof module:pinia-jsonapi.createJsonapiStore
 * @param {axios} api - an axios api instance
 * @param {object} conf - a pinia-jsonapi config object
 */

import get from 'lodash/get'
import merge from 'lodash/merge'

const actions = (api, conf, utils) => {
  // Short var name
  let jvtag = conf['jvtag']

  /**
   * Internal method to 'write' related items from the API.
   * This method is wrapped by `(delete|patch|post)Related` actions, and is not available directly as an action.
   *
   * @async
   * @memberof module:pinia-jsonapi.createJsonapiStore.actions
   * @param {object} args - A restructured object, specifying relationship(s)  - e.g. `{ _jv: { type: "endpoint", id: "1", relationships: {...} } }`
   * @param {object} args - A restructured object, specifying relationship(s)  - e.g. `{ _jv: { type: "endpoint", id: "1", relationships: {...} } }`
   * @return {object} Restructured representation of the 'parent' item
   */
  const writeRelated = async (store, args, method) => {
    let [data, config] = utils.unpackArgs(args)
    let [type, id] = utils.getTypeId(data)
    if (!type || !id) {
      throw 'No type/id specified'
    }

    let rels
    if (typeof data === 'object' && utils.hasProperty(data[jvtag], 'relationships')) {
      rels = data[jvtag]['relationships']
    } else {
      throw 'No relationships specified'
    }

    // Iterate over all records in rels
    let relPromises = []
    let includes = []
    for (let [relName, relItems] of Object.entries(rels)) {
      includes.push(relName)
      if (utils.hasProperty(relItems, 'data')) {
        let path = `${type}/${id}/relationships/${relName}`
        const apiConf = {
          method: method,
          url: path,
          data: relItems,
        }
        merge(apiConf, config)
        relPromises.push(api(apiConf))
      }
    }
    // Wait for all individual API calls to complete
    await Promise.all(relPromises)
    // Get the updated object from the API
    let params = {}
    // Also include related objects
    if (conf.relatedIncludes) {
      params['include'] = includes.join()
    }
    return store.get([`${type}/${id}`, { params: params }])
  }

  return {
    /**
     * Get items from the API
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(string|object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {string}  - A URL path to an item - e.g. `endpoint/1`
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the requested item(s)
     */
    get(args) {
      const [data, config] = utils.unpackArgs(args)
      const path = utils.getURL(data)
      const apiConf = { method: 'get', url: path }
      // https://github.com/axios/axios/issues/362
      config['data'] = config['data'] || {}
      merge(apiConf, config)
      return api(apiConf).then((results) => {
        let resData = utils.jsonapiToNorm(results.data.data)
        let [type, id] = utils.getTypeId(data)
        if (!id && conf.clearOnUpdate) {
          let record = resData
          if (Object.keys(resData).length === 0 && type) {
            // No records - assume type == endpoint
            record = { _jv: { type: type } }
          }
          if (record) {
            this.clearRecords(record)
          }
        } else {
          this.addRecords(resData)
        }
        let includes = utils.getIncludedRecords(results)
        this.mergeRecords(includes)
        resData = utils.checkAndFollowRelationships(this, resData)
        resData = utils.preserveJSON(resData, results.data)
        return resData
      })
    },
    /**
     * Get related items from the API
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(string|object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {string}  - A URL path to an item - e.g. `endpoint/1`
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the requested item(s)
     */
    async getRelated(args) {
      const [data, config] = utils.unpackArgs(args)
      let [type, id, relName] = utils.getTypeId(data)
      if (!type || !id) {
        throw 'No type/id specified'
      }

      let rels
      if (typeof data === 'object' && utils.hasProperty(data[jvtag], 'relationships')) {
        rels = data[jvtag]['relationships']
      } else {
        let record = await this.get(args)

        rels = get(record, [jvtag, 'relationships'], {})
        if (relName && utils.hasProperty(rels, relName)) {
          // Only process requested relname
          rels = { [relName]: rels[relName] }
        }
      }

      // We can't pass multiple/non-promise vars in a promise chain,
      // so must define such vars in a higher scope
      let relNames = []
      let relPromises = []

      // Iterate over all records in rels
      for (let [relName, relItems] of Object.entries(rels)) {
        let relData
        // relationships value might be empty if user-constructed
        // so fetch relationships resource linkage for these
        if (!relItems) {
          try {
            const resLink = await api.get(`${type}/${id}/relationships/${relName}`, config)
            relItems = resLink.data
          } catch (error) {
            throw `No such relationship: ${relName}`
          }
        }
        // Use related link if provided
        if (utils.hasProperty(relItems, 'links')) {
          relData = relItems['links']['related']
          if (!(typeof relData === 'string')) {
            relData = relData['href']
          }
          relData = [relData]
          // Or extract relationships from 'data' (type/id)
          // empty to-one rels (null) are special-cased
        } else if (utils.hasProperty(relItems, 'data') && relItems['data'] !== null) {
          relData = relItems['data']
          if (!Array.isArray(relData)) {
            // Treat as if always an array
            relData = [relData]
          }
        }
        if (relData) {
          for (let entry of relData) {
            // Rewrite 'data' objects to normalised form
            if (!(typeof entry === 'string')) {
              entry = { [jvtag]: entry }
            }
            relNames.push(relName)
            relPromises.push(this.get([entry, config]))
          }
        } else {
          // Empty to-one rels should have a relName but no data
          relNames.push(relName)
          // prettier-ignore
          relPromises.push(new Promise((resolve) => { resolve({}) }))
        }
      }
      // 'Merge' all promise resolution/rejection
      return Promise.all(relPromises).then((results) => {
        let related = {}
        results.forEach((result, i) => {
          // Get the relName from the same array position as the result item
          let relName = relNames[i]
          let normItem = { [relName]: {} }
          if (utils.hasProperty(result, jvtag)) {
            normItem[relName][result[jvtag]['type']] = {
              [result[jvtag]['id']]: result,
            }
          }
          merge(related, normItem)
        })
        return related
      })
    },
    /**
     * DELETE an object's relationships via its `relationships URL`
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} args - A restructured object, specifying relationship(s)  - e.g. `{ _jv: { type: "endpoint", id: "1", relationships: {...} } }`
     * @return {object} Restructured representation of the 'parent' item
     */
    deleteRelated(args) {
      return writeRelated(this, args, 'delete')
    },
    /**
     * PATCH an object's relationships via its `relationships URL`
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} args - A restructured object, specifying relationship(s)  - e.g. `{ _jv: { type: "endpoint", id: "1", relationships: {...} } }`
     * @return {object} Restructured representation of the 'parent' item
     */
    patchRelated(args) {
      return writeRelated(this, args, 'patch')
    },
    /**
     * POST to an object's relationships via its `relationships URL`
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} args - A restructured object, specifying relationship(s)  - e.g. `{ _jv: { type: "endpoint", id: "1", relationships: {...} } }`
     * @return {object} Restructured representation of the 'parent' item
     */
    postRelated(args) {
      return writeRelated(this, args, 'post')
    },
    /**
     * Post an item to the API
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the posted item
     */
    post(args) {
      let [data, config] = utils.unpackArgs(args)
      const path = utils.getURL(data, true)
      const apiConf = {
        method: 'post',
        url: path,
        data: utils.normToJsonapi(data),
      }
      merge(apiConf, config)
      return api(apiConf).then((results) => {
        let includes = utils.getIncludedRecords(results)
        this.mergeRecords(includes)

        // If the server handed back data, store it (to get id)
        // spec says 201, but some servers (wrongly) return 200
        if (results.status === 200 || results.status === 201) {
          data = utils.jsonapiToNorm(results.data.data)
        }
        this.addRecords(data)
        return utils.preserveJSON(this.getData(data), results.data)
      })
    },
    /**
     * Patch an item in the API
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the patched item
     */
    patch(args) {
      let [data, config] = utils.unpackArgs(args)
      if (conf.cleanPatch) {
        data = utils.cleanPatch(data, this.$state, conf.cleanPatchProps)
      }
      const path = utils.getURL(data)
      const apiConf = {
        method: 'patch',
        url: path,
        data: utils.normToJsonapi(data),
      }
      merge(apiConf, config)
      return api(apiConf).then((results) => {
        // If the server handed back data, store it
        if (results.status === 200 && utils.hasProperty(results.data, 'data')) {
          // Full response
          this.deleteRecord(data)
          data = utils.jsonapiToNorm(results.data.data)
          this.addRecords(data)
        } else {
          // 200 (meta-only), or 204 (no resource) response
          // Update the store record from the patch
          this.mergeRecords(data)
        }

        // NOTE: We deliberately process included records after any `deleteRecord` mutations
        // to avoid deleting any included records that we just added.
        let includes = utils.getIncludedRecords(results)
        this.mergeRecords(includes)
        return utils.preserveJSON(this.getData(data), results.data)
      })
    },
    /**
     * Delete an item from the API
     *
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(string|object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {string}  - A URL path to an item - e.g. `endpoint/1`
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the deleted item
     */
    delete(args) {
      const [data, config] = utils.unpackArgs(args)
      const path = utils.getURL(data)
      const apiConf = { method: 'delete', url: path }
      merge(apiConf, config)
      return api(apiConf).then((results) => {
        let includes = utils.getIncludedRecords(results)
        this.mergeRecords(includes)
        this.deleteRecord(data)
        if (results.data) {
          return utils.preserveJSON(utils.jsonapiToNorm(results.data.data), results.data)
        } else {
          return data
        }
      })
    },
    /**
     * Get items from the API without updating the Pinia store
     *
     * @see module:pinia-jsonapi.createJsonapiStore.actions.get
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(string|object|array)} args - See {@link module:pinia-jsonapi.createJsonapiStore.actions} for a summary of args
     * @param {string}  - A URL path to an item - e.g. `endpoint/1`
     * @param {object}  - A restructured object  - e.g. `{ _jv: { type: "endpoint", id: "1" } }`
     * @param {array}  - A 2-element array, consisting of a string/object and an optional axios config object
     * @return {object} Restructured representation of the posted item
     */
    search(args) {
      // Set 'write' to false to prevent store updates
      return this.get(args, false)
    },
    /**
     * Alias for {@link module:pinia-jsonapi.createJsonapiStore.actions.get}
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     */
    fetch(args) {
      return this.get(args)
    },
    /**
     * Alias for {@link module:pinia-jsonapi.createJsonapiStore.actions.post}
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     */
    create(args) {
      return this.post(args)
    },
    /**
     * Alias for {@link module:pinia-jsonapi.createJsonapiStore.actions.patch}
     * @async
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     */
    update(args) {
      return this.patch(args)
    },
    /**
     * Add record(s) to the store, according to `mergeRecords` config option
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} records - The record(s) to be added
     */
    addRecords(records) {
      utils.updateRecords(this, records)
    },
    /**
     * Delete all records from the store (of a given type) other than those included in a given record
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} records - A record with type set.
     */
    clearRecords(records) {
      Object.assign(this.$state, utils.normToStore(records))
    },
    /**
     * Delete a record from the store.
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {(string|object)} record - The record to be deleted
     */
    deleteRecord(record) {
      const [type, id] = utils.getTypeId(record, false)
      if (!type || !id) {
        throw `deleteRecord: Missing type or id: ${record}`
      }
      try {
        delete this.$state[type][id]
      } catch (err) {
        if (err instanceof TypeError) {
          // Trying to delete non-existent object - ignore
        } else {
          throw err
        }
      }
    },
    /**
     * Merge (or add) records to the store
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} records - The record(s) to be merged
     */
    mergeRecords(records) {
      utils.updateRecords(this, records, true)
    },
    /**
     * Replace (or add) record(s) to the store
     * @memberof module:pinia-jsonapi.createJsonapiStore.actions
     * @param {object} records - The record(s) to be replaced
     */
    replaceRecords(records) {
      utils.updateRecords(this, records, false)
    },
  }
}

export default actions